前言
垃圾收集算法是JVM中垃圾收集器的方法论,所以了解算法是必要的,在算法领域只做最简单的介绍,力求文章的简单易懂。垃圾收集算法主要有以下几种:标记-清除算法(mark-sweep)、复制算法(copying)和标记-整理算法(mark-compact)。随着jdk版本的升级,垃圾收集器也在不断的升级,现在最新的垃圾收集器已经能够对Java堆中一部分进行回收,也能够对Java堆中另一部分进行回收,这一成果在jdk1.7中得到体现。但是垃圾收集器的底层算法是深入垃圾收集器所必须了解的,这篇文章将主要讨论垃圾收集算法。
标记-清除算法
算法的执行过程与名字一样,先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。该算法有两个问题:1)标记和清除过程效率不高。主要由于垃圾收集器需要从GC Roots根对象中遍历所有可达的对象,并给这些对象加上一个标记,表明此对象在清除的时候被跳过,然后在清除阶段,垃圾收集器会从Java堆中从头到尾进行遍历,如果有对象没有被打上标记,那么这个对象就会被清除。显然遍历的效率是很低的;2)会产生很多不连续的空间碎片,所以可能会导致程序运行过程中需要分配较大的对象的时候,无法找到足够的内存而不得不提前出发一次垃圾回收。
复制算法
复制算法是为了解决标记-清除算法的效率问题的,其思想如下:将可用内存的容量分为大小相等的两块,每次只使用其中的一块,当这一块内存使用完了,就把存活着的对象复制到另外一块上面,然后再把已使用过的内存空间清理掉。这样当垃圾收集器进行回收的时候就不用考虑空间碎片的问题,缺点在于把内存缩小为原来的一半,代价未免有点大。
当然正是由于其缩小内存为原来的一半代价大的问题,现代的JVM并不是按照1:1划分内存空间的,而是将内存分为一块较大的Eden区和两块较小的Survivor区,每次使用其中的Eden和一块Survivor区。当回收的时候,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor中,最后把Eden和Survivor的空间清理出来。其实这里还有一个问题:就是如果垃圾回收后,存活的对象需要的空间大于剩余一块Survivor的空间怎么办?答案是需要依赖其他内存进行分配(这里主要指的是老年代)。
标记-整理算法
与标记-清除算法过程一样,只不过在标记后不是对未标记的内存区域进行清理,而是让所有的存活对象都向一端移动,然后清理掉边界外的内存
分代收集算法
目前商用虚拟机都使用“分代收集算法”,所谓分代就是根据对象的生命周期把内存分为几块,一般把Java堆中分为新生代和老年代,这样就可以根据对象的“年龄”选择合适的垃圾回收算法。垃圾回收时,新生代对象中都会有大批量的对象死亡,就选择复制算法(因为存活的对象较少,而死亡的对象过多,如果使用标记-清除算法的话,需要遍历标记,显然效率较低,而使用复制算法就可以把存活的较少的对象复制到可用内存区域中,这样效率就较高);对于老年代对象,其存活率较高,所以就可以使用“标记-清除”算法或者“标记-整理”算法。
Hotspot垃圾收集算法实现
我们回到标记-清除算法,在清除阶段,为了枚举未被标记的对象,所以需要从根节点(GC Roots)开始查找引用链,这个过程会导致GC停顿,意思就是在GC的时候Java的执行线程都被停顿,好像被冻结在某一个时间点,也叫“Stop the world”。然而目前主流的Java虚拟机都是用准确式GC(所谓准确式GC,即虚拟机知道内存中的某个位置的数据是什么类型),当“Stop the world”的时候并不需要检查所有的引用位置,虚拟机通过使用OopMap这个数据结构知道哪些地方存放着对象的引用。
现在我们使用OopMap,虚拟机已经知道哪些位置存放着对象,从而GC Roots可以迅速的枚举可达对象的引用链。但是问题来了:是不是需要对所有的指令都使用OopMap呢?答案是否定的。实际上,虚拟机只在“特定的位置”记录了对象的引用信息,比如我们使用方法调用或者循环的时候,就会设定这样的位置,如果越过这个位置的继续执行指令,然而程序是不允许因为指令流长度太长而执行过长时间,所以这个“特定位置“就成为了程序是否具有长时间运行的分界点。这个”特定的位置“也称为安全点。
现在虚拟机有了安全点,于是只会到安全点寻找对象的引用信息,并且在安全点暂停Java执行线程,然而还有一个问题:如果一个线程的执行位置距离安全点比较远怎么办呢?在Hotspot使用主动式中断执行线程,其思想如下:当GC的时候不需要直接对线程操作来中断线程,仅仅是设置一个标志,然后让执行线程去轮询这个标志,发现中断标志为真的时候就自己中断线程。需要注意的是,轮询标志的地方与安全点的位置是重合的,另外再加上创建对象需要分配的地方。现在有一个问题:什么情况下,轮询标志才会为真呢?(这个需要根据执行的时候指令来确定,属于机器级别的指令)
OK,现在通过GC Roots和安全点,程序能够在不太长的时间就可以到达安全点,并暂停执行线程。那么如果程序在阻塞或者睡眠的状态的时候,执行线程如何中断呢?想象这么一个场景,去电影院买票,你在排队,但是由于人太多你一直在等待,所以一直没买着票这时即使售票员说了票卖完了,但是人太多太吵很多人都没听到,所以你仍然在排队。对于这种情况,JVM设置了安全区域。安全区域就是指在这个区域内,对象的引用关系不会发生改变,在这个范围暂停线程都是可以的,枚举根节点的时候,得到的引用信息还是完整的。