1. GC概述

1.1 GC(Garbage Collection)是JVM的核心组件,它在JVM中以单独的线程(daemon thread)运行,作用于内存堆区域(Stack Space),扫描那些经过new关键字创建的无用的对象并清除以释放内存,必要时整理内存。

只作用于堆区域吗?
也会扫描方法区(永久代)
只处理经过new关键字创建的对象吗?
也会处理无用常量和无用类(把无用类卸载掉,尤其是在大量使用动态代理类场景,卸载类很有必要)

1.2 Stop-the-World:GC是一个“Stop-the-World”事件,当GC执行时,所有应用程序线程会临时停止运行。

为什么要stop-the-world呢?
需要在某一时刻确定哪些对象可以回收(无引用),因为对象引用情况随时可能发生变化

2. 回收基本策略

2.1 标记-清除(Mark-Sweep)

步骤:
扫描堆内存区域,识别正在使用对象和无引用对象
删除无引用对象,并将释放内存块加入内存分配器链表(保存可分配内存块起始地址及大小)
缺点:
需要扫描大片内存区域(对于生命周期长的对象频繁扫描没有意义)
清理完成会产生大量内存碎片

2.2 标记复制

步骤:
至少准备两块内存区域A和B,其中一块B区域保持为空,回收过程中在A区域标记完,将存活对象移到B区域,然后将A区域整体回收
优点:
扫描内存区域变小了?
A区域整体回收,不会产生内存碎片?
缺点:
B区域内存空间浪费了
待讨论点:
如果在A区域回收完仍有大量对象存活,那么移动大量对象到B区域效率也不高?
A和B之间内存分配比例该如何决定?—-预估A区域能回收多少对象呢?
可担保空间:A区域回收完之后,留下来的对象,B区域容纳不了怎么办?—向高级区域借用内存空间?
IBM研究:
98%的对象都是“朝生夕死”,这些对象放在A区域,经回收就剩不了对少对象了,移动到B区域效率就高了?B区域的内存大小要求也不高?
结论:
新生代的垃圾回收适合采取“标记复制”策略
新生代的内存配比:Eden : survivor = 8 :2,只允许有 1/10的内存空间浪费
可担保空间:当survivor中容纳不下时,放到年老代去

2.3 标记整理

步骤:
扫描标记可以被回收的对象
让所有存活的对象向一端移动,然后直接清理掉边界以外的内存
相对“标记复制”的优势?
当扫描过后有大量存活对象时,复制的效率不高
是否有可担保空间
结论:
年老代的垃圾回收适合采取“标记整理”策略

3. 分带回收(GC高级策略)

3.1 分带背景

3.1.1 当对象越来越多时,对象扫描非常耗时

3.1.2 研究表明绝大部分的对象生命周期是非常短暂的

3.2 GC分带

image.png

3.2.1 为了提升内存扫描和回收效率,将堆分为三个区域:年轻代、年老代和永久代,其中年轻代又细分为eden、survivor0、survivor1。

3.2.2 年轻代是新创建对象被分配区域,当年轻代被充满后,会触发minor GC。由于大部分新创建对象被分配在年轻代,并且其生命周期很短,因此在minor GC时,基本针对一些“死亡对象”,内存回收效率很高。minor GC会导致stop-the-world。

3.2.3 年老代用于存储生命周期较长的对象(如何计算?对每次minor GC后仍存活的对象记录,并慢慢转移到年老代中),当年老代被充满后,会触发Major GC。年老代空间通常要比年轻代大一些,这是为了降低major GC的次数。major GC同样会导致stop-the world。

3.2.4 永久代用于存放Class MetaData,包括System Libraries、third Party Libraries,由于Class是按需加载,并且可以卸载,所以当永久代空间不够时,GC会查找哪些Class已经不再被引用,将其卸载掉以腾出空间加载新的Class,即永久代也会发生GC。

3.2.5 GC详细过程:

4. 常见GC类型

4.1 Serial GC (-XX:+UseSerialGC)

(1)Young Generation使用单线程,Tenure Generation也使用单线程
(2)GC日志中user=real
(3)GC过程中 Stop-the-world,时间长,如果发生Full GC,持续时间更长
(4)系统吞吐量(throughput)比较不稳定
(5)GC线程和Application线程不能并行运行,在FullGC时,系统吞吐量可能降为0

4.2 Parallel GC (-XX:+UseParallelGC)

(1)Young Generation使用多线程,Tenure Generation使用单线程
(2)GC日志中user>real
(3)GC过程中Stop-the-world时间相对SerialGC短(多个线程同时GC更快),如果发生Full GC,持续时间比Serial GC短一些
(4)系统吞吐量(throughput)比较稳定
(5)GC线程和Application线程不能并行运行,在FullGC时,系统吞吐量可能降为0

4.3 Parallel Old GC(-XX:+UseParallelOldGC)

与Parallel GC相比,在Tenure Generation回收过程中使用多线程进行

4.4 CMS GC (-XX:+UseConcMarkSweepGC)

(1)Young Generation使用多线程,在Tenure Generation使用CMS,力求最低的暂停时间,但是采用CMS有可能出现“Concurrent Mode Failure”,如果出现了对Tenure Generation也是采用单线程回收
(2)分为6个步骤,其中Initial Remark和remark会引起短暂的Stop-the-world
(3)相对于其它GC,CMS可以做到GC线程和Application线程并行运行,在Full GC时不会导致系统吞吐量几乎为0的情形
(4)CMS GC必须与相关参数一起使用才能发挥良好效果

4.5 Garbage First (G1) GC

5. 优化策略

6. GC监控

6.1 JDK自带工具监控

6.1.1 jps

说明:列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)的名称,以及这些进程的本地虚拟机的唯一ID(LVMID,Local Virtual Machine Identifier)
语法:jps [option] [hostid]
语法说明:

6.1.2 jstat

说明:用于监控虚拟机各种运行状态信息的命令行工具。它可以显示本地或远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据
语法:jstat [option vmid [interval[s|ms] [count]] ]
语法说明:

6.1.3 jinfo

说明:作用是实时地查看和调整虚拟机的各项参数。使用jps的命令的-v参数可以查看虚拟机启动时显示指定的参数列表,但如果想知道未被显示指定的参数的系统默认值,除了去找资料外,就只能使用jinfo的-flag选项进行查询

6.1.4 jmap

说明:用于生产堆转储快照(一般称为heapdump或dump文件),还可以查询finalize执行队列,Java堆和永久代的详细信息,如空间使用率、当前用的是那种收集器
语法:jmap [option] vmid
语法说明:

6.1.5 jhat

说明:与jmap搭配使用,来分析jmap生成的堆转储快照

6.1.6 jstack

说明:用于生成虚拟机当前时刻的线程快照(一般称为threaddump或javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因

6.2 可视化软件监控

6.2.1 JConsole

位置:JAVA_HOME/bin/

6.2.2 Visual JVM

位置:JAVA_HOME/bin/
功能比JConsole强大
说明:安装相关插件如Visual GC之后,可以用于显示虚拟机进程及进程的配置和环境信息(jps,jinfo),监视应用程序的CPU、GC、堆、方法区及线程的信息(jstat、jstack)

6.3 GC日志分析

6.3.1 开启GC日志

—-在JVM启动参数中添加-verbose:gc -Xloggc:D:/tomcat_gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
—-打印样例:2.002: [GC [PSYoungGen: 46080K->7677K(53760K)] 46080K->9721K(186880K), 0.0109905 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
—-参数说明:
-verbose:gc:打印简单GC信息,例如[GC 72104K->9650K(317952K), 0.0130635 secs]
-XX:+PrintGCDetails:打印详细GC信息,例如[GC [PSYoungGen: 46080K->7677K(53760K)] 46080K->9721K(186880K), 0.0109905 secs]
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式,例如上一行中的2.002)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2017-07-04T21:53:59.234+0800,比TimeStamps要详细)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc: path/gc.log 日志文件的输出路径
—-GC监控:一般使用-Xloggc、-XX:+PrintGCDetails、-XX:+PrintGCDateStamps

6.3.2 分析GC日志

打印样例:2.002: [GC [PSYoungGen: 46080K->7677K(53760K)] 46080K->9721K(186880K), 0.0109905 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
(1)2.002:时间戳
(2)[PSYoungGen: 46080K->7677K(53760K)]:年轻代GC回收前大小—>回收后大小(总大小)
(3)46080K->9721K(186880K):整个堆GC回收前大小—>回收后大小(总大小)
(4)0.0109905 secs:Minor GC耗费时间
(5)[Times: user=0.03 sys=0.00, real=0.01 secs]:real表示GC实际耗费时间,user表示耗费所有CPU时间总和。注意在SerialGC中只有一个线程(CPU)执行GC,所以user=real,但是在Parallel GC中,有多个CPU参与GC,所以相对于Serial GC,real会缩短,user会增大

什么是Full GC:Full GC是对年轻代、年老代,以及持久代的统一回收,由于是对整个空间的回收,因此比较慢,系统中应当尽量减少Full GC的次数
什么时候发生Full GC:
(1) 年老代不足
(2) 持久代不足
(3) 显式调用system.gc();(可以DisableExplicitGC来禁止)
什么是OOM:(java.lang.OutOfMemoryError: Java heap space)、(java.lang.OutOfMemoryError: Java Perm space)
什么时候抛出OOM:
在JVM中如果98%时间是用于GC且可用的Heap size 不足2%的时候将抛出此异常信息

7. 总结

(1)GC调优没有公式可循,唯一的办法是实验
(2)默认GC:如果没有刻意设置JVM参数,默认是使用Parallel GC
点击原文

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

毕生所求无它,爱与自由而已