今天就跟你一块知道知道垃圾回收算法
内容参考《深入理解Java虚拟机》
要谈垃圾回收,首先我们得知道究竟谁是垃圾?
垃圾回收主要关注的是堆中的内存,而堆中存放的是各种各样的的对象实例,也就是说,我们要找到那些已经“死掉”的对象,怎么判断对象死没死呢,有一种非常简单理解的算法—引用计数算法
给对象添加一个引用计数器,每有一个地方引用他,就+1,引用失效就-1,计数器是0了的对象就是一个不可在被使用的对象。实现简单,判定效率也很高,但是他存在循环引用的问题,比如说
public class ReferenceCountingGc{ public Object instance = null; //还有开一个数组占空间,看GC日志用的代码,就写了 } public static void testGC(){ ReferenceCountingGc objA = new ReferenceCountingGc(); ReferenceCountingGc objB = new ReferenceCountingGc(); objA.instance = objB; objB.instance = objA; }
除此之外,没有其他的引用,那么这两个对象已经不能再被访问了,但是他们互相引用对方,引用计数器不会成0,所以GC收集器就不会回收他们
可达性分析算法
就是画一个树,从GC Root作为起始点,从这些节点开始向下搜索,如果一个对象没有任何的引用链能到GC Root,那么它就被判定为可回收的对象
一般来说,有以下几种可作为GC Root的对象
1、虚拟机栈中引用的对象
2、方法区中类静态属性引用的对象
3、方法区中常量引用的对象
4、本地房发展中JNI引用的对象
垃圾收集算法
确定了垃圾之后,就要开始想用什么办法去收集这些垃圾。说深了我也不懂,也是简单的记录一下这几种算法的思想
1、标记—清除算法
跟他的名字一样,思想也非常简单,首先标记出来所有需要被回收的对象,标记完之后统一的进行回收所有被标记了的对象。
算法最主要的不足体现在
1、效率问题
2、空间问题(清楚之后产生很多不连续的内存碎片,可能会在需要分配较大对象的时候无法找到足够的连续的内存,不得不提前触发另一次垃圾收集)
2、复制算法
将可以用的内存分成两块,每次使用其中的一块,用完之后对这个区域进行垃圾回收,将这部分存活的对象赋值到另外一块上面,然后对这整个半块区域回收
灰色为存活的对象,黑色是待清理的对象,这个时候,把左半边的存活的对象,复制到右半边,然后对整个左半边进行回收
当然,这种算法也存在问题,将内存缩小了一半也未免太高了。
但是新生代中的对象98%都是朝生夕死,所以不用按1:1的比例来划分内存区域
将内存分成一块较大的Eden空间和两块较小的Survivor空间
每次使用Eden和其中一块Survivor,回收的时候将存活的对象放到令一块Survivor空间上,然后清理掉之前用的那一块Survivor和Eden。
HotSpot虚拟机默认的比例大小是8:1也就是每次新生代可以使用的内存空间是容量的90%。当然也不能保证存活的对象在剩下的10%空间中就一定能存放,这个时候,如果空间不够用,就需要依赖其他内存(老年代)进行分配担保
如果存活的对象太多了,这个算法也就显得效率有点低了,于是我们有
3、标记—整理算法
跟标记—清除算法一样,先进行标记,不过不是标记之后清除,而是让所有存活的对象都向一端移动,然后直接清理掉边界之外的内存
黑色是被标记,等待清理的内存
分代收集
刚才也提到了新生代和老年代,其实就是按对象存活的周期划分一下,存活时间较短就是在新生代,存活时间比较长就是在老年代,根据不同的特点采取不同的垃圾收集算法。
前面只是说了一些算法的基本思想,但是在好好想想也会出现许多的问题
枚举根节点
很多应用,光方法区动辄数百兆,挨个检查必然消耗太多的时间,这个分析工作必须是在分析的过程中,对象的引用关系不能再继续不断的变化
虚拟机是有办法直接知道哪些地方存放着对象的引用,在HotSpot的实现中,是用一组OopMap的数据结构来达到这个目的,类加载的时候他就把对象什么偏移量上是什么类型的数据计算出来,编译过程中,也会在特定的位置记录下栈和寄存器中那些位置是引用
安全点
也不是为每一条指令都生成了OopMap,只是在特定的位置记录的这些信息,那么这些特定的位置就称为安全点。程序执行时并非在所有的地方都能停顿下来开始GC,只有到达安全点的时候才能暂停。
下面,如何在GC调用的时候让所有线程都跑到安全点然后停顿呢?
1、抢先式中断
GC开始的时候,先把所有的线程都中断,然后看谁没到安全点,就让他恢复,继续跑到安全点上;现在几乎没有虚拟机是用这种方式
2、主动式中断
GC需要中断线程,不对线程操作,而是设置一个标志位,线程主动去轮询这个标志,发现中断标志位真的时候就自己中断挂起,轮询标志的地方和安全点是重合的,也就是说在安全点的时候才回去轮询
安全区域
还有一种特殊情况上面没有考虑到,就是这个线程当前处于阻塞状态,他没办法相应请求,跑到安全的地方去中断挂起,JVM也不大可能等待线程被重新分配CPU时间,这时候就需要设置安全区域来解决
安全区域是说在一段代码片段中,引用关系不会发生变化。在这个区域的任意地方开始GC都是安全的。在线程执行到Safe Region中的代码的时候,标识自己进入了Safe Region,当发起GC的时候,就不用管标识自己为Safe Region的线程了, 在线程要离开Safe Region的时候,首先检查系统是不是已经完成了根节点的枚举,如果完成了,就继续执行;如果还没完成就停下等待完成的信号,然后继续执行。
关于收集器的东西,下次再写