找出 Java 应用频繁 Full GC 的原因

原文地址:https://alphahinex.github.io/2021/12/12/find-the-reason-of-full-gc/

cover

description: "找到内存钉子户"
date: 2021.12.12 10:26
categories:
- Java
tags: [Java, JVM]
keywords: JVM, full gc, gc, heap, dump, jps, jstat, jmap, jvisualvm, MAT


现象

一个线上的 Java 应用(JDK1.8,默认 Parallel GC)在运行几天之后,出现频繁 Full GC 的现象,使用 jstat -gcutil <pid> 2s 观察,状态大致如下:

$ jstat -gcutil 23426 2s
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00   0.00  93.17  99.99  83.81  78.48   1814   90.687    45  261.660  352.347
  0.00   0.00  97.30  99.99  83.81  78.48   1814   90.687    45  261.660  352.347
  0.00   0.00 100.00  99.99  83.81  78.48   1814   90.687    46  261.660  352.347
  0.00   0.00 100.00  99.99  83.81  78.48   1814   90.687    46  261.660  352.347
  0.00   0.00 100.00  99.99  83.81  78.48   1814   90.687    46  261.660  352.347
  0.00   0.00 100.00  99.99  83.81  78.48   1814   90.687    46  261.660  352.347
  0.00   0.00  14.38  99.99  83.81  78.48   1814   90.687    46  269.050  359.737
  0.00   0.00  22.19  99.99  83.81  78.48   1814   90.687    46  269.050  359.737

在 Eden 区用满后,并不进行 Young GC(1814),而是直接触发 Full GC(45 => 46),导致 CPU 使用率上升,应用响应慢。

重启应用后,CPU 使用率恢复正常,老年代占用空间降低,新生代空间占满后,执行 Young GC,不引发 Full GC。

运行一段时间,老年代占用空间持续上升,Full GC 也不会有空间被回收,直至老年代空间占满,再次出现最初的情况。

工具

jps

JDK 中自带的 jps(Java Virtual Machine Process Status Tool) 命令,可以快速查询 Java 的进程标识(pid),如:

$ jps
7168 Jps
7114 hello.jar

但有时可能会遇到类似下面的情况:

$ jps
12929 -- process information unavailable
31097 -- process information unavailable
12878 Jps

某些 pid 后面并未显示出对应的 java 应用信息,原因是运行 java 应用的用户与执行 jps 命令的用户不一致,通过 su 等切换为相同的用户再执行 jps 命令即可。

jstat

获得 Java 应用的 pid 之后,可以使用 jstat(Java Virtual Machine statistics monitoring tool)对指定 Java 应用进行监控,以上面 pid 为 7114 的 Java 应用为例:

$ jstat -gcutil 7114 2s
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00   0.00   6.11   7.94  92.59  90.24      6    0.028     2    0.068    0.096
  0.00   0.00   6.11   7.94  92.59  90.24      6    0.028     2    0.068    0.096
  0.00   0.00   6.11   7.94  92.59  90.24      6    0.028     2    0.068    0.096
  0.00   0.00   6.11   7.94  92.59  90.24      6    0.028     2    0.068    0.096
  0.00   0.00   6.11   7.94  92.59  90.24      6    0.028     2    0.068    0.096
  0.00   0.00   6.11   7.94  92.59  90.24      6    0.028     2    0.068    0.096

上面命令意为获得 pid 为 7114 的 Java 应用的垃圾收集统计信息,时间间隔 2 秒统计一次。

统计信息每列的具体含义如下:

Column Description
S0 Survivor space 0 utilization as a per-
centage of the space's current capacity.
S1 Survivor space 1 utilization as a per-
centage of the space's current capacity.
E Eden space utilization as a percentage of
the space's current capacity.
O Old space utilization as a percentage of
the space's current capacity.
M Metaspace utilization as a percentage of
the space's current capacity.
CCS Compressed class space utilization as a
percentage.
YGC Number of young generation GC events.
YGCT Young generation garbage collection time.
FGC Number of Full GC events.
FGCT Full garbage collection time.
GCT Total garbage collection time.

关于 jstat 命令更多的说明,可见 官方文档

通过上述命令,可以观察到最初现象中描述的故障时 Java 应用的 GC 状态。

GC 日志

jstat 统计的是应用在执行命令时的状态,通常我们不能预知故障发生,并提前使用 jstat 进行监控,这时可以选择将应用的 GC 状况输出到日志文件中,以便后续查询和分析。在启动 Java 应用时,可以选择添加如下参数:

-Xloggc:/home/admin/filelogs/gc.log 
-XX:+PrintGCDetails
-XX:+UseGCLogFileRotation 
-XX:NumberOfGCLogFiles=5 
-XX:GCLogFileSize=200m

Java 的参数大致可以分为三类:

  1. 标准参数(-),如:-jar-Dproperty=value
  2. 非标准参数(-X),如:-Xms-Xmx
  3. 高级参数(-XX),如:-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=path

上面添加的 GC 日志相关参数的意思为:

每次 GC 时,将 GC 日志输出至指定文件,并启用日志文件滚动,滚动文件数量 5 个,每个文件大小 200mb

各类参数的详细说明,可参见 官方文档

也可通过如下命令查看所使用的 Java 版本支持的所有高级参数:

$ java -XX:+PrintFlagsFinal -version

jmap -heap

通过 GC 统计及日志,可以发现 GC 的执行时间、次数、耗时,以及每次 GC 所释放的内存情况。

正常情况下,对象均应在年轻代被创建,使用过后被年轻代 GC 回收,回收不了的对象进入幸存区,在年轻代经历多次 GC 仍不能被回收的对象,会被移至老年代,当老年代空间也被使用满或无法放入新对象时,触发 Full GC。

YGC 执行时间短,对系统影响较小,而 FGC 会引发 STW(Stop the world),占用大量计算资源,执行时间较长,对系统影响较大。所以 YGC 的次数,应该明显高于 FGC,且每次 FGC 应该能够有内存空间被释放出来。

当频繁发生 FGC,且对系统造成比较大的影响时,可以先通过 jmap(memory map)命令查看堆的内存分配及使用情况,如:

$ jmap -heap 7114
Attaching to process ID 7114, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.121-b13

using thread-local object allocation.
Parallel GC with 8 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 20971520 (20.0MB)
   NewSize                  = 3145728 (3.0MB)
   MaxNewSize               = 6815744 (6.5MB)
   OldSize                  = 7340032 (7.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 2621440 (2.5MB)
   used     = 538160 (0.5132293701171875MB)
   free     = 2083280 (1.9867706298828125MB)
   20.5291748046875% used
From Space:
   capacity = 1572864 (1.5MB)
   used     = 1572864 (1.5MB)
   free     = 0 (0.0MB)
   100.0% used
To Space:
   capacity = 2097152 (2.0MB)
   used     = 0 (0.0MB)
   free     = 2097152 (2.0MB)
   0.0% used
PS Old Generation
   capacity = 14155776 (13.5MB)
   used     = 11533024 (10.998748779296875MB)
   free     = 2622752 (2.501251220703125MB)
   81.47221317997685% used

13542 interned Strings occupying 1172704 bytes.

分析

通过 jmap -heap <pid> 观察对比应用重启后,以及发生故障时堆的分配情况,发现故障时,S0(From Space/Survivor1) 及 S1(To Space/Survivor2) 被分配的空间非常小(几 mb,甚至为 0mb),堆的大部分空间被老年代占据且无法释放,当 Eden 区也被占满后,本应进行 Young GC,但 S0 和 S1 均没有空间了,只能触发 Full GC 希望能从老年代中释放出一些空间。然而此时老年代中的对象均未能释放,导致 Full GC 时间长且无效果,进而引起频繁 Full GC。

出现这种状况时,基本可以断定应用中出现了内存泄露,导致垃圾收集器无法回收内存。

要解决这个问题,我们首先需要知道内存中究竟都存放了哪些对象,以及这些对象为什么不能被垃圾回收。

jmap -histo

使用 jmap 命令,还可以获得堆中对象的柱状图统计信息,如:

$ jmap -histo:live 7114 | more

 num     #instances         #bytes  class name
----------------------------------------------
   1:         29510        2809856  [C
   2:          6998         769912  java.lang.Class
   3:         29465         707160  java.lang.String
   4:         21190         678080  java.util.concurrent.ConcurrentHashMap$Node
   5:          6549         318440  [Ljava.lang.Object;
   6:          2043         306528  [B
:

注意::live 参数会触发 FGC,以保证统计出的都是存活的对象。如果想保留现场,可不添加此参数。

jmap -dump

从柱状图统计信息中,可以了解到堆中占用内存最多的类及实例数量,但无法得知这些对象为什么不能被垃圾回收,此时可以进行堆转储(heap dump),以便进一步分析:

$ jmap -h
...
    -dump:<dump-options> to dump java heap in hprof binary format
                         dump-options:
                           live         dump only live objects; if not specified,
                                        all objects in the heap are dumped.
                           format=b     binary format
                           file=<file>  dump heap to <file>
...
$ jmap -dump:live,format=b,file=/path/to/heap.hprof 7114
Dumping heap to /path/to/heap.hprof ...
Heap dump file created

live 参数同上,不想触发 FGC,可直接去掉,如:

$ jmap -dump:format=b,file=/path/to/heap.hprof 7114

jvisualvm

分析堆 dump 文件,可以使用 JDK 自带的 jvisualvm 工具,命令行中直接输入即可:

$ jvisualvm

打开界面后,选择 文件 -> 装入,选择 hprof 类型,并将堆 dump 文件导入即可:

load
jvisualvm

在装载比较大的 heap dump 文件时,可能会出现内存溢出等问题,可使用 -J 指定 Java 标准参数,如:

$ jvisualvm -J-Xms2048m -J-Xmx4096m

jvisualvm 虽然比 jmap -histo 直观了一些,但用来分析问题,能够获取的信息还是比较少,并且在分析比较大的堆转储文件时,性能低下,实用性较低。

MAT

Eclipse 出品的 Memory Analyzer (MAT) 工具是一个更好的选择,能够快速的对大 dump 文件进行分析,并且功能丰富。

在分析较大堆文件时,也可能会遇到软件本身内存溢出的问题,可通过在配置文件中指定 Java 标准参数中的内存参数进行调整,如在 Mac 中,可在 /Applications/mat.app/Contents/Eclipse/MemoryAnalyzer.ini 中添加 Xmx 的配置。

MAT 解析完堆文件后,会提供泄露分析报告等,方便快速定位问题。

关于 MAT 的使用,可以参照 官方文档

原因

使用 MAT 分析本文最初描述现象的故障节点 heap dump 文件,在 Dominator Tree 视图中,发现大量内存空间被某类线程所占用:

mat

而线程中存在大量线程变量引用的集合对象,类似 ThreadLocal<Map<String, List<Object>>> 这种结构,因为使用了线程池,线程使用后没有对 ThreadLocal 对象进行 clear 操作,导致线程变量泄露,占用了大量内存,无法被 GC 回收,进而导致了最初的故障现象。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,701评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,649评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,037评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,994评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,018评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,796评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,481评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,370评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,868评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,014评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,153评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,832评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,494评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,039评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,437评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,131评论 2 356

推荐阅读更多精彩内容