1 标记-清除算法(Mark-Sweep)
这是最基础的算法,这个算法分为两个阶段标记和清除。首先标记出所有需要清除的对象,然后统一回收标记的对象。这种算法是最简单的,后续的算法都是在它的基础上改进得到的。它存在两个问题:
- 它的标记和清除效率都不高
-
是空间问题,标记清除后,产生大量的不连续的内存碎片。这可能导致在以后的对象分配的过程中,需要占用空间较大的对象可能找不到合适的连续内存空间而分配失败,触发另一次内存收集
2 复制算法
这种算法把内存按容量划分为相等两个区域,每次只使用其中的一块。当一块内从用完了,就把还存活的对象复制到另外一个内存上,然后把整块内存清除掉。这样每次都是对整个半区进行回收,在分配内存的时候也不用考虑内存碎片的问题,只要移动堆顶指针按顺序分配就可以了,实现简单。
现在商业的虚拟机都使用这种方式回收新生代。研究表明,98%的新生代对象都是很快消亡的,所有不需要按照1:1来划分空间,而是将内存分为一块较大的Eden区,和两个较小的Survivor空间,每次使用Eden和其中的一块survivor空间。当回收时,将Eden和survior空间上还存活的对象放入另一块survivor空间,清理掉Eden和刚刚使用的survivor区域。HotSpot区域默认的 Eden与survivor的大小比为8:1,也就是新生代每次占用90%的新生代空间进行内存分配,只有10%被浪费掉。当回收复制时,survivor空间不足时,就要依赖老年代进行分配担保。3 标记-整理算法
复制算法如果在对象存活率较高的情况下就要进行较多的复制操作,效率会变得很低。特别对应对象100%存活的极端情况,所以在老年代中,不宜采用复制算法。根据老年代的特点,提出了“标记-整理”算法。它的标记过程是跟前面的标记算法一样的,但是不是对可回收对象直接进行回收,而是让所有存活的对象都向一端移动,然后直接清理掉边界意外的内存空间。
4 分代收集
目前虚拟机都采用分代收集的算法,把java堆分为新生代和老年代。新生代中对象消亡的比较快,采用复制算法,老年代中对象存活率较高,采用标记-清除或标记整理进行回收
5 HotSpot的算法实现
5.1 枚举根节点
由前面可知,确定对象是否存活源于GCROOT链。可以作为GCROOts的节点主要在全局性的引用(例如常量或静态属性)与执行上下文(如栈帧中的本地变量表)中,然后这种遍历式地检查引用会消耗很多时间。
另外这个过程是对时间敏感的,需要保证这个检查结果建立在GC停顿上,也就是这项分析工作在一个确保一致性的快照中进行。就好像系统冻结在某一时刻一样,不能随着分析进行,对象的引用关系还在发生变化,这样就无法保证准确性。正是因为这点的考虑,GC必须停顿Java的所有线程(stop-the -world),及时在CMS收集器中,这个GC停顿也是必须的。
为了能够快速的在系统停顿后检查引用关系,在HotSpot中实现了一个OopMap的数据结构来达到这个目的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的的数据计算出来,在JFT编译的时候,也会在特定的位置记录下栈和寄存器中哪些为位置是引用。这样GC扫描就可以直接得到这种信息
5.2 安全点
前面提到在特定的位置记录下引用信息放入OopMap中,这个特定的位置就是安全点。程序只有到达安全点的时候,才可以发生GC停顿。安全点的选择要合理,太少可能导致GC停顿时间过长,太多将会导致GC停顿频繁发生,一般来讲在方法调用、循环跳转、异常跳转时,才产生安全点
还有一个问题就是,如何让GC发生时,所有程序都“跑到”安全点停顿下来。有两种解决方案
- 抢先式中断,GC发生时,所有线程全部中断。如果发生有线程不在安全点上,就恢复线程,让它跑到安全点停顿下来,现在机会没有虚拟机采用这种方式
- 主动式中断,为每个线程设置一个标志,线程主要去轮询这个标志,标志为真时把自己挂起,这个标志就是与安全点重合的位置。这样保证了挂起的时刻永远在安全点。
5.3 安全区域
安全点似乎解决了程序如何进入GC的问题,但是这只针对于“跑着”的线程是有用的,如果线程本身已经处于sleep或者blocked状态,这个线程就无法响应JVM中断请求,这时就需要安全区。
安全区是指一段代码中,引用关系不会发生变化的区域。在这个区域中开始GC是安全的。当线程代码进入到安全区以后,这段时间内发生的GC就不管进入安全区的线程。当线程想要离开安全区的时候,要检查系统是否处于GC状态。如果在发生GC,就要等待系统发生可以离开安全区的信号。