在前一篇文章Java虚拟机简介里,我们对JVM作了简单的介绍。这篇文章将重点介绍JVM中的垃圾回收器(Garbage Collector)并讲解其工作机制。
第一章:垃圾回收简介
1.1 什么是自动垃圾回收
自动垃圾回收
(Automatic Garbage Collection)是一个在堆区(Heap Area)中寻找、识别哪些对象正在被使用,哪些对象没有被使用,并删除无用对象的过程。一个有用对象,或者说被引用对象,意味着程序的某些部分仍然保留着指向它的指针。一个无用对象,或者说未被引用对象,意味着不再被程序的任何部分引用。因此无用对象所使用的内存应该被回收。
在像C语言这样的程序中,分配和回收内存需要手动完成。在Java中,内存的回收由垃圾回收器来自动处理。下面我们来介绍自动回收的基本流程。
1.2 自动垃圾回收基本流程
第一步:标记 Marking
处理的第一步称作 标记
,在这步,垃圾回收器识将别哪些对象正在被使用,哪些对象没有被使用。

如图:标记之后,被引用的对象显示为蓝色,未被引用的对象显示为金色。在标记阶段将扫描所有对象以进行确定。如果必须扫描系统中的所有对象,这将是个十分耗时的过程。
第二步a:正常删除 Normal Deletion
正常删除过程将未被引用的对象删除,留下被引用的对象,以及指向可用空间的指针。

内存分配器将持有可用空间的引用,以分配给新的对象使用。
第二步b:压缩删除 Deletion with Compacting
为了进一步提升性能,也可以使用压缩删除。压缩删除除了删除未被引用的对象之外,还会把存活的对象移到一起,如下图所示。通过把引用对象压缩到一起,可减少内存的碎片化,提高内存可利用率,并使得新的内存分配更加快速且容易。

1.3 分代垃圾回收 Genrational Garbage Collection
为什么使用分代垃圾回收?
如前面所说的,在JVM中扫描、标记和压缩所有的对象是一件低效的事。随着越来越多的对象被分配,导致垃圾回收的时间越来越长。然而,根据对应用程序的经验分析已经表明,大多数对象的存活时间都很短。
以下图为例:Y轴表示分配的字节数,X轴表示随着时间的推移字节数的变化。

由上图可见:随着时间的推移,越来越少的对象仍保留在内存中。事实上大多数对象如图中左侧的值一样存活期都非常短。
JVM分代
上面的关于对象的内存分配行为可以用于提高JVM的性能。因此,堆区(Heap Area)被分为更小的几个部分或者说几代。这几部分分别是:新生代(Young Generation)、老年代(Old Generation)、永久代(Permanent Generation)。

- 新生代:新生代是所有新生对像被分配和老化的地方。当新生代被填满的时候,会触发一次
小型垃圾回收
(Minior Garbage Collection / minor GC)。在对象死亡率居高的情况下,新生代的死亡对象回收非常快。有些存活的对象会老化然后最终被移到老年代。- 世界停止事件(Stop the World Event):所有的
minor GC
都是Stop the World
事件。这意味着所有的应用线程都将被停止,直到垃圾回收完成。
- 世界停止事件(Stop the World Event):所有的
- 老年代:老年代用于存储长时间存活对象。通常,会给新生代设置一个阀值,当存活年龄达到该值时,对象就会被移入老年代。最终老年代也需要垃圾回收,这一事件被称为
大型垃圾回收
(Major Garbage Collection / major GC)。- 大型垃圾回收也是
Stop the World
事件。通常major GC
要慢得多,因为它会囊括所有存活的对象。因此对于响应式应用程序来说,应该尽量减少大型垃圾回收。同时请注意,major GC
的Stop the World
事件的持续时间受到老年代空间的垃圾回收器类型的影响。
- 大型垃圾回收也是
- 永久代:永久代包含JVM所需的类的元数据和相关描述信息,以及与类相关的信息,比如静态成员。另外,Java SE库的类和方法也可能被存在这里。
- 如果JVM发现一个类不再被需要,而又有其它的类需要空间,那么可能会回收(卸载)该类。
注意:由于永久代内存经常不够用或发生内存泄露,爆出异常
java.lang.OutOfMemoryError: PermGen
,自JDK8之后,永久代已经被元空间
(Metaspace)取代了。元空间的作用和永久代十分相似,最大的不同在于元空间使用的是本地内存,而非JVM内存。因此元空间的大小不受JVM内存的限制,仅受本地内存限制。
第二章:分代垃圾回收过程
通过前面的内容我们明白了为什么堆区要分成不同的世代以及它们之间的区别。现在,我们要探寻它们之间是如何交互的。
-
新生代分为三块空间:伊甸空间(Eden Space),from生存空间(“from” survivor space)以及 to生存空间(“to” survivor space)。任何新对象都会先被分配在
Eden
空间,两个Survivor
空间一开始都是空的。 -
当
Eden
空间被填满之后,会触发一次minor GC
。然后被引用的对象将会被移到第一个Survivor
空间,这里我们不妨称之为S0
。然后未被引用对象将被删除掉。如下图所示:
-
当下一次
Eden
空间被填满,再次触发minor GC
。同样,未被引用的对象被删除,被引用的对象被移到另一个Survivor
空间,我们不妨称之为S1
。要注意,连同之前处于S0
的被引用对象都会被移到S1
。这两个Survivor
空间是交替使用的。当被引用对象在这三块空间中移动一次,该对象的年龄就会增加一次,那么Survivor
空间里面就会存在不同年龄的对象。如下图所示:
-
上面的过程一直重复,生存下来的对象的年龄持续老化,当对象的年龄值达到一个既定的阀值之后,他们就会被从新生代晋升(即移动)到老年代。如下图所示,图中的晋升阀值以8为例:

- 随着
minor GC
持续触发,上面的过程持续重复,越来越多的对象会被移动到老年代。最终,将会触发老年代的major GC
和空间压缩。
以上就是分代垃圾回收的大致流程。
第三章:垃圾收集器配置
这章我们将介绍几种不同类型的垃圾回收器,以及选择及配置它们的命令行开关。
3.1 堆相关的参数配置
JVM中有许多可用的命令行开关,以下是几种常用开关:
开关(Switch) | 作用 | 备注 |
---|---|---|
-Xms | 设置Heap区的初始值 | |
-Xmx | 设置Heap区的最大值 | |
-Xmn | 设置新生代的值 | |
-XX:PermSize | 设置永久代的起始值 | jdk8及之后已经没有永久代,该配置无效 |
-XX:MaxPermSize | 设置永久代的最大值 | jdk8及之后已经没有永久代,该配置无效 |
-XX:MetaspaceSize | 设置元空间的初始值 | jdk8及之后才有该配置 |
-XX:MaxMetaspaceSize | 设置元空间的最大值 | jdk8及之后才有该配置 |
对于Android开发,这些参数可以在项目的
gradle.properties
文件里配置。
3.2 串行垃圾回收器 The Serial GC
串行回收器在JavaSE 5和6中是客户端机器的默认配置。使用该回收器,minor GC
和 major GC
都是串行完成的(使用单个虚拟CPU)。此外,它使用我们前面提到过的 标记压缩
(mark-compact) 回收方法。
使用场景:
-
对于大多数运行于客户端机器并且没有
低暂停时间要求
的应用程序来说,Serial GC
是首选的垃圾回收器。它仅使用单个虚拟处理器来进行垃圾回收工作。在当今的硬件上,Serial GC
仍能有效管理堆内存高达数百兆(MB)的大型应用程序,并在最坏情况下暂停相对较短的时间(一次完整的垃圾回收大约花几秒钟)。低暂停时间要求
:由于垃圾回收会暂停应用线程,即前面所说的会导致Stop the World
事件,所以暂停时间越短对用户来说体验就越好。
-
Serial GC
的另一种流行用法是在同一台机器上运行大量JVM的环境中(某些情况下,JVM比处理器更多)。在这种环境中,当JVM进行垃圾回收时,最好只使用一个处理器以最大程度地减少对其余JVM的干扰,即使这可能会延长垃圾回收的时间。 -
最后,随着具有低内存和少量内核的嵌入式硬件的兴起,
Serial GC
可能会卷土重来。
使用Serial GC的命令行开关为:
-XX:+UseSerialGC
3.3 并行垃圾回收器 The Parallel GC
并行垃圾回收器使用多个线程来处理新生代的垃圾回收。默认情况下,在拥有N个CPU的主机上,并行回收器将使用N个垃圾回收线程来工作。并行回收器工作时使用的线程数量可以使用如下命令行开关来控制:
-XX:ParallelGCThreads=<desired number>
在只有单个CPU的主机上,即使要求使用Parallel GC
,JVM仍然会使用默认的垃圾回收器。在有两个CPU的主机上,Parallel GC
和默认的垃圾回收器在性能上差别不大。在拥有两个以上CPU的主机上,使用Parallel GC
可以预期减短新生代的垃圾回收时间。并行垃圾回收器存在两种类型,我们会在使用场景中介绍。
使用场景:
并行回收器也称作 吞吐量回收器(throughput collector)
(且这样翻译),因为它能使用多个CPU以加快应用程序的吞吐速度。当有大量的工作需要完成,并且可以接受长时间的暂停时,就应该使用该回收器。例如:像打印报告或账单或者执行大量的数据库查询这类的批处理工作。
上面已经提到过,并行垃圾回收器存在两种类型,因此也有两个不同的命令行开关用于开启他们:
- -XX:+UseParallelOldGC :使用该命令行选项,你将得到一个多线程的新生代回收器和一个单线程的老年代回收器。该选项还会在老年代中执行单线程压缩。
- XX:+UseParallelOldGC :使用该选项,回收器既是新生代多线程回收器,也是老年代多线程回收器。它也是一个多线程压缩回收器。而由于新生代中采用的是复制回收的方式,所以没有压缩的必要,压缩只发生在老年代中。
3.4 并发标记扫描回收器 The Concurrent Mark Sweep Collector
并发标记扫描回收器
(CMS)用于回收老年代的对象。它试图与应用线程并发执行大多数垃圾回收工作,来最大程度地减少垃圾回收导致的应用线程暂停。默认情况下,该回收器不会复制或压缩存活的对象。
注意:CMS回收器在新生代上使用和并行回收器同样的回收算法
使用场景:
CMS垃圾回收器应使用于要求低暂停时间并且能和垃圾回收器共享资源的应用程序。比如需要响应事件的桌面UI应用程序,需要响应请求的网络服务器,或者响应查询的数据库。
CMS收集器的相关的命令行:
//设置使用
-XX:+UseConcMarkSweepGC
//设置使用的线程数
-XX:ParallelCMSThreads=<n>
3.5 G1垃圾回收器 The G1 Carbage Collector
Java7中提供了 G1垃圾回收器(Garbage-First Garbage Collector)
,它是一个服务器类型的垃圾回收器,适用于具有大内存的多处理器计算机。G1回收器是一个并行、并发且渐进压缩的低暂停时间的垃圾回收器,其布局和前面所述的垃圾回收器截然不同。(至于有怎样的不同,这篇文章就不讨论了)
使用G1回收器的命令行开关为:
-XX:+UseG1GC
本章只对几种垃圾回收器作简单的介绍,如果想进一步了解,不妨阅读官方的垃圾回收器文档:Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide
参考文档:Java Garbage Collection Basics
与其说参考,不如说该篇文章是翻译自上述文档,但也没有完全翻译。可能存在翻译或理解有误的地方,请不吝指教。