Java GC随笔
最近发生了一些C#程序运行时的一些问题,发现是GC导致的问题,然后稍微研究了一下GC,因为知道Java的GC要比.NET稍微复杂一点,所以我觉得要是能弄懂Java的GC的原理,对.NET的GC的理解也能更深入一些。所以把研究到的整理做了个笔记,以免以后忘记。
什么样的对象会被GC判定要回收的对象:
主流JVM采用可达性分析算法来判断一个对象是否需要回收。基本思想是通过称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索走过的路径称为引用链,当一个对象不与任何引用链相连的时候,说明此对象是不可用的。
引用计数算法也是用来判断一个对象是否应该被GC回收的一个算法。基本思想是给对象添加一个引用计数器,每当有地方引用它时,计数器加1,引用失效则减1.在任何时刻计数器为0的对象就是不可用对象。优点:实现简单,判定效率高。缺点:不能解决对象之间循环引用的问题。比如objA.instance = objB;objB.instance=objA;此时双方计数都不为0,则无法通知GC去回收他们。所以此方法并没有被主流JVM所采用。
可达性分析中,如何检查和处理其中包含的引用:
首先需要了解检查过程中的问题:
- 现在很多引用光方法区就会有数百兆,如果要逐一检查其中的引用,必然会花费大量时间。
- 分析过程中,会出现GC停顿,如果在分析过程中引用关系不断变化,那么分析结果的准确性得不到保证,所以必须GC过程中必须要停顿所有Java执行线程,既Stop The World。
为了解决上述问题,主流JVM采用准确式GC:采用一组称为OopMap的数据结构达到这个目的,在类加载完成、JIT编译过程中,会在特定位置记录下栈和寄存器中哪些是引用。
但是还有一个问题,除了上述情况之外,很多指令都有可能改变引用关系,如果为每个指令都生成相应的OopMap,那会需要大量额外空间,所以JVM采用SafePoint来生成OopMap。SafePoint是按照“是否具有让程序长时间执行的特征”为标准进行选定的,明显特征是指令序列的复用,如方法调用,循环跳转,异常跳转等,除此之外,在类加载完成、JIT编译过程中记录下栈和寄存器中哪些是引用的地方也会生成SafePoint。
对于SafePoint,如何让所有线程都跑到最近的SafePoint是一个需要考虑的问题。对于这个问题,采用的是主动式中断。中心思想是当GC需要中断线程时,不直接对线程进行操作,而是设置一个标志,每个线程执行时主动轮询这个标志,当发现中断标志为真时就在运行到SafePoint时自己中断挂起。
有一种情况,有些线程本身就没有被分配CPU时间,如Sleep或Blocked状态,这样的线程无法轮询标志,也无法执行到SafePoint上自行中断。对于这种情况,JVM视此线程为安全区域,是指在这段代码片段中,引用关系不会发生变化,在这个区域中任意地方开始GC都是安全的。
引用的生命周期:
- 强引用:代码中普遍存在的,类似Object obj = new Object();只要强引用还在,被引用的对象就不会被回收。
- 软引用:有用但是非必须的引用,只有在内存将要发生内存溢出异常之前,才会被列进回收范围中,如果回收之后依 然内存不足,才会抛出内存溢出异常。
- 弱引用:非必须对象,比软引用更弱一些,只能生存到下次GC之前,无论内存是否足够,GC工作时都会回收。
- 虚引用:无法通过虚引用获得对象实例,唯一作用是在GC时收到系统通知。
被判定要被回收的对象,在哪些情况下可以自救(不被回收):
当一个对象没有覆盖finalize()方法,或者finalize()已经被调用过的话,GC都会将其回收。
当一个对象没有覆盖finalize()方法,或者finalize()已经被调用过的话,GC都会将其回收。
当不满足上述条件时,被GC判定要回收的对象会放入F-Queue的队列中,当一个对象在finalize()方法中重新和GC Roots建立连接,就可以达到自救的目的。
注意:如果该对象在finalize()方法中实现自救,在下次被判定要被回收的时候,因为之前finalize()被调用过,所以此次回收finalize()不会被调用,故无论怎样,都会被GC回收。finalize()方法最初是为了满足C++程序员设计的方法,此方法运行代价高,不确定性大,所以在开发过程中避免使用此方法。
在方法区中,什么条件下会被GC回收:
- 该类所有的实例都被回收,Java堆中不存在该类实例。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
堆中在被判定要回收的对象后,如何进行回收:
HotSpot采用的是分代收集算法来完成回收后的内存分配问题。基本思想是:将堆分为4部分,1个Eden空间、2个Survivor空间(From,To)和1个老年代空间。默认Eden:Survivor = 8:1.
步骤:
- 开始状态,对象会存在与Eden和From的空间中,在GC发生时,通过可达性分析算法判断要回收的对象,并按照回收规则进行回收,在Eden中存活的对象会放入To中,From中存活的对象的年龄加1,放入To,此时Eden和From空间是被清空的。
- To和From倒置,原来的To变为From,From变为To。运行一段时间后,Eden区和From区又新添加了一些对象,GC发生时,按照上述规则将Eden区存活的对象放入To中,From中存活的年龄加1放入To中。
- 以此反复,当From中年龄加1后到达一个阈值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)时,将此对象放入老年代中。
- 当To区被填满时,无论年龄如何,都会清空To区,将其中所有的对象放入老年代中。
老年代中的对象通过什么方式进行回收:
因为老年代中对象存活率高,没有额外空间进行分配担保,所以老年代使用“标记-清理”或者“标记-整理”算法进行回收。
因为老年代中对象存活率高,没有额外空间进行分配担保,所以老年代使用“标记-清理”或者“标记-整理”算法进行回收。
标记-清理:对堆中需要回收的对象进行标记,在标记完成后统一回收要回收的对象。效率不高,会产生碎片。
标记-整理:在标记清理后,对堆进行一次压缩,让对象连续存于内存中。
回收采用的收集器都有哪些,各有什么特点: