一、串行回收器
串行回收器成熟、历经生产考验、极为高效
jdk中最基本的垃圾回收器之一
特点:
仅使用单线程进行垃圾回收
独占式回收:stop the world,回收时java应用程序中的线程都需要暂停等垃圾回收完成。
- 新生代串行回收
新生代串行回收器使用复制算法,实现相对简单、逻辑处理高效且没有线程切换的开销。
使用-XX:+UseSerialGC
参数可以指定使用新生代串行收集器和老年代串行收集器。当虚拟机在Client模式下运行时,它是默认的垃圾收集器。
输出GC日志格式为
[GC 0.844:[DefNew:]]
- 老年代串行回收器
老年代串行回收器使用标记压缩算法,也是串行的、独占式的垃圾回收器,回收比新生代使用更长时间。
可以与多种新生代回收器配合使用:
-XX:+UseSerialGC
:新、老都使用串行回收-XX:+UseParNewGC
:新生代使用ParNew
回收器,老年代使用串行-XX:+UseParalleelGC
:新生代使用ParallelGC
回收器,老年代使用串行[Full GC 0.844:[Tenured: xxxk->xxxk(xxxk)]]
二、并行回收器
使用多个线程进行垃圾回收,对于并行 能力强的计算机,可以有效缩减回收时间。
- 新生代ParNew回收器
将串行回收器多线程化,回收策略、算法、参数与新生代串行回收器一样(复制算法)。也独占,也STW,CPU并发强停顿时间就短,单CPU或CPU并发能力弱表现可能比串行回收还差。
开启使用以下参数:
-XX:+UseParNewGC
: 新生代使用ParNew回收器,老年代使用串行回收-XX:+UseConcMarkSweepGC
: 新生代使ParNew回收器,老年代使用CMS
回收线程数量可以使用-XX:ParallelGCThreads
参数指定。如-XX:ParallelGCThreads=8
一般最好与CPU数量相当,避免过多的线程数影响回收性能。
默认情况下当CPU数量小于8个时,
ParallelGCThreads
的值等于CPU数量。当CPU数大于8个时,ParallelGCThreads的值等于 3+((5*CPU_COUNT)/8)
日志格式:
[GC 0.834: [ParNew: xxxK -> xxxK(xxxK)]]
- 新生代ParallelGC回收器
新生代ParalelGC
回收器也是复制算法、多线程、独占式。
区别:
- 非常关注系统的吞量
使用方式:
-XX:+UseParallelGC
: 新生代使用ParallelGC
回收器,老年代使用串行回收器。-XX:+UseParallelOldGC
: 新生代使用ParallelGC
回收器,老年代使用ParallelOldGC
回收器。
控制系统吞量的两个重要参数,这两个参数相互矛盾的。
-XX:MaxGCPauseMilis
:设置最大垃圾回收停顿时间。值是一个大于0的整数,ParallelGC在工作时会调整Java堆大小或其他一些参数,尽可能地把停顿时间控制在MaxGCPauseMilis以内。- 如果把这个值设置的很小,为了达到预期的停顿时间,虚拟机可能会使用一个小较小的堆(小堆比大堆回收快),这将导致垃圾回收变得很频繁,从而增加了垃圾回收总时间,降低了吞吐量。
- 默认情况下,VM没有暂停时间目标值。GC的暂停时间主要取决于堆中实时数据的数量与实时数据量。
-XX:GCTimeRatio
:设置吞量大小。值是从0到100的整数,如果值设置为n,则系统垃圾收集时间不超过 1/(1+n),默认值为99(可以增加-XX:+PrintFlagsFinal
参数查看该值)
ParallelGC
回收器与ParNew
回收器另一个不同是,ParallelGC
可以使用 -XX:+UseAdaptiveSizePolicy
打开自适应GC调节策略。这种模式下,新生代大小、eden与survivior的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿之间的平衡点。手工调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标吞吐量和停顿时间,让虚拟机自己完成调优工作。这个参数的默认是开着的。
ParalelGC
的回收日志格式:
[GC[PSYoungGen: xxxK -> xxxK(xxxk)]
- 老年代ParallelOldGC回收器
也是多线程并发的收集器。
使用方式:-XX+UseParallelOldGC
回收日志格式:
[FULL GC [PSYoungGen:xxK->xxK(xxK)][ParOldGen:xxK->xxK(xxK)]]
三、CMS回收器
CMS回收器主要关注于系统的停顿时间。CMS是 Concurrent Mark Sweep的缩写,意为并发标记清除,使用的是标记清除算法,且多线程并行回收。
CMS 不会对新生代做垃圾回收,默认只针对老年代进行垃圾回收。此外,CMS 还可以开启对永久代的垃圾回收(或元空间),避免由于 PermGen 空间耗尽带来 Full GC。
- CMS主要工作步骤
CMS回收器主要步骤有:
初始标记
标记根对象
GC Roots 到底是什么东西呢,哪些对象可以作为 GC Root 呢?
虚拟机栈(栈帧中的本地变量表)中引用的对象
本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
并发标记
标记所有对象
预清理
是并发的。
为正式清理做准备和检查,还会尝试控制一次停顿时间。
- 刻意等一次新生代GC的发生,然后根据历史性能数据预测下一次新生代GC可能发生的时间,然后在当前旱和预测时间的中间时刻进行得新标记,从最大程序避免新生代GC和重新标记重合,尽量减少一次停顿的时间。
可以通过关闭开关
-XX:-CMSPrecleaningEnabled
不进行预清理。重新标记
并发清除
并发重置
并发重置是干嘛?
| 独占系统资源 | 可以和用户线程一起执行 | | ———— | ———————- | | 初始标记 | 预清理 | | 重新标记 | 并发标记 | | | 并发清除 | | | 并发重置 |
整体上说CMS收集不是独占式的,可以在应用程序运行过程中进行垃圾回收。
- CMS主要的设置参数
启用CMS回收器的参数是 -XX:+UseConcMarkSweepGC
CMS默认启动的并发线程数是((ParallelGCThreads+3)/4)
。ParallelGCThreads
表示GC并行时使用的线程数量 。
并发指收集器和应用线程交替执行,并行是指应用程序停止,同时由多个线程一起执行GC。因为并行回收器并不是并发的。因为并行回收器执行时,应用程序完全挂起,不存在交替执行的步骤。
由于CMS回收器不是独占式的回收器,在CMS回收过程中,应用程序仍工作且产生新垃圾,新生成的垃圾在当前CMS回收过程中是无法清除的。同时CMS回收过程中还要确保应用程序有足够的内存可用。因此,CMS回收器不会等堆内在饱和时才进行垃圾回收,而是当堆内存使用率达到某一阈值时便开始进行回收,以确保应用在CMS工作时依然有足够内存支持运行。
回收阈值可以使用 -XX:CMSInitiatingOccupancyFraction
来指定当老年代空间使用率到达多少时进行一次CMS垃圾回收。默认是68,即当老年代空间使用率达到68%时会执行一次CMS回收。如果应用程序的内在使用率增长很快,在CMS的执行过程中,已经出现了内存不足的情况,此时,CMS回收就会失效,虚拟机将启动老年代串行收集器进行垃圾回收。如果这样,应用程序STW直到垃圾回收完成,这时应用程序的停顿时间可能会较长。
因此,根据应用程序的特点,可以对-XX:CMSInitiatingOccupancyFraction
进行调优。如果内存增长缓慢,则可以设置一个稍微大的值来降低CMS的触发频率,减少老年代回收次数。相反,如果内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。
CMS基于标记清除算法,会造成大量内存碎片,回收后即使堆内存仍有很大剩余空间,也会被迫进行一次垃圾回收来换取可用的连续内存。为此CMS回收器还提供了几个用于内存压缩整理的参数。
-XX:+UseCMSCompactAtFullCollection
开关可以使CMS在垃圾收集完成后进行一次内在碎片整理内存碎片整理不是并发进行的。-XX:CMSFullGCsBeforeCompaction
参数可以用于设定进行多少次CMS回收后进行一次内存压缩。
- CMS日志分析
CMS回收的日志可以结合cms工作流程图来看。
这个日志仍是使用StopWorldTest.java
的代码跑的,代码及jvm参数在本节下面。
###初始标记
[GC (CMS Initial Mark) [1 CMS-initial-mark: 524103K(1048064K)] 524175K(1048512K), 0.0004341 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
###并发标记
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.118/0.150 secs] [Times: user=0.54 sys=0.03, real=0.15 secs]
###预清理
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.015/0.020 secs] [Times: user=0.04 sys=0.00, real=0.02 secs]
###重新标记
[GC (CMS Final Remark) [YG occupancy: 112 K (448 K)][Rescan (parallel) , 0.0007684 secs][weak refs processing, 0.0000362 secs][class unloading, 0.0011978
secs][scrub symbol table, 0.0006013 secs][scrub string table, 0.0002530 secs][1 CMS-remark: 530161K(1048064K)] 530274K(1048512K), 0.0029381 secs] [Times:
###并发清理
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.078/0.091 secs] [Times: user=0.18 sys=0.01, real=0.09 secs]
###并发重置
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.005/0.007 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
CMS回收器在运行时,还可能会输出如下日志
(concurrent mode failure): 1047294K->19809K(1048064K), 0.1619026 secs] 1047666K->19809K(1048512K), [Metaspace: 8794K->8794K(1056768K)], 0.1633398 secs] [Times: user=0.20 sys=0.01, real=0.16 secs]
这说明CMS回收器并发收集失败。这很可能由于应用程序在运行过程中老年代空间不够导致。如果频繁出现并发模式失败,就应该考虑进行调整预留较大老年代空间或设置一个较小的 -XX:CMSInitiatingOccupancyFraction
参数降低 CMS触发的阈值,使CMS在执行过程中仍有较大的老年代空闲空间供应用程序使用。
/**
* 使用的jvm参数如下
* -Xmx1g -Xms1g -Xmn512k -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:CMSInitiatingOccupancyFraction=50
*/
public class StopWorldTest {
public static void main(String args[]) {
MyThread t = new MyThread();
PrintThread p = new PrintThread();
t.start();
p.start();
}
public static class MyThread extends Thread {
HashMap map = new HashMap();
@Override
public void run() {
try {
while (true) {
// System.out.println((map.size() * 512) / 1024 / 1024);
if (map.size() * 512 / 1024 / 1024 >= 880) {
map.clear();
System.out.println("clean map");
}
byte[] b1;
for (int i = 0; i < 100; i++) {
b1 = new byte[512];
map.put(System.nanoTime(), b1);
}
Thread.sleep(1);
}
} catch (Exception e) {
}
}
}
public static class PrintThread extends Thread {
public static final long starttime = System.currentTimeMillis();
@Override
public void run() {
try {
while (true) {
long t = System.currentTimeMillis() - starttime;
System.out.println(t / 1000 + "." + t % 1000);
Thread.sleep(100);
}
} catch (Exception e) {
}
}
}
}
- Class回收
CMS回收器回收Perm区需要打开 -XX:+CMSClassUnloadingEnabled
开关,打开后如果条件允许,系统会使用CMS的机制回收Perm区的Class数据。输出日志如下
[class unloading, 0.0012854 secs][scrub symbol table, 0.0005998 secs][scrub string table, 0.0002410 secs][1 CMS-remark: 1026980K(1048064K)] 1027325K(1048512K), 0.0028075 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
四、G1回收器
G1回收器(Garbage-First)是JDK1.7正式使用的全新垃圾回收器。既分代(年轻代[eden区、survivor区]、老年代),但堆结构上不要求整个eden区、年轻代、老年代都连续。使用全新的分区算法。
G1收集器使用 -XX:+UseG1GC
开启
特点:
- 并行性:多GC线程同时工作
- 并发性:G1拥有与应用程序交替执行的能力 ,部分工作可以和应用程序同时执行,因此不会在整个回收期间完全阻塞应用程序。
- 分代GC:同时兼顾新生代和老年代。
- 空间整理:G1每次回收都会有效地复制对象,减少空间碎片。
- 可预见性:由于分区的原因,G1可以只选取部分区域进行内存回收,缩小回收的范围,对全局停顿有较好的控制。
G1的内存划分和主要收集过程
G1收集器将堆分区,划分为一个个的区域,每次收集时只收集其中的几个区域,以此来控制垃圾回收产生的一次停顿时间。
G1收集过程可能有4个阶段
- 新生代GC
- 并发标记周期
- 混合收集
- 如果需要可能会进行Full GC
- G1的新生代GC
新生代GC收集过程与其他新生代收集器比没什么变化。
sequenceDiagram
eden区 ->> survivor区: eden区部分对象移到survivor区后,eden区被清空
Note right of survivor区: survivor被收集一部分,至少还存在一个
eden区 ->> old区: 部分对象晋升到老年代
survivor区 ->> old区: 部分对象晋升到老年代
- G1并发标记周期
G1的并发阶段和CMS有些类似,都是为了降低一次停顿时间而将可以和程序并发的部分单独提取出来执行。
并发标记周期步骤:
- 初始标记
标记从根节点直接可达的对象, STW
GC Roots 到底是什么东西呢,哪些对象可以作为 GC Root 呢?
虚拟机栈(栈帧中的本地变量表)中引用的对象
本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
- 根区域扫描
由于初始标记必然伴随一次新生代GC,所以在初始化标记后,eden区被清空,存活的对象被移入survivor区。
在根区域扫描这个阶段,将扫描survivor区直接可达老年区的对象并标记。
tips:根区域扫描不能和新生代GC同时执行(因为根区域扫描依赖survivor区的对象,而新生代GC会修改这个区域),因些,如果恰七在些时需要进行新生代GC,GC就要等待根区域扫描结束后才能进行,如果发生这种情况,这次新生代的GC的时间就会延长。
- 并发标记
与CMS类似,扫描并查找整个堆的存活对象,并做好标记。是并发过程,并可以被一次新生代GC打断。
- 重新标记
与CMS一样,重新标记也STW。由于在并发标记过程中应用程序仍在运行,因此标记结果可能需要修正,所以在重新标记过程中对并发标记的结果进行补充。在G1中,这个过长使用SATB(SNAPSHOT-AT-THE-BEGINNING)算法完成,即G1在标记之初为存活对象创建一个快照,这个快照有用于加速重新标记的速度。
- 独占清理
这个阶段也STW,它将计算各个区域的存活对象和GC回收比例并进行排序,识别可供混合回收的区域。在这个阶段,还会更新记忆集(Remebered Set),该阶段给出了需要被混合回收的域名并进行了标记,给混合回收阶段使用。
记忆集是G1中维护的一个数据结构,简称RS。每个G1区域都有一个RS与之关联。由于G1回收时是按区域回收的,如回收区域A的对象时,很可能并不回收区域B的对象,此时,为了回收区域A的对象 ,要扫描区域B甚至是整个堆来判断区域A中哪些对象不可达,这样代价很大。因此,G1在区域A的RS中,记录了在区域A中被其他区域引用的对象,这样在回收区域A时,只要将RS视为区域A根集中的一部分即可,从而避免做整个堆的扫描。由于系统在运行过程中,对象之间的关系不停变化 ,因此,为了更高效的跟踪这些引用关系,会将这些变化 记录在UPDATE Buffers中。
- 并发清理阶段
识别并清理完全空闲的区域。并发清理,不会引起停顿。
graph TD
初始标记 --> 根区域扫描
根区域扫描 --> 并发标记
并发标记 --> 重新标记
重新标记 --> 独占清理
独占清理 --> 并发清理阶段
下图显示并发标记周期前后堆的可能情况,由于并发标记周期包含一次新生代GC(这点是不是真的?上面写的是根区域扫描时会伴随一次新生代GC,而并发标记时可能会被一次新生代GC打断,但并没有说必定会包含一次新生代GC,这里要待确认下),帮新生代会被整理,但由于并发标记周期执行时,应用程序依然在运行,因此,并发标记周期结束后,又会有新的eden空间被使用。并发标记周期执行前后最大的不同是在该阶段后,系统增加了一些标记为G的区域。这些区域被标记,是因为它内部的垃圾比例较高,因此希望在后续的混合GC中进行收集(注意在并发标记周期中并未正式收集这些区域)。这些将要被回收的区域会被G1记录在一个称为Collection Sets(回收集)的集合中。
并发回收阶段工作流程如下图,初始标记、重新标记、独占清理都是STW的,其他几个阶段都是和应用程序并发执行的。
G1日志
初始标记,伴随一次新生代GC
# 初始化标记的日志 [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0029507 secs] [Parallel Time: 2.5 ms, GC Workers: 10] [GC Worker Start (ms): Min: 9905.2, Avg: 9905.2, Max: 9905.3, Diff: 0.2] [Ext Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 1.2] [Update RS (ms): Min: 1.6, Avg: 1.9, Max: 2.1, Diff: 0.5, Sum: 18.9] [Processed Buffers: Min: 2, Avg: 2.4, Max: 3, Diff: 1, Sum: 24] [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] [Object Copy (ms): Min: 0.1, Avg: 0.3, Max: 0.6, Diff: 0.5, Sum: 2.9] [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1] [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 10] [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1] [GC Worker Total (ms): Min: 2.2, Avg: 2.3, Max: 2.4, Diff: 0.2, Sum: 23.1] [GC Worker End (ms): Min: 9907.5, Avg: 9907.5, Max: 9907.5, Diff: 0.0] [Code Root Fixup: 0.0 ms] [Code Root Purge: 0.0 ms] [Clear CT: 0.1 ms] [Other: 0.4 ms] [Choose CSet: 0.0 ms] [Ref Proc: 0.2 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] # 新生代GC的日志 [Eden: 1024.0K(1024.0K)->0.0B(1024.0K) Survivors: 1024.0K->1024.0K Heap: 461.1M(1024.0M)->461.8M(1024.0M)] [Times: user=0.02 sys=0.00, real=0.00 secs]
根区域扫描
[GC concurrent-root-region-scan-start] [GC concurrent-root-region-scan-end, 0.0001889 secs]
并发标记,可以被新生代GC打断。下面
## 并发标记开始 [GC concurrent-mark-start] ## 被新生代GC打断 [GC pause (G1 Evacuation Pause) (young), 0.0031241 secs] [Parallel Time: 2.5 ms, GC Workers: 10] [GC Worker Start (ms): Min: 9974.3, Avg: 9974.3, Max: 9974.4, Diff: 0.2] [Ext Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 1.1] [Update RS (ms): Min: 1.4, Avg: 1.8, Max: 2.1, Diff: 0.7, Sum: 17.7] [Processed Buffers: Min: 1, Avg: 2.5, Max: 4, Diff: 3, Sum: 25] [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] [Object Copy (ms): Min: 0.0, Avg: 0.3, Max: 0.6, Diff: 0.6, Sum: 2.6] [Termination (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.9] [Termination Attempts: Min: 1, Avg: 10.6, Max: 26, Diff: 25, Sum: 106] [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1] [GC Worker Total (ms): Min: 2.2, Avg: 2.2, Max: 2.3, Diff: 0.2, Sum: 22.3] [GC Worker End (ms): Min: 9976.6, Avg: 9976.6, Max: 9976.7, Diff: 0.1] [Code Root Fixup: 0.0 ms] [Code Root Purge: 0.0 ms] [Clear CT: 0.1 ms] [Other: 0.5 ms] [Choose CSet: 0.0 ms] [Ref Proc: 0.3 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: 1024.0K(1024.0K)->0.0B(1024.0K) Survivors: 1024.0K->1024.0K Heap: 462.8M(1024.0M)->463.2M(1024.0M)] [Times: user=0.02 sys=0.00, real=0.00 secs] 。。。省略了几个新生代GC。。。 ## 并发标记结束 [GC concurrent-mark-end, 0.1297480 secs]
重新标记,STW
[GC remark [Finalize Marking, 0.0003260 secs] [GC ref-proc, 0.0000479 secs] [Unloading, 0.0005812 secs], 0.0030680 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
独占清理:重新计算个个区域的存活对象
[GC cleanup 467M->467M(1024M), 0.0016913 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
并发清理,根据独占清理计算出的每价目区域的存活对象数理,直接回收已不包含存活对象的区域。
[GC concurrent-cleanup-start] [GC concurrent-cleanup-end, 0.0000074 secs]
混合回收
混合回收阶段同时回收了新生代和老年代。被清理区域的存活对象会被移动到其他区域,减少空间碎片。
并发标记周期总体回收的比例很低,但G1已经知道哪些区域有比较多的垃圾对象,混合回收阶段就可以专门针对这些区域进行回收。
G1全称 Garbage First Garbage Collector 垃圾优先的垃圾回收器。就是回收时优先选取垃圾比例高的区域。
混合回收日志
[GC pause (G1 Evacuation Pause) (mixed), 0.0058625 secs]
[Parallel Time: 5.0 ms, GC Workers: 10]
[GC Worker Start (ms): Min: 12768.1, Avg: 12768.2, Max: 12768.3, Diff: 0.1]
[Ext Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.1, Sum: 1.0]
[Update RS (ms): Min: 1.1, Avg: 1.4, Max: 1.7, Diff: 0.7, Sum: 14.5]
[Processed Buffers: Min: 2, Avg: 2.4, Max: 4, Diff: 2, Sum: 24]
[Scan RS (ms): Min: 0.9, Avg: 1.2, Max: 1.6, Diff: 0.7, Sum: 12.1]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 1.7, Avg: 2.0, Max: 2.0, Diff: 0.3, Sum: 19.7]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination Attempts: Min: 1, Avg: 1.1, Max: 2, Diff: 1, Sum: 11]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[GC Worker Total (ms): Min: 4.7, Avg: 4.7, Max: 4.8, Diff: 0.1, Sum: 47.4]
[GC Worker End (ms): Min: 12773.0, Avg: 12773.0, Max: 12773.0, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.2 ms]
[Other: 0.7 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.3 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.2 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.1 ms]
[Eden: 1024.0K(1024.0K)->0.0B(1024.0K) Survivors: 1024.0K->1024.0K Heap: 540.0M(1024.0M)->534.7M(1024.0M)]
[Times: user=0.04 sys=0.01, real=0.01 secs]
混合GC会执行多次,直到回收了足够多的内在空间,然后会触发一次新生代GC。新生代GC后又可能会发生一次并发标记周期的处理,最后又引起混合GC的执行,因此整个流程可能如下。
graph TD
A[年轻代GC] --> B[并发标记周期]
B --> C[混合GC]
C --> A
- 必要时的FULL GC
内存不充足情况G1会转入FULL GC。
另外,如果在混合GC时发生空间不足或在新生代GC时survivor区和老年代无法容纳幸存对象,老会导致一次FULL GC。
G1日志
## 发生了一次新生代GC,在初始标记时发生的,耗时0.0029032秒,程序至少暂停了0.0029032秒 [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0029032 secs] ## 后续并行时间,表示所有GC线程的总花费时间 2.3ms,垃圾回收工作线程10个 [Parallel Time: 2.3 ms, GC Workers: 10] ## GC线程的执行情况 值都是线程的启动时间 如Min是最小启动时间,在程序运行的XXX秒后启动,Avg平均启动时间 Max最晚启动时间 Diff 最早和最晚相差时间 [GC Worker Start (ms): Min: 7438.1, Avg: 7438.2, Max: 7438.3, Diff: 0.1] ## 根扫描耗时GC线程的耗时 [Ext Root Scanning (ms): Min: 0.1, Avg: 0.1, Max: 0.2, Diff: 0.1, Sum: 1.1] ## 更新记忆集(Remember Set 简称RS)耗时 [Update RS (ms): Min: 1.3, Avg: 1.7, Max: 2.0, Diff: 0.7, Sum: 16.5] ## 处理Update Buffers(记录对象间的引用关系的缓存)耗时 [Processed Buffers: Min: 1, Avg: 2.5, Max: 5, Diff: 4, Sum: 25] ## 扫描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.0, Avg: 0.3, Max: 0.6, Diff: 0.6, Sum: 3.3] ## GC工作线程的终止信息,GC线程花在终止阶段的耗时。在GC线程终止前会看其他GC线程是否有没处理完的数据,如果还有,请求终止的GC线程会帮助它尽快完成,然后再尝试终止。 Termination Attempts展示了每个工作线程尝试终止的次数。 [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.5] [Termination Attempts: Min: 1, Avg: 5.0, Max: 10, Diff: 9, Sum: 50] [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1] # GC工作线程耗时 [GC Worker Total (ms): Min: 2.1, Avg: 2.1, Max: 2.2, Diff: 0.1, Sum: 21.4] # GC工作线程的完成时间 [GC Worker End (ms): Min: 7440.3, Avg: 7440.4, Max: 7440.4, Diff: 0.0] [Code Root Fixup: 0.0 ms] [Code Root Purge: 0.0 ms] # 清空CardTable的时间,RS就是靠 CardTable记录哪些对象是存活的。 [Clear CT: 0.1 ms] # 其他几个任务的耗时 [Other: 0.5 ms] # 选择 CSet(Collection Sets)的时间,Collection Sets 表示被选取的、将要被回收的区域的集合 [Choose CSet: 0.0 ms] # 处理弱引用、软引用的时间 [Ref Proc: 0.2 ms] # 弱引用、软引用入队时间 [Ref Enq: 0.0 ms] [Redirty Cards: 0.1 ms] [Humongous Register: 0.0 ms] [Humongous Reclaim: 0.0 ms] # 释放被回收的CSet中获取的时间,包括他们的RS [Free CSet: 0.0 ms] # GC回收的整体情况 [Eden: 1024.0K(1024.0K)->0.0B(1024.0K) Survivors: 1024.0K->1024.0K Heap: 459.1M(1024.0M)->459.8M(1024.0M)] [Times: user=0.02 sys=0.00, real=0.00 secs]
G1相关参数
-XX:+UseG1GC
: 打开G1收集器开关-XX:MaxGCPauseMillis
: G1设置最重要的参数,指定目标最大停顿时间。如果任何一次停顿超过这个设置值,G1就会尝试调整新生代和老年代的比例,调整堆大小、调整晋升年龄等手段,试图达到预设目标。
如果停顿时间缩短,意味着更频繁的新生代GC,老年代为了更短的停顿时间,在混合GC时,一次收集的区域数量也会变少,增加了Full GC的可能性
-XX:ParallelGCThreads
:用于设置并行回收时,GC的工作线程数量-XX:InitiatingHeapOccupancyPercent
:指定当整个堆使用率达到多少时触发并发标记周期的执行,默认值是45.一旦设置,始终都不会被G1收集器修改,如果该值设置的偏大,会导致并发周期迟迟不启动,Full GC的可能性也大大增加。反之,过小的值会使得并发周期非常频繁,大量GC线程抢占CPU,导致应用程序性能下降。
五、细节问题
- 禁用System.gc()
System.gc()
会显式直接触发Full GC,同时对老年代和新生代进行回收。使用-XX:+DisableExplicitGC
可以禁用显式GC.
- 让System.gc()使用并发回收
使用-XX:+ExplicitGCInvokesConcurrent
参数,可以改变System.gc()
的默认行为,使用并发方式进行回收。
- 使用并行回收器在Full GC前会额外触发新生代GC
目的是减少Full GC的停顿时间,如果不需要可以使用 -XX:-ScavengeBeforeFullGC
参数去除Full GC前的新生代GC。默认是打开不去除的。
对象何时进入老年代
对象首次创建时会被放在新生代的eden区,如果没有GC,这些对象不会离开eden区
/** * -Xmx64M -Xms64M -XX:+PrintGCDetails * @author geym */ public class AllocEden { public static final int _1K = 1024; public static void main(String args[]) { for (int i = 0; i < 5 * _1K; i++) { byte[] b = new byte[_1K]; } } }
从下面的运行结果可以看到,程序运行结束后,堆内存对象都在eden区中。,其他区域使用量都是0%
Heap PSYoungGen total 18944K, used 6898K [0x00000007beb00000, 0x00000007c0000000, 0x00000007c0000000) ## 都在这里面 eden space 16384K, 42% used [0x00000007beb00000,0x00000007bf1bc918,0x00000007bfb00000) from space 2560K, 0% used [0x00000007bfd80000,0x00000007bfd80000,0x00000007c0000000) to space 2560K, 0% used [0x00000007bfb00000,0x00000007bfb00000,0x00000007bfd80000) ParOldGen total 44032K, used 0K [0x00000007bc000000, 0x00000007beb00000, 0x00000007beb00000) object space 44032K, 0% used [0x00000007bc000000,0x00000007bc000000,0x00000007beb00000) Metaspace used 2700K, capacity 4486K, committed 4864K, reserved 1056768K class space used 289K, capacity 386K, committed 512K, reserved 1048576K
- 老年对象进入老年代
eden对象年龄达到一定大小,就会自然离开年轻代进入老年代(晋升)。对象年龄由对象经历过的GC次数决定。
-XX:MaxTenuringThreshold=15
用来决定新生代对象最大年龄,默认为15/** * 最大的晋升年龄 * MaxTenuringThreshold 默认值15 * <p> * -Xmx1024M -Xms1024M -XX:+PrintGCDetails -XX:MaxTenuringThreshold=15 -XX:+PrintHeapAtGC -XX:-UseAdaptiveSizePolicy * 注意,跑的时候要增加 -XX:-UseAdaptiveSizePolicy参数,关掉自调整大小策略,否则会在第8次GC后自动调整from区的大小,然后from区容量到达97%,自动清空然后对象晋升老年代。 * * @author geym */ public class MaxTenuringThreshold { public static final int _1M = 1024 * 1024; public static final int _1K = 1024; public static void main(String args[]) { //创建一个map,往里面放了5M的数据,分5000次放 //在map里面新建的这些byte数组有引用就不会被回收 Map<Integer, byte[]> map = new HashMap<Integer, byte[]>(); for (int i = 0; i < 5 * _1K; i++) { byte[] b = new byte[_1K]; map.put(i, b); } //不断申请内存,相当于不断向eden区放数据,eden区满了,就会进行新生代GC //会将eden区的存活数据放到 from区 for (int k = 0; k < 17; k++) { for (int i = 0; i < 270; i++) { byte[] g = new byte[_1M]; } } } }
运行结果如下:
##第一次GC前 {Heap before GC invocations=1 (full 0): PSYoungGen total 305664K, used 261493K [0x00000007aab00000, 0x00000007c0000000, 0x00000007c0000000) ## eden区使用了99% eden space 262144K, 99% used [0x00000007aab00000,0x00000007baa5d5b8,0x00000007bab00000) from space 43520K, 0% used [0x00000007bd580000,0x00000007bd580000,0x00000007c0000000) to space 43520K, 0% used [0x00000007bab00000,0x00000007bab00000,0x00000007bd580000) ParOldGen total 699392K, used 0K [0x0000000780000000, 0x00000007aab00000, 0x00000007aab00000) object space 699392K, 0% used [0x0000000780000000,0x0000000780000000,0x00000007aab00000) Metaspace used 2702K, capacity 4486K, committed 4864K, reserved 1056768K class space used 288K, capacity 386K, committed 512K, reserved 1048576K [GC (Allocation Failure) [PSYoungGen: 261493K->6096K(305664K)] 261493K->6104K(1005056K), 0.0029389 secs] [Times: user=0.01 sys=0.01, real=0.01 secs] ## 第一次GC后 Heap after GC invocations=1 (full 0): PSYoungGen total 305664K, used 6096K [0x00000007aab00000, 0x00000007c0000000, 0x00000007c0000000) ## eden区被清空 eden space 262144K, 0% used [0x00000007aab00000,0x00000007aab00000,0x00000007bab00000) ## 存活对象移动到from区 from space 43520K, 14% used [0x00000007bab00000,0x00000007bb0f4020,0x00000007bd580000) to space 43520K, 0% used [0x00000007bd580000,0x00000007bd580000,0x00000007c0000000) ParOldGen total 699392K, used 8K [0x0000000780000000, 0x00000007aab00000, 0x00000007aab00000) object space 699392K, 0% used [0x0000000780000000,0x0000000780002000,0x00000007aab00000) Metaspace used 2702K, capacity 4486K, committed 4864K, reserved 1056768K class space used 288K, capacity 386K, committed 512K, reserved 1048576K } ## 第16次GC前 {Heap before GC invocations=16 (full 0): PSYoungGen total 305664K, used 267845K [0x00000007aab00000, 0x00000007c0000000, 0x00000007c0000000) eden space 262144K, 99% used [0x00000007aab00000,0x00000007baabd5f0,0x00000007bab00000) from space 43520K, 13% used [0x00000007bab00000,0x00000007bb0d4020,0x00000007bd580000) to space 43520K, 0% used [0x00000007bd580000,0x00000007bd580000,0x00000007c0000000) ParOldGen total 699392K, used 8K [0x0000000780000000, 0x00000007aab00000, 0x00000007aab00000) object space 699392K, 0% used [0x0000000780000000,0x0000000780002000,0x00000007aab00000) Metaspace used 2702K, capacity 4486K, committed 4864K, reserved 1056768K class space used 288K, capacity 386K, committed 512K, reserved 1048576K [GC (Allocation Failure) [PSYoungGen: 267845K->0K(305664K)] 267853K->6293K(1005056K), 0.0023287 secs] [Times: user=0.00 sys=0.01, real=0.00 secs] ## 第16次GC后 Heap after GC invocations=16 (full 0): PSYoungGen total 305664K, used 0K [0x00000007aab00000, 0x00000007c0000000, 0x00000007c0000000) eden space 262144K, 0% used [0x00000007aab00000,0x00000007aab00000,0x00000007bab00000) ## from区清空 from space 43520K, 0% used [0x00000007bd580000,0x00000007bd580000,0x00000007c0000000) to space 43520K, 0% used [0x00000007bab00000,0x00000007bab00000,0x00000007bd580000) ##这里可以看到老年代的空间由原来的8k使用变为了 6293K使用,说明经历过15次GC的对象晋升到老年代了 ParOldGen total 699392K, used 6293K [0x0000000780000000, 0x00000007aab00000, 0x00000007aab00000) object space 699392K, 0% used [0x0000000780000000,0x0000000780625558,0x00000007aab00000) Metaspace used 2702K, capacity 4486K, committed 4864K, reserved 1056768K class space used 288K, capacity 386K, committed 512K, reserved 1048576K }