JAVA垃圾回收器与垃圾回收算法

时间:2022-12-27 21:41:31

1. 垃圾回收算法

比较出名的有:
标记-清除算法;
标记-整理算法;
复制算法;
分代收集算法。

垃圾回收算法,都首先要依赖一个基础算法,姑且称为标记算法,用于找出哪些对象可以回收。比较出名的是引用计数算法可达性分析算法

1.1 标记算法

1.1.1 引用计数算法

这是一个容易理解,又不实用的算法。他的做法如下:
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器减1;任何时刻计数器都为0的对象就是不可能再被使用的。
引用计数算法的实现简单,判断效率也很高,在大部分情况下它都是一个不错的算法。但是JVM中没有选用引用计数算法来管理内存,其中最主要的一个原因是它很难解决对象之间相互循环引用的问题。
举个例子:

/** 
* 执行后,objA和objB会不会被GC呢?
*/

public class ReferenceCountingGC {
public Object instance = null;

private static final int _1MB = 1024 * 1024;

/**
* 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否被回收过
*/

private byte[] bigSize = new byte[2 * _1MB];

public static void main(String[] args) {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;

objA = null;
objB = null;

//假设在这行发生了GC,objA和ojbB是否被回收
System.gc();
}
}

结果是,不会被回收,因为第一个对象,有2个引用:objA和对二个对象的instance。即使objA = null,对一个对象来说,还有instance引用在(objB = null不会导致第二个对象回收,因为它也有第一个对象的intance在引用)

1.1.2 可达性分析算法

在主流的商用程序语言中(Java和C#),都是使用可达性分析算法判断对象是否存活的。这个算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,下图对象object5, object6, object7虽然有互相判断,但它们到GC Roots是不可达的,所以它们将会判定为是可回收对象。
JAVA垃圾回收器与垃圾回收算法
在Java语言里,可作为GC Roots对象的包括如下几种:
a.虚拟机栈(栈桢中的本地变量表)中的引用的对象
b.方法区中的类静态属性引用的对象
c.方法区中的常量引用的对象
d.本地方法栈中JNI的引用的对象

1.2 回收算法

1.2.1 标记-清除算法Mark-Sweep

先标记出哪些对象需要回收,再做清理。没啥突出优点,缺点是,清除后会产生大量的内存碎片。具体如下:
JAVA垃圾回收器与垃圾回收算法

1.2.2 标记-整理算法Mark-Compact

先标记,再把所有存活的对象向一端移动,然后直接清理端边界意外的内存。
JAVA垃圾回收器与垃圾回收算法

1.2.3 复制算法

“复制”(Copying)的收集算法,它将可用内存按需要分成两块(实际情况不一定2块,块的大小比例不一定),每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

复制算法一般是使用在新生代中,因为新生代中的对象一般都是朝生夕死的,存活对象的数量并不多,这样使用复制算法进行拷贝时效率比较高。

突然说个新生代,可能有点懵逼,其实是2.6要说的分代回收算法,会对内存区域做一个划分:

将Heap 内存划分为新生代(Young Generation)与老年代(Tenured Generation);
此时又把方法区称为永久代Perm Generation);


新生代的目标就是尽可能快速的手机掉那些生命周期短的对象。老年代的目标是存大对象或声明周期长的对象。

将新生代划分为Eden(伊甸园) 与2块Survivor Space(幸存者区,分别叫From Survivor Space 和 To Survivor Space) 。

然后在Eden –>Survivor Space 以及From Survivor Space 与To Survivor Space 之间实行Copying 算法。 不过jvm在应用复制算法时,并不是把内存按照1:1来划分的,这样太浪费内存空间了。

实际情况是,按照8:1:1来划分,每次GC,把Eden 和某一个 Survivor的存活对象,复制到另一个Survivor,然后把Eden和那个Survivor清空。这样,意味着每次GC后,可以有90%的内存会被利用,只有10%浪费。

如果万一存活对象放不下那个Survivor,会找老年代借一下,称为分配担保。

下图给出内存的具体划分:
JAVA垃圾回收器与垃圾回收算法

具体工作流程如下:

在GC开始的前,对象只会存在于Eden区和From Survivor区,To Survivor区是空的。

紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置,默认15)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

对象年龄计算:没经过1次GC,年龄+1。

复制算法的优缺点:
优点:避免内存碎片产生,实现简单,运行高效。
缺点:1. 会浪费一部分内存空间; 2. 持续复制长生存期的对象则导致效率降低。

1.2.4 分代回收算法

当前商业虚拟机的垃圾收集器,大多支持分代收集算法(Generational Collection),这种算法其实没有新的思想,无非是利用前面的算法组合一下。

如上文所说,这种综合算法,把堆内存分为新生代和老年代。新生代用上面说的复制算法,老年代,因对象存活率高,也没有额外空间进行分配担保,就必须使用“标记-清理”或“标记-整理”算法。

2. 垃圾收集器

垃圾收集器是垃圾回收算法的具体实现,Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大的差别。
Sun HotSpot虚拟机1.6版包含了如下收集器:Serial、ParNew、Parallel Scavenge、CMS、Serial Old、Parallel Old。这些收集器以不同的组合形式配合工作来完成不同分代区的垃圾收集工作。1.7增加了G1收集器,可以说是收集器的最新研究成果。

2.1 各种收集器简介

2.1.1 Serial收集器

用于新生代的单线程收集器。用复制算法;垃圾收集的过程中会Stop The World(服务暂停)
优点:简单高效
缺点:垃圾收集的过程中会Stop The World(服务暂停),停顿时间长。

2.1.2 Serial Old收集器

用于老年代的单线程收集器。用标记-整理算法。

2.1.3 ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本。用于新生代,也是复制算法。

2.1.4 Parallel Scavenge收集器

新生代收集器,复制算法,几乎和ParNew没啥区别。但是,他主要追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。

2.1.5 Parallel Old收集器

Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先。用标记-整理算法

2.1.6 CMS(Concurrent Mark Sweep)收集器

高并发、低停顿,追求最短GC回收停顿时间。适合重视服务响应速度的应用(如网站),和 Parallel Old的追求目标相反。基于标记-清除算法

优点:并发收集、低停顿
缺点:产生大量空间碎片、并发阶段会降低吞吐量

2.1.7 G1(Garbage First)收集器

G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与CMS收集器相比G1收集器有以下特点:

  1. 并行并发,充分利用硬件的多核硬件优势,大大缩短Stop-The-World时间,使得GC时,Java程序可以继续执行。

  2. 空间整合,G1收集器从整理看,是基于标记整理算法,从局部(两个Region)看,是基于复制算法,但无论如何,都不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。

  3. 可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

  4. 分代收集,上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。

G1收集器原理比较大,可以参考此文:深入理解 Java G1 垃圾收集器

2.2 收集器总结

参考

  1. 深入理解JAVA虚拟机(周志明)
  2. java垃圾回收算法之-coping复制
  3. 新生代Eden与两个Survivor区的解释