第五章 垃圾回收器和内存分配(一)垃圾回收篇
第五章 垃圾回收器和内存分配(一)垃圾回收篇
一心一意一件事:串行回收器
串行回收器是使用单线程进行垃圾回收的回收器。每次回收时,只有一个工作线程。串行回收器可以在新生代和老年代使用,根据不同的堆空间分为新生代串行回收器、老年代串行回收器。
新生代串行回收器
串行回收器是所有垃圾回收器中最古老的一种,也是JDK中最基本的垃圾回收器之一。主要有两个特点:
-
它仅仅使用单线程进行垃圾回收。 -
他是独占式垃圾回收方式。
串行回收器回收时,Java应用程序中的线程都需要暂停工作
,等待垃圾回收完成。
这种现象称为“Stop-The-World”。会造成非常糟糕的用户体验。
新生代串行回收器使用的是复制算法
,实现相对简单,逻辑处理特别高效且没有线程切换的开销。
使用下面参数可以指定使用新生代串行回收器或老年代串行回收器。
-XX:+UseSerialGC
当虚拟机在Client模式下运行时,它是默认的垃圾回收器。
新生代串行回收器输出日志如下:(使用-XX:+PrintGCDetails
)
表示一次垃圾回收前的新生代内存空间占用量和回收后的新生代内存占用量,以及垃圾回收所消耗的时间。
老年代串行回收器
老年代串行回收器使用的是标记压缩法
。同样也是一个串行、独占式的垃圾回收器。
由于老年代垃圾回收通常比新生代需要更多的时间,一旦老年代串行回收器启动,应用程序很可能会因此停顿较长时间。
老年代串行回收器可以和多种新生代回收器配合使用,同时,也是CMS回收器的备用收集器。
启用老年代串行回收器,参数如下:
-
-XX:+UseSerialGC
:新生代、老年代都使用串行回收器。 -
-XX:+UseParNewGC
:(JDK9、10已经删除,因为ParNew需要和CMS搭配工作,而CMS已经被G1替代,所以不再支持此参数):新生代使用ParNew回收器,老年代使用串行回收器。 -
-XX:+UseParallelGC
:新生代使用ParallelGC回收器,老年代使用串行回收器。
老年代串行回收器输出日志如下:
显示了回收前老年代和永久代的内存占用量,以及垃圾回收后老年代和永久代的内存占用量。
人多力量大:并行回收器
并行回收器可以使用多个线程同时进行垃圾回收,对于并行能力强的计算机,可以有效减少垃圾回收所需要的实际时间。
新生代ParNew回收器
ParNew回收器是一个工作在新生代
的垃圾回收器。只是简单地将串行回收器多线程化,它的回收策略、算法及参数和新生代串行回收器一样。
在单CPU或者并发能力较弱的系统中,并行回收器的效率不会比串行回收器好。
开启ParNew回收器使用如下参数:
-
-XX:+UseParNewGC
:(JDK9、10已经删除,因为ParNew需要和CMS搭配工作,而CMS已经被G1替代,所以不再支持此参数),新生代使用ParNew回收器,老年代使用串行回收器。 -
-XX:+UseConcMarkSweepGC
:(JDK9、10不建议使用,建议使用G1垃圾回收器),新生代使用ParNew回收器,老年代使用CMS回收器。
ParNew回收器工作时的线程数可以使用-XX:ParallelGCThreads
参数指定。一般建议与CPU数量相当,避免过多线程数量影响垃圾回收性能。默认情况下,当CPU数量小于8时,ParallelGCThreads的值等于CPU数量,当CPU数量大于8时,ParallelGCThreads的值等于3+( (5*CPU_COUNT) / 8)。
ParNew回收器的输出日志如下:
和新生代串行回收基本一样,只有回收器标识符不同。
新生代ParallelGC回收器
新生代ParallelGC回收器也是使用复制算法
的回收器。从表明上看,它和ParNew回收器一样,都是多线程、独占式的回收器。但是,ParallelGC回收器有一个重要的特点:非常关注系统的吞吐量。
新生代ParallelGC回收器使用如下参数启用:
-
-XX:+UseParallelGC
: 新生代使用ParallelGC回收器,老年代使用串行回收器。 -
-XX:+UseParallelOldGC
:新生代使用ParallelGC回收器,老年代使用ParallelOldGC回收器。
ParallelGC回收器提供了两个重要的参数用于控制系统的吞吐量。
-
-XX:MaxGCPauseMillis
:设置最大垃圾回收停顿时间。它的值是一个大于0的整数。ParallelGC在工作时,会调整Java堆大小或者其他参数,尽可能把停顿时间控制在MaxGCPauseMillis之内。 -
-XX:GCTimeRatio
:设置吞吐量大小。它的值是一个0到100之间的整数。假设值为n,那么系统将会花费不超过1/(1+n)的时间进行垃圾回收。假设 n = 19,则系统用于垃圾回收的时间不超过1/(1+19) = 5%。 默认情况下,它的取值是99,即不超过1/(1+99)=1%的时间用于垃圾回收。
除此之外,ParallelGC回收器与ParNew回收器另一个不同之处就是,它还支持自适应的GC调节策略。使用
-XX:+UseAdaptiveSizePolicy
可以打开自适应GC策略。在这种模式下,新生代大小、eden区和survivor区比例、晋升老年代的对象年龄等参数都会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。在手工调优比较困难的时候,可以直接使用自适应的方式,仅指定虚拟机最大堆、目标吞吐量(GCTimeRatio)、停顿时间(MaxGCPauseMillis),让虚拟机自己完成调优工作。
ParallelGC回收器输出日志如下:
显示了回收前内存大小、回收后内存大小、花费的时间。
注意:ParallelGC回收器关注系统吞吐量。可以通过
-XX:MaxGCPauseMillis
和-XX:GCTimeRatio
设置期望的停顿时间和吞吐量。但鱼和熊掌不可兼得,这两个参数是互相矛盾的,通常如果减少一次收集的最大停顿时间,就会同时减少系统吞吐量,反之亦然。
老年代ParallelOldGC回收器
老年代ParallelOldGC回收器也是一种多线程并发回收器,也是一种关注吞吐量的回收器,并且和ParallelGC新生代回收器搭配使用。
老年代ParallelOldGC回收器使用标记压缩法
,在JDK1.6及以后才可以使用。
使用-XX:+UseParallelOldGC
可以在新生代使用ParallelGC回收器,老年代使用ParallelOldGC回收器。在对吞吐量敏感的系统中,可以考虑使用。
参数-XX:ParallelGCThreads
也可以设置用于垃圾回收时的线程数。
显示了新生代、老年代、永久代回收前、后的情况,以及Full GC所消耗的时间。
一心多用都不落下:CMS收集器
与ParallelGC和ParallelOldGC不同,CMS回收器主要关注系统停顿时间,CMS是Concurrent Mark Sweep的缩写,意为并发标记清除,从名称上可以得知,它使用的是标记清除法
,同时,他还是一个多线程并发回收的垃圾回收器。
CMS主要工作步骤
CMS回收器的工作过程与其他垃圾回收器相比,略显复杂。
CMS工作时的主要步骤有:初始标记、并发标记、预清理、重新标记、并发清除、并发重置。
其中初始标记和重新标记是独占系统资源
的,而预清理、并发标记、并发清除、并发重置是可以和用户线程一起执行
的。从整体上说,CMS并不是独占式的。
初始标记、并发标记、重新标记都是为了标记出需要回收的对象。并发清理则是在标记完成后,正式回收垃圾对象。并发重置是指在垃圾回收完成后,重新初始化CMS数据结构和数据,为下一次垃圾回收做准备。
在CMS回收过程中,默认情况下,在并发标记后会有一个预清理的操作(该参数可以关闭预清理``-XX:-CMSPrecleaningEnabled` ),预清理是并发的,除了为正式清理做准备和检查,还会尝试控制一次停顿时间。由于重新标记是独占CPU的,如果新生代GC发生后,立即触发一次重新标记的话,那么一次停顿时间可能很长。为了避免这种情况,预处理会可以等待一次新生代GC的发生,然后根据历史性能数据预测下一次新生代GC可能发生的时间,在当前时间和预测时间的中间时刻进行重新标记。
这样可尽量避免新生代GC和重新标记重合,尽可能能减少一次停顿的时间。
CMS主要的参数
启用CMS回收器的参数是:-XX:+UseConcMarkSweepGC
。CMS是多线程回收器,所以设置合理的工作线程数量对系统性能有重要的影响。
CMS默认启动的并发线程数是(ParallelGCThreads+3)/4
。ParallelGCThreads表示GC并行时使用的线程数量,如果新生代使用ParNew,那么ParallelGCThreads也就是新生代GC的线程数量。
如果只有4个ParallelGCThreads时,那就只有1个并发线程,而有两个并发线程时,有5~8个ParallelGCThreads线程。
并发线程数量也可以通过-XX:ConcGCThreads
或者-XX:ParallelCMSThreads
参数手动指定。
注意:并发是指垃圾收集器和应用线程交替执行。并行是应用程序完全挂起,不存在交替执行。
因为CMS不是独占式回收器,在CMS回收过程中,应用程序还在不断的运行,在这时候又会不断的产生垃圾。这些新的垃圾在CMS回收过程中是无法清除的,同时,应用程序没有中断,所以在CMS回收过程中,还应该确保有足够的内存可用,因此CMS并不是等待内存饱和后才进行垃圾回收,而是当堆内存使用率达到某一阈值时,便开始回收,以确保在CMS工作中,依然有足够的内存可用。
这个回收阈值可以使用:-XX:CMSInitiatingOccupancyFraction
指定,默认是68。也就是当老年代使用率达到68%时,会执行回收。
由于CMS是使用标记清除法
,所以在垃圾回收后是有垃圾碎片的。
如果此时需要存放一个大对象,离散的内存空间可能无法存放,此时就会被迫进行一次垃圾回收,以换取一块可用的连续内存。
为了解决这个情况可以使用-XX:+UseCMSCompactAtFullCollection
参数,指定在CMS垃圾回收后,进行一次内存碎片整理,内存碎片整理不是并发进行
的。
-XX:CMSFullGCsBeforeCompaction
参数可以用于设定进行多少次CMS回收后,进行一次内存压缩。
注意:CMS回收器是一个关注停顿的垃圾回收器。
有关Class的回收
在使用CMS回收器时,如果需要回收Perm区,默认情况下还需要触发一次Full GC。
如果希望CMS回收Perm区,则必须打开-XX:+CMSClassUnloadingEnabled
参数,如果条件允许,系统会使用CMS来回收Perm区的Class数据。
未来我做主:G1回收器
G1回收器是在jdk1.7中正式使用的全新垃圾回收器,并且是jdk9及之后版本的默认回收器。从分代上看,G1 依然属于分代垃圾回收器,它会区分年轻代和老年代,依然有eden区和survivor区,但从堆的结构上看,它并不要求整个eden区、年轻代或者老年代都连续。G1使用了全新的分区算法,特点如下:
-
并行性
:G1在回收期间,可以由多个GC线程同时工作,有效利用多核计算能力。 -
并发性
:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,一般来说,不会在整个回收期间完全阻塞应用程序。 -
分代GC
:G1依然是一个分代回收器,但是和之前的回收器不同,它同时兼顾年轻代和老年代,其他回收器或者工作在年轻代,或者工作在老年代。 -
空间整理
:G1在回收过程中,会进行适当的对象移动,不像CMS,只是简单地标记清理对象,在若干次GC后,CMS必须进行一次碎片整理。而G1不同,它每次回收都会有效地复制对象,减少碎片空间。 -
可预见性
:由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,全局停顿也能得到较好的控制。
G1的内存划分和主要收集过程
G1将堆进行分区,划分为一个个的区域,每次回收时,只回收其中几个区域,以此来控制垃圾回收产生的一次停顿时间。
G1的回收过程可能有4个阶段:
-
新生代GC -
并发标记周期 -
混合回收 -
如果需要,可能会进行Full GC
G1的新生代GC
新生代GC的主要工作是回收eden区和survivor区。一旦eden区被占满,新生代GC就会启动。新生代GC只处理eden区和survivor区,回收后所有的eden区都应该被清空,而survivor区会被回收一部分数据,但是应该至少依然存在一个survivor区,类比其他新生代回收器,这一点没什么太大变化。另一个重要变化就是,老年代区域会增多,因为部分eden区或者survivor区对象会晋升到老年代。
新生代GC后,如果开启了PrintGCDetails
参数,就会看到下面部分日志信息。
从日志中可以看到,eden区从235MB直接清空,survivor区从5MB增长到11MB,整个堆合计为400MB,从回收前239MB下降到10.5MB。
G1的并发标记周期
G1 的并发阶段和 CMS 有点类似,它们都是为了降低一次停顿时间,而将可以和应用程序并发的部分单独提取出来执行。
并发标记周期可以分为以下几步:
-
初始标记
:标记从根节点直接可达的对象。这个阶段会伴随一次新生代GC, 它是产生全局停顿的,应用程序线程在这个阶段必须停止执行。 -
根区域扫描
:由于初始标记必然会伴随一次新生代GC,所以在初始化标记后,eden区被清空,并且存活对象被移入survivor区。在这个阶段,将扫描由survivor区直接可达的老年代区域,并标记这些直接可达的对象。 这个过程是可以和应用程序并发执行的,但是根区域扫描不能和新生代GC同时执行,因为如果恰巧在此时需要进行新生代GC,就需要等待根区域扫描结束后才能进行。如果发生这种情况,这次新生代GC的时间就会延长。 -
并发标记
:和CMS类似,并发标记将会扫描并查找整个堆的存活对象,并做好标记。 这是一个并发的过程,并且这个过程可以被一次新生代GC打断。 -
重新标记
:和CMS一样, 重新标记也是会产生应用程序停顿的。由于并发标记过程中,应用程序依然在运行,因此标记结果可能需要修正,所以在此对上一次的标记结果进行补充。在 G1 中,这个过程使用SATB(Snapshot-At-The-Beginning
)算法完成,即G1会在标记之初为存活对象创建一个快照,这个快照有助于加速重新标记的速度。 -
独占清理
: 这个阶段是会引起停顿的。它将计算各个区域的存活对象和GC回收比例,并进行排序,识别可供混合回收的区域。识别可供混合回收的区域。在这个阶段,还会更新记忆集(Remebered Set
)。该阶段给出了需要被混合回收的区域并进行了标记,在混合回收阶段需要这些信息。 -
并发清理
:这里会识别并清理完全空闲的区域。它是并发的清理,不会引起停顿。
由于并发标记周期包含一次新生代GC,故新生代会被清理,但由于并发标记周期执行时,应用程序仍在运行,所以并发标记周期后,又会有新的eden区空间被使用。并发标记周期执行前后最大的不同是在该阶段后,系统增加了一些标记为G
的区域,这些区域被标记,是因为他们内部的垃圾比例较高,希望在后续的混合GC中进行收集。这些将要被回收的区域会被G1记录在一个称为Collection Sets(回收集)的集合中。
下图可以看到,除了初始标记、重新标记和独占清理,其他几个阶段都可以和应用程序并发执行。
在并发标记周期中,G1会产生如下日志。
(1
)初始标记,它伴随着一次新生代GC
可以看到,初始化标记时,eden区被清空,并部分复制到survivor区。
(2
)一次并发的根区域扫描,并发扫描过程不能被新生代GC中断
根区域扫描不会引发停顿。
(3
)并发标记,并发标记可以被新生代GC中断
日志显示,并发标记被打断了三次。
(4
)重新标记,是会引起应用程序停顿
(5
)独占清理,独占清理会重新计算各个区域的存活对象,并可以得到每个区域的GC的效果(回收比)
(6
)并发清理,是并发执行的,根据独占清理阶段得出的每个区域存活对象量,直接回收不包含存活对象的区域
混合回收
在并发标记周期中,虽然有部分对象被回收,但是总体上说,回收的比例是相当低的。但是在并发标记周期后,G1已经明确知道哪些区域含有比较多的垃圾对象,在混合回收阶段就可以专门针对这些区域进行回收。
这个阶段叫做混合回收,是因为这个阶段既会执行正常的年轻代GC,又会选取一些被标记的老年代区域进行回收,它同时处理了新生代和老年代。
如下图:因为新生代GC的原因,eden区必然被清空,此外,有两块被标记为G的垃圾比例最高的区域被清理。被清理区域中存活的对象会被移动到其他区域,这样做的好处是减少垃圾碎片
。
混合GC会产生如下日志:
混合GC会执行多次,直到回收了足够多的内存空间,然后它会触发一次新生代GC。新生代GC后,又可能会发生一次并发标记周期的处理,最后又会引起混合GC的执行。
必要时的Full GC
和CMS类似,并发回收由于让应用程序和GC线程交替工作,总是不能完全避免在特别繁忙的场合出现在回收过程中内存不充足的情况。当遇到这种情况时,G1也会转入一个Full GC。
示例:当G1在并发标记时,由于老年代被快速填充,G1会终止并发标记而转入一个Full GC:
此外,如果在混合GC时空间不足,或者在新生代GC时survivor区和老年代无法容纳幸存对象,都会导致一次Full GC。
G1的日志
# 表示应用程序发生了一次新生代GC,这是在初始标记时发生的,耗时0.0018193秒,意味着程序至少暂停了0.0018193秒
[GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0018193 secs]
# 后续并行时间,表示所有GC线程总的花费时间,这里为0.9毫秒,workers为8表示有8个GC线程
[Parallel Time: 0.9 ms, GC Workers: 8]
# GC线程的执行情况,这里统计了8个线程的统计值,如 平均、最小、最大和差值(最大值与最小值之差),
# 106.6 表示在应用程序启动 106.6 毫秒后,启动了该GC线程
[GC Worker Start (ms): Min: 106.6, Avg: 106.7, Max: 106.7, Diff: 0.1]
# 根扫描时间的统计值
[Ext Root Scanning (ms): Min: 0.4, Avg: 0.4, Max: 0.5, Diff: 0.1, Sum: 3.3]
# 更新记忆集(Remember Set)的耗时。
# 记忆集是G1中维护的一个数据结构,简称RS。每一个G1区域都有一个RS与之关联。由于G1回收时是按照区域
# 回收的,比如在回收区域A的对象时,很可能并不回收区域B的对象,为了回收区域A的对象,要扫描区域B甚
# 至整个堆来判定区域A中哪些对象不可达,这样做的代价显然很大。因此,G1在区域A的RS中,记录了在区域
# A中被其他区域引用的对象,这样在回收区域A时,只要将RS视为区域A根集的一部分即可,从而避免做整个堆
# 的的扫描。由于系统在运行过程中,对象之间的引用关系是可能时刻变化的,为了更高效地跟踪这些引用关系,
# 会将这些变化记录在Update Buffers中。这里的Processed Buffers指的就是处理这个Update Buffers数据。
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
# 扫描RS的时间
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
# 在正式回收前,G1 会对被回收区域的对象进行疏散,即将存活对象放置在其他区域中,因此需要进行对象复制
[Object Copy (ms): Min: 0.2, Avg: 0.3, Max: 0.4, Diff: 0.2, Sum: 2.2]
# 给出GC工程线程终止的信息,这里的终止时间是线程花在终止阶段的耗时。在GC线程终止前,它们会检查其
# 它GC线程的工作队列,查看是否仍然还有对象引用没有处理完,如果其他线程仍然有没有处理完的数据,请求
# 终止的线程会帮助它尽快完成,随后再尝试终止。其中,Termination Attempts 展示了工作线程的终止次数。
[Termination (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 1.0]
[Termination Attempts: Min: 1, Avg: 2.9, Max: 6, Diff: 5, Sum: 23]
# 显示GC线程花费在其他任务中的耗时
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[GC Worker Total (ms): Min: 0.8, Avg: 0.8, Max: 0.9, Diff: 0.1, Sum: 6.6]
# GC 工作线程的完成时间
[GC Worker End (ms): Min: 107.5, Avg: 107.5, Max: 107.5, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
# 显示清空 CardTable的时间,RS 就是依靠CardTable来记录哪些是存活对象的
[Clear CT: 0.1 ms]
# 显示其他几个任务的耗时,比如选择CSet(Collection Sets,表示被选取的、将要被回收的区域的集合)的时
# 间、Ref Proc(处理弱引用、软引用的时间)、Ref End(弱引用、软引用入队时间)和Free CSet(释放被
# 回收的CSet中区域的时间,包括它们的RS)
[Other: 0.8 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.7 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 2048.0K(14.0M)->0.0B(13.0M) Survivors: 0.0B->1024.0K Heap: 13.8M(40.0M)->2656.1K(40.0M)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
G1相关的参数
对于G1,可以使用-XX:+UseG1GC
参数使用G1,对G1进行设置时,最关键的参数就是-XX:MaxGCPauseMillis
,它用于指定目标最大停顿时间。如果任何一次停顿时间超过这个值,G1就会尝试调整新生代和老年代的比例、堆大小、晋升年龄等。
对于性能调优来说,有时候鱼和熊掌不可兼得,如果停顿时间缩短,对于新生代来就要增加GC次数。对于老年代来说为了获得更短的停顿时间,在混合GC时,一次收集的区域数量也会变少,这样无疑增加了进行Full GC的可能性。
-XX:ParallelGCThreads
: 设置并行回收时GC的工作线程数量。
-XX:InitatingHeapOccupancyPercent
: 指定当整个堆使用率达到多少时,触发并发标记周期的执行,默认值是45.InitatingHeapOccupancyPercent
一旦设置,始终都不会被G1修改,这意味着G1不会试图改变这个值来满足MaxGCPauseMillis
的目标,如果InitatingHeapOccupancyPercent
设置的过大,那么引起FullGC的可能性也大大增加,反之,一个过小的InitatingHeapOccupancyPercent
值会使得并发标记周期执行非常频繁,大量GC线程抢占CPU,导致应用程序的性能有所下降。
其它参数
-
-XX:MaxTenuringThreshold
:设置从新生代到老年代需要晋升的年龄,默认15(每经历一次GC,如果没有被回收年龄则加1)。 -
-XX:TargetSurvivorRatio
:MaxTenuringThreshold参数并不是晋升的必要条件,没有达到指定年龄也会晋升,TargetSurvivorRatio
的作用就是设置survivor区目标使用率(默认50,即如果survivor区使用率超过50%,那么就很可能会使用较小的age作为晋升年龄)。
注意:对象的实际晋升年龄是根据survivor区的使用情况动态得到的。
MaxTenuringThreshold
只是设置年龄的最大值。
-
-XX:PretenureSizeThreshold
:表示对象直接晋升到老年代的阈值,单位是字节。只要对象大于指定值,就会直接在老年代分配。 这个参数只对串行回收器和ParNew有效,对于ParallelGC无效。默认下该值为0
,也就是不指定最大的晋升大小,一切由运行情况决定。