深入理解Java虚拟机
深入理解Java虚拟机
1、运行时数据区域
2、对象的创建
- 虚拟机遇到一条new指令;
- 检查指令参数是否能在常量池中定位到一个类的符号引用,并检查符号引用代表的类是否已被加载、解析和初始化过。如果没有进行初始化:
- 则需先执行相应类的加载过程,加载检查通过后,虚拟机为新生对象分配内存;
- 分配的内存空间除对象头外都初始化为零值;
- 在对象头中存入是哪个类的实例、对象的hash码、GC分代年龄等;
- 执行init()方法,将对象按照程序员的意愿初始化。
3、对象可回收(标记对象)
-
引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加1;引用失效时,计数器就减1。任何时刻计数器为0的对象就是不可能再被使用的对象。
缺点:难解决对象间的相互引用。
-
可达性分析算法:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索走过的路径称为“引用链”。当一个对象到GC Roots没有任何引用链相连则证明此对象不可用。
4、宣告对象死亡
至少两次标记过程:对对象进行可达性分析,若无引用链,那么对其进行一次标记并进行一次筛选,筛选条件为它是否有必要执行finalize()方法:
- 若对象无覆盖finalize()方法或finalize()方法已被虚拟机调用过了(任何对象的此方法只能被系统自动调用一次),则没有必要执行。
- 若有必要执行,则将该对象放入到一个叫F-Queue的队列中。
GC对F-Queue中的对象进行第二次小规模的标记。
5、回收方法区
回收废弃常量和无用的类:
- 废弃常量:例如一个字符串"abc"已被放入到常量池中,但是系统没有任何一个String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
- 无用的类:
- 该类的所有实例都已被回收;
- 加载该类的ClassLoader 已经被回收;
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
6、垃圾收集算法
-
标记-清除算法:首先标记出所有需要回收的对象,再统一回收被标记的对象。
缺点:标记清除后会产生大量不连续的内存碎片,空间碎片过多可能会导致下次需要分配大对象时,连续内存不足而不得不提前触发另一次垃圾收集动作。
-
复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完,就将还存活的对象复制到另外一块,然后把此块内存空间一次清理掉。
缺点:可用内存缩小到原来的一半,且复制需要浪费时间。
-
标记-整理算法:让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
-
分代收集算法:把Java堆分为新生代和老年代。
- 新生代:有大批对象死去,只有少数存活,采取复制算法。
- 老年代:对象存活率高,没有额外空间,使用标记-清除算法或标记-整理算法。
7、垃圾收集器
CMS:
- 初始标记:仅标记一下 GC Roots能直接关联到的对象(根-子)。
- 并发标记:进行GC Roots Tracing过程。
- 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。
- 并发清除
并发标记和并发清除过程中,收集器线程可与用户线程一起工作。
缺点:
-
对CPU资源非常敏感。在并发阶段,虽然不会导致用户进程停顿,但会因占用一部分线程(或CPU资源)而导致应用程序变慢,总吞吐量降低。默认启动回收线程数:(CPU数量 + 3)/ 4。
-
无法处理浮动垃圾。CMS并发清除阶段用户线程还在运行,伴随程序运行自然会有新垃圾产生,CMS无法在本次GC中处理它们,这部分垃圾就称为浮动垃圾。
由于此原因,CMS不会等老年代几乎填满再进行收集(默认68%),需要预留一部分空间提供并发收集时程序运行使用。
-
CMS使用标记-清除算法,此算法会导致大量的空间碎片。
8、内存分配
- 对象优先在Eden分配;
- 大对象直接进入老年代;
- 长期存活的对象将进入老年代(年龄默认为15);
- 动态年龄判定。Survivor空间中相同年龄的所有对象大小总和大于Survivor空间的一半,年龄大于或等于此年龄的对象直接进入老年代。
- 空间分配担保。在发生Minor GC前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间:
- 大于,则Minor GC可用确保是安全的;
- 小于,虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,则继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小:
- 大于,进行一次Minor GC(虽然有风险)。
- 小于或HandlePromotionFailure设置不允许冒险,则进行一次Full GC。
9、类的生命周期
类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括以下7个阶段:
有且仅有以下5种情况必须立即对类进行初始化(主动引用):
- 遇到new、getstatic、putstatic(读取或设置一个类的静态字段)或invokestatic(调用一个类的静态方法)的时候。
- 使用java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,若发现其父类还没进行初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包括main()方法的那个类),虚拟机会先初始化这个主类。
- 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
10、类加载过程
-
加载:
- 通过一个类的全限定名来获取定义此类的二进制字节流;
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
结果:加载完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中。然后在内存中实例化一个java.lang.Class类的对象(并没有明确规定是在Java堆中,对于HotSpot虚拟机而言,Class对象存放在方法区里面),这个对象将作为程序访问方法区中的这些类型数据的外部接口。
-
验证:这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证大致分为4个阶段:
- 文件格式验证。例如:Class文件是否以魔数0xCAFEBABE开头。
- 元数据验证。例如:这个类是否有父类(除了java.lang.Object之外,所有的类都应该有父类)。
- 字节码验证。例如:保证方法体中的类型转换是有效的,如可以把一个子类对象赋值给父类数据类型,但把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的。
- 符号引用验证。例如:符号引用中通过字符串描述的全限定名是否能找到对应的类。
-
准备:正式为类变量(仅指被static修饰的变量)分配内存并设置类变量初始值(一般为零值),这些变量所使用的内存都将在方法区中进行分配。
-
解析:虚拟机将常量池内的符号引用替换为直接引用的过程。
-
初始化:执行类构造器<clinit>()方法的过程。
11、类加载器和双亲委派模型
类加载器
通过一个类的全限定名来获取描述此类的二进制字节流这个动作的代码模块称为类加载器。比较两个类是否“相等”,只有在这两个类由同一个类加载器加载的前提下才有意义。
双亲委派模型
双亲委派模型的工作过程:如果一个类加载器收到类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器中,每一层次的类加载器都是如此。因此,所有的类加载请求最终都应传送到顶层的启动器类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
12、Java内存模型(JMM)
主要目标:定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。
volatile关键字
当一个变量定义为volatile后,它将具备两种特性:
- 保证此变量对所有线程的可见性。对volatile变量所有的写操作都会立刻反应到其他线程之中,换句话说,volatile变量在各个线程中是一致的。
- 禁止指令重排序优化(添加内存屏障)。
并发的三种特性
-
原子性:由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write,我们大致可以认为基本数据类型的访问读写时具备原子性的。
-
可见性:可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
实现方式:Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的。volatile的特殊规则保证了新值能立即同步到主内存,以及使用前立即从主内存刷新。
-
有序性。实现方式:
- volatile关键字:本身包含了禁止指令重排序的含义。
- synchronized:一个变量在同一时刻只允许一条线程对其进行lock操作(串行进入)。
13、线程状态转换
14、线程安全的实现方法
-
互斥同步(阻塞同步)
实现手段:
- synchronized关键字
- java.util.concurrent包中的重入锁(ReentrantLock)。相对于synchronized,增加了一些高级功能。
- 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
- 公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁(默认synchronized和ReentrantLock都是非公平的)。
- 锁可以绑定多个条件:ReentrantLock对象通过多次调用newCondition()方法同时绑定多个Condition对象。
-
非阻塞同步
CAS(Compare And Swap,比较与交换):CAS指令需要3个操作数:
- 参数1:变量的内存地址,用V表示;
- 参数2:旧的预期值,用A表示;
- 参数3:新值,用B表示。
当且仅当V符合预期值A时,处理器就用新值B更新V的值,否则它就不执行更新。但是,无论是否更新V的值,都会返回V的旧值。
15、锁优化
- 自旋锁与自适应自旋锁:在线程等待的过程中,让线程执行一个忙循环(自旋),避免了线程切换的开销。自适应自选锁意味着自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
- 锁消除:指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行清除。
- 锁粗化:如果一系列的连续操作都是对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中,那即使没有线程竞争,频换地进行互斥同步操作会导致不必要的性能损耗。如果虚拟机探测到一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列外部。
- 轻量级锁。在无竞争的情况下使用CAS操作去清除同步使用的互斥量的开销。
自旋时间及锁的拥有者的状态来决定。 - 偏向锁:“偏“字,意思是这个锁会偏向于第一个获得它的线程,如果接下来的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步。