参考资料
- 《深入理解Java虚拟机》JVM高级特性与最佳实践 | 周志明
- 《Java虚拟机规范》Java SE 8版
- 小白JVM学习指南 | 陈树义Blog
- JVM核心技术32讲 | learn.lianglianglee.com
前言
本文大纲结构如下图所示。
什么是垃圾
如果一个对象不可能再被引用,那么这个对象就是垃圾,应该被回收。
那么,如何判断一个对象垃圾呢?有如下两种算法
- 第 1 代垃圾回收算法中,采用「引用计数法」
- 第 2 代垃圾回收算法中,采用「
GC Root Tracing
算法(或称为引用追踪算法)」
引用计数法
记录一个对象被引用的次数,当对象被引用时,计数加一;当对象被去除引用时,计数减一。这样就可以通过判断引用计数是否为零,来判断一个对象是否为垃圾。这种方法被称为「引用计数法」。
「引用计数法」存在一个问题,即「循环引用」问题。
- A 引用了 B,B 引用了 C,C 引用了 A,它们各自的引用计数都为 1。但这三个对象从未被其他对象引用,只有它们自身互相引用。
- 此时,对象 A、B、C 可认为是垃圾,但根据「引用计数法」,却无法将其判断为垃圾。
GC Root Tracing 算法
目前 Java 虚拟机判断一个对象是不是垃圾,采用的都是 GC Root Tracing
算法,该算法可以解决「引用计数法」中的「循环引用」问题。
GC Root Tracing
算法中,将对象划分为
- 可触达
- 根据引用情况,又分为强引用、软引用、弱引用、虚引用
- 可复活
- 不可触及
GC Root Tracing
算法中,定义了一个「垃圾收集根元素(GC Root
)」,从「垃圾收集根元素(GC Root
)」出发,所有可达的对象都是存活的对象,所有不可达的对象都是垃圾。
「垃圾收集根元素(GC Root
)」是一组活跃引用的集合,包括
- 局部变量(
Local variables
) - 活动线程(
Active threads
) - 静态域(
Static fields
) - JNI 引用(
JNI references
) - 其他对象
内存泄漏和内存溢出
- 内存溢出(OOM)是指可用内存不足。
- 内存泄漏(Memory Leak)是指本来无用的对象却继续占用内存,没有再恰当的时机释放占用的内存。
- 两者关系如下
- 如果存在严重的内存泄漏问题,随着时间的推移,则必然会引起内存溢出。
- 内存泄漏一般是资源管理问题和程序 Bug,内存溢出则是内存空间不足和内存泄漏的最终结果。
垃圾回收算法
垃圾回收算法,可分为 3 种
- 标记清除算法(Mark and Sweep)
- 标记复制算法,可简称为复制算法(Mark-Sweep-Copy)
- 标记压缩算法(Mark-Sweep-Compact)
3 种垃圾回收算法的对比如下表。
垃圾回收算法 | 优点 | 缺点 |
---|---|---|
标记清除算法 | 不需要移动太多对象 | 会产生空间碎片问题 |
标记复制算法 | 解决了空间碎片问题 | 将内存空间折半,且需要移动存活对象 |
标记压缩算法 | 解决了空间碎片问题 | 需要移动存活对象 |
标记清除算法
「标记清除算法」分为两个阶段
- 标记阶段
- 标记所有由 GC Root 触发的可达对象,所有未被标记的对象就是垃圾对象
- 清除阶段
- 清除所有未被标记的对象
「标记清除算法」会产生「空间碎片问题」。如果空间碎片过多,则会导致内存空间的不连续。虽然对大多数对象来说,也可以分配在不连续的空间中,但效率要低于连续的内存空间。
标记复制算法
「标记复制算法」的核心思想是将原有的内存空间分为两块,每次只使用一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中。之后清除正在使用的内存块中的所有对象,「标记复制算法」最后交换两个内存块的角色,完成垃圾回收。
「标记复制算法」的缺点是要将内存空间折半,极大地浪费了内存空间。
标记压缩算法
「标记压缩算法」是「标记清除算法」的优化版,该算法分为两个阶段
- 标记阶段
- 标记所有由 GC Root 触发的可达对象,所有未被标记的对象就是垃圾对象
- 压缩阶段
- 将所有存活的对象压缩在内存的一边,之后清理边界外的所有空间
垃圾回收的设计思想
上文中介绍了 3 种垃圾回收算法,每种算法均有各自的优点和缺点。若单独采用任何一种算法,最终垃圾回收的效率都不会太好。所以,在设计「垃圾回收机制」时,采用了下面两种思想。
- 分代思想
- 分区思想
分代思想
「分代思想」中,对 JVM 内存的不同区域,采用不同的垃圾回收算法。
- 对年轻代这种存活对象较少的区域,适合采用标记复制算法。这样只需要复制少量对象,便可完成垃圾回收,并且还不会有内存碎片。
- 对老年代这种存活对象较多的区域,适合采用标记压缩算法或标记清除算法。这样不需要移动太多的内存对象。
下面,对「年轻代中采用标记复制算法」做必要的说明。
标记复制算法的一种最简单实现便是折半内存使用,另一半备用。但实际上,年轻代(Young generation)并没有被等分为两部分,而是被划分了为 3 个部分
- 新生代(Eden space)
- 存活区 S0 (From Survivor 0 区)
- 存活区 S1 (To Survivor 1 区)
默认的虚拟机配置中,Eden : from : to
= 8:1:1。这个比例是 IBM 公司根据大量统计得出的结果。根据 IBM 公司对对象存活时间的统计,他们发现 80% 的对象存活时间都很短,是朝生夕死的,于是他们将新生代(Eden space)设置为年轻代的 80%,这样可以减少内存空间的浪费,提高内存空间利用率。在我们可以观察到的任何时刻,S0 和 S1 总有一个是空的。两个存活区一般较小,并不浪费多少空间。
分区思想
「分区思想」中,将整个堆空间划分成连续的不同小区间。每一个小区间都独立使用,独立回收。这样做的好处是,可以控制一次回收多少个区间,可以较好地控制 GC 时间。
垃圾回收的类型
垃圾回收有如下几种类型
Minor GC
(Young GC
)Major GC
(Old GC
)Full GC
下面将对这 3 种垃圾回收类型进行介绍,最后对 Stop-The-World
进行简要介绍。
Minor GC
从年轻代空间回收内存被称为 Minor GC
,有时候也称之为 Young GC
。
- 当 JVM 无法为一个新的对象分配空间时会触发
Minor GC
,比如当Eden
区满了。所以Eden
区越小,越频繁执行Minor GC
。 - 当年轻代中的
Eden
区分配满的时候,年轻代中的部分对象会晋升到老年代,所以Minor GC
后老年代的占用量通常会有所升高。 - 所有的
Minor GC
都会触发Stop-The-World
,停止应用程序的线程。
Major GC
从老年代空间回收内存被称为 Major GC
,有时候也称之为 Old GC
。
- 许多
Major GC
是由Minor GC
触发的,所以很多情况下将这两种 GC 分离是不太可能的。 - 分配对象内存时发现内存不够,会触发
Minor GC
。Minor GC
会将对象移到老年代中,如果此时老年代空间不够,就会触发Major GC
。
Full GC
Full GC
是清理整个堆空间,包括年轻代、老年代和永久代(如果有的话)。因此, Full GC
可以说是 Minor GC
和 Major GC
的结合。
当准备要触发一次 Minor GC
时,如果发现年轻代的剩余空间比以往晋升的空间小,则不会触发 Minor GC
而是转为触发 Full GC
。 因为 JVM 此时认为,之前这么大空间的时候已经发生对象晋升了,那现在剩余空间更小了,那么很大概率上也会发生对象晋升。既然如此,那就直接来一次 Full GC
,整理一下老年代和年轻代的空间。
另外,在永久代分配空间时,若已经没有足够空间时,也会触发 Full GC
。
Stop-The-World
Stop-The-World
(全世界暂停),指的是在进行垃圾回收时因为标记或清理的需要,必须让所有执行任务的线程停止执行任务,从而让垃圾回收线程回收垃圾的时间间隔。
在 Stop-The-World
这段时间里,所有非垃圾回收线程都无法工作,都暂停下来。只有等到垃圾回收线程工作完成才可以继续工作。可以看出,Stop-The-World
时间的长短将关系到应用程序的响应时间。因此在 GC 过程中,Stop-The-World
的时间是一个非常重要的指标。
垃圾回收器
Java 虚拟机的垃圾回收器,可以分为 4 大类
- 串行回收器
- 并行回收器
- CMS 回收器
- G1 回收器
从串行回收器,到并行回收器、CMS回收器,再到 G1 回收器,垃圾回收器不断改进,使得垃圾回收效率不断提升。特别是「分区」思想诞生后,对于垃圾回收停顿时间的控制更加细腻,可以让应用有更完美的延时控制,保证更好的用户体验。
可使用下面参数指定使用的垃圾回收器。
-XX:+UseSerialGC
-XX:+UseParallelGC
-XX:+UseParallelGC -XX:+UseParallelOldGC
-XX:+UseConcMarkSweepGC
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+UseG1GC
串行回收器
串行回收器是指使用单线程进行垃圾回收的回收器,每次回收时只有一个线程。
串行回收器进行垃圾回收时,会触发 Stop-The-World
现象,即其他线程都需要暂停,等待垃圾回收完成。
串行回收器可以在新生代和老年代使用。根据作用的堆空间,可分为
- 新生代串行回收器
- 使用标记复制算法
- 老年代串行回收器
- 使用标记压缩算法
并行回收器
并行回收器中,使用多线程进行垃圾回收,可有效缩短垃圾回收使用的时间。
根据作用内存区域的不同,并行回收器可划分为
- 新生代 ParNew 回收器
- 只是简单地将串行回收器多线程化,其回收策略、算法以及参数和新生代串行回收器都一样。
- 使用标记复制算法。
- 新生代 ParallelGC 回收器
- 和新生代 ParNew 回收器非常类似,同样使用标记复制算法,都是多线程、独占式的收集器,也会导致
Stop-The-World
。 - 和新生代 ParNew 回收器不同的是,新生代 ParallelGC 回收器有一个自适应 GC 调节策略,可以保证系统的吞吐量。
- 自适应 GC 调节策略中,可通过
-XX:MaxGCPauseMillis
设置垃圾回收的最大停顿时间;通过-XX:GCTimeRatio
设置吞吐量的大小(0~100 的值)。若GCTimeRatio
的值为n
,那么系统将花费不超过1/(1+n)
的时间用于垃圾回收。
- 和新生代 ParNew 回收器非常类似,同样使用标记复制算法,都是多线程、独占式的收集器,也会导致
- 老年代 ParallelGC 回收器
- 使用标记压缩算法
CMS 回收器
CMS 回收器(Mostly Concurrent Mark and Sweep Garbage Collector
),即最大并发—标记—清除—垃圾收集器,非常关注系统的停顿时间。
- 对年轻代采用并发标记复制算法
- 对老年代采用并发标记清除算法
CMS 回收器的设计目标是避免在老年代垃圾收集时出现长时间的卡顿,非常关注系统的停顿时间。CMS 回收器主要通过两种手段来达成此目标
- 不对老年代进行整理(这也造成了老年代内存碎片问题),而是使用空闲列表(
free-lists
)来管理内存空间的回收。 - 在
mark-and-sweep
(标记—清除)阶段的大部分工作,和应用线程一起并发执行。
也就是说,在标记—清除阶段并没有明显的应用线程暂停。但值得注意的是,它仍然和应用线程争抢 CPU 时间(CMS 回收器整个过程中将产生两次 Stop-The-World
)。默认情况下,CMS 使用的并发线程数等于 CPU 核心数的 1/4。
CMS 回收器的工作步骤包括
- 初始标记(第一次
Stop-The-World
) - 并发标记
- 并发预清理
- 可取消的并发预清理
- 最终标记(第二次
Stop-The-World
) - 并发清除
- 并发重置
G1 回收器
G1 回收器(Garbage-First
,即垃圾优先)是 JDK 1.7 中使用的全新垃圾回收器,从长期目标来看,其是为了取代 CMS 回收器。
G1 回收器拥有独特的垃圾回收策略,这跟之前所有垃圾回收器采用的垃圾回收策略不同。从「分代」看,G1 依然属于分代垃圾回收器。但它使用了「分区」算法,从而使得新生代(Eden 区)、存活区 S0、存活区 S1 和老年代等各块内存不必连续。
串行回收器、并行回收器、CMS 回收器中,内存分配都是连续的一块内存,如下图所示。
G1 回收器中,使用了「分区」算法,将一大块的内存分为许多细小的区块,不要求内存是连续的。
如上图所示,G1 回收器中,将一大块内存划分为许多细小的区块(Region
)。
- 每个区块(
Region
)被标记为 E、S、O 和 H。H
即Humongous
,表示该Region
存储的是巨型对象(humongous object
,H-obj)。 - G1 回收器中,当新建对象大小超过
Region
大小一半时,会直接在新的一个或多个连续Region
中分配,并标记为H
。
G1 收集器的工作阶段分为 4 个阶段
- 新生代 GC
- 并发标记周期
- 混合收集
- 如果需要,可能进行 FullGC
垃圾回收器的选择
综合来看,G1 回收器 是 JDK 11 之前 HotSpot JVM 中最先进的准产品级垃圾收集器。重要的是,HotSpot 工程师的主要精力都放在不断改进 G1 上面。在更新的 JDK 版本中,将会带来更多强大的功能和优化。G1 作为 CMS 的代替者出现,解决了 CMS 中的各种疑难问题,包括暂停时间的可预测性,并终结了堆内存的碎片化。
如果 CPU 资源不受限制,那么 G1 可以说是 HotSpot 中最好的选择,特别是在最新版本的 JVM 中。当然这种降低延迟的优化也不是没有代价的,由于额外的写屏障和守护线程,G1 的开销会更大。 如果系统属于吞吐量优先型的,又或者 CPU 持续占用 100%,而又不在乎单次 GC 的暂停时间,那么 CMS 是更好的选择。
总之,G1 适合大内存,需要较低延迟的场景。 对于内存大小的考量
- 若内存在 4G 以上,算是比较大,用 G1 的性价比较高。
- 若内存超过 8G,如 16G、64G 内存,非常推荐使用 G1 GC。
选择正确的 GC 算法和垃圾回收器,唯一可行的方式就是去尝试,并找出不合理的地方。一般性的指导原则如下
- 如果系统考虑吞吐优先,CPU 资源都用来最大程度处理业务,用 Parallel GC。
- 如果系统考虑低延迟有限,每次 GC 时间尽量短,用 CMS GC。
- 如果系统内存堆较大,同时希望整体来看平均 GC 时间可控,使用 G1 GC。
Java11、12中的垃圾回收器
JDK 8 的默认 GC 策略是并行 GC。G1 成为 JDK9 以后版本的默认 GC 策略,同时,ParNew
+ SerialOld
这种组合不被支持。
后续 JDK 版本,不断优化 GC,吸收了「Pauseless GC」设计思想,并引入了新的垃圾回收器。
- Java 11中引入了
ZGC(Zero GC)
- Java 12中引入了
Shenandoah GC
Pauseless GC
早在 2005 年,Azul Systems 公司的三位工程师就给出了非常棒的解决方案,在论文《无停顿 GC 算法(The Pauseless GC Algorithm)》中提出了「Pauseless GC
」设计。他们发现,低延迟的秘诀主要在于两点
- 使用读屏障
- 使用增量并发垃圾回收
Pauseless GC
算法主要分为 3 个阶段
- 标记(Mark)
- 重定位(Relocate)
- 重映射(Remap)
每个阶段都是完全并行的,而且每个阶段都是和业务线程并发执行的。
JDK 11 中引入的 ZGC 垃圾收集器,JDK 12 中引入的 Shenandoah GC
都借鉴并采用了 Pauseless GC
的设计思想。
Java 11中的ZGC
ZGC
即 Z Garbage Collector
,这是一款低停顿、高并发,基于小堆块(region)、不分代的增量压缩式垃圾收集器,平均 GC 耗时不到 2 毫秒,最坏情况下的暂停时间也不超过 10 毫秒。
ZGC
的主要特点如下
- GC 最大停顿时间不超过 10ms
- 堆内存支持范围广,小至几百 MB 的堆空间,大至 4TB 的超大堆内存(JDK 13 升至 16TB)
- 与 G1 相比,应用吞吐量下降不超过 15%
- 当前只支持 Linux/x64 位平台,预期 JDK14 后支持 macOS 和 Windows 系统
- ZGC 使用了两项关键技术,着色指针和读屏障
Java 12中的Shenandoah GC
Java 12 中引入了 Shenandoah GC
垃圾回收器(音译 “谢南多厄”,美国一个地名)。Shenandoah
是一款超低延迟垃圾收集器(Ultra-Low-Pause-Time Garbage Collector),其设计目标是管理大型的多核服务器上,超大型的堆内存。GC 线程与应用线程并发执行,使得虚拟机的停顿时间非常短暂。
Java 13对ZGC的改进
Java 13 对 ZGC 的改进,主要体现在下面几点
- 可以释放不使用的内存给操作系统
- 最大堆内存支持从 4TB 增加到 16TB
- 添加参数
-XX:SoftMaxHeapSize
来软限制堆大小
GC 线程数设置参考准则
- 若 CPU 核数
N
小于等于 8,推荐设置 GC 线程数ParallelGCThreads
等于 CPU 核数N
。 - 若 CPU 核数
N
大于 8,推荐设置 GC 线程数ParallelGCThreads
为于 CPU 核数N
的5/8
。
-XX:ParallelGCThreads= N*(5/8)
GC日志
GC日志相关参数
-verbose:gc
- 和其他 GC 参数组合使用,在 GC 日志中输出详细的 GC 信息,包括每次 GC 前后各个内存池的大小,堆内存的大小,提升到老年代的大小,以及消耗的时间。
- 此参数支持在运行过程中动态开关。
-XX:+PrintGCDetails
和-XX:+PrintGCTimeStamps
- 打印 GC 细节与发生时间。
-Xloggc:file
- 与
-verbose:gc
功能类似,只是将每次 GC 事件的相关情况记录到一个文件中。文件位置最好在本地,以避免网络的潜在问题。 - 若与
verbose:gc
命令同时出现在命令行中,则以-Xloggc
为准。
- 与
GC日志解读工具
可以借助第三方可视化工具来分析 GC 日志(当然,也可以直接阅读 GC 日志)。
- GCEasy 工具提供了下面 3 种产品
- GCEasy 是一款在线的 GC 日志分析工具,支持各种版本的 GC 日志格式
- FastThread 是一款 Java 线程分析工具(
Java Thread Dump Analyzer
) - HeapHero 是一款 Heap Dump 分析工具(
Heap Dump Analyzer
)
- GCViwer