JVM垃圾回收机制
垃圾回收需要考虑的三件事:
- 哪些内存需要回收;
- 什么时候回收;
- 如何回收;
为什么要学习GC呢?当需要排查各种内存溢出。内存泄漏的问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。
1. 判断对象是否需要被回收的算法
1. 引用计数法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器的值就加1;当引用失效时,计数器的值就减1;任何时刻计数器为0的对象就是不可能被使用的。
引用计数法的实现简单,判定效率也很高,但是在主流的JVM中并没有选用引用计数法来管理内存,最主要的原因是它很难解决对象间互相循环引用的问题。比如:
MyObject1 obj1 = new MyObject1();
MyObject2 obj2 = new MyObject2();
obj1.innerField = obj2;
obj2.innerFiled = obj1;
obj1 = null;
obj2 = null;
这个时候两个对象已经不会再被使用了,但是由于对象内部还存在对方的引用,因此引用计数器并不是0,无法被标记为回收状态。
2. 可达性分析法
基本思路是通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到GC Roots没有任何时(也就是从GC Roots到这个对象不可达),则证明此对象是不可用的。
在java语言中,可以作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中JNI引用的对象;
2. 浅谈finalize()方法
JVM真正的决定一个对象是否需要回收至少需要两次标记过程,如果一个对象不可达GC Roots,那它会被进行第一个筛选并进行一次过滤,过滤的条件是对象有没有必要执行finalize()
方法。当对象没有覆盖finalize()
方法或者finalize()
方法已经被虚拟机调用过,那么都被视为“没有必须执行”。
如果对象有必要执行finalize()
方法,那么这个对象会被放在一个叫做F-Queue队列中,稍后虚拟机会执行finalize()
方法,这里的执行只是会触发它,并不承诺等待它运行结束,这样做是因为如果一个对象在finalize()
方法中执行缓慢,或者发生了死循环,将很可能导致F-Queue队列中的其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()
方法时对象逃脱死亡的最后一次机会。稍后GC会对F-Queue队列中的对象进行第二次标记,如果对象成功在finalize()
方法中拯救自己,那么就会被移除“即将回收”的集合。
从上面的刷选条件可以看出,finalize()
方法只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()
方法不会再被执行。
不鼓励大家使用finalize()
,因为它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,如果需要做“关闭外部资源”之类的工作,使用try-catch或者其他方式会做的更好、更及时。这个方法并不是C/C++中的析构函数,而是Java刚诞生时为了使C/C++程序员更容易接受所做的一个妥协。
3. 垃圾收集算法
1. 标记 - 清除算法
标记-清除算法分为标记和清除两个阶段:首先标记处所有需要回收的对象,然后在标记完成后统一回收被标记的对象。它是最基础的收集算法,后面的收集算法都是基于这种思路并对其不足进行改进而得到的。主要不足有两个:
1)效率问题,标记和清除两个过程的效率都不高;
2)空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次的垃圾回收动作。
标记清除算法.png2. 复制算法
复制算法的出现是为了解决效率问题,它将可用内存按照容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次性清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑碎片等复杂情况,只要移动堆顶的指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。代价有点高了。
现在的商业虚拟机都采用这种收集算法来回收“新生代”,因为新生代中的对象大部分都是“朝生夕死”的,所以并不需要按照1:1来划分内存空间。这样就不会浪费50%的那么多的空间了。
复制算法.png3. 标记 - 整理算法
复制算法在存活对象较高时就要进行较多的复制操作,效率将会变低。更关键的时,如果不想浪费50%的空间,就需要额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况。所以老年代一般不能选用这种算法。
标记 - 整理算法,标记的过程和标记 - 清除算法一样,但后续的步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
标记整理算法.png4. 分代收集算法
当前商业虚拟机都采用分代收集算法,将对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死亡,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高,没有额外的空间对它进行分配担保,就必须使用标记 - 清除算法或者标记 - 整理算法来进行回收。
4. 垃圾收集器
如果说垃圾收集算法是内存回收的理论,那么垃圾收集器就是内存回收的具体实现。HotSpot虚拟机提供了以下几个垃圾收集器。每个垃圾收集器都有各自的特性。基本原理和使用场景,没有最好的,只有最适合的,我们要根据具体的应用场景应用最合适的收集器。
适合新生代的收集器:
Serial、ParNew、Parallel Scavenge、GI;
适合老年代的收集器:
CMS、Serial Old(MSC)、Parallel Old、GI;
…loading 后续再整理每个收集器的特性、原理和使用场景
记录自《深入理解Java虚拟机》的笔记,供自己以后复习参考
网友评论