JVM 学习笔记(3) 垃圾回收与垃圾回收算法

垃圾回收与垃圾回收算法

如何确定垃圾

引用计数法
在 Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关联的引用, 即他们的引用计数都为 0, 则说明对象不太可能再被用到,那么这个对象就是可回收对象。

引用计数法存在的问题
但是Java并不采用引用计数法,原因如下:

jdk从1.2开始增加了多种引用方式:软引用、弱引用、虚引用,且在不同引用情况下程序应进行不同的操作。如果我们只采用一个引用计数法来计数无法准确的区分这么多种引用的情况。

上面这个原因并不是致命的,因为可以通过增加逻辑区分这几种情况,让引用计数法真正无法使用的是下面的原因。

如果一个对象A持有对象B,而对象B也持有一个对象A,那发生了类似操作系统中死锁的循环持有,这种情况下A与B的counter恒大于1,会使得GC永远无法回收这两个对象。这个就是循环引用问题。

可达性分析
为了解决此循环引用问题,Java 使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。 要注意的是,不可达对象不等价于可回收对象, 不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

什么对象可以作为GC roots呢?

虚拟机栈(栈帧中的本地变量表)中引用的对象;
方法区中类静态属性引用的对象;
方法区中常量引用的对象;
本地方法栈中JNI(即一般说的Native方法)引用的对象;
总结就是,方法运行时,方法中引用的对象;类的静态变量引用的对象;类中常量引用的对象;Native方法中引用的对象。

垃圾回收算法
标记清除法(Mark-Sweep)
最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。如图
JVM 学习笔记(3) 垃圾回收与垃圾回收算法

从图中我们就可以发现,该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。

复制算法(copying)
为了解决 Mark-Sweep 算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉,如图:

JVM 学习笔记(3) 垃圾回收与垃圾回收算法

这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话, Copying 算法的效率会大大降低。

标记压缩算法(Mark-Compact)
结合了以上两个算法,为了避免缺陷而提出。标记阶段和 Mark-Sweep 算法相同, 标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。如图:

JVM 学习笔记(3) 垃圾回收与垃圾回收算法

分代收集算法
根据 MinorGC 的过程(复制->清空->互换)可以知道新生代采用的是复制算法。因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照 1: 1 来划分新生代。一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用Eden 空间和其中的一块 Survivor To空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor From空间中

因为老年代每次只回收少量对象,所以老年代使用的是标记整理算法(Mark-Compact)

GC垃圾收集器

Serial 垃圾收集器(单线程、 复制算法)
ParNew 垃圾收集器(Serial+多线程)
Parallel Scavenge 收集器(多线程复制算法、高效)
Serial Old 收集器(单线程标记整理算法 )
Parallel Old 收集器(多线程标记整理算法)
CMS 收集器(多线程标记清除算法)
G1 收集器