JVM虚拟机之调优实战

GC日志详解

调优之前首先要能看懂GC的日志,通过收集关键的日志信息,然后结合我们的系统情况,来推测和验证是参数设置的不合理,还是代码写的不够优雅。上一篇《JVM虚拟机之调优命令》已经把常用的调优命令介绍了一下,这次就结合具体的例子,在调优的过程中加深理解。

我在本地创建了一个Web项目,打成了Jar包,然后通过命令行启动这个项目:

java -XX:PermSize=256M -XX:MaxPermSize=256M -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:./gc.log -jar register-center-0.0.1-SNAPSHOT.jar

找到我们的GC日志文件,来看一下里面的信息:

Java HotSpot(TM) 64-Bit Server VM (25.202-b08) for bsd-amd64 JRE (1.8.0_202-b08), built on Dec 15 2018 20:16:16 by "java_re" with gcc 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00)
Memory: 4k page, physical 8388608k(853840k free)

/proc/meminfo:

CommandLine flags: -XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC 
2020-05-23T12:31:23.404-0800: 0.813: [GC (Allocation Failure) [PSYoungGen: 33280K->3132K(38400K)] 33280K->3148K(125952K), 0.0059454 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
2020-05-23T12:31:23.651-0800: 1.060: [GC (Allocation Failure) [PSYoungGen: 36412K->2940K(38400K)] 36428K->2964K(125952K), 0.0044915 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
2020-05-23T12:31:23.855-0800: 1.264: [GC (Allocation Failure) [PSYoungGen: 36220K->3644K(38400K)] 36244K->3676K(125952K), 0.0048109 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
2020-05-23T12:31:23.986-0800: 1.395: [GC (Allocation Failure) [PSYoungGen: 36924K->4412K(71680K)] 36956K->4452K(159232K), 0.0068782 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
2020-05-23T12:31:24.254-0800: 1.663: [GC (Allocation Failure) [PSYoungGen: 70972K->4940K(71680K)] 71012K->4988K(159232K), 0.0091530 secs] [Times: user=0.02 sys=0.01, real=0.01 secs] 
2020-05-23T12:31:24.596-0800: 2.005: [GC (Allocation Failure) [PSYoungGen: 71500K->5612K(138240K)] 71548K->5684K(225792K), 0.0079396 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
2020-05-23T12:31:25.009-0800: 2.418: [GC (Allocation Failure) [PSYoungGen: 138220K->5867K(138752K)] 138292K->8009K(226304K), 0.0131049 secs] [Times: user=0.03 sys=0.00, real=0.02 secs] 
2020-05-23T12:31:25.060-0800: 2.469: [GC (Allocation Failure) [PSYoungGen: 138475K->3265K(271360K)] 140617K->6078K(358912K), 0.0042673 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
2020-05-23T12:31:25.380-0800: 2.789: [GC (Allocation Failure) [PSYoungGen: 268481K->4724K(271872K)] 271294K->8672K(359424K), 0.0105148 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
2020-05-23T12:31:25.590-0800: 2.999: [GC (Metadata GC Threshold) [PSYoungGen: 241857K->2368K(430592K)] 245805K->7321K(518144K), 0.0059205 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
2020-05-23T12:31:25.596-0800: 3.005: [Full GC (Metadata GC Threshold) [PSYoungGen: 2368K->0K(430592K)] [ParOldGen: 4953K->7054K(52224K)] 7321K->7054K(482816K), [Metaspace: 20214K->20213K(1069056K)], 0.0482479 secs] [Times: user=0.12 sys=0.00, real=0.05 secs] 
2020-05-23T12:31:26.945-0800: 4.354: [GC (Allocation Failure) [PSYoungGen: 423936K->6793K(431104K)] 430990K->13855K(483328K), 0.0155527 secs] [Times: user=0.02 sys=0.01, real=0.02 secs] 

CommandLine flags就是我们在命令行设置的启动参数,用jinfo命令也可以很方便的查看。

Minor GC的日志

2020-05-23T12:31:23.404-0800: 0.813: [GC (Allocation Failure) [PSYoungGen: 33280K->3132K(38400K)] 33280K->3148K(125952K), 0.0059454 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 

0.813是具体发生GC的时间点,这个时间不是系统的时间,前面的才是系统时间,这是从JVM启动开始计算的一个时间。这里意思是从JVM启动后0.813秒发生了Minor GC。

GC (Allocation Failure)是指因为Eden区内存分配失败而触发了一次Minor GC。

PSYoungGen是指发生了新生代的Minor GC。

  • PSYoungGen是年轻代的GC
  • ParOldGen是老年代的GC
  • Metaspace是元空间的GC

33280K->3132K(38400K)是指Minor GC之前占用了33280K,Minor GC之后占用了3132K,新生代的大小是38400K。

33280K->3148K(125952K)是指整个堆GC之前占用了33280K,GC之后占用了3148K,当前堆空间的大小是125952K。

0.0059454 secs是指发生Minor GC消耗的时间。

Times: user=0.01 sys=0.00, real=0.01 secs是指用户耗时,系统耗时和实际耗时。

Full GC的日志

2020-05-23T13:22:34.538-0800: 2.832: [Full GC (Metadata GC Threshold) [PSYoungGen: 1312K->0K(540160K)] [ParOldGen: 8478K->5972K(48128K)] 9790K->5972K(588288K), [Metaspace: 20248K->20248K(1067008K)], 0.0268460 secs] [Times: user=0.07 sys=0.00, real=0.03 secs]

2.832表示从JVM启动后2.832秒发生了Full GC。

Full GC (Metadata GC Threshold)表示因为元空间大小达到了阈值触发了Full GC。

PSYoungGen: 1312K->0K(540160K)表示Full GC之前新生代占用了1312K,Full GC之后占用了0K,新生代的大小是540160K。

ParOldGen: 8478K->5972K(48128K)表示Full GC之前老年代占用了8478K,Full GC之后占用了5972K,老年代的大小是48128K。

Metaspace: 20248K->20248K(1067008K)表示Full GC之前元空间占用了20248K,Full GC之后占用了20248K,元空间的大小是1067008K。

0.0268460 secs是指发生Full GC消耗的时间。

Times: user=0.07 sys=0.00, real=0.03 secs是指用户耗时,系统耗时和实际耗时。

调优不是一步完成的

一般JVM刚启动一段时间的GC日志是没有参考价值的,这时候正在初始化,结论应该是基于大量统计数据后得出的,不能因为这一点日志信息就得出优化的结论。实际的线上调优也是,首先要分析大量的GC日志,如果不是秒杀这样的瞬间就有大量的用户访问的系统,一般要几周甚至个把月的GC日志作为分析的依据。
其次是调优不是一蹴而就的,一般都是分析出可能的问题,然后修正启动参数,再部署到线上收集日志进行验证,这样循环往复,一步一步的将我们的系统调整到一个最佳的状态。

调优分析工具

先以上面这个例子作为调优的教材,假设我们得出了元空间分配的内存太小的结论,那么首先我们要修正这个启动参数,将元空间的内存分配的大一点,然后重新部署项目,再对日志进行分析来验证我们的结论。

JVM加载类的时候,需要记录类的元数据,这些数据会保存在一个单独的内存区域内,在JDK7的时候,这个空间被称为永久代(Permgen),JDK8后进行了优化,使用元空间(Metaspace)代替了永久代。

使用JDK8以后,关于元空间的JVM参数有两个:-XX:MetaspaceSize=N-XX:MaxMetaspaceSize=N,对于64位的JVM来说,元空间的默认初始大小是20.75MB,元空间的默认最大值是无限。

修正后的启动参数:

java -XX:PermSize=256M -XX:MaxPermSize=256M -XX:MetaspaceSize=128M -XX:MaxMetaspaceSize=128M -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:./gc.log -jar register-center-0.0.1-SNAPSHOT.jar

现在的日志信息:

Java HotSpot(TM) 64-Bit Server VM (25.202-b08) for bsd-amd64 JRE (1.8.0_202-b08), built on Dec 15 2018 20:16:16 by "java_re" with gcc 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00)
Memory: 4k page, physical 8388608k(635576k free)

/proc/meminfo:

CommandLine flags: -XX:CompressedClassSpaceSize=125829120 -XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:MaxMetaspaceSize=134217728 -XX:MetaspaceSize=134217728 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC 
2020-05-23T14:52:57.233-0800: 0.588: [GC (Allocation Failure) [PSYoungGen: 33280K->3149K(38400K)] 33280K->3157K(125952K), 0.0042374 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
2020-05-23T14:52:57.423-0800: 0.779: [GC (Allocation Failure) [PSYoungGen: 36429K->2872K(38400K)] 36437K->2888K(125952K), 0.0042335 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
2020-05-23T14:52:57.593-0800: 0.949: [GC (Allocation Failure) [PSYoungGen: 36152K->3592K(38400K)] 36168K->3616K(125952K), 0.0036863 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
2020-05-23T14:52:57.711-0800: 1.066: [GC (Allocation Failure) [PSYoungGen: 36872K->4424K(71680K)] 36896K->4456K(159232K), 0.0062428 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
2020-05-23T14:52:57.948-0800: 1.303: [GC (Allocation Failure) [PSYoungGen: 70984K->4876K(71680K)] 71016K->4916K(159232K), 0.0061587 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
2020-05-23T14:52:58.228-0800: 1.583: [GC (Allocation Failure) [PSYoungGen: 71436K->5608K(138240K)] 71476K->5688K(225792K), 0.0078316 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
2020-05-23T14:52:58.634-0800: 1.989: [GC (Allocation Failure) [PSYoungGen: 138180K->6056K(138752K)] 138260K->8210K(226304K), 0.0119794 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 
2020-05-23T14:52:58.689-0800: 2.044: [GC (Allocation Failure) [PSYoungGen: 138664K->3184K(272384K)] 140818K->5993K(359936K), 0.0048027 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
2020-05-23T14:52:58.976-0800: 2.331: [GC (Allocation Failure) [PSYoungGen: 268400K->4751K(272384K)] 271209K->8690K(359936K), 0.0103901 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
2020-05-23T14:52:59.162-0800: 2.518: [GC (Allocation Failure) [PSYoungGen: 269967K->2560K(431616K)] 273906K->7507K(519168K), 0.0055924 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
2020-05-23T14:53:00.332-0800: 3.688: [GC (Allocation Failure) [PSYoungGen: 427008K->7142K(431616K)] 431955K->14384K(519168K), 0.0149645 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 

可以看到现在没有Full GC的日志信息了,这里也假设验证了我们的分析结果,相信大家对调优过程已经有了一个整体的认识。

一般我们的系统都会指定垃圾收集器,如果指定了垃圾收集器,则GC日志会将收集器的收集过程也打印出来,比如我们用ParNew收集器收集新生代,CMS收集器收集老年代,启动命令如下:

java -XX:PermSize=256M -XX:MaxPermSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:./gc.log -jar register-center-0.0.1-SNAPSHOT.jar

日志信息:

Java HotSpot(TM) 64-Bit Server VM (25.202-b08) for bsd-amd64 JRE (1.8.0_202-b08), built on Dec 15 2018 20:16:16 by "java_re" with gcc 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00)
Memory: 4k page, physical 8388608k(382924k free)

/proc/meminfo:

CommandLine flags: -XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:MaxNewSize=348966912 -XX:MaxTenuringThreshold=6 -XX:OldPLABSize=16 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseParNewGC 
......
2020-05-23T15:16:50.071-0800: 2.119: [GC (CMS Initial Mark) [1 CMS-initial-mark: 5005K(87424K)] 7470K(126720K), 0.0012375 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2020-05-23T15:16:50.073-0800: 2.120: [CMS-concurrent-mark-start]
2020-05-23T15:16:50.107-0800: 2.154: [CMS-concurrent-mark: 0.034/0.034 secs] [Times: user=0.09 sys=0.01, real=0.03 secs] 
2020-05-23T15:16:50.107-0800: 2.154: [CMS-concurrent-preclean-start]
2020-05-23T15:16:50.108-0800: 2.155: [CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2020-05-23T15:16:50.108-0800: 2.155: [CMS-concurrent-abortable-preclean-start]
2020-05-23T15:16:50.214-0800: 2.261: [GC (Allocation Failure) 2020-05-23T15:16:50.214-0800: 2.261: [ParNew: 36968K->2614K(39296K), 0.0032082 secs] 41974K->7622K(126720K), 0.0032835 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
2020-05-23T15:16:50.369-0800: 2.416: [CMS-concurrent-abortable-preclean: 0.084/0.261 secs] [Times: user=0.69 sys=0.04, real=0.26 secs] 
2020-05-23T15:16:50.369-0800: 2.416: [GC (CMS Final Remark) [YG occupancy: 25285 K (39296 K)]2020-05-23T15:16:50.369-0800: 2.416: [Rescan (parallel) , 0.0039886 secs]2020-05-23T15:16:50.373-0800: 2.420: [weak refs processing, 0.0000567 secs]2020-05-23T15:16:50.373-0800: 2.420: [class unloading, 0.0039091 secs]2020-05-23T15:16:50.377-0800: 2.424: [scrub symbol table, 0.0034886 secs]2020-05-23T15:16:50.380-0800: 2.428: [scrub string table, 0.0002853 secs][1 CMS-remark: 5007K(87424K)] 30293K(126720K), 0.0122717 secs] [Times: user=0.04 sys=0.00, real=0.02 secs] 
2020-05-23T15:16:50.381-0800: 2.429: [CMS-concurrent-sweep-start]
2020-05-23T15:16:50.384-0800: 2.431: [CMS-concurrent-sweep: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2020-05-23T15:16:50.384-0800: 2.431: [CMS-concurrent-reset-start]
2020-05-23T15:16:50.387-0800: 2.434: [CMS-concurrent-reset: 0.003/0.003 secs] [Times: user=0.01 sys=0.01, real=0.00 secs] 
......

可以看到日志信息多了一些东西,主要是这些东西:

  • CMS Initial Mark:初始标记
  • CMS-concurrent-mark:并发标记
  • CMS-concurrent-preclean:并发预清理
  • CMS-concurrent-abortable-preclean:可中止的预清理
  • CMS Final Remark:最终标记
  • CMS-concurrent-sweep:并发清除
  • CMS-concurrent-reset:并发重置

随着系统运行时间的推移,GC日志文件会变得越来越大,靠肉眼来分析变得越来越困难,这里推荐一个自动分析GC日志文件的工具,不仅能自动分析还能给出调优的意见,因为其背后可靠的机器学习算法和用户量,现在已经收费了,网址:

https://gceasy.io/

将之前我们需要调优的日志文件上传后,它给出的调优意见需要收费,但是可以免费统计出很多GC相关的信息,比如:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

调优的基本思路

我们知道现在的垃圾收集器新生代都采用复制算法,老年代采用标记清除或者标记整理算法,Full GC消耗的时间比Minor GC多的多,且对系统的正常使用有影响,所以调优的基本方向就是尽量减少Full GC的次数。
减少Full GC的次数意味着老年代在相当长一段时间里要有足够的空间容纳对象,根据之前写的《JVM虚拟机之内存分配与回收》这篇博客,我们知道什么情况下会将对象分配到老年代中:

  • 大对象直接进入老年代
  • 长期存活的对象将进入老年代
  • 对象动态年龄判断
  • Minor GC后存活的对象Survivor区放不下
  • 老年代空间分配担保机制

根据JVM的这些分配规则,我们可以归纳一些调优的基本思路:

  • 不要在代码中定义大对象和需要连续内存空间的对象,比如数组等
  • 不需要的对象及时释放引用链,能朝生夕死就不要让其多存活一会
  • 尽量让垃圾对象在Monir GC时就被回收掉,让Spring的一些Bean对象挪到老年代中
  • 尽量让每次Minor GC后的存活对象小于Survivor区域的一半,这样可以将尽量多的对象都留存在年轻代中

Minor GC一般几分钟或者几十分钟发生一次,Full GC一般半个小时、几个小时或者若干天发生一次比较合理,如果GC特别频繁,就需要排查一下问题了。

调优的主要步骤

现在假设有一台2核4G的服务器部署了一个用户系统,JVM的内存是2G,系统运行了一段时间发现频繁发生卡顿现象,现在我们来定位原因和参数调优。

系统的启动参数如下:

-Xms1536M -Xmx1536M -Xmn512M -Xss256K -XX:SurvivorRatio=6 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:./gc.log

查看垃圾回收统计信息

既然系统频繁发生卡顿现象,我们首先猜想是Full GC的发生频率太高,于是先去服务器上用jstat命令查看垃圾回收的统计情况。

jstat -gc 进程ID 1000 10

这里每隔1000毫秒打印一次垃圾回收信息,一共打印10次,信息如下:


 S0C    S1C      S0U    S1U      EC       EU        OC         OU       MC       MU     CCSC   CCSU     YGC     YGCT    FGC    FGCT     GCT
65536.0 65536.0  0.0   65536.0 393216.0 201567.7 1048576.0   640570.6  32256.0 29928.7 4352.0 3926.9   1101   33.953  621    21.097   55.050
65536.0 65536.0 65536.0  0.0   393216.0   0.0    1048576.0   475060.9  32256.0 29928.7 4352.0 3926.9   1111   34.263  627    21.283   55.546
65536.0 65536.0  0.0   65536.0 393216.0 95570.3  1048576.0   963751.7  32256.0 29928.7 4352.0 3926.9   1122   34.569  634    21.449   56.018
65536.0 65536.0 65536.0  0.0   393216.0   0.0    1048576.0   545341.6  32256.0 29928.7 4352.0 3926.9   1131   34.869  638    21.591   56.460
65536.0 65536.0  0.0    0.0   393216.0 355074.8 1048576.0   157553.7  32256.0 29928.7 4352.0 3926.9   1142   35.152  644    21.775   56.928
65536.0 65536.0  0.0   65536.0 393216.0   0.0    1048576.0   947345.7  32256.0 29928.7 4352.0 3926.9   1152   35.503  651    21.934   57.437
65536.0 65536.0  0.0   65536.0 393216.0 393216.0 1048576.0   393022.6  32256.0 29928.7 4352.0 3926.9   1162   35.772  655    22.148   57.921
65536.0 65536.0  0.0    0.0   393216.0 66319.0  1048576.0   242456.9  32256.0 29928.7 4352.0 3926.9   1171   36.070  661    22.352   58.422
65536.0 65536.0 65536.0  0.0   393216.0 13757.1  1048576.0   933367.1  32256.0 29928.7 4352.0 3926.9   1182   36.439  667    22.422   58.861
65536.0 65536.0  0.0    0.0   393216.0 365788.5 1048576.0   81561.0   32256.0 29928.7 4352.0 3926.9   1191   36.632  672    22.660   59.291

可以看到每次Minor GC后Survivor区都被占满,系统如此频繁的发生GC一定是有问题的。

描绘内存模型

为了进一步定位问题,可以通过jmap命令查看一下堆的整体使用情况:

jmap -heap 进程ID
Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 1610612736 (1536.0MB)
   NewSize                  = 536870912 (512.0MB)
   MaxNewSize               = 536870912 (512.0MB)
   OldSize                  = 1073741824 (1024.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 6
   MetaspaceSize            = 268435456 (256.0MB)
   CompressedClassSpaceSize = 260046848 (248.0MB)
   MaxMetaspaceSize         = 268435456 (256.0MB)
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
New Generation (Eden + 1 Survivor Space):
   capacity = 469762048 (448.0MB)
   used     = 387038744 (369.1089096069336MB)
   free     = 82723304 (78.8910903930664MB)
   82.39038160869053% used
Eden Space:
   capacity = 402653184 (384.0MB)
   used     = 387038744 (369.1089096069336MB)
   free     = 15614440 (14.891090393066406MB)
   96.12211187680562% used
From Space:
   capacity = 67108864 (64.0MB)
   used     = 0 (0.0MB)
   free     = 67108864 (64.0MB)
   0.0% used
To Space:
   capacity = 67108864 (64.0MB)
   used     = 0 (0.0MB)
   free     = 67108864 (64.0MB)
   0.0% used
concurrent mark-sweep generation:
   capacity = 1073741824 (1024.0MB)
   used     = 144419448 (137.72911834716797MB)
   free     = 929322376 (886.270881652832MB)
   13.450109213590622% used

根据这些数据我们能清晰的看到新生代分配了512M,其中Eden区分配了384M,Survivor区分配了64M,老年代分配了1024M,元空间分配了256M。
所以大概就是这个样子:
在这里插入图片描述

推测可能的原因

有了运行时数据区的数据模型,我们就可以根据JVM的内存分配规则,来推测可能出现的问题,内存分配规则是:

  • 大对象直接进入老年代
  • 长期存活的对象将进入老年代
  • 对象动态年龄判断
  • Minor GC后存活的对象Survivor区放不下
  • 老年代空间分配担保机制

长期存活的对象进入老年代在正常情况下是没有问题的,JDK8也默认开启了分配担保机制的参数,所以一般也不会有什么问题。
如果有大对象或者Survivor区过小发生了对象的动态年龄判断,那么很多应该被处理的对象就不会在Minor GC中被回收,而是进入了老年代,老年代对象越来越多,就会发生Full GC,频繁的进行垃圾回收,最终就会导致系统出现卡顿。

分析GC日志

由于GC日志变得越来越大,很难靠肉眼来进行分析,我们可以借助上面提到的分析工具,用统计结果来佐证我们的猜想。
比如我们上面猜测可能是有了大对象,或者Survivor区分配的空间太小,再结合分析工具为我们统计的结果来排除猜想或者验证猜想。
假设我们先推测是Survivor区分配的空间太小导致的,于是将新生代分配的内存增加一倍:

-Xms1536M -Xmx1536M -Xmn1024M -Xss256K -XX:SurvivorRatio=6 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:./gc.log

这样修改后,对象的动态年龄判断机制可以减少一部分对象提前进入老年代,但是再次用jstat命令查看垃圾回收统计信息时,发现GC的频率还是很高,于是我们继续验证大对象的猜想。

查看堆中的对象信息

此时我们可以来看看堆中存放了哪些对象,可以用jmap命令来查看:

jmap -histo 进程ID > map.txt

在这里插入图片描述
发现堆中除了JDK和Spring框架中的对象外,我们自已定义的一个User对象实例和占用的内存一直很靠前,于是我们猜想这是不是就是我们要找的大对象,怎么才能定位程序在哪里使用了这个对象呢,可以通过topjstack命令:

top -pid 进程ID

然后按H获取每个线程的内存情况,找到内存和CPU占用最高的线程ID,将其转换为十六进制比如说0x1234,然后执行下面这个命令:

jstack 线程ID | grep -A 10 0x1234

可以得到线程堆栈中1234这个线程所在行的后面10条数据,查看对应的堆栈信息找出可能存在问题的代码。

如果嫌这种方式比较麻烦,也可以使用JDK提供的图形化工具Java VisualVm,具体使用方式可以去网上搜一下。
在这里插入图片描述
定位到调用比较频繁的几个方法,去程序中对应的方法中查看代码,发现是从数据库中获取用户的方法,一般会用分页一次获取几百上千个没有问题,但是这个用户对象比较大,一个用户对象有100K,如果一次获取上千个,那么获取一次就会创建上百M的对象,短时间内这么多对象肯定会对内存分配造成影响。当然这里我是加快了GC的进程,将对象设置的比较大,为了让大家在短时间内直观的看到现象和影响。

那么这次模拟的调优就告一段落了,不知道大家在这个寻宝的过程中有没有收获,喜欢的可以点个赞,下一篇MySQL调优再见。

猜你喜欢

转载自blog.csdn.net/CX610602108/article/details/106298928