JVM垃圾回收算法
来自:后端开发技术
谈到Java不得不谈GC,谈到GC不得不谈垃圾回收算法
对象已死吗
在进行垃圾回收之前,第一件事就是判断哪些对象还存活着,哪些对象已死需要被回收。
1.引用计数算法
很多判断对象是否存活的算法是这样解答的:给对象中添加一个引用计数器,每当有个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1,任何时刻计数器为0的对象就是不可能再被使用的,该对象将被回收。
客观的说,引用计数法实现简单,效率高。但是没有主流JVM选择引用计数法管理内存,因为他无法解决循环引用的问题。例如a.objB=b,b.objA=a,
此时对象a、b的计数器永远至少为1,当两个对象都不在使用时,并不会置为0,所以难以被回收。并且,引用计数器要求在每次因引用产生和消除的时候,伴随一个加法操作和减法操作,对系统性能会有一定的影响。
2.可达性分析算法
主流JVM都是称通过可达性分析来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链( Reference Chain),当一个对象到GC Roots 没有任何引用链相连,用图论的话来说,就是从GC Roots 到这个对象不可达) 时,则证明此对象是不可用的。如图所示,对象object 5、object 6、object 7 虽然互相有关联,但是它们到GC Roots 是不可达的,所以它们将会被判定为是可回收的对象。
垃圾回收算法
1.引用技术算法
前面已经介绍
2.标记-清除算法
最基础的收集算法是“标记- 清除”(Mark-Sweep) 算法,如同它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象(可达性分析)。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。
不足:效率问题,标记和清除两个过程的效率都不高; 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
3.标记-整理算法(标记-压缩)
算法分为“标记”、“压缩”和“清除”三个阶段:首先标记出所有需要回收的对象,把所有存活的对象压缩到一段,然后清理掉端边界以外的内存。这样
将不会产生磁盘碎片。但是,压缩阶段占用了系统的消耗,并且如果标记对象过多的话,损耗可能会很大,在标记对象相对较少的时候,效率较高。
4.复制算法
为了解决效率问题,一种称为“复制”(Copying) 的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存话着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,未免太高了一点。
不足:对象较多时效率低,并且有一半的空间浪费。
5.分代收集算法
目前主流JVM垃圾收集都采用“分代收集”(Generational Collection) 算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保(如果有空间,通过分配担保机制进入老年代),就必须使用“标记一清理”或者“标记一整理”算法来进行回收。
对于新生代和老年代来说,通常新生代回收的频率很高,但是每次回收的时间都很短,而老年代回收的频率比较低,但是被消耗很多的时间。为了支持高频率的新生代回收,虚拟机可能使用一种叫做卡表的数据结构,卡表为一个比特位集合,每一个比特位可以用来表示老年代的某一区域中的所有对象是否持有新生代对象的引用,
这样以来,新生代GC时,可以不用花大量时间扫描所有老年代对象,来确定每一个对象的引用关系,而可以先扫描卡表,只有当卡表的标记为1时,才需要扫描给定区域的老年代对象,而卡表为0的所在区域的老年代对象,一定不含有新生代对象的引用。
卡表中每一位表示老年代4KB的空间,卡表记录为0的老年代区域没有任何对象指向新生代,只有卡表为1的区域才有对象包含新生代对象的引用,因此在新生代GC时,只需要扫面卡表为1所在的老年代空间,使用这种方式,可以大大加快新生代的回收速度。
6.分区算法
分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成不同小区间。每个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。
一般来说,在相同的条件下,堆空间越大,一次GC时所需要的时间就越长,从而产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割为多个小块,根据目标的停顿时间,每次合理的回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。