一、 对象存活与否
1. 引用计数算法
可以使用引用计数算法来判断对象是否存活。 给每个对象添加一个引用计数器,当有对对象的引用时,将引用计数器加一。 当引用失效时,计数器减一。 当计数器为0时,对象就失效了。 引用计数器算法实现简单,效率高但是无法解决循环引用的问题。当前主流的虚拟机中没有使用这个算法实现对对象存活的判断。
2. 可达性分析算法
使用可达性算法来判断对象是否存活。 有一系列称为“GC Roots”的对象作为起点, 如果堆中的对象不能到达GC Roots起点的话,那么对象是不可用的。
可用作GC Roots的对象有: 虚拟机栈中引用的对象、方法区中的静态属性引用的对象、方法区中常量引用的对象、本地方法栈中的JNI引用的对象。
3. 对象引用
强引用: 如Object obj = new Object() 这类的引用,强引用存在的话对象是不会被回收的。
软引用: 描述有用但不是必须的对象。 在系统将要发生内存溢出异常前会对这部分的引用的对象进行回收。
弱引用: 非必需对象。 其关联的对象只能生存到下一次垃圾回收发生之前。 当发生回收时,无论内存是否足够都会回收这部分对象。
虚引用: 其唯一目的是这个引用的对象回收之前能收到一个系统通知。
存在意义: 丰富对象引用状态,当空间足够时能够保存在内存中,当空间紧张时可以抛弃这些对象。
4. 对象消亡过程
对象消亡的过程经过两次标记过程。
当对象不可达时进行第一次标记,然后进行筛选。 如果对象没有覆盖finalize方法或者finalize方法以及执行了,那么对象不能经过筛选,死亡。 否则对象进行筛选, 此时将对象放入F-Queue队列中,稍后由虚拟机自动创建的低优先级的Finalizer线程进行触发。finalize方法是对象复活的唯一机会,稍后GC对F-Queue队列中的对象进行第二次标记。如果此时对象重新和引用挂钩的话,那么对象存活,否则对象正式死亡。 finalize()方法只会被系统调用一次,也就是说,对象最多只能复活一次。
注意: 要尽量避免使用finalize()方法,因为它的运行代价大,不确定性大,无法保证各个对象的调用顺序。
二、 垃圾收集算法
1. 标记-清除算法
标记清除算法首先标记出所有需要清除的对象,在标记完成后统一回收所有被标记的对象。
不足: 标记和清除的效率都不高,标记清除后产生空间碎片。
2. 复制算法
其将可用的内存划分为相等的两块,每次只是用其中一块,当这块内存用完了就将活着的对象移动到另一块上, 在把已使用过的内存空间一次清理。没有内存碎片,简单运行高效。代价: 牺牲了一半的内存。
由于新生代中的对象98%都是朝生夕死,所有不需要按照1:1划分内存空间。将内存分成一块较大的Eden和两块较小的survivor空间。每次使用Eden和一块survivor,回收时,将Eden和survivor中存活的对象一次性复制到另一个survivor上。最后清理Eden和第一块survivor.Hotspot默认的Eden和survivor是8:1。 当存活对象大于survivor时,需要进行“啃老”, 由老年代进行分配担保,这些对象将直接进入老年代。
一般新生代内存用来存放新建的对象, 老年代内存用来存放稳定的对象。
3. 标记-整理算法
复制算法在对象存活率较高时需要进行较多的复制操作。效率将减低,同时又空间浪费以及分配担保等问题。老年代一般不能直接选用这种算法。 标记整理算法和标记清除算法过程一样, 但是后续步骤不是直接对可回收对象进行清理,而是让所有存活对象向一端移动, 然后直接清理掉端边界以外的内存。
三、 HotSpot算法实现
枚举根节点:
在进行可达性分析时,需要进行GC停顿,因为进行可达性分析时需要确保在一致性的快照中进行。否则如果在进行可达性分析的过程中对象的引用关系还在不断变化那就不能保证分析结果的准确性了。
由于可作为GC Roots的节点有很多,不可能全部进行枚举。 目前主流的java虚拟机使用的都是准确式GC(准确式内存 管理,虚拟机可以知道内存中某个位置的数据的具体类型), 在HotSpot中使用一组称为OopMap的数据结构来告诉虚拟机哪些地方存放对象的引用,这样虚拟机就不需要一个不漏地检查所有执行上下文和全局引用位置。
安全点:
在OopMap的帮助下,HotSpot可以快速准确地完成GCRoots的枚举。 如果对所有的指令进行OopMap记录的话,需要大量的额外空间,因此HotSpot只在特定的位置记录了这些信息,这些位置称为安全点,程序只有执行到安全点时才能暂停进行GC。安全点不能太多或者太少,安全点的选定是以“是否具有让程序长时间执行的特征【方法调用、循环跳转等】”为标准进行选定的。
在GC时如何使程序在安全点停下来? ==》 抢先式中断和主动式中断。
抢先式中断: 发生GC时首先暂停所有线程,如果有线程不在安全点上就恢复线程让其跑到安全点上。现在几乎没有虚拟机实现采用这种方式。
主动式中断: 当GC需要线程中断时,不对线程操作,仅仅设置一个标志,各个线程执行时主动轮询这个标志,发现中断标志就挂起。
安全区域:
安全点保证了程序执行时遇到GC时的情形。 当程序不执行时,由于线程无法响应JVM中断请求,此时需要安全区域来解决。
安全区域是指在一段代码片段中,引用关系不会发生变化。 在这个区域中的任意地方开始GC都是安全的。 当线程要离开安全区域时,需要检查系统是否已经完成了根节点的枚举或者整个GC过程。如果完成了那么线程可以继续执行, 否则需要等到起完成才可以离开。
理解: 我们要进行GCRoots枚举,不能再任意的地方进行,要在特定的地方进行枚举来保证分析的一致性,即在分析过程中的引用不能改变。 这个特定的地方就是安全点,对于阻塞的线程来说就是安全域。 到了这些地方,那就需要进行枚举所有的GCRoots了,但是由于要枚举的数量很大(方法区中的引用、栈中的引用),逐个检查开销很大,所以hotspot使用准确式GC,当执行系统停顿下来后并不需要逐个检查,虚拟机通过准确式gc可以知道哪些地方存在对象的引用,在hotspot中使用oopMap这个数据结构来存储这些信息。因此到了安全点或者安全域后,直接堆oopMap中的数据解析GCRoots枚举即可。
--------------------------------------------------
深入理解java虚拟机JVM高级特性与最佳实现 读书笔记