JMM简介
1.内存模型概述
-
保留目前JVM的安全保证,以进行类型的安全检查:
-
提供(out-of-thin-air safety)无中生有安全性,这样“正确同步的”应该被正式而且直观地定义
-
程序员要有信心开发多线程程序,当然没有其他办法使得并发程序变得很容易开发,但是该规范的发布主要目标是为了减轻程序员理解内存模型中的一些细节负担
-
提供大范围的流行硬件体系结构上的高性能JVM实现,现在的处理器在它们的内存模型上有着很大的不同,JMM应该能够适合于实际的尽可能多的体系结构而不以性能为代价,这也是Java跨平台型设计的基础
-
提供一个同步的习惯用法,以允许发布一个对象使他不用同步就可见,这种情况又称为初始化安全(initialization safety)的新的安全保证
-
对现有代码应该只有最小限度的影响
2.JMM结构:
-
一个写入线程释放的同步锁和紧随其后进行读取的读线程的同步锁是同一个
从本质上讲,释放锁操作强迫它的隶属线程【释放锁的线程】从工作内存中的写入缓存里面刷新(专业上讲这里不应该是刷新,可以理解为提供)数据(flush操作),然后获取锁操作使得另外一个线程【获得锁的线程】直接读取前一个线程可访问域(也就是可见区域)的字段的值。因为该锁内部提供了一个同步方法或者同步块,该同步内容具有线程排他性,这样就使得上边两个操作只能针对单一线程在同步内容内部进行操作,这样就使得所有操作该内容的单一线程具有该同步内容(加锁的同步方法或者同步块)内的线程排他性,这种情况的交替也可以理解为具有“短暂记忆效应”。
这里需要理解的是同步的双重含义:使用锁机制允许基于高层同步协议进行处理操作,这是最基本的同步;同时系统内存(很多时候这里是指基于机器指令的底层存储关卡memory barrier,前边提到过)在处理同步的时候能够跨线程操作,使得线程和线程之间的数据是同步的。这样的机制也折射出一点,并行编程相对于顺序编程而言,更加类似于分布式编程。后一种同步可以作为JMM机制中的方法在一个线程中运行的效果展示,注意这里不是多个线程运行的效果展示,因为它反应了该线程愿意发送或者接受的双重操作,并且使得它自己的可见区域可以提供给其他线程运行或者更新,从这个角度来看,使用锁和消息传递可以视为相互之间的变量同步,因为相对其他线程而言,它的操作针对其他线程也是对等的。 -
一旦某个字段被申明为volatile,在任何一个写入线程在工作内存中刷新缓存的之前需要进行进一步的内存操作,也就是说针对这样的字段进行立即刷新,可以理解为这种volatile不会出现一般变量的缓存操作,而读取线程每次必须根据前一个线程的可见域里面重新读取该变量的值,而不是直接读取。
-
当某个线程第一次去访问某个对象的域的时候,它要么初始化该对象的值,要么从其他写入线程可见域里面去读取该对象的值;这里结合上边理解,在满足某种条件下,该线程对某对象域的值的读取是直接读取,有些时候却需要重新读取。
这里需要小心一点的是,在并发编程里面,不好的一个实践就是使用一个合法引用去引用不完全构造的对象,这种情况在从其他写入线程可见域里面进行数据读取的时候发生频率比较高。从编程角度上讲,在构造函数里面开启一个新的线程是有一定的风险的,特别是该类是属于一个可子类化的类的时候。Thread.start由调用线程启动,然后由获得该启动的线程释放锁具有相同的“短暂记忆效应”,如果一个实现了Runnable接口的超类在子类构造子执行之前调用了Thread(this).start()方法,那么就可能使得该对象在线程方法run执行之前并没有被完全初始化,这样就使得一个指向该对象的合法引用去引用了不完全构造的一个对象。同样的,如果创建一个新的线程T并且启动该线程,然后再使用线程T来创建对象X,这种情况就不能保证X对象里面所有的属性针对线程T都是可见的除非是在所有针对X对象的引用中进行同步处理,或者最好的方法是在T线程启动之前创建对象X。 -
若一个线程终止,所有的变量值都必须从工作内存中刷到主存,比如,如果一个同步线程因为另一个使用Thread.join方法的线程而终止,那么该线程的可见域针对那个线程而言其发生的改变以及产生的一些影响是需要保证可知道的。
-
从操作线程的角度看来,如果所有的指令执行都是按照普通顺序进行,那么对于一个顺序运行的程序而言,可排序性也是顺序的
-
从其他操作线程的角度看来,排序性如同在这个线程中运行在非同步方法中的一个“间谍”,所以任何事情都有可能发生。唯一有用的限制是同步方法和同步块的相对排序,就像操作volatile字段一样,总是保留下来使用
3.原始JMM缺陷:
内存模型 (memory model)
内存模型描述的是程序中各变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存取出变量这样的低层细节.
不同平台间的处理器架构将直接影响内存模型的结构.
在C或C++中, 可以利用不同操作平台下的内存模型来编写并发程序. 但是, 这带给开发人员的是, 更高的学习成本.相比之下, Java利用了自身虚拟机的优势, 使内存模型不束缚于具体的处理器架构, 通过Java内存模型真正实现了跨平台.(针对hotspot jvm, jrockit等不同的jvm, 内存模型也会不相同)
内存模型的特征:
a, Visibility 可视性 (多核,多线程间数据的共享)
b, Ordering 有序性 (对内存进行的操作应该是有序的)
Java内存模型 ( java memory model )
根据Java Language Specification中的说明, jvm系统中存在一个主内存(Main Memory或Java Heap Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。
每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。
其中, 工作内存里的变量, 在多核处理器下, 将大部分储存于处理器高速缓存中, 高速缓存在不经过内存时, 也是不可见的.
jmm怎么体现可视性(Visibility) ?
在jmm中, 通过并发线程修改变量值, 必须将线程变量同步回主存后, 其他线程才能访问到.
jmm怎么体现有序性(Ordering) ?
通过Java提供的同步机制或volatile关键字, 来保证内存的访问顺序.
缓存一致性(cache coherency)
什么是缓存一致性?
它是一种管理多处理器系统的高速缓存区结构,其可以保证数据在高速缓存区到内存的传输中不会丢失或重复。(来自wikipedia)
举例理解:
假如有一个处理器有一个更新了的变量值位于其缓存中,但还没有被写入主内存,这样别的处理器就可能会看不到这个更新的值.
解决缓存一致性的方法?
a, 顺序一致性模型:
要求某处理器对所改变的变量值立即进行传播, 并确保该值被所有处理器接受后, 才能继续执行其他指令.
b, 释放一致性模型: (类似jmm cache coherency)
允许处理器将改变的变量值延迟到释放锁时才进行传播.
Java内存模型的缓存一致性模型 - "happens-before ordering(先行发生排序)"
一般情况下的示例程序:
- x = 0;
- y = 0;
- i = 0;
- j = 0;
- // thread A
- y = 1;
- x = 1;
- // thread B
- i = x;
- j = y;
在如上程序中, 如果线程A,B在无保障情况下运行, 那么i,j各会是什么值呢?
答案是, 不确定. (00,01,10,11都有可能出现),这里没有使用Java同步机制, 所以Java内存模型有序性和可视性都无法得到保障. happens-before ordering( 先行发生排序) 如何避免这种情况? 排序原则已经做到:
a, 在程序顺序中, 线程中的每一个操作, 发生在当前操作后面将要出现的每一个操作之前.
b, 对象监视器的解锁发生在等待获取对象锁的线程之前.
c, 对volitile关键字修饰的变量写入操作, 发生在对该变量的读取之前.
d, 对一个线程的 Thread.start() 调用 发生在启动的线程中的所有操作之前.
e, 线程中的所有操作 发生在从这个线程的 Thread.join()成功返回的所有其他线程之前.
为了实现 happends-before ordering原则, Java及JDK提供的工具:
a, synchronized关键字
b, volatile关键字
c, final变量
d, java.util.concurrent.locks包(since jdk 1.5)
e, java.util.concurrent.atmoic包(since jdk 1.5)
使用了happens-before ordering的例子:
1) 获取对象监视器的锁(lock)
(2) 清空工作内存数据, 从主存复制变量到当前工作内存, 即同步数据 (read and load)
(3) 执行代码,改变共享变量值 (use and assign)
(4) 将工作内存数据刷回主存 (store and write)
(5) 释放对象监视器的锁 (unlock)
注意: 其中4,5两步是同时进行的.
这边最核心的就是第二步, 他同步了主内存,即前一个线程对变量改动的结果,可以被当前线程获知!(利用了happens-before ordering原则)
对比之前的例子
如果多个线程同时执行一段未经锁保护的代码段,很有可能某条线程已经改动了变量的值,但是其他线程却无法看到这个改动,依然在旧的变量值上进行运算,最终导致不可预料的运算结果。
了解Java的同步秘密之前,先来看看JMM(Java Memory Model)。
Java被设计为跨平台的语言,在内存管理上,显然也要有一个统一的模型。而且Java语言最大的特点就是废除了指针,把程序员从痛苦中解脱出来,不用再考虑内存使用和管理方面的问题。
可惜世事总不尽如人意,虽然JMM设计上方便了程序员,但是它增加了虚拟机的复杂程度,而且还导致某些编程技巧在Java语言中失效。
JMM主要是为了规定了线程和内存之间的一些关系。对Java程序员来说只需负责用synchronized同步关键字,其它诸如与线程/内存之间进行数 据交换/同步等繁琐工作均由虚拟机负责完成。如图1所示:根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主 存完成。
图1 Java内存模型示例图
线程若要对某变量进行操作,必须经过一系列步骤:首先从主存复制/刷新数据到工作内存,然后执行代码,进行引用/赋值操作,最后把变量内容写回Main Memory。Java语言规范(JLS)中对线程和主存互操作定义了6个行为,分别为load,save,read,write,assign和 use,这些操作行为具有原子性,且相互依赖,有明确的调用先后顺序。具体的描述请参见JLS第17章。
我们在前面的章节介绍了synchronized的作用,现在,从JMM的角度来重新审视synchronized关键字。
假设某条线程执行一个synchronized代码段,其间对某变量进行操作,JVM会依次执行如下动作:
(1) 获取同步对象monitor (lock)
(2) 从主存复制变量到当前工作内存 (read and load)
(3) 执行代码,改变共享变量值 (use and assign)
(4) 用工作内存数据刷新主存相关内容 (store and write)
(5) 释放同步对象锁 (unlock)
可见,synchronized的另外一个作用是保证主存内容和线程的工作内存中的数据的一致性。如果没有使用synchronized关键字,JVM不 保证第2步和第4步会严格按照上述次序立即执行。因为根据JLS中的规定,线程的工作内存和主存之间的数据交换是松耦合的,什么时候需要刷新工作内存或者 更新主内存内容,可以由具体的虚拟机实现自行决定。如果多个线程同时执行一段未经synchronized保护的代码段,很有可能某条线程已经改动了变量 的值,但是其他线程却无法看到这个改动,依然在旧的变量值上进行运算,最终导致不可预料的运算结果。
二、DCL失效
这一节我们要讨论的是一个让Java丢脸的话题:DCL失效。在开始讨论之前,先介绍一下LazyLoad,这种技巧很常用,就是指一个类包含某个成员变量,在类初始化的时候并不立即为该变量初始化一个实例,而是等到真正要使用到该变量的时候才初始化之。
例如下面的代码:
代码1
class Foo {
private Resource res = null ;
public Resource getResource() {
if (res == null )
res = new Resource();
return res;
}
}
由于LazyLoad可以有效的减少系统资源消耗,提高程序整体的性能,所以被广泛的使用,连Java的缺省类加载器也采用这种方法来加载Java类。
在单线程环境下,一切都相安无事,但如果把上面的代码放到多线程环境下运行,那么就可能会出现问题。假设有2条线程,同时执行到了if(res == null),那么很有可能res被初始化2次,为了避免这样的Race Condition,得用synchronized关键字把上面的方法同步起来。代码如下:
代码2
Class Foo {
Private Resource res = null ;
Public synchronized Resource getResource() {
If (res == null )
res = new Resource();
return res;
}
}
现在Race Condition解决了,一切都很好。
N天过后,好学的你偶然看了一本Refactoring的魔书,深深为之打动,准备自己尝试这重构一些以前写过的程序,于是找到了上面这段代码。你已经不 再是以前的Java菜鸟,深知synchronized过的方法在速度上要比未同步的方法慢上100倍,同时你也发现,只有第一次调用该方法的时候才需要 同步,而一旦res初始化完成,同步完全没必要。所以你很快就把代码重构成了下面的样子:
代码3
Class Foo {
Private Resource res = null ;
Public Resource getResource() {
If (res == null ){
synchronized ( this ){
if (res == null ){
res = new Resource();
}
}
}
return res;
}
}
这种看起来很完美的优化技巧就是Double-Checked Locking。但是很遗憾,根据Java的语言规范,上面的代码是不可靠的。
造成DCL失效的原因之一是编译器的优化会调整代码的次序。只要是在单个线程情况下执行结果是正确的,就可以认为编译器这样的“自作主张的调整代码次序” 的行为是合法的。JLS在某些方面的规定比较自由,就是为了让JVM有更多余地进行代码优化以提高执行效率。而现在的CPU大多使用超流水线技术来加快代 码执行速度,针对这样的CPU,编译器采取的代码优化的方法之一就是在调整某些代码的次序,尽可能保证在程序执行的时候不要让CPU的指令流水线断流,从 而提高程序的执行速度。正是这样的代码调整会导致DCL的失效。为了进一步证明这个问题,引用一下《DCL Broken Declaration》文章中的例子:
设一行Java代码:
Objects[i].reference = new Object ();
经过Symantec JIT编译器编译过以后,最终会变成如下汇编码在机器中执行:
0206106A mov eax,0F97E78h
0206106F call 01F6B210 ;为Object申请内存空间
; 返回值放在eax中
02061074 mov dword ptr [ebp],eax ; EBP 中是objects[i].reference的地址
; 将返回的空间地址放入其中
; 此时Object尚未初始化
02061077 mov ecx,dword ptr [eax] ; dereference eax所指向的内容
; 获得新创建对象的起始地址
02061079 mov dword ptr [ecx],100h ; 下面4行是内联的构造函数
0206107F mov dword ptr [ecx+4],200h
02061086 mov dword ptr [ecx+8],400h
0206108D mov dword ptr [ecx+0Ch],0F84030h
可见,Object构造函数尚未调用,但是已经能够通过objects[i].reference获得Object对象实例的引用。
如果把代码放到多线程环境下运行,某线程在执行到该行代码的时候JVM或者操作系统进行了一次线程切换,其他线程显然会发现msg对象已经不为空,导致 Lazy load的判断语句if(objects[i].reference == null)不成立。线程认为对象已经建立成功,随之可能会使用对象的成员变量或者调用该对象实例的方法,最终导致不可预测的错误。
原因之二是在共享内存的SMP机上,每个CPU有自己的Cache和寄存器,共享同一个系统内存。所以CPU可能会动态调整指令的执行次序,以更好的进行 并行运算并且把运算结果与主内存同步。这样的代码次序调整也可能导致DCL失效。回想一下前面对Java内存模型的介绍,我们这里可以把Main Memory看作系统的物理内存,把Thread Working Memory认为是CPU内部的Cache和寄存器,没有synchronized的保护,Cache和寄存器的内容就不会及时和主内存的内容同步,从而 导致一条线程无法看到另一条线程对一些变量的改动。
结合代码3来举例说明,假设Resource类的实现如下:
Class Resource{
Object obj;
}
即Resource类有一个obj成员变量引用了Object的一个实例。假设2条线程在运行,其状态用如下简化图表示:
图2
现在Thread-1构造了Resource实例,初始化过程中改动了obj的一些内容。退出同步代码段后,因为采取了同步机制,Thread-1所做的 改动都会反映到主存中。接下来Thread-2获得了新的Resource实例变量res,由于没有使用synchronized保护所以Thread- 2不会进行刷新工作内存的操作。假如之前Thread-2的工作内存中已经有了obj实例的一份拷贝,那么Thread-2在对obj执行use操作的时 候就不会去执行load操作,这样一来就无法看到Thread-1对obj的改变,这显然会导致错误的运算结果。此外,Thread-1在退出同步代码段 的时刻对ref和obj执行的写入主存的操作次序也是不确定的,所以即使Thread-2对obj执行了load操作,也有可能只读到obj的初试状态的 数据。(注:这里的load/use均指JMM定义的操作)
有很多人不死心,试图想出了很多精妙的办法来解决这个问题,但最终都失败了。事实上,无论是目前的JMM还是已经作为JSR提交的JMM模型的增强,DCL都不能正常使用。在William Pugh的论文《Fixing the java Memory Model》中详细的探讨了JMM的一些硬伤,更尝试给出一个新的内存模型,有兴趣深入研究的读者可以参见文后的参考资料。
如果你设计的对象在程序中只有一个实例,即singleton的,有一种可行的解决办法来实现其LazyLoad:就是利用类加载器的LazyLoad特性。代码如下:
Class ResSingleton {
public static Resource res = new Resource();
}
这里ResSingleton只有一个静态成员变量。当第一次使用ResSingleton.res的时候,JVM才会初始化一个Resource实例,并且JVM会保证初始化的结果及时写入主存,能让其他线程看到,这样就成功的实现了LazyLoad。
除了这个办法以外,还可以使用ThreadLocal来实现DCL的方法,但是由于ThreadLocal的实现效率比较低,所以这种解决办法会有较大的性能损失,有兴趣的读者可以参考文后的参考资料。
最后要说明的是,对于DCL是否有效,个人认为更多的是一种带有学究气的推断和讨论。而从纯理论的角度来看,存取任何可能共享的变量(对象引用)都需要同 步保护,否则都有可能出错,但是处处用synchronized又会增加死锁的发生几率,苦命的程序员怎么来解决这个矛盾呢?事实上,在很多Java开源 项目(比如Ofbiz/Jive等)的代码中都能找到使用DCL的证据,我在具体的实践中也没有碰到过因DCL而发生的程序异常。个人的偏好是:不妨先大 胆使用DCL,等出现问题再用synchronized逐步排除之。也许有人偏于保守,认为稳定压倒一切,那就不妨先用synchronized同步起 来,我想这是一个见仁见智的问题,而且得针对具体的项目具体分析后才能决定。还有一个办法就是写一个测试案例来测试一下系统是否存在DCL现象,附带的光 盘中提供了这样一个例子,感兴趣的读者可以自行编译测试。不管结果怎样,这样的讨论有助于我们更好的认识JMM,养成用多线程的思路去分析问题的习惯,提 高我们的程序设计能力。
三、Java线程同步增强包
相信你已经了解了Java用于同步的3板斧:synchronized/wait /notify,它们的确简单而有效。但是在某些情况下,我们需要更加复杂的同步工具。有些简单的同步工具类,诸如 ThreadBarrier,Semaphore,ReadWriteLock等,可以自己编程实现。现在要介绍的是牛人Doug Lea的Concurrent包。这个包专门为实现Java高级并行程序所开发,可以满足我们绝大部分的要求。更令人兴奋的是,这个包公开源代码,可自由 下载。且在JDK1.5中该包将作为SDK一部分提供给Java开发人员。
Concurrent Package提供了一系列基本的操作接口,包括sync,channel,executor,barrier,callable等。这里将对前三种接口及其部分派生类进行简单的介绍。
sync接口: 专门负责同步操作,用于替代Java提供的synchronized关键字,以实现更加灵活的代码同步。其类关系图如下:
图3 Concurrent包Sync接口类关系图
Semaphore:和前面介绍的代码类似,可用于pool类实现资源管理限制。提供了acquire()方法允许在设定时间内尝试锁定信号量,若超时则返回false。
Mutex:和Java的synchronized类似,与之不同的是,synchronized的同步段只能限制在一个方法内,而Mutex对象可以作为参数在方法间传递,所以可以把同步代码范围扩大到跨方法甚至跨对象。
NullSync:一个比较奇怪的东西,其方法的内部实现都是空的,可能是作者认为如果你在实际中发现某段代码根本可以不用同步,但是又不想过多改动这段 代码,那么就可以用NullSync来替代原来的Sync实例。此外,由于NullSync的方法都是synchronized,所以还是保留了“内存壁 垒”的特性。
ObservableSync:把sync和observer模式结合起来,当sync的方法被调用时,把消息通知给订阅者,可用于同步性能调试。
TimeoutSync:可以认为是一个adaptor,其构造函数如下:
public TimeoutSync(Sync sync, long timeout){…}
具体上锁的代码靠构造函数传入的sync实例来完成,其自身只负责监测上锁操作是否超时,可与SyncSet合用。
Channel接口: 代表一种具备同步控制能力的容器,你可以从中存放/读取对象。不同于JDK中的Collection接口,可以把Channel看作是连接对象构造者(Producer)和对象使用者(Consumer)之间的一根管道。如图所示:
图4 Concurrent包Channel接口示意图
通过和Sync接口配合,Channel提供了阻塞式的对象存取方法(put/take)以及可设置阻塞等待时间的offer/poll方法。实现 Channel接口的类有 LinkedQueue,BoundedLinkedQueue,BoundedBuffer,BoundedPriorityQueue,SynchronousChannel,Slot 等。
图5 Concurrent包Channel接口部分类关系图
使用Channel我们可以很容易的编写具备消息队列功能的代码,示例如下:
代码4
Package org.javaresearch.j2seimproved.thread;
Import EDU.oswego.cs.dl.util.concurrent.*;
public class TestChannel {
final Channel msgQ = new LinkedQueue(); //log信息队列
public static void main( String [] args) {
TestChannel tc = new TestChannel();
For( int i = 0;i < 10;i ++){
Try{
tc.serve();
Thread .sleep(1000);
} catch ( InterruptedException ie){
}
}
}
public void serve() throws InterruptedException {
String status = doService();
//把doService()返回状态放入Channel,后台logger线程自动读取之
msgQ.put(status);
}
private String doService() {
// Do service here
return "service completed OK! " ;
}
public TestChannel() { // start background thread
Runnable logger = new Runnable () {
public void run() {
try {
for (; ; )
System .out.println( "Logger: " + msgQ.take());
}
catch ( InterruptedException ie) {}
}
};
new Thread (logger).start();
}
}
Excutor/ThreadFactory接口: 把相关的线程创建/回收/维护/调度等工作封装起来,而让调用者只专心于具体任务的编码工作(即实现Runnable接口),不必显式创建Thread类实例就能异步执行任务。
使用Executor还有一个好处,就是实现线程的“轻量级”使用。前面章节曾提到,即使我们实现了Runnable接口,要真正的创建线程,还是得通过 new Thread()来完成,在这种情况下,Runnable对象(任务)和Thread对象(线程)是1对1的关系。如果任务多而简单,完全可以给每条线程 配备一个任务队列,让Runnable对象(任务)和Executor对象变成n:1的关系。使用了Executor,我们可以把上面两种线程策略都封装 到具体的Executor实现中,方便代码的实现和维护。
具体的实现有: PooledExecutor,ThreadedExecutor,QueuedExecutor,FJTaskRunnerGroup等
类关系图如下:
图6 Concurrent包Executor/ThreadFactory接口部分类关系图
下面给出一段代码,使用PooledExecutor实现一个简单的多线程服务器
代码5
package org.javaresearch.j2seimproved.thread;
import java .net.*;
import EDU.oswego.cs.dl.util.concurrent.*;
public class TestExecutor {
public static void main( String [] args) {
PooledExecutor pool =
new PooledExecutor( new BoundedBuffer(10), 20);
pool.createThreads(4);
try {
ServerSocket socket = new ServerSocket (9999);
for (; ; ) {
final Socket connection = socket.accept();
pool.execute( new Runnable () {
public void run() {
new Handler ().process(connection);
}
});
}
}
catch ( Exception e) {} // die
}
static class Handler {
void process( Socket s){
}
}
}
内存模型描述的是程序中各变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存取出变量这样的低层细节。
每一个线程有一块工作内存区,其中保留了被所有线程共享的主内存中的变量的值的拷贝。为了存取一个共享的变量,一个线程通常先获取锁定并且清除它的工作内存区,这保证该共享变量从所有线程的共享内存区正确地装入到线程的工作内存区,当线程解锁时保证该工作内存区中变量的值写回到共享内存中。
下面简单给出了规则的重要推论:
1、 适当运用同步结构,能够正确地把一个或一组值通过共享变量从一个线程传送到另一个线程。
2、 当一个线程使用一变量的值时,它获取的值其实是由它本身或其它线程在变量中存储的值。即使用程序中没有使用正确的同步也是如此。例如,如果两个线程把不同对象的引用存储到同一个共享引用变量中,那么该引用变量值将要么是这个线程、要么是那个线程所拥有的对象的引用的值,而该共享引用变量的值不可能由多个线程引用值组合而成(对于共享基本类型long、double除外)。
3、 在java应用程序中,如果没有给出明确的同步,那么可以以一个令人惊奇的自由地更新主内存内容。所以Java程序开发者如果想避免这种情况,那么应该使用明确的同步技术。
一个变量是Java程序可以存取的一个地址,它不仅包括基本类型变量、引用类型变量,而且还包括数组类型变量。保存在主内存区的变量可以被所有线程共享,但一个线程存取另一个线程的参数或局部变量是不可能的,所以我们不必担心这些变量的线程安全性问题。
每一个线程有一个工作内存区,其中保留了它必须使用或赋值的变量的一个自己的工作拷贝。当线程执行时,它在这些工作内存区的拷贝上操作。主内存中包含的是每个变量的主拷贝,当允许线程(或线程需要,如读)把它的工作拷贝中的内容写回主内存中(或者反之)时,是有一些规则对此加以限制的,这些规则在后面会提到。
一个线程可以执行的动作有使用(use)、赋值(assing)、装载(load)、存储(store)、锁定(lock)、解锁(unlock),而主内存可以执行的动作有read、write、lock、unlock,每一个这样的动作都是原子的。下面是JMM内存模式图:
使用(use)和赋值(assing)操作是线程的执行引擎(为上图中的Thread Execution Engine)和线程的工作内存(为上图中的Working Memory)之间紧密藕合(直接,即一步就可以完成的)的交互过程;锁定(lock)和解锁(unlock)操作是线程的执行引擎和主内存之间紧密藕合的交互过程;但在主内存和线程的工作内存间的数据传送是松散藕合的,当数据从主内存复制到工作存储时,必须出现两个动作:由主内存执行的读(read)动作,一段时间后是由工作内存执行的相应的load动作;当数据从工作内存拷贝到主内存时,必须出现两种动作:由工作内存执行的存储(store)动作,一段时间后是由主内存执行的相应的写(write)动作。在主内存和工作内存间传送数据需一定的传送时间,而且对每次的传送的传送时间可能是不同的:因此在另一个线程看来,线程对不同变量所执行的动作可能是按照不同的顺序(与程序代码语义顺序)执行的(比如说,线程内的程序代码是先给变量a赋值,再给变量b赋值,而在另一线程看来有可能先看见主内存中的b变量更新,再看见a变量更新),然而,任何一个线程在主内存中对一个变量的所有动作一定是按照这个线程中所有对该变量动作的相同次(也是指与程序代码语议的顺序)序执行。
每种操作的详细定义:
- 线程的use动作把一个变量的线程工作拷贝的内容传送给线程执行引擎。每当线程执行一个用到变量的值的虚拟机指令时执行这个动作。
- 线程的assign动作把一个值从线程执行引擎传送到变量的线程工作拷贝。每当线程执行一个给变量赋值的虚拟机指令时执行这个动作。
- 主内存的read动作把一个变量的主内存拷贝的内容传输到线程的工作内存以便后面的load动作使用。
- 线程的load动作把read动作从主内存中得到的值放入变量的线程工作拷贝中。
- 线程的store动作把一个变量的线程工作拷贝内容传送到主内存中以便后面的write动作使用。
- 主内存的write动作把store动作从线程工作内存中得到的值放入主内存中一个变量的主拷贝。
- 和主内存紧密同步的线程的lock动作使线程获得一个独占锁定的声明。
- 和主内存紧密同步的线程的unlock动作使线程释放一个独占锁定的声明。
这样,线程和变量的相互作用由use、assign、load和store动作的序列组成。主内存为每个load动作执行read动作,为每个Store动作执行write动作。线程的锁定的相互作用由lock或unlock动作顺序组成。
线程的每个load动作有唯一一个主内存的read动作和它相匹配,这个load动作跟在read动作的后面;线程的每个store动作有唯一一个主内存的write动作和它相匹配,这个write动作跟在store动作的后面。
变量规则:不允许一个线程丢弃它的最近的assign操作;不允许一个线程无原因地把数据从线程的工作内存写回到主内存中;一个新的变量只能在主内存中产生并且不能在任何线程的工作内存中初始化。
假设动作A是线程T对变量V执行的另外的load或store动作,假设动作P是主内存对变量V执行的相应的read或write动作。类似地,假设动作B是线程T对同一个变量V执行的另外的load或store动作,假设动作Q是主内存对变量V执行的相应的read或write动作。如果A等于B,那么必须有P先于Q。(不很严格地:为了一个线程,主内存执行对给定的一个变量的主拷贝动作必须遵循线程执行时要求的先后顺序。)注意,这条规则只适用于一个线程对于同一个变量不同动作的情况,是针对单线程提出的。然而,对于volatile 类型的变量有更严格的规则,请看后面volatile变量规则最后一条。
double和long类型变量的非原子处理:如果一个double或者long变量没有声明为volatile ,则变量的read或write动作,实际在主内存处理时是把它当作两个32位的read或write动作,这两个动作在时间上是分开的,可能会有其它的动作介于它们之间。这样的结果是,如果两个并发的线程对共享的非volatile 类型的double或long变量赋不同的值,那么随后对该变量的使用而获取的值可能不等于任何一个线程所赋的值,而可能是依赖于具体应用的两个线程所赋的值的混合。基于目前32芯片技术,在共享double和long变量时必须同步。
在一个时刻,对同一个锁,只能有一个线程拥有它,而且一个线程可以对同一个锁执行多次lock动作,只有当对这个锁执行相同次数的unlock动作后,线程才会释放该锁定。
一个线程如果没有拥有锁,那么它不允许对该锁实施unlock动作。
如果一个线程对任何一个锁定实施unlock,线程必须先把它工作内存中的赋的值写回到主内存中(即unlock动作会引发对变量的store -> write -> unlock 动作序列)。
一个lock动作发生时会清空线程工作内存中所有变量,所以在使用它们的时候必须从主内存中载入或重新赋值(即lock动作会引发对变量的lock -> read -> load 或lock -> assign -> store动作序列)。
volatile 类型变量的规则:如果一个变量声明为volatile 类型,那么每个线程对该变量实施的动作有以下附加的规则,假定T表示一个线程,V,W表示volatile 类型变量:
- 只有当线程T对变量V执行的前一个动作是load的时候,线程T才能对变量V执行use动作;并且,只有当线程T对变量V执行的后一个动作是use的时候,线程T才能对变量V执行load动作。线程T对就是V的use动作可以认为是和线程T对变量V的load动作相应的read动作相关联(这样可以保证看其他线程对变量V所做的修改后的值,即使用时先去从主内存中加载)。
- 只有当线程T对变量V执行的前一个动作是assign的时候,线程T才能对变量V执行store动作;并且,只有当线程T对变量V执行的后一个动作是store的时候,线程T才能对变量V执行assign动作。线程T对就是V的assign动作可以认为是和线程T对变量V的store动作相应的write动作相关联(这样可以保证其他线程可以看到自己对变量V所做的修改,即修改后写回主内存中)。
- 假定动作A是线程T对变量V实施的use或assign动作,假定动作F是和动作A相关联的load或store动作,假定动作P是和动作F相应的对变量V的read或write动作;类似的,假定动作B是线程T对变量W实施的use或assign动作,假定动作G是和动作B相关联的load或store动作,假定动作Q是和动作G相应的对变量W的read或write动作。如果A先于B,那么P先于Q(不严格地:为了一个线程T,主内存实施对给定的volatile变量的主拷贝的动作必须遵循和线程执行时要求的一样的先后顺序。也即将V,W变量写回到主内存的顺序与程序代码行对V,W赋值先后顺序一样;线程将V,W变量从主内存读取出来的顺序与程序代码行对V,W使用先后顺序一样。即volate禁止了变量间的重新排序问题)。该规则进一步加强了多线程访问共享变量的安全性,这条规则是针对多线程提出的。
对声明为volatile 的变量的规则有效地保证了:线程对一个声明为volatile 的变量的每个use或assign动作只要访问主内存一次,并且依照线程的执行语义所指定的次序访问主内存,然而,对没有声明为volatile 的变量的read或write动作,这样的内存动作是没有次序限制的。
volatile 的变量除了具有可见性外,还禁止了多个变量间的Reordering。
范例:可能的交换
- class Sample{
- int a=1,b=2;
- void hither(){
- a=b;
- }
- void yon(){
- b=a;
- }
- }
让我们考虑调用hither的线程,按照规则,该线程必须执行变量b的use动作,在它后面要执行变量a的assign动作,这是对hither的最低要求(即同一线程内一定是按照程序语义顺序来执行)。
现在线程对变量b的第一个动作不能为use,但是可以为assign或load。这里对b的一个assign动作不可能发生,因为这里根本就没有赋值调用,所以这里只有对变量b的load动作。而线程对这个load动作必须有一个更早的主内丰对变量b的read动作。
在对变量a进行assign动作后,线程可选地(因为没有使用同步)存储变量a的值,如果线程要存储这个值,那么线程实施store动作,并且主内存接着实施变量a的write动作。
调用方法yon的线程的情况是类似的,只是a和b交换了各自的角色。所有的动作序列可以由下面图描述,运行时可能会从这些任意的箭头中切换到另一线程执行:
假定ha和hb是调用hither的线程的变量a和b的工作拷贝,假定ya和yb是调用yon线程的变量a和b的工作拷贝,假定ma和mb是主内存中变量a和变量b的主拷贝,假定初始化ma=1,mb=2,下面是动作的可能结果:
1、 ha=2,hb=2,ya=2,yb=2,ma=2,mb=2(结果是b拷贝给了a)
2、 ha=1,hb=1,ya=1,yb=1,ma=1,mb=1(结果是a拷贝给了b)
3、 ha=2,hb=2,ya=1,yb=1,ma=2,mb=1(结果是a、b交换了)
使用以下程序进行测试:
- class Sample {
- /*
- * 不管 a,b是否使用volatile 修饰,都会出现 a、b值交换。因为a=b、b=a并不是原子性
- * 的,因为这两条语句都会涉及到使用与赋值两个动作,完全有可能在访问操作后切换到
- * 另一线程,而volatile并不像synchronized那样具有原子特性
- */
- volatile int a = 1;
- volatile int b = 2;
- void hither() {
- a = b;
- }
- synchronized void yon() {
- b = a;
- }
- }
- public class Test {
- public static void main(String[] args) throws Exception {
- while (!Thread.currentThread().isInterrupted()) {
- final Sample s = new Sample();
- final Thread hither = new Thread() {
- public void run() {
- s.hither();
- }
- };
- final Thread yon = new Thread() {
- public void run() {
- s.yon();
- }
- };
- hither.start();
- yon.start();
- new Thread() {
- public void run() {
- try {
- hither.join();
- yon.join();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- if (s.a != s.b) {
- // 某次打印结果Thread-332984: a=2 b=1
- System.out.println(this.getName() + ": a=" + s.a + " b=" + s.b);
- System.exit(0);
- }
- }
- }.start();
- Thread.yield();
- }
- }
- }
上面使用volatile 同时修改这两个变量还是不行的,除非两个方法同时(注,只一个方法使用也是不管用的)使用synchronized:
- class Sample {
- int a = 1;
- int b = 2;
- synchronized void hither() {
- a = b;
- }
- synchronized void yon() {
- b = a;
- }
- }
lock和unlock动作对主内存的动作次序提出了更多的限制。在一个线程的lock动作和unlock动作之间,另一个线程不能实 施lock动作,而且,unlock动作前需要实施store动作和write动作,下面是仅可能发现的顺序,从结果看出要么是a,要么 是b,不可能出现两都交换的情况:
1、 ha=2,hb=2,ya=2,yb=2,ma=2,mb=2(结果是b拷贝给了a)
2、 ha=1,hb=1,ya=1,yb=1,ma=1,mb=1(结果是a拷贝给了b)
范例:无序写入
下面的例子和前面的例子很相似,只是一个方法对两个变量赋值,而中一个方法读取两个变量的值:
- class Sample {
- int a = 1;
- int b = 2;
- String result;
- synchronized void to() {
- a = 3;
- b = 4;
- }
- void fro() {
- // 按理来说不可能出现 a=1,b=4
- result = "a=" + a + ",b=" + b;
- }
- }
从上图可以看出,在线程内:调用方法to的线程在方法结束而实施unlock动作前,必须实施stroe动作将所赋的值写回到主内存中。调用方法fro的线程必须同样的次序使用变量a和b(即先use a再use b),并且必须从主内存对变量a和b实施load动作以将值装入a和b。
在主内存中:动作发生的次序是这样的呢?注意规则并不要求对变量a的write动作要先于对变量b的write动作;而且也不要求对变量a的read动作要先于对变量b的read动作。甚至由于方法to是同步的,方法fro没有同步,所以不能防止在lock动作和unlock动作间发生read动作(即,声明一个方法为同步的,这种机制本身不能使方法的行为是原子的)。
上面结果输出有可能是a=1,b=4,这说明尽管一个线程对变量a的assign动作先于变量b的assign动作,在另一个线程看来,主内存实施可能是按照相反的次序实施相应的write动作,但如果是volatile变量,则会以程序语义执行的顺序写回主内存。
什么是重新排序?
在一些情况下,对程序变量(对象实例变量、静态变量、数组元素)进行访问的时候,会发现访问的执行顺序与程序中所指定的顺序并不一致。只要不改变程序的语义,编译器为了进行程序的优化可以自由地reorder指令(instructions)。处理器也可能以不同的顺序去执行指令:数据可能会以不同于程序中所指定的顺序在处理器寄存器、处理器缓存以及住内存之间移动。
在单线程的情况下,程序不必去关注指令的真实执行顺序,同时也不必在意reordering的影响。然而,在多线程的情况下,如果程序没有被正确地synchronized,线程就会受到reordering的影响,即一个线程可能会看到另一个线程对变量访问过程的次序与程序中指定的次序不同的结果。例如,如果一个线程先写入a字段,然后再写入b字段,如果b的值不依赖于a的值,则编译器可以自由地recorder这些操作,而且可以将缓存中b的值先写回到主内存中再写回a,这样另一线程会先看到b,最后该线程看到的结果与程序中指定的次序不同。
可能进行reorder的地方包括:编译器、JIT、处理器缓存。
JMM 允许编译器和缓存以数据在处理器特定的缓存(或寄存器)和主存之间移动的次序拥有重要的特权,除非程序员已经使用 synchronized 或 final 明确地请求了某些可见性保证。这意味着在缺乏同步的情况下,从不同的线程角度来看,内存的操作是以不同的次序发生的。
例子:
- Class Reordering {
- int x = 0, y = 0;
- public void writer() {
- x = 1;
- y = 2;
- }
- public void reader() {
- int r1 = y;
- int r2 = x;
- }
- }
假设有两个线程分别执行上面的示例程序代码中的writer和reader方法。在writer方法的程序中,x被指定在y之前进行赋值。但是由于对y的赋值并不依赖于x的值,因此编译器可以reorder这些操作。另外,执行writer线程的处理器也完全可以先将cache中y的值写回内存,然后再将x的值写回内存。在两个操作的中间(y的值已经被写回内存,但是x的值尚未被写回内存的时候),执行reader方法的线程得到的结果便是 r1 = 2,但是 r2 = 0,而不是x的真实值1。
Volatile关键字规则:
volatile字段被用来在线程之间communicate state(交流规则)。任意线程所read的volatile字段的值都是最新的。原因有以下有4点:
(1) 编译器和JVM会阻止将volatile字段的值放入处理器寄存器(register);
(2) 在write volatile字段之后,其值会被flush出处理器cache,写回memory;
(3) 在read volatile字段之前,会invalidate(验证)处理器cache。
因此,上述两条便保证了每次read的值都是从memory中的,即具有“可见性”这一特性。
(4) 禁止reorder(重排序,即与原程序指定的顺序不一致)任意两个volatile变量,并且同时严格限制(尽管没有禁止)reorder volatile变量周围的非volatile变量。这一点即volatile具有变量的“顺序性”,即指令不会重新排序,而是按照程序指定的顺序执行。
注:在旧的内存模型下,对volatile修改的变量的访问顺序不能进行重新排序,但可以对非volatile变量进行排序,但这样又可能还是会导致volatile变量可见性问题,所以老的旧内存模型没有从根本上解决volatile 变量的可见性问题。在新的内存模型下,仍然是不允许对volatile变量进行reorder的,不同的是再也不轻易(虽然没有完全禁止掉)允许对它周围的非volatile变量进行排序。
由于第(4)条中对volatile字段以及周围非volatile字段(或变量)reorder的限制,如下程序中,假设线程A 正在执行reader方法,同时,线程B正在执行writer方法。线程B完成对volatile字段 v 的赋值后,相应的结果被写回内存。如果此时线程 A 便得到的 v 的值正好为true,那么线程A也可以安全地引用 x 的值。然而,需要注意的是,假如v不是volatile的,那么上述结果就不一定了,因为x和v赋值的顺序可能被reorder。
- class VolatileSample1 {
- int x = 0;
- volatile boolean v = false;
- public void writer() {
- x = 42;
- v = true;
- }
- public void reader() {
- /* 由于volatile的特点,这里要想v为true,则x的肯定已经执行赋值(assign)动作
- * 且已写回(writer)主内了,所以不会出现 v=true,x=0的情形。但时,如果这里先
- * 访问的是x变量,则由于volatile不具有原子性,则还是会出现v=true,x=0的情形,
- * 具体请看后面测试
- */
- if (v == true) {
- //uses x - 确保能看见42.
- }
- }
- }
假设一个线程调用writer,一个线程调用reader。写线程将V写回到主内存中,读线程从主内存中获取v。因此,如果读的线程能够看到v的值为true,这就能确保该读线程能看到x的值为42,因为x是在v前面赋值,所以也会先写回到内存。如果v不是volatile,编译器就可能在写回到主内存时对v与x进行reorder,这样的就可能在读的线程看到v为true时,x却还是为0,因为写线程对写回主内存动作进行重新reoder过了。
下面是对volatile变量的测试:
- class VolatileSample2 {
- int x = 0;
- volatile boolean v = false;
- String result;
- public void writer() {
- x = 42;
- v = true;
- }
- public void reader() {
- /*
- * 如果是 result="x="+x+",v="+v;,则快就会出现 x=0,v=true 这样的结果,因为这
- * 条语 句完全可能先访问x后,另外线程再执行writer方法,待writer方法执行完成后
- * ,再接着访问v,此时就会出现 x=0,v=true 的结果;
- *
- * 但如果是result="v="+v+",x="+x;,则要想出现 v=true,x=0 的结果,则一定要等
- * writer方法执行完并写回到主内存后再执行reader方法,由于v声明的是volatile变
- * 量,volatile变量会禁止reorder任意两个者volatile变量,并且同时严格限制
- * reorder volatile变量周围 的非 volatile变量,所以由于x 比v前赋值,则写回主
- * 存时也会一定按照此顺序,所以当v为true时,则主存中的x肯定是42,绝不会是0(
- * 这里不要注意的是,volatile的变量也会在读取主内存时严格按照程序的顺序执行,
- * 所以这里根本不会先访问x再v的可能,如果这那样,则也会出现v=true,x=0的结果)。
- *
- * 这里只将x声明成volatile其结果也是一样的。当然如果两个都声明成volatile时,
- * 会更安全,因为这里会完全“禁止”重排,而一个的话只是“严格限制”而已,可能还是
- * 不会很安全,所以一般 两个都设置为最安全。
- *
- * 当x,v都是volatile时, result="x="+x+",v="+v;执行的结果还是有可能为
- * x=0,v=true, 因为volatile只是保证了可见性与顺序性两个特点为,但并不能保证
- * 原子性。此种情况下要得到 x=0,v=true,只需reader方法先执行,等访问x完后而v
- * 还未访问时,开始调试writer方法, 待writer整个方法执行完后并将x,v写回主内存
- * 后,再执行reader方法,继续访问v,此时的结 果就是x=0,v=true。 另外,
- * result="x="+x+",v="+v;也会严格安照程序的顺序来执行访问操作(即volatile不
- * 只是在写回内存时是按程序语义的执行顺序来执行,在读的时候也是这样 要按照程序
- * 的访问顺序来,但如果不是volatile变量时,则read动作就可能不会按照程序顺序来
- * 执行,但这好像对纯粹的访问操作没有什么影响,这好只有访问操作的不变对象一样
- * ,不会出现线程不安全的问题),即先访问x后再能访问v,但这绝不是原子性的,很
- * 有可能从他们中 间 切换到其他线程。
- * 另外,在测试的过程中发现writer方法的原子性要比reader的原子性要强,即多个访
- * 问操作在 一起不如多个赋值语句原子性强
- */
- result = "x=" + x + ",v=" + v;
- //result = "v=" + v + ",x=" + x;
- }
- }
- public class VolatileTest {
- public static void main(String[] args) {
- while (!Thread.currentThread().isInterrupted()) {
- final VolatileSample2 s = new VolatileSample2();
- final Thread w = new Thread(){
- public void run() {
- s.writer();
- }
- };
- final Thread r = new Thread(){
- public void run() {
- s.reader();
- }
- };
- r.start();
- w.start();
- new Thread(){
- public void run() {
- try {
- w.join();
- r.join();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- if (s.result.equals("x=0,v=true")) {
- System.out.println(this.getName() + " " + s.result);
- System.exit(0);
- }
- }
- }.start();
- Thread.yield();
- }
- }
- }
双重检测在新的内存模型下能很好地工作吗?
- // double-checked-locking - don't do this!
- private static Something instance = null;
- public Something getInstance() {
- if (instance == null) {
- synchronized (this) {
- if (instance == null)
- instance = new Something();//1
- }
- }
- return instance;
- }
首先,如果上面程序不加任何修改,这个在旧的或是新的内存模型下都不能正确的工作。上面程序 //1 处在多线程的情况下会有问题,如果一个线程在 //1 处已调用完构造器,但Something的实例域可能还没有被写回到内存,而在这之前会将创建好的对象(但并非初始化完全的对象,因为没有将它的实例域完全写回到主内存)赋值给了instance引用,这样另一个线程拿到的instance所指向的对象其实是不完整的,即所指向的对象的实例域还不可见,这样在使用这个instnace时就会有问题。在1.5或之后的版本中,我们可以将instance设置为volatile就可以了,这样就会确保将实例域的数据写回到主内存的动作在将实例赋值给instance引用动作之前发生(即volatile的 happens-before 规则),所以这样就确保了在使用前对象已完全初始化完成。
在新的内存模型下怎么才能使用final域正常的工作呢?
JSR 133新的目标中提出了一个初始化安全的新保障:如果一个对象被安全、适当的构造(在构造器中将当前正在构造的对象this暴露给外界是不安全的,“安全构造”技术请参考这里),这样其他线程可以在不使用同步的情况下看到该对象在构造器里设置的final域的值。
在构造期间,不要公布“this”引用,即在构造函数完成之前,使 this 引用暴露给另一个线程,这种暴露可能是显示的,也可能是隐式的。
- class FinalFieldExample {
- final int x;
- int y;
- static FinalFieldExample f;
- public FinalFieldExample() {
- x = 3;
- y = 4;
- }
- static void writer() {
- f = new FinalFieldExample();
- }
- static void reader() {
- if (f != null) {
- int i = f.x;
- int j = f.y;
- }
- }
- }
上面的类中展示了怎么使用final域。能够确保另一调用 reader 的线程它能看到f.x的值是因为f.x是final的,但不能保证它能看到f.y的值是4,因为它不是final的。如果FinalFieldExample的构造器像这样:
- public FinalFieldExample() { // bad!
- x = 3;
- y = 4;
- // bad construction - allowing this to escape
- global.obj = this;// 暴露this
- }
这样其他线程通过global.obj读x的值将不能确保是3。
上面列举的例子是final类型的基本类型变量,如果final修饰的是一个引用类型,则也会有这样的保障:在拿到final引用类型前这个引用所指向的对象的所有域将完全初始化构造完成。
-----
happens-before规则:这里
“原始 JMM 的缺点”请看这里。
JSR 133新的目标可以看这里。最主要的是第6点,它进一步说明了提高初始化安全的保障:即使在没有使用同步的情况下,也能看到由其他线程在构造器中对final域所赋的值。
旧的内存模型下,“问题1:String对象不可变对象不是不可变的”请参见这里。不过这里还是要说明一下的就是,旧的内存模型是允许的这样的,有此JVM还出现过这样的问题,不过在新的Java内存模型中是不合法,即不会再出这种问题。从1.5中的代码我们也可以看出,它在1.4源码的基本上将value[]、offset、count定义成了final,这样在新的内存模型下通过final避免了这个问题。
Java 多线程内存模型
Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果。在此之前,主流程序怨言(如C/C++等)直接使用物理硬件(或者说操作系统的内存模型),因此,会由于不同的平台上内存模型差异,导致程序在一套平台上并发完成正常,而在另一套平台上并发访问却经常出错,因此经常需要针对不同的平台来编写程序。
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。
Java内存模型规定了所有变量都存储在主内存中,每条线程都有自己的工作内存,线程的工作内存保存了被该线程使用到变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程也不能直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程,主内存,工作内存三者的交互关系如图所示。
关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作同步回主内存之类的实现细节,Java内存模型中定义了八种操作来完成:
- lock(锁定):作用于主内存的变量它拔一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内的变量,把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入):作用于工作内存的变量,把read读取操作从主内存中得到的变量值放入工作内存的变量拷贝中。
- use(使用):作用于工作内存的变量,把工作内存中一个变量的值传递给java虚拟机执行引擎,每当虚拟机遇到一个需要使用到变量值的字节码指令时将会执行该操作。
- assign(赋值):作用于工作内存变量,把一个从执行引擎接收到的变量的值赋值给工作变量,每当虚拟机遇到一个给变量赋值的字节码时将会执行该操作。
- store(存储):作用于工作内存的变量,把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
- write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量值放入主内存的变量中。
如果把一个变量从主内存复制到工作内存,那就需要顺序地执行read和load操作,如果要把变量从工作内存同步到主内存,就要顺序地执行store和write操作。注意Java内存模型只要求上述两个操作必须按照顺序执行,而没有保证必须是连续执行。也即是说read和load之间,store和write之间是可插入其他指令的,如对主内存中的变量a,b进行访问时,一种可能的顺序是read a,read b,load b,load a。除此之外,Java内存模型还规定了在执行上述八种基本操作时必须满足如下规则:
- 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取可但工作内存不接受,或者从工作内存发起了回写但竹内薰不接受的情况出现。
- 不允许一个线程丢弃它的最近的assign赋值操作,即工作内存变量值改变之后必须同步回主内存。
- 只有发生过assign赋值操作的变量才需要从工作内存同步回主内存。
- 一个新变量只能在主内存中产生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,即一个变量在进行use和store操作之前,必须先执行过assgin和load操作
- 一个变量在同一时刻只允许一条线程对其进行lock锁定操作,但是lock锁定可以被一条线程重复执行多次,多次执行lock之后,只有执行相同次数的unlock操作变量才会被解锁
- 如果对一个变量执行lock锁定操作,将会清空工作内存中该变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
- 如果一个变量事先没有被lock锁定,则不允许对这个变量进行unlock解锁操作,也不允许对一个被别的线程锁定的变量进行unlock解锁。
- 一个变量进行unlock解锁操作之前,必须先把此变量同步回主内存中(执行store和write操作)。
JVM指令集
1)操作数栈
变量到操作数栈:iload,iload_,lload,lload_,fload,fload_,dload,dload_,aload,aload_
操作数栈到变量:istore,istore_,lstore,lstore_,fstore,fstore_,dstore,dstor_,astore,astore_
常数到操作数栈:bipush,sipush,ldc,ldc_w,ldc2_w,aconst_null,iconst_ml,iconst_,lconst_,fconst_,dconst_
把数据装载到操作数栈:baload,caload,saload,iaload,laload,faload,daload,aaload
从操作数栈存存储到数组:bastore,castore,sastore,iastore,lastore,fastore,dastore,aastore
操作数栈管理:pop,pop2,dup,dup2,dup_xl,dup2_xl,dup_x2,dup2_x2,swap
2)运算与转换
加:iadd,ladd,fadd,dadd
减:is ,ls ,fs ,ds
乘:imul,lmul,fmul,dmul
除:idiv,ldiv,fdiv,ddiv
余数:irem,lrem,frem,drem
取负:ineg,lneg,fneg,dneg
移位:ishl,lshr,iushr,lshl,lshr,lushr
按位或:ior,lor
按位与:iand,land
按位异或:ixor,lxor
类型转换:i2l,i2f,i2d,l2f,l2d,f2d(放宽数值转换)
i2b,i2c,i2s,l2i,f2i,f2l,d2i,d2l,d2f(缩窄数值转换)
3)条件转移
有条件转移:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnonnull,if_icmpeq,if_icmpene,
if_icmplt,if_icmpgt,if_icmple,if_icmpge,if_acmpeq,if_acmpne,lcmp,fcmpl,fcmpg,dcmpl,dcmpg
复合条件转移:tableswitch,lookupswitch
无条件转移:goto,goto_w,jsr,jsr_w,ret
4)类与数组
创建类实便:new
创建新数组:newarray,anewarray,multianwarray
访问类的域和类实例域:getfield,putfield,getstatic,putstatic
获取数组长度:arraylength
检相类实例或数组属性:instanceof,checkcast
5)调度与返回加finally
调度对象的实便方法:invokevirt l
调用由接口实现的方法:invokeinterface
调用需要特殊处理的实例方法:invokespecial
调用命名类中的静态方法:invokestatic
方法返回:ireturn,lreturn,freturn,dreturn,areturn,return
异常:athrow
finally关键字的实现使用:jsr,jsr_w,ret
1.凡是带const的表示将什么数据压操作数栈。
如:iconst_2 将int型数据2压入到操作数栈
aconst_null 将null值压入栈。
2.bipush和sipush 表示将单字节或者短整形的常量值压入操作数栈。
3.带ldc的表示将什么类型数据从常量池中压入到操作数栈。
如:ldc_w 将int或者flat或者string类型的数据压入到操作数栈。(宽索引)
ldc2_w 将long或者double类型的数据压入到操作数栈。(宽索引)
4.凡是带load的指令表示将某类型的局部变量数据压入到操作数栈的栈顶。
如:iload 表示将int类型的局部变量压入到操作数栈的栈顶。
aload 以a开头的表示将引用类型的局部变量压入到操作数栈的栈顶。
iload_1 将局部变量数组里面下标为1的int类型的数据压入到操作数栈。
iaload 将int型数组的指定索引的值压入到操作数栈。
5.凡是带有store指令的表示将操作数栈顶的某类型的值存入指定的局部变量中。
如:istore 表示将栈顶int类型的数据存入到指定的局部变量中。
istore_3 表示将栈int类型的数据存入到局部变量数组的下标为3的元素中。
6.pop 将栈顶数据弹出。pop2将栈顶的一个long或者double数据从栈顶弹出来。
7.dup 复制栈顶的数据并将复制的值也压入到栈顶。
dup2 复制栈顶一个long或者是double的数据并将复制的值也压入到栈顶。
8.swap 将栈最顶端的两个值互换。
9.iadd 将栈顶两个int型的数据相加然后将结果再次的压入到栈顶。
isub 将栈顶两个int型的数据相减然后将结果再次的压入到栈顶。
imul 将栈顶两个int型的数据相乘然后将结果再次的压入到栈顶。
idiv 将栈顶两个int型的数据相除然后将结果再次的压入到栈顶。
irem 将栈顶两个int型的数据取模运算然后将结果再次的压入到栈顶。.
ineg 将栈顶的int数据取负将结果压入到栈顶。
iinc 将指定的int变量增加指定值(i++,i--,i+=2)
i2l 将栈顶int类型数据强制转换成long型将结果压入到栈顶。
lcmp 将栈顶两long型数据的大小进行比较,并将结果(1,0,-1)压入栈顶。
10。以if开头的指令都是跳转指令。
11。tableswitch、lookupswitch 表示用switch条件跳转。
12。ireturn 从当前方法返回int型数据。
13。getstatic 获取指定类的静态域,将将结果压入到栈顶。
putstatic 为指定的类的静态域赋值。
getfield 获取指定类的实例变量,将结果压入到栈顶。
putfield 为指定类的实例变量赋值。
invokevirtual 调用实例方法。
invokespacial 调用超类构造方法,实例初始化方法,私有方法。
invokestatic 调用静态方法。
invokeinterface 调用接口方法。
new 创建一个对象,并将其引用压入到栈顶。
newarray 创建一个原始类型的数组,并将其引用压入到栈顶。
arraylength 获得一个数组的长度,将将结果压入到栈顶。
athrow 将栈顶的异常抛出。
checkcast 检验类型转换,转换未通过,将抛出ClassCastException.
instanceof 检验对象是否是指定的类的实例,如果是将1压入栈顶,否则将0压入栈顶
monitorenter 获得对象的锁,用于同步方法或同步块
monitorexit 释放对象的锁,用于同步方法或同步块
ifnull 为null时跳转
ifnonnull 不为null时跳转
指令码 |
助记符 |
说明 |
0x00 | nop | 什么都不做 |
0x01 | aconst_null | 将null推送至栈顶 |
0x02 | iconst_m1 | 将int型-1推送至栈顶 |
0x03 | iconst_0 | 将int型0推送至栈顶 |
0x04 | iconst_1 | 将int型1推送至栈顶 |
0x05 | iconst_2 | 将int型2推送至栈顶 |
0x06 | iconst_3 | 将int型3推送至栈顶 |
0x07 | iconst_4 | 将int型4推送至栈顶 |
0x08 | iconst_5 | 将int型5推送至栈顶 |
0x09 | lconst_0 | 将long型0推送至栈顶 |
0x0a | lconst_1 | 将long型1推送至栈顶 |
0x0b | fconst_0 | 将float型0推送至栈顶 |
0x0c | fconst_1 | 将float型1推送至栈顶 |
0x0d | fconst_2 | 将float型2推送至栈顶 |
0x0e | dconst_0 | 将double型0推送至栈顶 |
0x0f | dconst_1 | 将double型1推送至栈顶 |
0x10 | bipush | 将单字节的常量值(-128~127)推送至栈顶 |
0x11 | sipush | 将一个短整型常量值(-32768~32767)推送至栈顶 |
0x12 | ldc | 将int,float或String型常量值从常量池中推送至栈顶 |
0x13 | ldc_w | 将int,float或String型常量值从常量池中推送至栈顶(宽索引) |
0x14 | ldc2_w | 将long或double型常量值从常量池中推送至栈顶(宽索引) |
0x15 | iload | 将指定的int型本地变量推送至栈顶 |
0x16 | lload | 将指定的long型本地变量推送至栈顶 |
0x17 | fload | 将指定的float型本地变量推送至栈顶 |
0x18 | dload | 将指定的double型本地变量推送至栈顶 |
0x19 | aload | 将指定的引用类型本地变量推送至栈顶 |
0x1a | iload_0 | 将第一个int型本地变量推送至栈顶 |
0x1b | iload_1 | 将第二个int型本地变量推送至栈顶 |
0x1c | iload_2 | 将第三个int型本地变量推送至栈顶 |
0x1d | iload_3 | 将第四个int型本地变量推送至栈顶 |
0x1e | lload_0 | 将第一个long型本地变量推送至栈顶 |
0x1f | lload_1 | 将第二个long型本地变量推送至栈顶 |
0x20 | lload_2 | 将第三个long型本地变量推送至栈顶 |
0x21 | lload_3 | 将第四个long型本地变量推送至栈顶 |
0x22 | fload_0 | 将第一个float型本地变量推送至栈顶 |
0x23 | fload_1 | 将第二个float型本地变量推送至栈顶 |
0x24 | fload_2 | 将第三个float型本地变量推送至栈顶 |
0x25 | fload_3 | 将第四个float型本地变量推送至栈顶 |
0x26 | dload_0 | 将第一个double型本地变量推送至栈顶 |
0x27 | dload_1 | 将第二个double型本地变量推送至栈顶 |
0x28 | dload_2 | 将第三个double型本地变量推送至栈顶 |
0x29 | dload_3 | 将第四个double型本地变量推送至栈顶 |
0x2a | aload_0 | 将第一个引用类型本地变量推送至栈顶 |
0x2b | aload_1 | 将第二个引用类型本地变量推送至栈顶 |
0x2c | aload_2 | 将第三个引用类型本地变量推送至栈顶 |
0x2d | aload_3 | 将第四个引用类型本地变量推送至栈顶 |
0x2e | iaload | 将int型数组指定索引的值推送至栈顶 |
0x2f | laload | 将long型数组指定索引的值推送至栈顶 |
0x30 | faload | 将float型数组指定索引的值推送至栈顶 |
0x31 | daload | 将double型数组指定索引的值推送至栈顶 |
0x32 | aaload | 将引用型数组指定索引的值推送至栈顶 |
0x33 | baload | 将boolean或byte型数组指定索引的值推送至栈顶 |
0x34 | caload | 将char型数组指定索引的值推送至栈顶 |
0x35 | saload | 将short型数组指定索引的值推送至栈顶 |
0x36 | istore | 将栈顶int型数值存入指定本地变量 |
0x37 | lstore | 将栈顶long型数值存入指定本地变量 |
0x38 | fstore | 将栈顶float型数值存入指定本地变量 |
0x39 | dstore | 将栈顶double型数值存入指定本地变量 |
0x3a | astore | 将栈顶引用型数值存入指定本地变量 |
0x3b | istore_0 | 将栈顶int型数值存入第一个本地变量 |
0x3c | istore_1 | 将栈顶int型数值存入第二个本地变量 |
0x3d | istore_2 | 将栈顶int型数值存入第三个本地变量 |
0x3e | istore_3 | 将栈顶int型数值存入第四个本地变量 |
0x3f | lstore_0 | 将栈顶long型数值存入第一个本地变量 |
0x40 | lstore_1 | 将栈顶long型数值存入第二个本地变量 |
0x41 | lstore_2 | 将栈顶long型数值存入第三个本地变量 |
0x42 | lstore_3 | 将栈顶long型数值存入第四个本地变量 |
0x43 | fstore_0 | 将栈顶float型数值存入第一个本地变量 |
0x44 | fstore_1 | 将栈顶float型数值存入第二个本地变量 |
0x45 | fstore_2 | 将栈顶float型数值存入第三个本地变量 |
0x46 | fstore_3 | 将栈顶float型数值存入第四个本地变量 |
0x47 | dstore_0 | 将栈顶double型数值存入第一个本地变量 |
0x48 | dstore_1 | 将栈顶double型数值存入第二个本地变量 |
0x49 | dstore_2 | 将栈顶double型数值存入第三个本地变量 |
0x4a | dstore_3 | 将栈顶double型数值存入第四个本地变量 |
0x4b | astore_0 | 将栈顶引用型数值存入第一个本地变量 |
0x4c | astore_1 | 将栈顶引用型数值存入第二个本地变量 |
0x4d | astore_2 | 将栈顶引用型数值存入第三个本地变量 |
0x4e | astore_3 | 将栈顶引用型数值存入第四个本地变量 |
0x4f | iastore | 将栈顶int型数值存入指定数组的指定索引位置 |
0x50 | lastore | 将栈顶long型数值存入指定数组的指定索引位置 |
0x51 | fastore | 将栈顶float型数值存入指定数组的指定索引位置 |
0x52 | dastore | 将栈顶double型数值存入指定数组的指定索引位置 |
0x53 | aastore | 将栈顶引用型数值存入指定数组的指定索引位置 |
0x54 | bastore | 将栈顶boolean或byte型数值存入指定数组的指定索引位置 |
0x55 | castore | 将栈顶char型数值存入指定数组的指定索引位置 |
0x56 | sastore | 将栈顶short型数值存入指定数组的指定索引位置 |
0x57 | pop | 将栈顶数值弹出(数值不能是long或double类型的) |
0x58 | pop2 | 将栈顶的一个(long或double类型的)或两个数值弹出(其它) |
0x59 | dup | 复制栈顶数值并将复制值压入栈顶 |
0x5a | dup_x1 | 复制栈顶数值并将两个复制值压入栈顶 |
0x5b | dup_x2 | 复制栈顶数值并将三个(或两个)复制值压入栈顶 |
0x5c | dup2 | 复制栈顶一个(long或double类型的)或两个(其它)数值并将复制值压入栈顶 |
0x5d | dup2_x1 | <待补充> |
0x5e | dup2_x2 | <待补充> |
0x5f | swap | 将栈最顶端的两个数值互换(数值不能是long或double类型的) |
0x60 | iadd | 将栈顶两int型数值相加并将结果压入栈顶 |
0x61 | ladd | 将栈顶两long型数值相加并将结果压入栈顶 |
0x62 | fadd | 将栈顶两float型数值相加并将结果压入栈顶 |
0x63 | dadd | 将栈顶两double型数值相加并将结果压入栈顶 |
0x64 | isub | 将栈顶两int型数值相减并将结果压入栈顶 |
0x65 | lsub | 将栈顶两long型数值相减并将结果压入栈顶 |
0x66 | fsub | 将栈顶两float型数值相减并将结果压入栈顶 |
0x67 | dsub | 将栈顶两double型数值相减并将结果压入栈顶 |
0x68 | imul | 将栈顶两int型数值相乘并将结果压入栈顶 |
0x69 | lmul | 将栈顶两long型数值相乘并将结果压入栈顶 |
0x6a | fmul | 将栈顶两float型数值相乘并将结果压入栈顶 |
0x6b | dmul | 将栈顶两double型数值相乘并将结果压入栈顶 |
0x6c | idiv | 将栈顶两int型数值相除并将结果压入栈顶 |
0x6d | ldiv | 将栈顶两long型数值相除并将结果压入栈顶 |
0x6e | fdiv | 将栈顶两float型数值相除并将结果压入栈顶 |
0x6f | ddiv | 将栈顶两double型数值相除并将结果压入栈顶 |
0x70 | irem | 将栈顶两int型数值作取模运算并将结果压入栈顶 |
0x71 | lrem | 将栈顶两long型数值作取模运算并将结果压入栈顶 |
0x72 | frem | 将栈顶两float型数值作取模运算并将结果压入栈顶 |
0x73 | drem | 将栈顶两double型数值作取模运算并将结果压入栈顶 |
0x74 | ineg | 将栈顶int型数值取负并将结果压入栈顶 |
0x75 | lneg | 将栈顶long型数值取负并将结果压入栈顶 |
0x76 | fneg | 将栈顶float型数值取负并将结果压入栈顶 |
0x77 | dneg | 将栈顶double型数值取负并将结果压入栈顶 |
0x78 | ishl | 将int型数值左移位指定位数并将结果压入栈顶 |
0x79 | lshl | 将long型数值左移位指定位数并将结果压入栈顶 |
0x7a | ishr | 将int型数值右(符号)移位指定位数并将结果压入栈顶 |
0x7b | lshr | 将long型数值右(符号)移位指定位数并将结果压入栈顶 |
0x7c | iushr | 将int型数值右(无符号)移位指定位数并将结果压入栈顶 |
0x7d | lushr | 将long型数值右(无符号)移位指定位数并将结果压入栈顶 |
0x7e | iand | 将栈顶两int型数值作“按位与”并将结果压入栈顶 |
0x7f | land | 将栈顶两long型数值作“按位与”并将结果压入栈顶 |
0x80 | ior | 将栈顶两int型数值作“按位或”并将结果压入栈顶 |
0x81 | lor | 将栈顶两long型数值作“按位或”并将结果压入栈顶 |
0x82 | ixor | 将栈顶两int型数值作“按位异或”并将结果压入栈顶 |
0x83 | lxor | 将栈顶两long型数值作“按位异或”并将结果压入栈顶 |
0x84 | iinc | 将指定int型变量增加指定值(i++,i--,i+=2) |
0x85 | i2l | 将栈顶int型数值强制转换成long型数值并将结果压入栈顶 |
0x86 | i2f | 将栈顶int型数值强制转换成float型数值并将结果压入栈顶 |
0x87 | i2d | 将栈顶int型数值强制转换成double型数值并将结果压入栈顶 |
0x88 | l2i | 将栈顶long型数值强制转换成int型数值并将结果压入栈顶 |
0x89 | l2f | 将栈顶long型数值强制转换成float型数值并将结果压入栈顶 |
0x8a | l2d | 将栈顶long型数值强制转换成double型数值并将结果压入栈顶 |
0x8b | f2i | 将栈顶float型数值强制转换成int型数值并将结果压入栈顶 |
0x8c | f2l | 将栈顶float型数值强制转换成long型数值并将结果压入栈顶 |
0x8d | f2d | 将栈顶float型数值强制转换成double型数值并将结果压入栈顶 |
0x8e | d2i | 将栈顶double型数值强制转换成int型数值并将结果压入栈顶 |
0x8f | d2l | 将栈顶double型数值强制转换成long型数值并将结果压入栈顶 |
0x90 | d2f | 将栈顶double型数值强制转换成float型数值并将结果压入栈顶 |
0x91 | i2b | 将栈顶int型数值强制转换成byte型数值并将结果压入栈顶 |
0x92 | i2c | 将栈顶int型数值强制转换成char型数值并将结果压入栈顶 |
0x93 | i2s | 将栈顶int型数值强制转换成short型数值并将结果压入栈顶 |
0x94 | lcmp | 比较栈顶两long型数值大小,并将结果(1,0,-1)压入栈顶 |
0x95 | fcmpl | 比较栈顶两float型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶 |
0x96 | fcmpg | 比较栈顶两float型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶 |
0x97 | dcmpl | 比较栈顶两double型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶 |
0x98 | dcmpg | 比较栈顶两double型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶 |
0x99 | ifeq | 当栈顶int型数值等于0时跳转 |
0x9a | ifne | 当栈顶int型数值不等于0时跳转 |
0x9b | iflt | 当栈顶int型数值小于0时跳转 |
0x9c | ifge | 当栈顶int型数值大于等于0时跳转 |
0x9d | ifgt | 当栈顶int型数值大于0时跳转 |
0x9e | ifle | 当栈顶int型数值小于等于0时跳转 |
0x9f | if_icmpeq | 比较栈顶两int型数值大小,当结果等于0时跳转 |
0xa0 | if_icmpne | 比较栈顶两int型数值大小,当结果不等于0时跳转 |
0xa1 | if_icmplt | 比较栈顶两int型数值大小,当结果小于0时跳转 |
0xa2 | if_icmpge | 比较栈顶两int型数值大小,当结果大于等于0时跳转 |
0xa3 | if_icmpgt | 比较栈顶两int型数值大小,当结果大于0时跳转 |
0xa4 | if_icmple | 比较栈顶两int型数值大小,当结果小于等于0时跳转 |
0xa5 | if_acmpeq | 比较栈顶两引用型数值,当结果相等时跳转 |
0xa6 | if_acmpne | 比较栈顶两引用型数值,当结果不相等时跳转 |
0xa7 | goto | 无条件跳转 |
0xa8 | jsr | 跳转至指定16位offset位置,并将jsr下一条指令地址压入栈顶 |
0xa9 | ret | 返回至本地变量指定的index的指令位置(一般与jsr,jsr_w联合使用) |
0xaa | tableswitch | 用于switch条件跳转,case值连续(可变长度指令) |
0xab | lookupswitch | 用于switch条件跳转,case值不连续(可变长度指令) |
0xac | ireturn | 从当前方法返回int |
0xad | lreturn | 从当前方法返回long |
0xae | freturn | 从当前方法返回float |
0xaf | dreturn | 从当前方法返回double |
0xb0 | areturn | 从当前方法返回对象引用 |
0xb1 | return | 从当前方法返回void |
0xb2 | getstatic | 获取指定类的静态域,并将其值压入栈顶 |
0xb3 | putstatic | 为指定的类的静态域赋值 |
0xb4 | getfield | 获取指定类的实例域,并将其值压入栈顶 |
0xb5 | putfield | 为指定的类的实例域赋值 |
0xb6 | invokevirtual | 调用实例方法 |
0xb7 | invokespecial | 调用超类构造方法,实例初始化方法,私有方法 |
0xb8 | invokestatic | 调用静态方法 |
0xb9 | invokeinterface | 调用接口方法 |
0xba | -- | |
0xbb | new | 创建一个对象,并将其引用值压入栈顶 |
0xbc | newarray | 创建一个指定原始类型(如int,float,char…)的数组,并将其引用值压入栈顶 |
0xbd | anewarray | 创建一个引用型(如类,接口,数组)的数组,并将其引用值压入栈顶 |
0xbe | arraylength | 获得数组的长度值并压入栈顶 |
0xbf | athrow | 将栈顶的异常抛出 |
0xc0 | checkcast | 检验类型转换,检验未通过将抛出ClassCastException |
0xc1 | instanceof | 检验对象是否是指定的类的实例,如果是将1压入栈顶,否则将0压入栈顶 |
0xc2 | monitorenter | 获得对象的锁,用于同步方法或同步块 |
0xc3 | monitorexit | 释放对象的锁,用于同步方法或同步块 |
0xc4 | wide | <待补充> |
0xc5 | multianewarray | 创建指定类型和指定维度的多维数组(执行该指令时,操作栈中必须包含各维度的长度值),并将其引用值压入栈顶 |
0xc6 | ifnull | 为null时跳转 |
0xc7 | ifnonnull | 不为null时跳转 |
0xc8 | goto_w | 无条件跳转(宽索引) |
0xc9 | jsr_w | 跳转至指定32位offset位置,并将jsr_w下一条指令地址压入栈顶 |
Java虚拟机中,数据类型可以分为两类:基本类型和引用类型。基本类型的变量保存原始值,即:他代表的值就是数值本身;而引用类型的变量保存引用值。“引用值”代表了某个对象的引用,而不是对象本身,对象本身存放在这个引用值所表示的地址的位置。
基本类型包括:byte, short, int, long, char, float, double, Boolean, returnAddress
引用类型包括:类类型,接口类型和数组。
堆与栈
堆和栈是程序运行的关键,很有必要把他们的关系说清楚。
栈是运行时的单位,而堆是存储的单位。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
在Java中一个线程就会相应有一个线程栈与之对应,这点很容易理解,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈。而堆则是所有线程共享的。栈因为是运行单位,因此里面存储的信息都是跟当前线程(或程序)相关信息的。包括局部变量、程序运行状态、方法返回值等等;而堆只负责存储对象信息。
为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?
第一,从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据。这样分开,使得处理逻辑更为清晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。
第二,堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如:共享内存),另一方面,堆中的共享常量和缓存可以被所有栈访问,节省了空间。
第三,栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力。而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分,使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可。
第四,面向对象就是堆和栈的完美结合。其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是,面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于自然方式的思考。当我们把对象拆开,你会发现,对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是运行逻辑,放在栈中。我们在编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。不得不承认,面向对象的设计,确实很美。
在Java中,Main函数就是栈的起始点,也是程序的起始点。
程序要运行总是有一个起点的。同C语言一样,java中的Main就是那个起点。无论什么java程序,找到main就找到了程序执行的入口:)
堆中存什么?栈中存什么?
堆中存的是对象。栈中存的是基本数据类型和堆中对象的引用。一个对象的大小是不可估计的,或者说是可以动态变化的,但是在栈中,一个对象只对应了一个4btye的引用(堆栈分离的好处:))。
为什么不把基本类型放堆中呢?因为其占用的空间一般是1~8个字节——需要空间比较少,而且因为是基本类型,所以不会出现动态增长的情况——长度固定,因此栈中存储就够了,如果把他存在堆中是没有什么意义的(还会浪费空间,后面说明)。可以这么说,基本类型和对象的引用都是存放在栈中,而且都是几个字节的一个数,因此在程序运行时,他们的处理方式是统一的。但是基本类型、对象引用和对象本身就有所区别了,因为一个是栈中的数据一个是堆中的数据。最常见的一个问题就是,Java中参数传递时的问题。
Java中的参数传递时传值呢?还是传引用?
要说明这个问题,先要明确两点:
1. 不要试图与C进行类比,Java中没有指针的概念
2. 程序运行永远都是在栈中进行的,因而参数传递时,只存在传递基本类型和对象引用的问题。不会直接传对象本身。
明确以上两点后。Java在方法调用传递参数时,因为没有指针,所以它都是进行传值调用(这点可以参考C的传值调用)。因此,很多书里面都说Java是进行传值调用,这点没有问题,而且也简化的C中复杂性。
但是传引用的错觉是如何造成的呢?在运行栈中,基本类型和引用的处理是一样的,都是传值,所以,如果是传引用的方法调用,也同时可以理解为“传引用值”的传值调用,即引用的处理跟基本类型是完全一样的。但是当进入被调用方法时,被传递的这个引用的值,被程序解释(或者查找)到堆中的对象,这个时候才对应到真正的对象。如果此时进行修改,修改的是引用对应的对象,而不是引用本身,即:修改的是堆中的数据。所以这个修改是可以保持的了。
对象,从某种意义上说,是由基本类型组成的。可以把一个对象看作为一棵树,对象的属性如果还是对象,则还是一颗树(即非叶子节点),基本类型则为树的叶子节点。程序参数传递时,被传递的值本身都是不能进行修改的,但是,如果这个值是一个非叶子节点(即一个对象引用),则可以修改这个节点下面的所有内容。
堆和栈中,栈是程序运行最根本的东西。程序运行可以没有堆,但是不能没有栈。而堆是为栈进行数据存储服务,说白了堆就是一块共享的内存。不过,正是因为堆和栈的分离的思想,才使得Java的垃圾回收成为可能。
Java中,栈的大小通过-Xss来设置,当栈中存储数据比较多时,需要适当调大这个值,否则会出现java.lang.StackOverflowError异常。常见的出现这个异常的是无法返回的递归,因为此时栈中保存的信息都是方法返回的记录点。
JVM是基于堆栈的虚拟机。JVM为每个新创建的线程都分配一个堆栈。也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。
我们知道,某个线程正在执行的方法称为此线程的当前方法。我们可能不知道,当前方法使用的帧称为当前帧。当线程激活一个Java方法,JVM就会在线程的Java堆栈里新压入一个帧。这个帧自然成为了当前帧。在此方法执行期间,这个帧将用来保存参数,局部变量,中间计算过程和其他数据。这个帧在这里和编译原理中的活动纪录的概念是差不多的。
从Java的这种分配机制来看,堆栈又可以这样理解:堆栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有先进后出的特性。
每一个Java应用都唯一对应一个JVM实例,每一个实例唯一对应一个堆。应用程序在运行中所创建的所有类实例或数组都放在这个堆中,并由应用所有的线程共享。跟C/C++不同,Java中分配堆内存是自动初始化的。Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配,也就是说在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。
Java栈与堆
堆:顺序随意
栈:后进先出(Last-in/First-Out).
Java的堆是一个运行时数据区,类的对象从中分配空间。这些对象通过new、newarray、anewarray和multianewarray等指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢.
栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量(int, short, long, byte, float, double, boolean, char)和对象句柄。
1. 栈(stack)与堆(heap)都是Java用来在Ram中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。
2. 栈的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。另外,栈数据可以共享,详见第3点。堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。
3. Java中的数据类型有两种。
一种是基本类型(primitive types), 共有8种,即int, short, long, byte, float, double, boolean, char(注意,并没有string的基本类型)。这种类型的定义是通过诸如int a = 3; long b = 255L;的形式来定义的,称为自动变量。值得注意的是,自动变量存的是字面值,不是类的实例,即不是类的引用,这里并没有类的存在。如int a = 3; 这里的a是一个指向int类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值固定定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于栈中。
另外,栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义:
复制内容到剪贴板代码:
int a = 3;
int b = 3;
编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。
特别注意的是,这种字面值的引用与类对象的引用不同。假定两个类对象的引用同时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个对象引用变量也即刻反映出这个变化。相反,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变的情况。如上例,我们定义完a与b的值后,再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到a=4;时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。
另一种是包装类数据,如Integer, String, Double等将相应的基本数据类型包装起来的类。这些类数据全部存在于堆中,Java用new()语句来显示地告诉编译器,在运行时才根据需要动态创建,因此比较灵活,但缺点是要占用更多的时间。 4. String是一个特殊的包装类数据。即可以用String str = new String("abc");的形式来创建,也可以用String str = "abc";的形式来创建(作为对比,在JDK 5.0之前,你从未见过Integer i = 3;的表达式,因为类与字面值是不能通用的,除了String。而在JDK 5.0中,这种表达式是可以的!因为编译器在后台进行Integer i = new Integer(3)的转换)。前者是规范的类的创建过程,即在Java中,一切都是对象,而对象是类的实例,全部通过new()的形式来创建。Java中的有些类,如DateFormat类,可以通过该类的getInstance()方法来返回一个新创建的类,似乎违反了此原则。其实不然。该类运用了单例模式来返回类的实例,只不过这个实例是在该类内部通过new()来创建的,而getInstance()向外部隐藏了此细节。那为什么在String str = "abc";中,并没有通过new()来创建实例,是不是违反了上述原则?其实没有。
5. 关于String str = "abc"的内部工作。Java内部将此语句转化为以下几个步骤:
(1)先定义一个名为str的对String类的对象引用变量:String str;
(2)在栈中查找有没有存放值为"abc"的地址,如果没有,则开辟一个存放字面值为"abc"的地址,接着创建一个新的String类的对象o,并将o的字符串值指向这个地址,而且在栈中这个地址旁边记下这个引用的对象o。如果已经有了值为"abc"的地址,则查找对象o,并返回o的地址。
(3)将str指向对象o的地址。
值得注意的是,一般String类中字符串值都是直接存值的。但像String str = "abc";这种场合下,其字符串值却是保存了一个指向存在栈中数据的引用!
为了更好地说明这个问题,我们可以通过以下的几个代码进行验证。
复制内容到剪贴板代码:
String str1 = "abc";
String str2 = "abc";
System.out.println(str1==str2); //true
注意,我们这里并不用str1.equals(str2);的方式,因为这将比较两个字符串的值是否相等。==号,根据JDK的说明,只有在两个引用都指向了同一个对象时才返回真值。而我们在这里要看的是,str1与str2是否都指向了同一个对象。
结果说明,JVM创建了两个引用str1和str2,但只创建了一个对象,而且两个引用都指向了这个对象。
我们再来更进一步,将以上代码改成:
复制内容到剪贴板代码:
String str1 = "abc";
String str2 = "abc";
str1 = "bcd";
System.out.println(str1 + "," + str2); //bcd, abc
System.out.println(str1==str2); //false
这就是说,赋值的变化导致了类对象引用的变化,str1指向了另外一个新对象!而str2仍旧指向原来的对象。上例中,当我们将str1的值改为"bcd"时,JVM发现在栈中没有存放该值的地址,便开辟了这个地址,并创建了一个新的对象,其字符串的值指向这个地址。
事实上,String类被设计成为不可改变(immutable)的类。如果你要改变其值,可以,但JVM在运行时根据新值悄悄创建了一个新对象,然后将这个对象的地址返回给原来类的引用。这个创建过程虽说是完全自动进行的,但它毕竟占用了更多的时间。在对时间要求比较敏感的环境中,会带有一定的不良影响。
再修改原来代码:
复制内容到剪贴板代码:
String str1 = "abc";
String str2 = "abc";
str1 = "bcd";
String str3 = str1;
System.out.println(str3); //bcd
String str4 = "bcd";
System.out.println(str1 == str4); //true
str3这个对象的引用直接指向str1所指向的对象(注意,str3并没有创建新对象)。当str1改完其值后,再创建一个String的引用str4,并指向因str1修改值而创建的新的对象。可以发现,这回str4也没有创建新的对象,从而再次实现栈中数据的共享。
我们再接着看以下的代码。
复制内容到剪贴板代码:
String str1 = new String("abc");
String str2 = "abc";
System.out.println(str1==str2); //false 创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。
String str1 = "abc";
String str2 = new String("abc");
System.out.println(str1==str2); //false
创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。
以上两段代码说明,只要是用new()来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即使与栈中的数据相同,也不会与栈中的数据共享。
6. 数据类型包装类的值不可修改。不仅仅是String类的值不可修改,所有的数据类型包装类都不能更改其内部的值。
7. 结论与建议:
(1)我们在使用诸如String str = "abc";的格式定义类时,总是想当然地认为,我们创建了String类的对象str。担心陷阱!对象可能并没有被创建!唯一可以肯定的是,指向String类的引用被创建了。至于这个引用到底是否指向了一个新的对象,必须根据上下文来考虑,除非你通过new()方法来显要地创建一个新的对象。因此,更为准确的说法是,我们创建了一个指向String类的对象的引用变量str,这个对象引用变量指向了某个值为"abc"的String类。清醒地认识到这一点对排除程序中难以发现的bug是很有帮助的。
(2)使用String str = "abc";的方式,可以在一定程度上提高程序的运行速度,因为JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象。而对于String str = new String("abc");的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。这个思想应该是享元模式的思想,但JDK的内部在这里实现是否应用了这个模式,不得而知。
(3)当比较包装类里面的数值是否相等时,用equals()方法;当测试两个包装类的引用是否指向同一个对象时,用==。
(4)由于String类的immutable性质,当String变量需要经常变换其值时,应该考虑使用StringBuffer类,以提高程序效率。
java中堆栈(stack)和堆(heap)【转摘】
地址:http://blog.csdn.net/jerryao/archive/2006/07/04/874101.aspx
堆栈(stack)和堆(heap)
(1)内存分配的策略
按照编译原理的观点,程序运行时的内存分配有三种策略,分别是静态的,栈式的,和堆式的.
静态存储分配是指在编译时就能确定每个数据目标在运行时刻的存储空间需求,因而在编译时就可以给他们分配固定的内存空间.这种分配策略要求程序代码中不允许有可变数据结构(比如可变数组)的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间需求.
栈式存储分配也可称为动态存储分配,是由一个类似于堆栈的运行栈来实现的.和静态存储分配相反,在栈式存储方案中,程序对数据区的需求在编译时是完全未知的,只有到运行的时候才能够知道,但是规定在运行中进入一个程序模块时,必须知道该程序模块所需的数据区大小才能够为其分配内存.和我们在数据结构所熟知的栈一样,栈式存储分配按照先进后出的原则进行分配。
静态存储分配要求在编译时能知道所有变量的存储要求,栈式存储分配要求在过程的入口处必须知道所有的存储要求,而堆式存储分配则专门负责在编译时或运行时模块入口处都无法确定存储要求的数据结构的内存分配,比如可变长度串和对象实例.堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放.
(2)堆和栈的比较
上面的定义从编译原理的教材中总结而来,除静态存储分配之外,都显得很呆板和难以理解,下面撇开静态存储分配,集中比较堆和栈:
从堆和栈的功能和作用来通俗的比较, 堆主要用来存放对象的,栈主要是用来执行程序的 .而这种不同又主要是由于堆和栈的特点决定的:
在编程中,例如C/C++中,所有的方法调用都是通过栈来进行的,所有的局部变量,形式参数都是从栈中分配内存空间的。实际上也不是什么分配,只是从栈顶向上用就行,就好像工厂中的传送带(conveyor belt)一样,Stack Pointer会自动指引你到放东西的位置,你所要做的只是把东西放下来就行.退出函数的时候,修改栈指针就可以把栈中的内容销毁.这样的模式速度最快,当然要用来运行程序了.需要注意的是,在分配的时候,比如为一个即将要调用的程序模块分配数据区时,应事先知道这个数据区的大小,也就说是虽然分配是在程序运行时进行的,但是分配的大小多少是确定的,不变的,而这个"大小多少"是在编译时确定的,不是在运行时.
堆是应用程序在运行的时候请求操作系统分配给自己内存,由于从操作系统管理的内存分配,所以在分配和销毁时都要占用时间,因此用堆的效率非常低.但是堆的优点在于,编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间,因此,用堆保存数据时会得到更大的灵活性。事实上,面向对象的多态性,堆内存分配是必不可少的,因为多态变量所需的存储空间只有在运行时创建了对象之后才能确定.在C++中,要求创建一个对象时,只需用new命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存.当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间!这也正是导致我们刚才所说的效率低的原因,看来列宁同志说的好,人的优点往往也是人的缺点,人的缺点往往也是人的优点(晕~).
(3)JVM中的堆和栈
JVM是基于堆栈的虚拟机.JVM为每个新创建的线程都分配一个堆栈.也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。
我们知道,某个线程正在执行的方法称为此线程的当前方法.我们可能不知道,当前方法使用的帧称为当前帧。当线程激活一个Java方法,JVM就会在线程的Java堆栈里新压入一个帧。这个帧自然成为了当前帧.在此方法执行期间,这个帧将用来保存参数,局部变量,中间计算过程和其他数据.这个帧在这里和编译原理中的活动纪录的概念是差不多的.
从Java的这种分配机制来看,堆栈又可以这样理解:堆栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有先进后出的特性。
每一个Java应用都唯一对应一个JVM实例,每一个实例唯一对应一个堆。应用程序在运行中所创建的所有类实例或数组都放在这个堆中,并由应用所有的线程共享.跟C/C++不同,Java中分配堆内存是自动初始化的。Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配,也就是说在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。
static、final修饰符、内部类和Java内存分配
static修饰符
static修饰符能够与属性、方法和内部类一起使用,表示静态的。类中的静态变量和静态方法能够与类名一起使用,不需要创建一个类的对象来访问该类的静态成员,所以,static修饰的变量又称作“类变量”。
static属性的内存分配
一个类中,一个static变量只会有一个内存空间,虽然有多个类实例,但这些类实例中的这个static变量会共享同一个内存空间。
static的变量是在类装载的时候就会被初始化,即,只要类被装载,不管是否使用了static变量,都会被初始化。
static的基本规则
·一个类的静态方法只能访问静态属性
·一个类的静态方法不能直接调用非静态方法
·如访问控制权限允许,static属性和方法可以使用类名加“.”的方式调用,也可以使用实例加“.”的方式调用
·静态方法中不存在当前对象,因而不能使用this,也不能使用super
·静态方法不能被非静态方法覆盖
·构造方法不允许声明为static的
注,非静态变量只限于实例,并只能通过实例引用被访问。
静态初始器——静态块
静态初始器是一个存在与类中方法外面的静态块,仅仅在类装载的时候执行一次,通常用来初始化静态的类属性。
final修饰符
在Java声明类、属性和方法时,可以使用关键字final来修饰,final所标记的成分具有终态的特征,表示最终的意思。
final的具体规则
·final标记的类不能被继承
·final标记的方法不能被子类重写
·final标记的变量(成员变量或局部变量)即成为常量,只能赋值一次
·final标记的成员变量必须在声明的同时赋值,如果在声明的时候没有赋值,那么只有一次赋值的机会,而且只能在构造方法中显式赋值,然后才能使用
·final标记的局部变量可以只声明不赋值,然后再进行一次性的赋值
·final一般用于标记那些通用性的功能、实现方式或取值不能随意被改变的成分,以避免被误用
如果将引用类型(即,任何类的类型)的变量标记为final,那么,该变量不能指向任何其它对象,但可以改变对象的内容,因为只有引用本身是final的。
内部类
在一个类(或方法、语句块)的内部定义另一个类,后者称为内部类,有时也称为嵌套类。
内部类的特点
·内部类可以体现逻辑上的从属关系,同时对于其它类可以控制内部类对外不可见等
·外部类的成员变量作用域是整个外部类,包括内部类,但外部类不能访问内部类的private成员
·逻辑上相关的类可以在一起,可以有效地实现信息隐藏
·内部类可以直接访问外部类的成员,可以用此实现多继承
·编译后,内部类也被编译为单独的类,名称为outclass$inclass的形式
内部类可以分为四种
·类级:成员式,有static修饰
·对象级:成员式,普通,无static修饰
·本地内部类:局部式
·匿名级:局部式
成员式内部类的基本规则
·可以有各种修饰符,可以用4种权限、static、final、abstract定义
·若有static限定,就为类级,否则为对象级。类级可以通过外部类直接访问,对象级需要先生成外部的对象后才能访问
·内外部类不能同名
·非静态内部类中不能声明任何static成员
·内部类可以互相调用
成员式内部类的访问
内部类访问外层类对象的成员时,语法为:
外层类名.this.属性
使用内部类时,由外部类对象加“.new”操作符调用内部类的构造方法,创建内部类的对象。
在另一个外部类中使用非静态内部类中定义的方法时,要先创建外部类的对象,再创建与外部类相关的内部类的对象,再调用内部类的方法。
static内部类相当于其外部类的static成分,它的对象与外部类对象间不存在依赖关系,因此可以直接创建。
由于内部类可以直接访问其外部类的成分,因此,当内部类与其外部类中存在同名属性或方法时,也将导致命名冲突。所以,在多层调用时要指明。
本地类是定义在代码块中的类,只在定义它们的代码块中可见。
本地类有以下几个重要特性:
·仅在定义了它们的代码块中可见
·可以使用定义它们的代码块中的任何本地final变量(注:本地类(也可以是局部内部类/匿名内部类等等)使用外部类的变量,原意是希望这个变量在本地类中的对象和在外部类中的这个变量对象是一致的,但如果这个变量不是final定义,它有可能在外部被修改,从而导致内外部类的变量对象状态不一致,因此,这类变量必须在外部类中加final前缀定义)
·本地类不可以是static的,里边也不能定义static成员
·本地类不可以用public、private、protected修饰,只能使用缺省的
·本地类可以是abstract的
匿名内部类是本地内部类的一种特殊形式,即,没有类名的内部类,而且具体的类实现会写在这个内部类里。
匿名类的规则
·匿名类没有构造方法
·匿名类不能定义静态的成员
·匿名类不能用4种权限、static、final、abstract修饰
·只可以创建一个匿名类实例
Java的内存分配
Java程序运行时的内存结构分成:方法区、栈内存、堆内存、本地方法栈几种。
方法区存放装载的类数据信息,包括:
·基本信息:每个类的全限定名、每个类的直接超类的全限定名、该类是类还是接口、该类型的访问修饰符、直接超接口的全限定名的有序列表。
·每个已装载类的详细信息:运行时常量池、字段信息、方法信息、静态变量、到类classloader的引用、到类class的引用。
栈内存
Java栈内存由局部变量区、操作数栈、帧数据区组成,以帧的形式存放本地方法的调用状态(包括方法调用的参数、局部变量、中间结果……)。
堆内存
堆内存用来存放由new创建的对象和数组。在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。
本地方法栈内存
Java通过Java本地接口JNI(Java Native Interface)来调用其它语言编写的程序,在Java里面用native修饰符来描述一个方法是本地方法。
String的内存分配
String是一个特殊的包装类数据,由于String类的值不可变性,当String变量需要经常变换其值时,应该考虑使用StringBuffer或StringBuilder类,以提高程序效率。
Java内存分配、管理小结
转自: http://legend26.blog.163.com/blog/static/13659026020101122103954365/
首先是概念层面的几个问题:
Java中运行时内存结构有哪几种?
Java中为什么要设计堆栈分离?
Java多线程中是如何实现数据共享的?
Java反射的基础是什么?
然后是运用层面:
引用类型变量和对象的区别?
什么情况下用局部变量,什么情况下用成员变量?
数组如何初始化?声明一个数组的过程中,如何分配内存?
声明基本类型数组和声明引用类型的数组,初始化时,内存分配机制有什么区?
在什么情况下,我们的方法设计为静态化,为什么
Java中运行时内存结构
1.1 方法区:
方法区是系统分配的一个内存逻辑区域,是JVM在装载类文件时,用于存储类型信息的(类的描述信息)。
方法区存放的信息包括:
1.1.1类的基本信息:
每个类的全限定名
每个类的直接超类的全限定名(可约束类型转换)
该类是类还是接口
该类型的访问修饰符
直接超接口的全限定名的有序列表
1.1.2已装载类的详细信息:
运行时常量池:
在方法区中,每个类型都对应一个常量池,存放该类型所用到的所有常量,常量池中存储了诸如文字字符串、final变量值、类名和方法名常量。它们以数组形式通过索引被访问,是外部调用与类联系及类型对象化的桥梁。(存的可能是个普通的字符串,然后经过常量池解析,则变成指向某个类的引用)
字段信息:
字段信息存放类中声明的每一个字段的信息,包括字段的名、类型、修饰符。
字段名称指的是类或接口的实例变量或类变量,字段的描述符是一个指示字段的类型的字符串,如private A a=null;则a为字段名,A为描述符,private为修饰符
方法信息:
类中声明的每一个方法的信息,包括方法名、返回值类型、参数类型、修饰符、异常、方法的字节码。
(在编译的时候,就已经将方法的局部变量、操作数栈大小等确定并存放在字节码中,在装载的时候,随着类一起装入方法区。)
在运行时,JVM从常量池中获得符号引用,然后在运行时解析成引用项的实际地址,最后通过常量池中的全限定名、方法和字段描述符,把当前类或接口中的代码与其它类或接口中的代码联系起来。
静态变量:
这个没什么好说的,就是类变量,类的所有实例都共享,我们只需知道,在方法区有个静态区,静态区专门存放静态变量和静态块。
到类classloader的引用:到该类的类装载器的引用。
到类class 的引用:虚拟机为每一个被装载的类型创建一个class 实例,用来代表这个被装载的类。
由此我们可以知道反射的基础:
在装载类的时候,加入方法区中的所有信息,最后都会形成Class类的实例,代表这个被装载的类。方法区中的所有的信息,都是可以通过这个Class类对象反射得到。我们知道对象是类的实例,类是相同结构的对象的一种抽象。同类的各个对象之间,其实是拥有相同的结构(属性),拥有相同的功能(方法),各个对象的区别只在于属性值的不同。
同样的,我们所有的类,其实都是Class类的实例,他们都拥有相同的结构-----Field数组、Method数组。而各个类中的属性都是Field属性的一个具体属性值,方法都是Method属性的一个具体属性值。
在运行时,JVM从常量池中获得符号引用,然后在运行时解析成引用项的实际地址,最后通过常量池中的全限定名、方法和字段描述符,把当前类或接口中的代码与其它类或接口中的代码联系起来。
1.2 Java栈
JVM栈是程序运行时单位,决定了程序如何执行,或者说数据如何处理。
在Java中,一个线程就会有一个线程的JVM栈与之对应,因为不过的线程执行逻辑显然不同,因此都需要一个独立的JVM栈来存放该线程的执行逻辑。
对方法的调用:
Java栈内存,以帧的形式存放本地方法的调用状态,包括方法调用的参数、局部变量、中间结果等(方法都是以方法帧的形式存放在方法区的),每调用一个方法就将对应该方法的方法帧压入Java 栈,成为当前方法帧。当调用结束(返回)时,就弹出该帧。
这意味着:
在方法中定义的一些基本类型的变量和引用变量都在方法的栈内存中分配。当在一段代码块定义一个变量时,Java 就在栈中为这个变量分配内存空间,当超过变量的作用域后(方法执行完成后),Java 会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作它用。--------同时,因为变量被释放,该变量对应的对象,也就失去了引用,也就变成了可以被gc对象回收的垃圾。
因此我们可以知道成员变量与局部变量的区别:
局部变量,在方法内部声明,当该方法运行完时,内存即被释放。
成员变量,只要该对象还在,哪怕某一个方法运行完了,还是存在。
从系统的角度来说,声明局部变量有利于内存空间的更高效利用(方法运行完即回收)。
成员变量可用于各个方法间进行数据共享。
Java 栈内存的组成:
局部变量区、操作数栈、帧数据区组成。
(1):局部变量区为一个以字为单位的数组,每个数组元素对应一个局部变量的值。调用方法时,将方法的局部变量组成一个数组,通过索引来访问。若为非静态方法,则加入一个隐含的引用参数this,该参数指向调用这个方法的对象。而静态方法则没有this参数。因此,对象无法调用静态方法。
由此,我们可以知道,方法什么时候设计为静态,什么时候为非静态?
前面已经说过,对象是类的一个实例,各个对象结构相同,只是属性不同。
而静态方法是对象无法调用的。
所以,静态方法适合那些工具类中的工具方法,这些类只是用来实现一些功能,也不需要产生对象,通过设置对象的属性来得到各个不同的个体。
(2):操作数栈也是一个数组,但是通过栈操作来访问。所谓操作数是那些被指令操作的数据。当需要对参数操作时如a=b+c,就将即将被操作的参数压栈,如将b 和c 压栈,然后由操作指令将它们弹出,并执行操作。虚拟机将操作数栈作为工作区。
(3):帧数据区处理常量池解析,异常处理等
1.3 java堆
java的堆是一个运行时的数据区,用来存储数据的单元,存放通过new关键字新建的对象和数组,对象从中分配内存。
在堆中声明的对象,是不能直接访问的,必须通过在栈中声明的指向该引用的变量来调用。引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。
由此我们可以知道,引用类型变量和对象的区别:
声明的对象是在堆内存中初始化的, 真正用来存储数据的。不能直接访问。
引用类型变量是保存在栈当中的,一个用来引用堆中对象的符号而已(指针)。
堆与栈的比较:
JAVA堆与栈都是用来存放数据的,那么他们之间到底有什么差异呢?既然栈也能存放数据,为什么还要设计堆呢?
1.从存放数据的角度:
前面我们已经说明:
栈中存放的是基本类型的变量or引用类型的变量
堆中存放的是对象or数组对象.
在栈中,引用变量的大小为32位,基本类型为1-8个字节。
但是对象的大小和数组的大小是动态的,这也决定了堆中数据的动态性,因为它是在运行时动态分配内存的,生存期也不必在编译时确定,Java 的垃圾收集器会自动收走这些不再使用的数据。
2.从数据共享的角度:
1).在单个线程类,栈中的数据可共享
例如我们定义:
Java代码
int a=3;
int b=3;
int a=3; int b=3;
编译器先处理int a = 3;首先它会在栈中创建一个变量为a 的引用,然后查找栈中是否有3 这个值,如果没找到,就将3 存放进来,然后将a 指向3。接着处理int b = 3;在创建完b 的引用变量后,因为在栈中已经有3这个值,便将b 直接指向3。这样,就出现了a 与b 同时均指向3的情况。
而如果我们定义:
Java代码
Integer a=new Integer(3);//(1)
Integer b=new Integer(3);//(2)
Integer a=new Integer(3);//(1) Integer b=new Integer(3);//(2)
这个时候执行过程为:在执行(1)时,首先在栈中创建一个变量a,然后在堆内存中实例化一个对象,并且将变量a指向这个实例化的对象。在执行(2)时,过程类似,此时,在堆内存中,会有两个Integer类型的对象。
2).在进程的各个线程之间,数据的共享通过堆来实现
例:那么,在多线程开发中,我们的数据共享又是怎么实现的呢?
如图所示,堆中的数据是所有线程栈所共享的,我们可以通过参数传递,将一个堆中的数据传入各个栈的工作内存中,从而实现多个线程间的数据共享
(多个进程间的数据共享则需要通过网络传输了。)
3.从程序设计的的角度:
从软件设计的角度看,JVM栈代表了处理逻辑,而JVM堆代表了数据。这样分开,使得处理逻辑更为清晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。
4.值传递和引用传递的真相
有了以上关于栈和堆的种种了解后,我们很容易就可以知道值传递和引用传递的真相:
1.程序运行永远都是在JVM栈中进行的,因而参数传递时,只存在传递基本类型和对象引用的问题。不会直接传对象本身。
但是传引用的错觉是如何造成的呢?
在运行JVM栈中,基本类型和引用的处理是一样的,都是传值,所以,如果是传引用的方法调用,也同时可以理解为“传引用值”的传值调用,即引用的处理跟基本类型是完全一样的。
但是当进入被调用方法时,被传递的这个引用的值,被程序解释(或者查找)到JVM堆中的对象,这个时候才对应到真正的对象。
如果此时进行修改,修改的是引用对应的对象,而不是引用本身,即:修改的是JVM堆中的数据。所以这个修改是可以保持的了。
最后:
从某种意义上来说对象都是由基本类型组成的。
可以把一个对象看作为一棵树,对象的属性如果还是对象,则还是一颗树(即非叶子节点),基本类型则为树的叶子节点。程序参数传递时,被传递的值本身都是不能进行修改的,但是,如果这个值是一个非叶子节点(即一个对象引用),则可以修改这个节点下面的所有内容。
其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别。
面向对象的引入,只是改变了我们对待问题的思考方式,而更接近于自然方式的思考。
当我们把对象拆开,其实对象的属性就是数据,存放在JVM堆中;而对象的行为(方法),就是运行逻辑,放在JVM栈中。我们在编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。
JVM执行的对象就是大家非常熟悉的class文件,我们也称为类文件,JVM规范定义的这个编译完成的代码文件(虽然并非强制要求是实际的文件)的格式非常的详实,但是我们这里只说一些宏观的内容,以后有机会再研究细节的内容吧。JVM要求的类文件的格式是和硬件和操作系统无关的一种二进制格式,它精确定义了类或者接口的表示,它甚至包含了字节顺序这样的细节,而字节顺序在特定平台的目标文件格式中一般都是固定的,不会进行说明。
JVM所支持的数据类型和Java语言规范中定义的几乎一样,请注意是几乎一样!也就是原始类型和引用类型,他们可以被存储在变量表中,也可以作为参数传递、被方法返回,更通常的就是成为操作的对象。为什么和Java语言规范中定义的不完全一样呢?因为JVM中有一种Java语言所没有的原始类型:返回地址类型(returnAddress type)。该类型是jsr, ret以及jsr_w指令需要使用到的,它的值是JVM指令的操作码的指针,并且它的值是不能被运行中的程序所修改的。
另外需要提到的就是布尔类型的值,虽然在Java语言中它是完全独立的值,但是在JVM中只提供了对它的有限支持,表现在:
没有单独的操作布尔类型的指令,源代码中的布尔类型的操作在编译以后是作为int类型的值进行操作的。
JVM直接支持布尔数组,newarray指令可以创建布尔数组,而它的访问和修改操作却是使用byte类型的数组的操作指令进行的:baload,bastore。(在JDK1.0,1,1以及1.2中,布尔数组被编码为byte数组,每个元素是8位)
JVM用1代表true,用0代表false,编译器将源代码中的布尔类型映射为JVM中的int类型,而且必须和JVM的要求一致。
另外JVM规范中对于浮点类型的数据有大段的说明,我没有怎么看,主要是讨论JVM的浮点型和IEEE 754的关系的。
关于类型的另外一个需要提一下的是类型检查。JVM期望几乎所有的类型检查已经在运行之前完成了(通常是由编译器进行检查的)而不用JVM自己来检查。原始类型的值不需要被标记或者在运行时被检查以确定他们的类型,同样他们也不用和引用类型的值进行区分,区分工作是由JVM的指令集来完成的,JVM的指令集使用不同指令来区分它要操作的值的类型,例如iadd, ladd, fadd以及dadd是用于将两个数字相加并产生数字类型结果的所有JVM指令,但是每个指令都是针对特定类型的,分别对应int, long, float以及double。
JVM包含对对象的显式支持。类是动态分配的类实例或者是一个数组,JVM中的引用类型就是对一个对象的引用,引用类型的值可以想象为对象的指针,一个对象同时可能存在多个对它的引用,对象总是通过引用被操作、传递或者测试的。
对于引用类型,需要提及的一点就是关于null,它最初是没有运行时类型的,但是它可以被转换为任何类型,而且对于null,JVM并没有要求任何具体的值与之对应。
说完上面这些,我们就开始进入我学习JVM时最想了解的部分了,大家可要打起精神哦。
JVM为运行一个程序定义了几种数据区(Data Area),包括:pc寄存器、JVM堆栈、堆、方法区(Method Area)、运行时常量池(Runtime Constant Pool)以及本机方法堆栈(Native Method Stacks),这些数据区根据其生存期可以分为两种,一种就是和JVM的生存期相同(包括堆和方法区),一种和线程的生存期相同(其它的),和JVM生存期相同的数据区在JVM启动的时候被创建并在JVM退出的时候被销毁,而和线程生存期相同的数据区是每个线程一个的,他们在线程创建的时候被创建,在线程被销毁的时候被销毁。
由于JVM可以同时支持运行多个线程,因此每个线程必然需要各自的PC(program counter)寄存器,无论从什么角度讲,每个JVM线程只能在一个时间只能执行一个方法,该方法也就是线程的当前方法,如果该方法不是本机方法,那么PC寄存器保存的就是当前指令(JVM的指令)的地址,如果是当前方法是本机方法,PC寄存器的值就没有被定义。JVM的PC寄存器的大小足够大,可以容纳一个returnAddress类型或者特定平台的本机指针。
每个JVM线程还拥有一个私有的JVM堆栈,它存储帧(下一篇文章会讲到)。JVM堆栈和像C这样的传统编程语言中的堆栈是类似的,它保存局部变量和部分结果,并且在方法调用和返回中也担任一些职责。因为除了对帧的压入和弹出操作外,对JVM堆栈不能直接进行操作,因此帧可能是在堆上分配的。如果一个线程中计算所需的JVM堆栈大于允许的大小,JVM会抛出StackOverflowError错误,如果JVM堆栈是可以动态伸缩的,如果需要扩展,但是又没有足够的内存可用或者没有足够的内存为一个新线程创建JVM堆栈,JVM会抛出OutOfMemoryError错误。
JVM只有一个为所有线程所共享的堆,所有的类实例和数组都是在堆中创建的。堆所存储的对象被一个自动存储管理系统回收(也就是我们所熟知的垃圾收集器(gc))。对象不能被显式的释放,JVM假设没有特定类型的自动存储管理系统,存储管理技术可以根据实现者的系统需求进行选择。如果计算所需的内存堆大于自动存储管理系统可以使用的大小,JVM会抛出OutOfMemoryError错误。
JVM只有一个为所有的线程所共享的方法区,方法区类似传统语言的已编译代码的存储区或者UNIX进程的“文本”段。它存储类结构,例如运行时常量池,成员和方法数据以及方法、构造方法的代码(包括用于类和实例的初始化以及接口类型初始化的特定方法(这些特定方法以后会讲到))。虽然从逻辑上讲方法区是堆的一部分,但是JVM的简单实现可以选择不对方法区进行垃圾收集或者压缩(以笔者的理解就是类不能进行卸载)。最新版本(第二版)的JVM规范没有要求方法区的位置或者管理已编译代码的策略。如果方法区的内存不能满足一个分配请求,JVM会抛出OutOfMemoryError。
运行时常量池是类文件中的常量池表的运行时表示,它包含几种常量,范围从编译时就已知的数字常量到运行时必须进行解析的方法和成员引用。运行时常量池扮演的功能类似于传统编程语言中的符号表(symbol table),但是它所包含的数据比典型的符号表更多。
每个运行时常量池时从JVM的方法区中分配的,对于特定方法或者接口的运行时常量池是JVM在创建类或者接口的时候创建的。
当创建一个类或者接口时,如果创建运行时常量池需要的内存比方法区中的可用内容更多的内存,JVM会抛出OutOfMemoryError。
关于常量池创建的更多内容以后可能会更详细的讲解。
JVM的实现可能使用传统的堆栈(更通常的讲就是C栈)以支持本机方法(不是使用JAVA语言编写的方法),本机方法堆栈也可以用于在像C语言这样的语言中为JVM指令集实现解析器,对于不能加载本机方法以及自身不依赖传统堆栈的JVM实现而言,它可以不提供本机方法堆栈,如果提供,本机方法堆栈通常在线程创建的时候为每个线程分配(以笔者的理解应该是需要使用本机方法的线程)。如果线程计算所需的内存比本机方法堆栈所允许的大,JVM会抛出StackOverflowError错误,如果本机方法堆栈可以动态伸缩,而当需要扩展的时候又没有足够的内存时,或者没有足够的内容用于创建一个本机方法堆栈,JVM会抛出OutOfMemoryError。
对于上面的这些数据区,JVM规范允许它们的大小是固定尺寸的,也可以是根据计算的需要动态伸缩的,如果是固定尺寸的,其尺寸可以在创建时自主选择。JVM的实现可以给程序员或者用户提供控制JVM堆栈的初始大小的方法,同样,在动态伸缩的情况下可以控制最大大小和最小大小,并且它们所使用的内存空间可以不是连续的。
1 JVM简介
JVM是我们Javaer的最基本功底了,刚开始学Java的时候,一般都是从“Hello World”开始的,然后会写个复杂点class,然后再找一些开源框架,比如Spring,Hibernate等等,再然后就开发企业级的应用,比如网站、企业内部应用、实时交易系统等等,直到某一天突然发现做的系统咋就这么慢呢,而且时不时还来个内存溢出什么的,今天是交易系统报了StackOverflowError,明天是网站系统报了个OutOfMemoryError,这种错误又很难重现,只有分析Javacore和dump文件,运气好点还能分析出个结果,运行遭的点,就直接去庙里烧香吧!每天接客户的电话都是战战兢兢的,生怕再出什么幺蛾子了。我想Java做的久一点的都有这样的经历,那这些问题的最终根结是在哪呢?—— JVM。
JVM全称是Java Virtual Machine,Java虚拟机,也就是在计算机上再虚拟一个计算机,这和我们使用 VMWare不一样,那个虚拟的东西你是可以看到的,这个JVM你是看不到的,它存在内存中。我们知道计算机的基本构成是:运算器、控制器、存储器、输入和输出设备,那这个JVM也是有这成套的元素,运算器是当然是交给硬件CPU还处理了,只是为了适应“一次编译,随处运行”的情况,需要做一个翻译动作,于是就用了JVM自己的命令集,这与汇编的命令集有点类似,每一种汇编命令集针对一个系列的CPU,比如8086系列的汇编也是可以用在8088上的,但是就不能跑在8051上,而JVM的命令集则是可以到处运行的,因为JVM做了翻译,根据不同的CPU,翻译成不同的机器语言。
JVM中我们最需要深入理解的就是它的存储部分,存储?硬盘?NO,NO, JVM是一个内存中的虚拟机,那它的存储就是内存了,我们写的所有类、常量、变量、方法都在内存中,这决定着我们程序运行的是否健壮、是否高效,接下来的部分就是重点介绍之。
2 JVM的组成部分
我们先把JVM这个虚拟机画出来,如下图所示:
从这个图中可以看到,JVM是运行在操作系统之上的,它与硬件没有直接的交互。我们再来看下JVM有哪些组成部分,如下图所示:
该图参考了网上广为流传的JVM构成图,大家看这个图,整个JVM分为四部分:
q Class Loader 类加载器
类加载器的作用是加载类文件到内存,比如编写一个HelloWord.java程序,然后通过javac编译成class文件,那怎么才能加载到内存中被执行呢?Class Loader承担的就是这个责任,那不可能随便建立一个.class文件就能被加载的,Class Loader加载的class文件是有格式要求,在《JVM Specification》中式这样定义Class文件的结构:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
需要详细了解的话,可以仔细阅读《JVM Specification》的第四章“The class File Format”,这里不再详细说明。
友情提示:Class Loader只管加载,只要符合文件结构就加载,至于说能不能运行,则不是它负责的,那是由Execution Engine负责的。
q Execution Engine 执行引擎
执行引擎也叫做解释器(Interpreter),负责解释命令,提交操作系统执行。
q Native Interface本地接口
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java诞生的时候是C/C++横行的时候,要想立足,必须有一个聪明的、睿智的调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies。目前该方法使用的是越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机,或者Java系统管理生产设备,在企业级应用中已经比较少见,因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍。
q Runtime data area运行数据区
运行数据区是整个JVM的重点。我们所有写的程序都被加载到这里,之后才开始运行,Java生态系统如此的繁荣,得益于该区域的优良自治,下一章节详细介绍之。
整个JVM框架由加载器加载文件,然后执行器在内存中处理数据,需要与异构系统交互是可以通过本地接口进行,瞧,一个完整的系统诞生了!
2 JVM的内存管理
所有的数据和程序都是在运行数据区存放,它包括以下几部分:
q Stack 栈
栈也叫栈内存,是Java程序的运行区,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束,该栈就Over。问题出来了:栈中存的是那些数据呢?又什么是格式呢?
栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,执行完毕后,先弹出F2栈帧,再弹出F1栈帧,遵循“先进后出”原则。
那栈帧中到底存在着什么数据呢?栈帧中主要保存3类数据:本地变量(Local Variables),包括输入参数和输出参数以及方法内的变量;栈操作(Operand Stack),记录出栈、入栈的操作;栈帧数据(Frame Data),包括类文件、方法等等。光说比较枯燥,我们画个图来理解一下Java栈,如下图所示:
图示在一个栈中有两个栈帧,栈帧2是最先被调用的方法,先入栈,然后方法2又调用了方法1,栈帧1处于栈顶的位置,栈帧2处于栈底,执行完毕后,依次弹出栈帧1和栈帧2,线程结束,栈释放。
q Heap 堆内存
一个JVM实例只存在一个堆类存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行,堆内存分为三部分:
Permanent Space 永久存储区
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。
Young Generation Space 新生区
新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分: 伊甸区(Eden space)和幸存者区(Survivor pace),所有的类都是在伊甸区被new出来的。幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再移动到养老区。
Tenure generation space养老区
养老区用于保存从新生区筛选出来的JAVA对象,一般池对象都在这个区域活跃。 三个区的示意图如下:
q Method Area 方法区
方法区是被所有线程共享,该区域保存所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。
q PC Register 程序计数器
每个线程都有一个程序计数器,就是一个指针,指向方法区中的方法字节码,由执行引擎读取下一条指令。
q Native Method Stack 本地方法栈
3 JVM相关问题
问:堆和栈有什么区别
答:堆是存放对象的,但是对象内的临时变量是存在栈内存中,如例子中的methodVar是在运行期存放到栈中的。
栈是跟随线程的,有线程就有栈,堆是跟随JVM的,有JVM就有堆内存。
问:堆内存中到底存在着什么东西?
答:对象,包括对象变量以及对象方法。
问:类变量和实例变量有什么区别?
答:静态变量是类变量,非静态变量是实例变量,直白的说,有static修饰的变量是静态变量,没有static修饰的变量是实例变量。静态变量存在方法区中,实例变量存在堆内存中。
问:我听说类变量是在JVM启动时就初始化好的,和你这说的不同呀!
答:那你是道听途说,信我的,没错。
问:Java的方法(函数)到底是传值还是传址?
答:都不是,是以传值的方式传递地址,具体的说原生数据类型传递的值,引用类型传递的地址。对于原始数据类型,JVM的处理方法是从Method Area或Heap中拷贝到Stack,然后运行frame中的方法,运行完毕后再把变量指拷贝回去。
问:为什么会产生OutOfMemory产生?
答:一句话:Heap内存中没有足够的可用内存了。这句话要好好理解,不是说Heap没有内存了,是说新申请内存的对象大于Heap空闲内存,比如现在Heap还空闲1M,但是新申请的内存需要1.1M,于是就会报OutOfMemory了,可能以后的对象申请的内存都只要0.9M,于是就只出现一次OutOfMemory,GC也正常了,看起来像偶发事件,就是这么回事。 但如果此时GC没有回收就会产生挂起情况,系统不响应了。
问:我产生的对象不多呀,为什么还会产生OutOfMemory?
答:你继承层次忒多了,Heap中 产生的对象是先产生 父类,然后才产生子类,明白不?
问:OutOfMemory错误分几种?
答:分两种,分别是“OutOfMemoryError:java heap size”和”OutOfMemoryError: PermGen space”,两种都是内存溢出,heap size是说申请不到新的内存了,这个很常见,检查应用或调整堆内存大小。
“PermGen space”是因为永久存储区满了,这个也很常见,一般在热发布的环境中出现,是因为每次发布应用系统都不重启,久而久之永久存储区中的死对象太多导致新对象无法申请内存,一般重新启动一下即可。
问:为什么会产生StackOverflowError?
答:因为一个线程把Stack内存全部耗尽了,一般是递归函数造成的。
问:一个机器上可以看多个JVM吗?JVM之间可以互访吗?
答:可以多个JVM,只要机器承受得了。JVM之间是不可以互访,你不能在A-JVM中访问B-JVM的Heap内存,这是不可能的。在以前老版本的JVM中,会出现A-JVM Crack后影响到B-JVM,现在版本非常少见。
问:为什么Java要采用垃圾回收机制,而不采用C/C++的显式内存管理?
答:为了简单,内存管理不是每个程序员都能折腾好的。
问:为什么你没有详细介绍垃圾回收机制?
答:垃圾回收机制每个JVM都不同,JVM Specification只是定义了要自动释放内存,也就是说它只定义了垃圾回收的抽象方法,具体怎么实现各个厂商都不同,算法各异,这东西实在没必要深入。
问:JVM中到底哪些区域是共享的?哪些是私有的?
答:Heap和Method Area是共享的,其他都是私有的,
问:什么是JIT,你怎么没说?
答:JIT是指Just In Time,有的文档把JIT作为JVM的一个部件来介绍,有的是作为执行引擎的一部分来介绍,这都能理解。Java刚诞生的时候是一个解释性语言,别嘘,即使编译成了字节码(byte code)也是针对JVM的,它需要再次翻译成原生代码(native code)才能被机器执行,于是效率的担忧就提出来了。Sun为了解决该问题提出了一套新的机制,好,你想编译成原生代码,没问题,我在JVM上提供一个工具,把字节码编译成原生码,下次你来访问的时候直接访问原生码就成了,于是JIT就诞生了,就这么回事。
问:JVM还有哪些部分是你没有提到的?
答:JVM是一个异常复杂的东西,写一本砖头书都不为过,还有几个要说明的:
常量池(constant pool):按照顺序存放程序中的常量,并且进行索引编号的区域。比如int i =100,这个100就放在常量池中。
安全管理器(Security Manager):提供Java运行期的安全控制,防止恶意攻击,比如指定读取文件,写入文件权限,网络访问,创建进程等等,Class Loader在Security Manager认证通过后才能加载class文件的。
方法索引表(Methods table),记录的是每个method的地址信息,Stack和Heap中的地址指针其实是指向Methods table地址。
问:为什么不建议在程序中显式的生命System.gc()?
答:因为显式声明是做堆内存全扫描,也就是Full GC,是需要停止所有的活动的(Stop The World Collection),你的应用能承受这个吗?
问:JVM有哪些调整参数?
答:非常多,自己去找,堆内存、栈内存的大小都可以定义,甚至是堆内存的三个部分、新生代的各个比例都能调整。
在JVM中,静态属性保存在Stack指令内存区,动态属性保存在Heap数据内存区。本文将从JVM的角度来讲解Java虚拟机的这一机制。
在JVM中,内存分为两个部分,Stack(栈)和Heap(堆),这里,我们从JVM的内存管理原理的角度来认识Stack和Heap,并通过这些原理认清Java中静态方法和静态属性的问题。
一般,JVM的内存分为两部分:Stack和Heap。
Stack(栈)是JVM的内存指令区。Stack管理很简单,push一定长度字节的数据或者指令,Stack指针压栈相应的字节位移;pop一定字节长度数据或者指令,Stack指针弹栈。Stack的速度很快,管理很简单,并且每次操作的数据或者指令字节长度是已知的。所以Java 基本数据类型,Java 指令代码,常量都保存在Stack中。
Heap(堆)是JVM的内存数据区。Heap 的管理很复杂,每次分配不定长的内存空间,专门用来保存对象的实例。在Heap 中分配一定的内存来保存对象实例,实际上也只是保存对象实例的属性值,属性的类型和对象本身的类型标记等,并不保存对象的方法(方法是指令,保存在Stack中),在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。而对象实例在Heap 中分配好以后,需要在Stack中保存一个4字节的Heap 内存地址,用来定位该对象实例在Heap 中的位置,便于找到该对象实例。
由于Stack的内存管理是顺序分配的,而且定长,不存在内存回收问题;而Heap 则是随机分配内存,不定长度,存在内存分配和回收的问题;因此在JVM中另有一个GC进程,定期扫描Heap ,它根据Stack中保存的4字节对象地址扫描Heap ,定位Heap 中这些对象,进行一些优化(例如合并空闲内存块什么的),并且假设Heap 中没有扫描到的区域都是空闲的,统统refresh(实际上是把Stack中丢失了对象地址的无用对象清除了),这就是垃圾收集的过程。
JVM的体系结构
我们首先要搞清楚的是什么是数据以及什么是指令。然后要搞清楚对象的方法和对象的属性分别保存在哪里。
1)方法本身是指令的操作码部分,保存在Stack中;
2)方法内部变量作为指令的操作数部分,跟在指令的操作码之后,保存在Stack中(实际上是简单类型保存在Stack中,对象类型在Stack中保存地址,在Heap 中保存值);上述的指令操作码和指令操作数构成了完整的Java 指令。
3)对象实例包括其属性值作为数据,保存在数据区Heap 中。
非静态的对象属性作为对象实例的一部分保存在Heap 中,而对象实例必须通过Stack中保存的地址指针才能访问到。因此能否访问到对象实例以及它的非静态属性值完全取决于能否获得对象实例在Stack中的地址指针。
非静态方法和静态方法的区别:
非静态方法有一个和静态方法很重大的不同:非静态方法有一个隐含的传入参数,该参数是JVM给它的,和我们怎么写代码无关,这个隐含的参数就是对象实例在Stack中的地址指针。因此非静态方法(在Stack中的指令代码)总是可以找到自己的专用数据(在Heap 中的对象属性值)。当然非静态方法也必须获得该隐含参数,因此非静态方法在调用前,必须先new一个对象实例,获得Stack中的地址指针,否则JVM将无法将隐含参数传给非静态方法。
静态方法无此隐含参数,因此也不需要new对象,只要class文件被ClassLoader load进入JVM的Stack,该静态方法即可被调用。当然此时静态方法是存取不到Heap 中的对象属性的。
总结一下该过程:当一个class文件被ClassLoader load进入JVM后,方法指令保存在Stack中,此时Heap 区没有数据。然后程序技术器开始执行指令,如果是静态方法,直接依次执行指令代码,当然此时指令代码是不能访问Heap 数据区的;如果是非静态方法,由于隐含参数没有值,会报错。因此在非静态方法执行前,要先new对象,在Heap 中分配数据,并把Stack中的地址指针交给非静态方法,这样程序技术器依次执行指令,而指令代码此时能够访问到Heap 数据区了。
静态属性和动态属性:
前面提到对象实例以及动态属性都是保存在Heap 中的,而Heap 必须通过Stack中的地址指针才能够被指令(类的方法)访问到。因此可以推断出:静态属性是保存在Stack中的,而不同于动态属性保存在Heap 中。正因为都是在Stack中,而Stack中指令和数据都是定长的,因此很容易算出偏移量,也因此不管什么指令(类的方法),都可以访问到类的静态属性。也正因为静态属性被保存在Stack中,所以具有了全局属性。
在JVM中,静态属性保存在Stack指令内存区,动态属性保存在Heap数据内存区。
注1:在JVM加载方法f()所属的类的时候,会在内存的方法区中开辟一个空间来存放常量池的信息。这里有一个隐藏的步骤:对于String字面值,JVM会在堆中分配一个叫做inner String的对象(相同的字面值只有唯一的一个Inner String)。然后在常量池中存放指向这个inner String的引用地址。这个过程是常量池解析的一部分。因此String s = new String("hello");实际上是有两个字符串对象在堆中存在的。
最后,当f()方法结束之后,栈中f()所对应的栈帧内所有的指令处理的中间数据全部执行完毕,这是栈帧会被回收,对象引用s的内存区域将不会存在,而堆中的s对象空间仍然会存在,当然,如果s对象以后再也不可能在其他地方引用到的话,JVM的垃圾回收机制会在某一个不确定的时候回收这一部分的对象空间。
注2:栈 Java虚拟机的栈有三个区域:局部变量区、运行环境区、操作数区。
(1)局部变量区 每个Java方法使用一个固定大小的局部变量集。它们按照与vars寄存器的字偏移量来寻址。局部变量都是32位的。长整数和双精度浮点数占据了两个局部变量的空间,却按照第一个局部变量的索引来寻址。(例如,一个具有索引n的局部变量,如果是一个双精度浮点数,那么它实际占据了索引n和n+1所代表的存储空间。)虚拟机规范并不要求在局部变量中的64位的值是64位对齐的。虚拟机提供了把局部变量中的值装载到操作数栈的指令,也提供了把操作数栈中的值写入局部变量的指令。
(2)运行环境区 在运行环境中包含的信息用于动态链接,正常的方法返回以及异常传播。
◆动态链接
运行环境包括对指向当前类和当前方法的解释器符号表的指针,用于支持方法代码的动态链接。方法的class文件代码在引用要调用的方法和要访问的变量时使用符号。动态链接把符号形式的方法调用翻译成实际方法调用,装载必要的类以解释还没有定义的符号,并把变量访问翻译成与这些变量运行时的存储结构相应的偏移地址。动态链接方法和变量使得方法中使用的其它类的变化不会影响到本程序的代码。
◆正常的方法返回
如果当前方法正常地结束了,在执行了一条具有正确类型的返回指令时,调用的方法会得到一个返回值。执行环境在正常返回的情况下用于恢复调用者的寄存器,并把调用者的程序计数器增加一个恰当的数值,以跳过已执行过的方法调用指令,然后在调用者的执行环境中继续执行下去。
◆异常和错误传播
异常情况在Java中被称作Error(错误)或Exception(异常),是Throwable类的子类,在程序中的原因是:①动态链接错,如无法找到所需的class文件。②运行时错,如对一个空指针的引用
(3)操作数栈区 机器指令只从操作数栈中取操作数,对它们进行操作,并把结果返回到栈中。选择栈结构的原因是:在只有少量寄存器或非通用寄存器的机器(如Intel486)上,也能够高效地模拟虚拟机的行为。操作数栈是32位的。它用于给方法传递参数,并从方法接收结果,也用于支持操作的参数,并保存操作的结果。例如,iadd指令将两个整数相加。相加的两个整数应该是操作数栈顶的两个字。这两个字是由先前的指令压进堆栈的。这两个整数将从堆栈弹出、相加,并把结果压回到操作数栈中。
每个原始数据类型都有专门的指令对它们进行必须的操作。每个操作数在栈中需要一个存储位置,除了long和double型,它们需要两个位置。操作数只能被适用于其类型的操作符所操作。例如,压入两个int类型的数,如果把它们当作是一个long类型的数则是非法的。在Sun的虚拟机实现中,这个限制由字节码验证器强制实行。但是,有少数操作(操作符dupe和swap),用于对运行时数据区进行操作时是不考虑类型的。
在windows中使用taskmanager查看java进程使用的内存时,发现有时候会超过 -Xmx制定的内存大小, -Xmx指定的是java heap,java还要分配内存做其他的事情,包括为每个线程建立栈。
VM的每个线程都有自己的栈空间,栈空间的大小限制vm的线程数量,太大了,实用的线程数减少,太小容易抛出java.lang.StackOverflowError异常。windows默认为1M,linux必须运行ulimit -s 2048。
在C语言里堆(heap)和栈(stack)里的区别
简单的可以理解为:
heap:是由malloc之类函数分配的空间所在地。地址是由低向高增长的。
stack:是自动分配变量,以及函数调用的时候所使用的一些空间。地址是由高向低减少。
一个由c/C++编译的程序占用的内存分为以下几个部分
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、在Java语言里堆(heap)和栈(stack)里的区别
1. 栈(stack)与堆(heap)都是Java用来在Ram中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。
2. 栈的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。另外,栈数据可以共享,详见第3点。堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。
3. Java中的数据类型有两种。
一种是基本类型(primitive types), 共有8种,即int, short, long, byte, float, double, boolean, char(注意,并没有string的基本类型)。这种类型的定义是通过诸如int a = 3; long b = 255L;的形式来定义的,称为自动变量。值得注意的是,自动变量存的是字面值,不是类的实例,即不是类的引用,这里并没有类的存在。如int a = 3; 这里的a是一个指向int类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值固定定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于栈中。
另外,栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义
int a = 3;
int b = 3;
编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。
特别注意的是,这种字面值的引用与类对象的引用不同。假定两个类对象的引用同时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个对象引用变量也即刻反映出这个变化。相反,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变的情况。如上例,我们定义完a与 b的值后,再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到a=4;时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。
另一种是包装类数据,如Integer, String, Double等将相应的基本数据类型包装起来的类。这些类数据全部存在于堆中,Java用new()语句来显示地告诉编译器,在运行时才根据需要动态创建,因此比较灵活,但缺点是要占用更多的时间。
4.每个JVM的线程都有自己的私有的栈空间,随线程创建而创建,java的stack存放的是frames ,java的stack和c的不同,只是存放本地变量,返回值和调用方法,不允许直接push和pop frames ,因为frames 可能是有heap分配的,所以j为ava的stack分配的内存不需要是连续的。java的heap是所有线程共享的,堆存放所有 runtime data ,里面是所有的对象实例和数组,heap是JVM启动时创建。
5. String是一个特殊的包装类数据。即可以用String str = new String("abc");的形式来创建,也可以用String str = "abc";的形式来创建(作为对比,在JDK 5.0之前,你从未见过Integer i = 3;的表达式,因为类与字面值是不能通用的,除了String。而在JDK 5.0中,这种表达式是可以的!因为编译器在后台进行Integer i = new Integer(3)的转换)。前者是规范的类的创建过程,即在Java中,一切都是对象,而对象是类的实例,全部通过new()的形式来创建。Java 中的有些类,如DateFormat类,可以通过该类的getInstance()方法来返回一个新创建的类,似乎违反了此原则。其实不然。该类运用了单例模式来返回类的实例,只不过这个实例是在该类内部通过new()来创建的,而getInstance()向外部隐藏了此细节。那为什么在String str = "abc";中,并没有通过new()来创建实例,是不是违反了上述原则?其实没有。
5. 关于String str = "abc"的内部工作。Java内部将此语句转化为以下几个步骤:
(1)先定义一个名为str的对String类的对象引用变量:String str;
(2)在栈中查找有没有存放值为"abc"的地址,如果没有,则开辟一个存放字面值为"abc"的地址,接着创建一个新的String类的对象o,并将o 的字符串值指向这个地址,而且在栈中这个地址旁边记下这个引用的对象o。如果已经有了值为"abc"的地址,则查找对象o,并返回o的地址。
(3)将str指向对象o的地址。
值得注意的是,一般String类中字符串值都是直接存值的。但像String str = "abc";这种场合下,其字符串值却是保存了一个指向存在栈中数据的引用!
为了更好地说明这个问题,我们可以通过以下的几个代码进行验证。
String str1 = "abc";
String str2 = "abc";
System.out.println(str1==str2); //true
注意,我们这里并不用str1.equals(str2);的方式,因为这将比较两个字符串的值是否相等。==号,根据JDK的说明,只有在两个引用都指向了同一个对象时才返回真值。而我们在这里要看的是,str1与str2是否都指向了同一个对象。
结果说明,JVM创建了两个引用str1和str2,但只创建了一个对象,而且两个引用都指向了这个对象。
我们再来更进一步,将以上代码改成:
String str1 = "abc";
String str2 = "abc";
str1 = "bcd";
System.out.println(str1 + "," + str2); //bcd, abc
System.out.println(str1==str2); //false
这就是说,赋值的变化导致了类对象引用的变化,str1指向了另外一个新对象!而str2仍旧指向原来的对象。上例中,当我们将str1的值改为"bcd"时,JVM发现在栈中没有存放该值的地址,便开辟了这个地址,并创建了一个新的对象,其字符串的值指向这个地址。
事实上,String类被设计成为不可改变(immutable)的类。如果你要改变其值,可以,但JVM在运行时根据新值悄悄创建了一个新对象,然后将这个对象的地址返回给原来类的引用。这个创建过程虽说是完全自动进行的,但它毕竟占用了更多的时间。在对时间要求比较敏感的环境中,会带有一定的不良影响。
再修改原来代码:
String str1 = "abc";
String str2 = "abc";
str1 = "bcd";
String str3 = str1;
System.out.println(str3); //bcd
String str4 = "bcd";
System.out.println(str1 == str4); //true
str3 这个对象的引用直接指向str1所指向的对象(注意,str3并没有创建新对象)。当str1改完其值后,再创建一个String的引用str4,并指向因str1修改值而创建的新的对象。可以发现,这回str4也没有创建新的对象,从而再次实现栈中数据的共享。
我们再接着看以下的代码。
String str1 = new String("abc");
String str2 = "abc";
System.out.println(str1==str2); //false
创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。
String str1 = "abc";
String str2 = new String("abc");
System.out.println(str1==str2); //false
创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。
以上两段代码说明,只要是用new()来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即使与栈中的数据相同,也不会与栈中的数据共享。
6. 数据类型包装类的值不可修改。不仅仅是String类的值不可修改,所有的数据类型包装类都不能更改其内部的值。
7. 结论与建议:
(1)我们在使用诸如String str = "abc";的格式定义类时,总是想当然地认为,我们创建了String类的对象str。担心陷阱!对象可能并没有被创建!唯一可以肯定的是,指向 String类的引用被创建了。至于这个引用到底是否指向了一个新的对象,必须根据上下文来考虑,除非你通过new()方法来显要地创建一个新的对象。因此,更为准确的说法是,我们创建了一个指向String类的对象的引用变量str,这个对象引用变量指向了某个值为"abc"的String类。清醒地认识到这一点对排除程序中难以发现的bug是很有帮助的。
(2)使用String str = "abc";的方式,可以在一定程度上提高程序的运行速度,因为JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象。而对于String str = new String("abc");的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。这个思想应该是享元模式的思想,但JDK的内部在这里实现是否应用了这个模式,不得而知。
(3)当比较包装类里面的数值是否相等时,用equals()方法;当测试两个包装类的引用是否指向同一个对象时,用==。
(4)由于String类的immutable性质,当String变量需要经常变换其值时,应该考虑使用StringBuffer类,以提高程序效率。
如果java不能成功分配heap的空间,将抛出OutOfMemoryError.
JVM体系结构
JVM体系结构博客:http://elihe2011.iteye.com/blog/1881787
JVM体系结构博客:http://weich-javadeveloper.iteye.com/blog/548247
JVM体系结构博客:http://scau-fly.iteye.com/blog/1946578
JVM体系结构博客:http://dylanxu.iteye.com/blog/1340118
JVM体系结构博客:http://liudeh-009.iteye.com/blog/1841355
JVM体系结构博客:http://hxraid.iteye.com/blog/676235
一.运行时数据区域
1.程序计数器
2.Java虚拟机栈
3.本地方法栈
4.Java堆
5.方法区
6.运行时常量
7.直接内存
二.OutOfMemoryError异常
1.Java堆溢出
2.虚拟机栈和本地方法栈溢出
3.运行时常量池溢出
4.方法区溢出
5.本地直接内存溢出
三.垃圾收集算法
1.标记-清除算法
2.复制算法
3.标记-整理算法
4.分代收集算法
四.垃圾收集器
1.Serial收集器
2.ParNew收集器
3.Parallel Scavenge收集器
4.Serial Old收集器
5.Parallel Old收集器
6.CMS收集器
7.G1收集器
五.内存分配与回收策略
1.对象优先在Eden分配
2.大对象直接进入老年代
3.长期存活的对象将进入老年代
4.动态对象年龄判定
5.空间分配担保
六.JDK命令行工具
1.jps:虚拟机进程状态工具
2.jstat:虚拟接统计信息监控工具
3.jinfo:Java配置信息工具
4.jmap:Java内存映像工具
5.jhat:虚拟接堆转换存储快照分析工具
6.jstack:Java堆栈跟踪工具
7.hsdis:JIT生成代码反汇编
七.JDK的可视化工具
1.JConsole:Java监视与管理控制台
2.Visual VM:多合一故障处理工具
八.Class类文件的结构
1.魔术与Class文件的版本
2.常量池
3.访问标志
4.类索引,父类索引与接口索引集合
5.字段表集合
6.方法表集合
7.属性表集合
九.虚拟机类加载过程
1.加载
2.验证
3.准备
4.解析
5.初始化
十.虚拟机类加载
1.类加载时机
2.双亲委派模型
十一.运行时栈帧接口
1.局部变量表
2.操作数栈
3.动态连接
4.方法返回地址
5.附加信息
十二.基于栈的字节码解释执行引擎
1.解释执行
2.基于栈的指令集与基于寄存器的指令集
3.基于栈的解释器执行过程
十三.程序编译与代码优化
1.编译期优化:javac编译器(解析与填充符号表,注解处理器,语义分析与字节码生成),java语法糖(泛型与类型擦出,自动装箱,拆箱,遍历循环,条件编译)
2.运行期优化:(公共表达式消除,数组边界检查消除,方法内联,逃逸分析)
十四.Java内存模型
1.主内存与工作内存
2.内存间的交互
3.对于volatile型变量的特殊规则
4.对用long和double型变量的特殊规则
5.原子性,可见性,有序性
6.先行发生原则
十五.锁优化
1.自旋锁与自适应自旋
2.锁消除
3.锁粗化
4.轻量级锁
5.偏向锁
十六.常用Java虚拟机
1.SunClassicExactVM
2.SunHotSpotVM
3.SunMobile—EmbeddedVMMeta—CircularVM
4.BEAJRockitIBMJ9VM
5.AzulVMBEALiquidVM
6.ApacheHarmonyGoogleAndroidDalvikVM
7.MicrosoftJVM及其他
十七.优案例分析与实战
1.高性能硬件上的程序部署策略
2.集群间同步导致的内存溢出
3.堆外内存导致的溢出错误
4.外部命令导致系统缓慢
5.服务器JVM进程崩溃
6.不恰当数据结构导致内存占用过大
7.由Windows虚拟内存导致的长时间停顿
详细参考:《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版》
前言
第一部分 走近Java
第1章 走近Java
1.1 概述
1.2 Java技术体系
1.3 Java发展史
1.4 Java虚拟机发展史
1.4.1 Sun Classic Exact VM
1.4.2 Sun HotSpot VM
1.4.3 Sun Mobile-Embedded VM Meta-Circular VM
1.4.4 BEA JRockit IBM J9 VM
1.4.5 Azul VM BEA Liquid VM
1.4.6 Apache Harmony Google Android Dalvik VM
1.4.7 Microsoft JVM及其他
1.5 展望Java技术的未来
1.5.1 模块化
1.5.2 混合语言
1.5.3 多核并行
1.5.4 进一步丰富语法
1.5.5 64位虚拟机
1.6 实战:自己编译JDK
1.6.1 获取JDK源码
1.6.2 系统需求
1.6.3 构建编译环境
1.6.4 进行编译
1.6.5 在IDE工具中进行源码调试
1.7 本章小结
第二部分 自动内存管理机制
第2章 Java内存区域与内存溢出异常
2.1 概述
2.2 运行时数据区域
2.2.1 程序计数器
2.2.2 Java虚拟机栈
2.2.3 本地方法栈
2.2.4 Java堆
2.2.5 方法区
2.2.6 运行时常量池
2.2.7 直接内存
2.3 HotSpot虚拟机对象探秘
2.3.1 对象的创建
2.3.2 对象的内存布局
2.3.3 对象的访问定位
2.4 实战:OutOfMemoryError异常
2.4.1 Java堆溢出
2.4.2 虚拟机栈和本地方法栈溢出
2.4.3 方法区和运行时常量池溢出
2.4.4 本机直接内存溢出
2.5 本章小结
第3章 垃圾收集器与内存分配策略
3.1 概述
3.2 对象已死吗
3.2.1 引用计数算法
3.2.2 可达性分析算法
3.2.3 再谈引用
3.2.4 生存还是死亡
3.2.5 回收方法区
3.3 垃圾收集算法
3.3.1 标记-清除算法
3.3.2 复制算法
3.3.3 标记-整理算法
3.3.4 分代收集算法
3.4 HotSpot的算法实现
3.4.1 枚举根节点
3.4.2 安全点
3.4.3 安全区域
3.5 垃圾收集器
3.5.1 Serial收集器
3.5.2 ParNew收集器
3.5.3 Parallel Scavenge收集器
3.5.4 Serial Old收集器
3.5.5 Parallel Old收集器
3.5.6 CMS收集器
3.5.7 G1收集器
3.5.8 理解GC日志
3.5.9 垃圾收集器参数总结
3.6 内存分配与回收策略
3.6.1 对象优先在Eden分配
3.6.2 大对象直接进入老年代
3.6.3 长期存活的对象将进入老年代
3.6.4 动态对象年龄判定
3.6.5 空间分配担保
3.7 本章小结
第4章 虚拟机性能监控与故障处理工具
4.1 概述
4.2 JDK的命令行工具
4.2.1 jps:虚拟机进程状况工具
4.2.2 jstat:虚拟机统计信息监视工具
4.2.3 jinfo:Java配置信息工具
4.2.4 jmap:Java内存映像工具
4.2.5 jhat:虚拟机堆转储快照分析工具
4.2.6 jstack:Java堆栈跟踪工具
4.2.7 HSDIS:JIT生成代码反汇编
4.3 JDK的可视化工具
4.3.1 JConsole:Java监视与管理控制台
4.3.2 VisualVM:多合一故障处理工具
4.4 本章小结
第5章 调优案例分析与实战
5.1 概述
5.2 案例分析
5.2.1 高性能硬件上的程序部署策略
5.2.2 集群间同步导致的内存溢出
5.2.3 堆外内存导致的溢出错误
5.2.4 外部命令导致系统缓慢
5.2.5 服务器JVM进程崩溃
5.2.6 不恰当数据结构导致内存占用过大
5.2.7 由Windows虚拟内存导致的长时间停顿
5.3 实战:Eclipse运行速度调优
5.3.1 调优前的程序运行状态
5.3.2 升级JDK 1.6的性能变化及兼容问题
5.3.3 编译时间和类加载时间的优化
5.3.4 调整内存设置控制垃圾收集频率
5.3.5 选择收集器降低延迟
5.4 本章小结
第三部分 虚拟机执行子系统
第6章 类文件结构
6.1 概述
6.2 无关性的基石
6.3 Class类文件的结构
6.3.1 魔数与Class文件的版本
6.3.2 常量池
6.3.3 访问标志
6.3.4 类索引、父类索引与接口索引集合
6.3.5 字段表集合
6.3.6 方法表集合
6.3.7 属性表集合
6.4 字节码指令简介
6.4.1 字节码与数据类型
6.4.2 加载和存储指令
6.4.3 运算指令
6.4.4 类型转换指令
6.4.5 对象创建与访问指令
6.4.6 操作数栈管理指令
6.4.7 控制转移指令
6.4.8 方法调用和返回指令
6.4.9 异常处理指令
6.4.10 同步指令
6.5 公有设计和私有实现
6.6 Class文件结构的发展
6.7 本章小结
第7章 虚拟机类加载机制
7.1 概述
7.2 类加载的时机
7.3 类加载的过程
7.3.1 加载
7.3.2 验证
7.3.3 准备
7.3.4 解析
7.3.5 初始化
7.4 类加载器
7.4.1 类与类加载器
7.4.2 双亲委派模型
7.4.3 破坏双亲委派模型
7.5 本章小结
第8章 虚拟机字节码执行引擎
8.1 概述
8.2 运行时栈帧结构
8.2.1 局部变量表
8.2.2 操作数栈
8.2.3 动态连接
8.2.4 方法返回地址
8.2.5 附加信息
8.3 方法调用
8.3.1 解析
8.3.2 分派
8.3.3 动态类型语言支持
8.4 基于栈的字节码解释执行引擎
8.4.1 解释执行
8.4.2 基于栈的指令集与基于寄存器的指令集
8.4.3 基于栈的解释器执行过程
8.5 本章小结
第9章 类加载及执行子系统的案例与实战
9.1 概述
9.2 案例分析
9.2.1 Tomcat:正统的类加载器架构
9.2.2 OSGi:灵活的类加载器架构
9.2.3 字节码生成技术与动态代理的实现
9.2.4 Retrotranslator:跨越JDK版本
9.3 实战:自己动手实现远程执行功能
9.3.1 目标
9.3.2 思路
9.3.3 实现
9.3.4 验证
9.4 本章小结
第四部分 程序编译与代码优化
第10章 早期(编译期)优化
10.1 概述
10.2 Javac编译器
10.2.1 Javac的源码与调试
10.2.2 解析与填充符号表
10.2.3 注解处理器
10.2.4 语义分析与字节码生成
10.3 Java语法糖的味道
10.3.1 泛型与类型擦除
10.3.2 自动装箱、拆箱与遍历循环
10.3.3 条件编译
10.4 实战:插入式注解处理器
10.4.1 实战目标
10.4.2 代码实现
10.4.3 运行与测试
10.4.4 其他应用案例
10.5 本章小结
第11章 晚期(运行期)优化
11.1 概述
11.2 HotSpot虚拟机内的即时编译器
11.2.1 解释器与编译器
11.2.2 编译对象与触发条件
11.2.3 编译过程
11.2.4 查看及分析即时编译结果
11.3 编译优化技术
11.3.1 优化技术概览
11.3.2 公共子表达式消除
11.3.3 数组边界检查消除
11.3.4 方法内联
11.3.5 逃逸分析
11.4 Java与CC++的编译器对比
11.5 本章小结
第五部分 高效并发
第12章 Java内存模型与线程
12.1 概述
12.2 硬件的效率与一致性
12.3 Java内存模型
12.3.1 主内存与工作内存
12.3.2 内存间交互操作
12.3.3 对于volatile型变量的特殊规则
12.3.4 对于long和double型变量的特殊规则
12.3.5 原子性、可见性与有序性
12.3.6 先行发生原则
12.4 Java与线程
12.4.1 线程的实现
12.4.2 Java线程调度
12.4.3 状态转换
12.5 本章小结
第13章 线程安全与锁优化
13.1 概述
13.2 线程安全
13.2.1 Java语言中的线程安全
13.2.2 线程安全的实现方法
13.3 锁优化
13.3.1 自旋锁与自适应自旋
13.3.2 锁消除
13.3.3 锁粗化
13.3.4 轻量级锁
13.3.5 偏向锁
13.4 本章小结
附 录
附录A 编译Windows版的OpenJDK
附录B 虚拟机字节码指令表
附录C HotSpot虚拟机主要参数表
附录D 对象查询语言(OQL)简介
附录E JDK历史版本轨迹
相关推荐
首先,JVM是一种抽象的计算机架构,它基于栈式架构设计,拥有自己的指令集和内存管理机制。JVM的主要功能是解释执行Java字节码或将其编译成本地代码执行,从而保证Java程序能够跨平台运行。Java源代码在编译后生成的...
1. **平台分析**:理解目标平台的体系结构,如指令集、内存模型、线程模型等。 2. **JVM实现**:根据平台特性实现JVM的各个组件,如类装载器、执行引擎等。 3. **字节码与机器码转换**:设计并实现字节码解释器或JIT...
- **计算机体系结构**:基于冯诺依曼模型,计算机包括存储器、运算器和控制器,它们共同负责数据的处理。 - **机器语言**:CPU能直接执行的指令,通常以二进制形式存在。 - **CPU指令集差异**:不同厂商的CPU有...
Java虚拟机规范详细描述了JVM的工作原理,包括它的体系结构、数据类型、指令集、执行引擎、内存模型以及异常处理机制等等。 1. JVM体系结构:JVM包括类加载器、运行时数据区、执行引擎、本地接口和垃圾回收器。类...
- **指令集**:提供了JVM执行Java字节码所需的指令集合。 - **方法区**:这是所有线程共享的一个区域,主要存储了类的信息、常量池、静态变量等。值得注意的是,方法区也可以被GC(Garbage Collector)清理。 - **...
模拟和仿真分别是软件和微程序层面实现其他计算机系统指令集的方式,如模拟器和虚拟机。 并行性是计算机在同一时刻处理多个任务的能力,分为同时性和并发性。资源重复和资源共享是提高并行计算性能的策略,而耦合度...
7. **字节码操作**:学习如何阅读和理解字节码,了解JVM指令集,这对于理解程序运行过程和优化代码有极大帮助。 通过对JVM的深入研究,开发者不仅能提升代码的运行效率,还能更好地解决实际开发中遇到的问题,从而...
4. **指令执行**:遍历并执行字节码指令,模拟JVM的指令集体系结构。 5. **垃圾收集**:设计并实现一套垃圾收集策略,如标记-清除、复制、标记-压缩或分代收集。 6. **内存管理**:分配和释放内存,确保程序不会因...
2. **抽象计算机模型**:JVM可以被视为一种抽象计算机,拥有自己的指令集和运行时内存管理机制。 3. **独立性**:JVM不依赖于特定的实现技术、硬件或操作系统,这为Java程序提供了一个通用的、与平台无关的执行环境...
这个项目可能包括了对计算机体系结构的基础概念、微处理器设计、内存层次结构、输入/输出系统、指令集架构以及并行计算等多个方面的探索。Java作为标签,暗示项目可能涉及到用Java语言来模拟或实现某些计算机体系...
《后端研究:一种Java处理器的体系结构设计与研究》 该研究主要关注的是Java处理器的体系结构设计,这是在理解和优化Java程序执行效率的重要环节。Java处理器是基于Java虚拟机(JVM)的一种硬件实现,它直接执行...
通过深入阅读和分析这两个虚拟机的源码,你可以学习到计算机体系结构、操作系统原理以及软件工程实践等多个方面的知识。同时,这也将有助于你未来开发自己的虚拟机或者理解现有虚拟机(如Java虚拟机JVM或.NET CLR)...
同时,文件可能详细解释了JVM指令集,这是理解JVM执行机制的基础。 6. **05_GC and Tuning.md**:垃圾收集(GC)是Java自动内存管理的核心。这个文件可能会详细讲解不同类型的垃圾收集器,如串行、并行、CMS、G1、...
六、Java虚拟机体系结构 JVM由指令集、寄存器、栈、垃圾回收堆和方法区域五个主要部分构成。指令集包含了约248个字节码指令,涵盖了基本的CPU运算,如算术操作、流程控制等。每个指令由一个操作码和零个或多个操作数...
Java虚拟机(JVM)是一个抽象的计算模型,它有自己的指令集和执行引擎,可以在运行时操控内存区域。JVM可以解读指令代码并与底层进行交互:包括操作系统平台和执行指令并管理资源的硬件体系结构。本文主要介绍Java...
Java体系结构由四个紧密相连的技术构成,分别是Java编程语言、Java类文件格式、Java应用编程接口(API)和Java虚拟机(JVM)。这四个组成部分共同构成了Java的完整生态。 1. **Java编程语言**:用于编写Java应用程序的...