学习JVM(3)——垃圾回收机制

前面介绍了JVM的内存模型和类的加载机制,这一篇我们来说说JVM的垃圾回收机制。Java语言产生之前,程序员们写的最多的是C或者C++的程序,C++是在C的基础上提供了面向对象的特性,但是C++在创建对象时需要不断地去内存开辟空间,当对象使用完毕的时候需要不断地去释放空间,这些重复的操作就使人们思考,能不能写一段程序来实现这些操作,用到的时候直接调用,实现代码复用。1960年,基于MIT的Lisp首先提出了垃圾回收的概念,用于处理C语言等不停的析构操作,所以GC(Garbage Collection,垃圾回收)的历史是相当悠久的。从那至今,无论GC如何发展,始终要回答下面三个问题:

  1. 哪些内存需要回收?
  2. 什么时候进行回收?
  3. 如何进行垃圾回收?

1. 哪些内存需要进行回收?

前面在JVM的内存模型,我们提到垃圾回收只发生在线程共享的两个区域——堆和方法区(HotSpot虚拟机中的永久代),主要是发生在堆。
根据Java虚拟机规范的规定,方法区无法满足内存分配需求时,也会抛出OutOfMemoryError异常,虽然规范中确实说过可以不要求虚拟机在方法区实现垃圾回收,因为在方法区进行垃圾回收的“性价比”一般比较低,而在堆中,尤其是在新生代,常规应用进行一次垃圾回收,一般可以回收70%~95%的空间,而永久代(方法区)的垃圾回收效率远低于此。但这并不代表方法区没有垃圾回收,下面简单说一下永久代的垃圾回收。
永久代的垃圾回收主要有两部分:废弃常量和无用的类。
废弃的常量主要包括两大类:字面量和符号引用。字面量比较接近Java语言中的常量概念。回收废弃常量与回收Java堆中的对象非常相似。以常量池中字面量的回收为例。假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果在这时候发生内存回收,而且必要的话,这个“abc”常量就会被清理出常量池。其中包括文本字符串、被声明为final的常量值等,而符号引用属于编译方面的概念。常量池中的其他类(接口)、方法、字段的符号引用也与此类似,包括:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
判断一个常量是否是“废弃常量”比较简单,而要判断一个类是否是“无用的类”的条件则相对苛刻了许多。类需要同时满足下面3个条件才能算是“无用的类”:

  1. 该类所有的实例都已经被回收,也就是说java堆中不存在该类的任何实例;
  2. 加载该类的ClassLoader已经被回收了;
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是可以,而不是和对象一样,不使用了就必然会回收。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class、-XX:+TraceClassLoading以及-XX:+TraceClassUnLoading查看类的加载和卸载信息。
在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
Java的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java自动内存管理最核心的功能是堆内存中对象的分配与回收。Java中的垃圾回收一般是在Java堆中进行,因为堆中几乎存放了Java中所有的对象实例。

2. 什么时候进行回收?

Java堆中存放着几乎所有的对象实例,垃圾收集器在堆进行回收前,首先需要确定哪些对象还“活着”,哪些已经“死亡”,也就是不会被任何途径使用的对象。

1. 引用计数法

引用计数法实现简单,效率较高,在大部分情况下是一个不错的算法。其原理是:给对象添加一个引用计数器,每当有一个地方引用该对象时,计数器加1,当引用失效时,计数器减1,当计数器值为0时表示该对象不再被使用。需要注意的是:引用计数法很难解决对象之间相互循环引用的问题,主流JVM没有选用引用计数法来管理内存。所谓对象之间的相互引用,比如说有两个对象objA和objB,除了对象objA和objB相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为相互引用对方,导致它们的引用计数器都不为0,于是引用计数算法无法通知GC回收器回收它们。

2. 可达性分析算法

这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
GC Roots包括:

  • 虚拟机栈(栈帧中的本地变量表)中的引用对象
  • 方法区中的静态属性实体引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(Native方法)引用的对象

无论是通过引用计数法判断对象引用数量,还是通过可达性分析算法判断的引用链是否可达,判定对象的存活都与“引用”相关。
JDK1.2之前,Java中引用的定义很传统:如果reference类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。
JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种。
在java中,对引用的概念简述如下(引用强度依次减弱):

  • 强引用:这类引用是Java程序中最普遍的,类似“Object object = new Object()”这种,只要强引用还存在,即使内存空间不足,JVM要抛出OutOfMemoryError,使程序异常终止,垃圾收集器也不会回收掉被强引用的对象。
  • 软引用:用来描述一些非必须的对象,在系统内存不够使用时,这类对象会被垃圾收集器回收,JDK提供了SoftReference类来实现软引用。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JVM就会把这个软引用加入到与之关联的引用队列中。
  • 弱引用:用来描述一些非必须的对象,只要发生GC,无论内存是否够用,这类对象就会被垃圾收集器回收,JDK提供了WeakReference类来实现弱引用。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,JVM就会把这个弱引用加入到与之关联的引用队列中。
  • 虚引用:与其他几种引用不同,它不影响对象的生命周期,如果这个对象是虚引用,则就跟没有引用一样,在任何时刻都可能会回收,JDK提供了PhantomReference类来实现虚引用。虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾回收的回收速度,可以维护系统的运行安全,防止内存溢出等问题的产生。

关于引用的代码相关示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ReferenceDemo {
public static void main(String[] arge) {
//强引用
Object object = new Object();
Object[] objects = new Object[100];

//软引用
SoftReference<String> stringSoftReference = new SoftReference<>(new String("SoftReference"));
System.out.println(stringSoftReference.get());
System.gc();
System.out.println(stringSoftReference.get()); //手动GC,这时内存充足,对象没有被回收

System.out.println();

//弱引用
WeakReference<String> stringWeakReference = new WeakReference<>(new String("WeakReference"));
System.out.println(stringWeakReference.get());
System.gc();
System.out.println(stringWeakReference.get()); //手动gc,这时,返回null,对象已经被回收

System.out.println();

//虚引用
//虚引用主要用来跟踪对象被垃圾回收器回收的活动。
//虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。
//当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中
ReferenceQueue<String> stringReferenceQueue = new ReferenceQueue<>();
PhantomReference<String> stringPhantomReference = new PhantomReference<>(new String("PhantomReference"), stringReferenceQueue);
System.out.println(stringPhantomReference.get());
}
}

需要注意的是,要真正宣告一个对象死亡,至少要经历两次标记过程 :

  • 如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法
  • 当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行
  • 如果该对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()方法
  • finalize()方法是对象逃脱死亡命运的最后一次机会(因为一个对象的finalize()方法最多只会被系统自动调用一次), 稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中让该对象重引用链上的任何一个对象建立关联即可
  • 而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉

3. 如何进行垃圾回收?

1. 垃圾收集算法

1. 标记-清除(Mark-Sweep)算法

这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下所示:

标记-清除算法采用从GC Roots集合进行扫描,对存活的对象进行标记,标记完毕后,再扫,再扫描整个空间中未被标记的对象,进行回收,如上图所示。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效。它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

2. 复制(Copying)算法

复制算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。具体过程如下所示:

复制算法从GC Roots集合扫描,并将这一半空间中的存活对象复制到另一半没有使用过的空间中,这种算法当空间存活的对象比较少时,极为高效,但是带来的成本是需要一块内存交换空间用于进行对象的移动。

3. 标记-整理(Mark-Compact)算法

标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但后续不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉边界以外的内存。具体过程如下所示:

4. 分代收集算法

GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。当前JVM的垃圾收集都采用分代收集算法。
“分代收集(Generational Collection)”算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
年轻代:在新生代中,由于对象生命周期非常短暂,所以每次垃圾回收的时候都会有大量的对象死去,只有少量存活,这样,采用”复制算法”,就只需要付出少量存活对象的复制成本,就能完成回收。是所有新对象产生的地方,年轻代被分为3个部分(Eden区和两个Survivor区,也叫From和To),复制算法中提到将空间对半平分,但是实际上在分代收集算法中是将新生代划分为一块较大的Eden区和两块较小的Survivor区(比例一般为8:1:1),如下图所示,当Eden区空间不足时,就会执行Minor GC,并把所有存活的对象转移到其中一个Survivor区(From),Minor GC同样会检查存活下来的对象,将存活的对象转移到另一个Survivor区(To),并将年龄加1,这样在一段时间内,总会有一个空闲的Survivor区,当达到一定阈值时,就将对象放到老年代,需要注意的是,Survivor两个区是对称的,没先后关系,from和to是相对的。
老年代:老年代的对象生命周期长,存活几率是比较高的,而且没有额外的空间对它进行分配担保。所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾回收。在年轻代中经历了N次回收后仍然没有被清除的对象,就会被放到老年代中,对于老年代,会执行Major GC来清理,在某些情况下,会触发Full GC来清理整个堆内存。
JDK1.8的堆内存示意图:

从上图可以看出堆内存分为新生代、老年代和元空间。新生代又被进一步分为:Eden区+S0区+S1区。值得注意的是,在JDK1.8之前有永久代,在JDK1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。
Minor GC:从年轻代空间(包括Eden和Survivor区)回收内存
Major GC:清理老年代
Full GC:清理整个堆空间,包括年轻代和老年代

2.分代收集算法的执行策略

1. 对象优先在Eden区生成

目前主流的垃圾收集器都会采用分代回收算法,因此需要将堆内存分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
大多数情况下,对象在新生代中Eden区分配,年轻代被分为3个部分。当Eden区没有足够的空间进行分配时,虚拟机将会发起一次Minor GC。

  • 新生代GC(Minor GC):指发生新生代的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
  • 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC经常会伴随至少一次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上。

    2. 大对象直接进入老年代

    大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。
    原因是:为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。

    3. 长期存活的对象将进入老年代

    既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。
    如果对象在Eden出生并经过第一次Minor GC后仍然能够存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设置为1,对象在Survivor中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代。对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置。

    4. 动态对象年龄判定

    为了更好的适应不同程序的内存情况,虚拟机不是永远要求对象年龄必须达到了某个阈值才能进入老年代,如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需达到要求的年龄。

    3. 常见的垃圾收集器

    如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
    下面介绍一些常见的垃圾收集器,这些收集器各自有各自的特点,没有能适用于所有场景的完美的垃圾收集器,我们能做的就是根据具体的应用场景选择最合适的垃圾收集器。

    1. Serial收集器

    Serial(串行)收集器是最基本、历史最悠久的垃圾收集器,JDK1.3之前广泛使用,目前也是ClientVM下ServerVM 4核 4GB以下机器的默认垃圾收集器。它是单线程收集器,单线程不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作时必须暂停其他所有的工作线程(Stop the world),直到它收集完成。
    新生代采用复制算法,老年代采用标记-整理算法。
    参数控制:-XX:+UseSerialGC
    图示:

    虚拟机的设计者们当然知道Stop the world带来的不良用户体验,所以在后续的设计中停顿的时间在不断的缩短,但是仍然有停顿。
    但是Serial收集器也有优于其他垃圾收集器的地方,与其他收集器的单线程相比,它简单而高效。Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。
    串行回收方式适合低端机器,是Client模式下的默认收集器,对CPU和内存的消耗不高,适合用户交互比较少,后台任务较多的系统。Serial收集器默认新老生代的回收器搭配为Serial+SerialOld。

    2. ParNew收集器

    ParNew收集器其实就是多线程版本的Serial收集器。新生代并行,老生代串行;新生代复制算法,老年代标记-整理算法。
    参数控制:-XX:+UseParNewGC ParNew收集器
    -XX:ParallelGCThreads 限制线程数量
    图示:

    同样有Stop the World的问题,它是多CPU模式下的首选回收器(该回收器在单CPU的环境下回收效率远远低于Serial收集器),也是Server模式下的默认收集器。除了Serial收集器外,只有它能与CMS收集器(真正意义上对的并发收集器)配合工作。

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

  • 并发(Concurrent):指用户线程与垃圾收集器线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续执行,而垃圾收集器运行在另一个CPU上。

    3. Parallel Scavenge收集器

    Parallel Scavenge收集器类似于ParNew收集器。Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例。
    新生代复制算法,老生代标记-整理算法。
    参数控制:-XX:+UseParallelGC 使用Parallel收集器+老年代串行。
    图示:

    吞吐量=程序运行时间/(JVM执行回收时间+程序运行时间),假设程序运行了100分钟,JVM的垃圾回收占用1分钟,那么吞吐量就是99%。在当今网络发达的今天,良好的响应速度就是提升用户体验的一个重要指标,多核并行云计算的发展要求程序尽可能的使用CPU和内存资源,尽快的计算出最终结果,因此在交互不多的云端,比较适合使用该回收器。

    4. ParallelOld收集器

    ParallelOld是老生代并行处理器的一种,使用标记整理算法,是老生代吞吐量优先的一个收集器。这个收集器是JDK1.6之后引入的一款收集器。早期没有ParallelOld之前,吞吐量优先的收集器老生代只能使用串行回收收集器,大大的拖累了吞吐量优先的性能,自从JDK1.6之后,才能真正做到较高效率的吞吐量优先。
    参数控制:-XX:+UseParallel01dGC 使用Parallel收集器+老年代并行
    图示:

    5. SerialOld收集器

    SerialOld是老生代Client模式下的默认收集器,单线程执行;在JDK1.6之前也是ParallelScvenge回收新生代模式下老年代的默认收集器,同时也是并发收集器CMS回收失败后的备用收集器。
    图示:

    6. CMS收集器

    CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
    CMS收集器是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
    从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种“标记-清除”算法实现的,它的运行过程相比于前面几种垃圾收集器来说更加复杂一些。
    整个过程分为四个步骤:
  1. 初始标记(initial mark):暂停所有的其他线程,并记录下直接与GC Roots相连的对象,速度很快;
  2. 并发标记(concurrent mark):同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束的时候,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录发生这些发生引用更新的地方;
  3. 重新标记(remark):重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短;
  4. 并发清除(concurrent sweep):开启用户线程,同时GC线程开始对未标记的区域做清扫。

参数控制:-XX:+UseConcMarkSweepGC 使用CMS收集器
-XX:+UseCMSCompactAtFullCollection Full GC之后,进行一次碎片整理,整理过程是独占的,引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)
图示:

优点:并发收集、低停顿
缺点:产生大量空间碎片,并发阶段会降低吞吐量,对CPU资源敏感,无法处理浮动垃圾

7. G1收集器

G1(Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器,以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。被视为JDK1,7中HotSpot虚拟机的一个重要进化特征,是为了替代CMS收集器。与CMS收集器相比G1收集器有以下特点:

  1. 空间整合,G1收集器采用标记-整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
  2. 可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征。
  3. 并行与并发,G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop the World停顿时间。部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  4. 分代收集,虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。

上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大区别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。
G1的新生代收集跟ParNew类似,当新生代占用达到一定比例时,开始触发收集。和CMS类似,G1收集器收集老年代对象会有短暂停顿。
收集步骤如下:

  1. 标记阶段,首先初始标记(Initial-Mark),这个阶段是停顿的(Stop the World),并且会触发一次Minor GC。对应GC log: GC pause(young) (inital-mark)
  2. Root Region Scanning,运行程序过程中会回收Survivor区(存活到老年代),这一过程必须在young GC之前完成。
  3. Concurrent Marking,在整个java堆中进行并发标记(和应用程序并发执行),此过程可能会被young GC中断,若发现区域对象中的所有对象都是垃圾,那个区域就会被立即回收,同时,并发标记过程中,会去计算每个区域的对象活性(区域中存活对象的比例)。
  4. Remarl,再标记,会有短暂停顿(STW),是用来收集并发标记阶段,产生新的垃圾(并发阶段和应用程序一同执行),G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning(SATB)
  5. Copy/Clean Up,多线程清除失活对象,会有STW,G1将回收区域的存活对象拷贝到新的区域,清除Remember Sets,并发清空回收区域,并把它返回到休闲的区域链表中。
  6. 复制/清除过程之后,回收区域的活性对象已经被收集器回收到“最近复制的年轻代”(recently copied in young generation)和“最近复制的老年代”(recently copied in old generation)区域中了。

G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。这种使用Region划分内存空间以及优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

8. 常见的垃圾收集器组合

新生代GC策略 老年代GC策略 说明
组合1 Serial Serial Old Serial和Serial Old都是单线程进行GC,特点就是GC时暂停所有应用线程。
组合2 Serial CMS+Serial Old CMS(Concurrent Mark Sweep)是并发GC,实现GC线程和应用线程并发工作,不需要暂停所有应用线程。另外,当CMS进行GC失败时,会自动使用Serial Old策略进行GC。
组合3 ParNew CMS 使用-XX:+UseParNewGC选项来开启。ParNew是Serial的并行版本,可以指定GC线程数,默认GC线程数为CPU的数量。可以使用-XX:ParallelGCThreads选项指定GC的线程数。
如果指定了选项-XX:+UseConcMarkSweepGC选项,则新生代默认使用ParNew GC策略。
组合4 ParNew Serial Old 使用-XX:+UseParNewGC选项来开启。新生代使用ParNew GC策略,年老代默认使用Serial Old GC策略。
组合5 Parallel Scavenge Serial Old Parallel Scavenge策略主要是关注一个可控的吞吐量:应用程序运行时间 / (应用程序运行时间 + GC时间),可见这会使得CPU的利用率尽可能的高,适用于后台持久运行的应用程序,而不适用于交互较多的应用程序。
组合6 Parallel Scavenge Parallel Old Parallel Old是Serial Old的并行版本
组合7 G1GC G1GC -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC #开启
-XX:MaxGCPauseMillis =50 #暂停时间目标
-XX:GCPauseIntervalMillis =200 #暂停间隔目标
-XX:+G1YoungGenSize=512m #年轻代大小
-XX:SurvivorRatio=6 #幸存区比例

参考文档:

  1. GC算法 垃圾收集器
  2. 深入理解JVM–JVM垃圾回收机制
  3. 搞定JVM垃圾回收就是这么简单
  4. jvm - 垃圾回收