当需要排查各种内存溢出,内存泄漏等问题时,当垃圾收集成为系统达到更高并发的瓶颈时,我们有必要深入GC的原理。
常见垃圾回收算法
在查看垃圾回收具体过程的时候,运行程序加上:
-XX:+PrintGCDetails打印详细的垃圾回收过程。
程序计数器,虚拟机栈,本地方法区是三个区域随着线程创建而创建,线程的销毁而销毁,不在垃圾回收的范围内。垃圾回收的区域主要集中在堆与方法区中。
引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效的时候,计数器就减1;任何时刻计数器为0的对象就是不可能再被使用的。
客观说:引用计数器实现简单,判定效率也足够高,在部分情况下是一个不错的算法。但JVM并没有使用引用计数法来管理内存。
可达性分析算法
主流的商业语言(Java,C#)都是通过可达性分析来判定对象是否存活的。
算法思路:通过一系列称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何的引用链,(也就是从GC Roots到这个对象不可达),那么证明此对象是不可用的。
如图:object5,object6,object7虽然互相有关联,但他们到GC Roots是不可达的,所以他们会被判定为可回收的对象。
Java中可作为GC Roots的对象:
- 虚拟机栈(栈帧中的本地变量表引用的对象)
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
标记-清除算法
如名字分为两个阶段:
标记:首先标记所有需要回收的对象,在标记完成之后统计回收所有被标记的对象,它的标记过程即为上面的可达性分析算法。
清除:清除所有被标记的对象
缺点:
- 效率不足,标记和清除效率都不高
- 空间问题,标记清除之后会产生大量不连续的内存碎片,导致大对象分配无法找到足够的空间,提前进行垃圾回收。
复制算法
将可用的内存按容量划分为大小相等的2块,每次只用一块,当这一块的内存用完了,就将存活的对象复制到另外一块上面,然后把已使用过的内存空间一次清理掉。
缺点:
- 将内存缩小了原本的一般,代价比较高
- 大部分对象是“朝生夕灭”的,所以不必按照1:1的比例划分。
现在商业虚拟机采用这种算法回收新生代,但不是按1:1的比例,而是将内存区域划分为eden 空间、from 空间、to 空间 3 个部分。
其中 from 空间和 to 空间可以视为用于复制的两块大小相同、地位相等,且可进行角色互换的空间块。from 和 to 空间也称为 survivor 空间,即幸存者空间,用于存放未被回收的对象。
在垃圾回收时,eden 空间中的存活对象会被复制到未使用的 survivor 空间中 (假设是 to),正在使用的 survivor 空间 (假设是 from) 中的年轻对象也会被复制到 to 空间中 (大对象,或者老年对象会直接进入老年带,如果 to 空间已满,则对象也会直接进入老年代)。此时,eden 空间和 from 空间中的剩余对象就是垃圾对象,可以直接清空,to 空间则存放此次回收后的存活对象。这种改进的复制算法既保证了空间的连续性,又避免了大量的内存空间浪费。
标记-压缩算法
在老年代的对象大都是存活对象,复制算法在对象存活率教高的时候,效率就会变得比较低。根据老年代的特点,有人提出了“标记-压缩算法(Mark-Compact)”
标记过程与标记-清除的标记一样,但后续不是对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。
这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
分代收集算法
根据对象存活的周期不同将内存划分为几块,一般是把Java堆分为老年代和新生代,这样根据各个年代的特点采用适当的收集算法。
新生代每次收集都有大量对象死去,只有少量存活,那就选用复制算法,复制的对象数较少就可完成收集。
老年代对象存活率高,使用标记-压缩算法,以提高垃圾回收效率。
最后
限于篇幅,先介绍常见的垃圾回收算法。