5.JVM调优工具及调优方法

247 阅读17分钟

思维导图:点击查看思维导图
文章图片:点击查看图片

1.jps工具

作用: 查看当前系统中有哪些 JAVA 进程

image.png

2.jmap

来查看内存信息,实例个数以及占用内存大小 文件较长,可以输出为对应的文件,也可以直接查看,慎用,执行的过程中为了保证 dump 的信息是可靠的,所以会暂停应用,不建议在生产环境执行。

# JVM会将整个heap的信息dump写入到一个文件,会暂停应用
jmap -dump 33980 > ./log.txt
# JVM会去统计perm区的状况,这整个过程也会比较的耗时,并且同样也会暂停应用
jmap -permstat > ./log.txt
# JVM会先触发 GC,然后再统计信息
jmap -histo:live

image.png

image.png

  • num:序号
  • instances:实例数量
  • bytes:占用空间大小
  • class name:类名称,[C is a char[],[S is a short[],[I is a int[],[B is a byte[]

查看堆内存占用情况(jmap -heap +进程id)  

image.png

image.png

堆内存dump 生成堆dump文件,该文件可以使用各种JVM工具打开装载,例如 jvisualvm,查看堆dump信息,分析内存溢出原因。

注意: 生产环境谨慎使用,需要导出当时的堆区对象快照,期间会引起程序暂停

jmap ‐dump:format=b,file=xxx.hprof 33980

也可以设置内存溢出自动导出dump文件(内存很大的时候,可能会导不出来)

  • -XX:+HeapDumpOnOutOfMemoryError
  • -XX:HeapDumpPath=./ (路径)
public class TestJVM {
    byte [] a = new byte[1024 *100]; // 100k 每次new该对象时占用100k
    // OOM时自动生成堆dump
    // -Xms5M -Xmx5M -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\jvm.dump
    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        while (true) {
            list.add(new TestJVM());
        }
    }
}
​

下面是jvisualvm装入的堆dump信息: 

image.png 可以从对象实例数大小进行分析(系统突然内存飙升/CPU飙升,查看是否有大量类生成) 从图中可以看出byte[]占堆内存 86%  

image.png 进入分析发现都属于 TestJVM 类

3. jstack

jstack 加进程 id 查找死锁:

E:>jps
12160 Jps
26960 TestJstackDeadLock
E:>jstack 26960

image.png

image.png

jvisualvm也能自动检查死锁: 线程的堆dump,点击可以查看到和jstack一样的信息 

image.png

jstack找出占用cpu高的线程堆栈信息:

1.使用 top 命令查看哪个进程占用 CPU 高

image.png

2.使用 top -p 命令 精确到该进程,显示该JAVA进程的内存情况,例如 top -p 21919 

image.png

3.按H(大写 shift + h),获取该进程下所有线程的 CPU 情况

53.png 4.找到内存和 CPU 占用最高的线程tid,比如 21920

5.转为十六进制得到 55a0 (jstack对应的 nid=0x55a0 ),此为线程id的十六进制表示

6.执行 jstack 21919 |grep -A 10 55a0,得到线程堆栈信息中 55a0 这个线程所在行的后面10行,从堆栈中可以发现导致 cpu 飙升的调用方法

4.jinfo

查看正在运行的 Java 应用程序的扩展参数 查看 JVM 的参数:

jinfo -flags <pid>

image.png

查看系统属性:jinfo -sysprops 34160

55.png

5.jstat

jstat 命令可以查看堆内存各部分的使用量,以及加载类的数量。命令的格式如下:  jstat [-命令选项] [vmid] [间隔时间(毫秒)] [查询次数]

# 每隔十秒运行一次,运行十次
jstat -gc 23924 10000 10 

注意:使用的 JDK 版本是 JDK 8

垃圾回收统计 jstat -gc pid 是最常用的 jstat 命令,可以评估程序内存使用及 GC 压力整体情况 

image.png

标识含义标识含义
S0C第一个幸存区大小 (KB)S1C第二个幸存区大小
S0U第一个幸存区已使用大小S1U第二个幸存区已使用大小
EC伊甸园区大小EU伊甸园区已使用大小
OC老年代大小OU老年代已使用大小
MC方法区(元空间)大小MU方法区(元空间)已使用大小
CCSC压缩类空间大小CCSU压缩类空间已使用大小
YGC年轻代垃圾回收次数YGCT年轻代垃圾回收消耗时间(s)
FGC老年代垃圾回收次数FGCT老年代垃圾回收消耗时间(s)
GCT垃圾回收消耗总时间(s)

堆内存统计 jstat -gccapacity  在这里插入图片描述

标识含义标识含义
NGCMN新生代最小容量NGCMX新生代最大容量
NGC当前新生代容量S0C第一个幸存区大小
S1C第二个幸存区的大小EC伊甸园区的大小
OGCMN老年代最小容量OGCMX老年代最大容量
OGC当前老年代大小OC当前老年代大小
MCMN最小元数据容量MCMX最大元数据容
MC当前元数据空间大小CCSMN最小压缩类空间大小
CCSMX最大压缩类空间大小CCSC当前压缩类空间大小
YGC年轻代gc次数)FGC老年代GC次数

新生代垃圾回收统计 jstat -gcnew pid

image.png

标识含义标识含义
S0C第一个幸存区的大小S1C第二个幸存区的大小
S0U第一个幸存区的使用大小S1U第二个幸存区的使用大小
TT对象在新生代存活的次数MTT对象在新生代存活的最大次数
DSS期望的幸存区大小EC伊甸园区的大小
EU伊甸园区的使用大小YGC年轻代垃圾回收次数
YGCT年轻代垃圾回收消耗时间

新生代内存统计 jstat -gcnewcapacity pid 在这里插入图片描述

标识含义标识含义
NGCMN新生代最小容量NGCMX新生代最大容量
NGC当前新生代容量S0CMX最大幸存1区大小
S0C当前幸存1区大小S1CMX最大幸存2区大小
S1C当前幸存2区大小ECMX最大伊甸园区大小
EC当前伊甸园区大小YGC年轻代垃圾回收次数
FGC老年代回收次数

老年代垃圾回收统计 jstat -gcold pid

image.png

标识含义标识含义
MC方法区大小MU方法区使用大小
CCSC压缩类空间大小CCSU压缩类空间使用大小
OC老年代大小OU老年代使用大小
YGC年轻代垃圾回收次数FGC老年代垃圾回收次数
FGCT老年代垃圾回收消耗时间GCT垃圾回收消耗总时间

老年代内存统计 jstat -gcoldcapacity pid image.png

标识含义标识含义
OGCMN老年代最小容量OGCMX老年代最大容量
OGC当前老年代大小OC老年代大小
YGC年轻代垃圾回收次数FGC老年代垃圾回收次数
FGCT老年代垃圾回收消耗时间GCT垃圾回收消耗总时间

元空间统计 jstat -gcmetacapacity pid

image.png

标识含义标识含义
MCMN最小元数据容量MCMX最大元数据容量
MC当前元数据空间大小CCSMN最小压缩类空间大小
CCSMX最大压缩类空间大小CCSC当前压缩类空间大小
YGC年轻代垃圾回收次数FGC老年代垃圾回收次数
FGCT老年代垃圾回收消耗时间GCT垃圾回收消耗总时间

垃圾回收信息统计(比例) jstat -gcutil pid

image.png

标识含义标识含义
S0幸存1区当前使用比例S1幸存2区当前使用比例
E伊甸园区使用比例O老年代使用比例
M元数据区使用比例CCS压缩使用比例
YGC年轻代垃圾回收次数FGC老年代垃圾回收次数
FGCT老年代垃圾回收消耗时间 GCT垃圾回收消耗总时间

JVM 最近编译情况  jstat -printcompilation pid

image.png

标识含义标识含义
Compiled最近编译方法的数量Size最近编译方法的字节码数量
Type最近编译方法的编译类型Method方法名标识。

5.JVM运行情况分析

在进行 JVM 调优之前,必须先获取 JVM 的运行情况。使用 jstat gc -pid 命令可以计算出一些至关重要的信息,有了这些信息就可以对 JVM 进行一些优化了,首先可以进行 JVM 参数的设置。例如:JVM堆内存大小,年轻代大小,Eden 和 Survivor 的比例,老年代的大小,大对象的阈值,大龄对象进入老年代的阈值等,在后期的测试中,也可以使用这些信息,定位问题之所在。通过 jstat 命令可以得到下面的一些关键信息:

  • 年轻代对象增长的速率 可以执行命令 jstat -gc pid 1000 1000 (每隔1秒执行1次命令,共执行1000次),通过观察 EU(Eden区已使用)估算每秒 Eden 大概新增多少对象,如果系统负载不高,可以把频率1秒换成1分钟,甚至10分钟来观察整体情况。注意,一般系统可能有高峰期和日常期,所以需要在不同的时间分别估算不同情况下对象增长速率。
  • Young GC 的触发频率和每次耗时 通过预估年轻代对象增长速率就能推根据 Eden 区的大小推算出 Young GC 触发频率,Young GC 的平均耗时可以通过 YGCT/YGC(年轻代垃圾回收消耗时间/年轻代垃圾回收次数)计算,由此我们就能知道大概多久系统会因为 Young GC 的执行而卡顿多久。
  • 每次 Young GC 后有多少对象存活和进入老年代 知道 Young GC 的触发频率后,假设是每两分钟触发一次 Young GC,那么就可以通过 jstat -gc pid 120000 10 ,观察每次执行后 Eden 区, survivor 区和老年代使用的变化情况。在每次 GC 后 Eden 区使用一般会大幅减少,survivor 区和老年代都有可能增长,这些增长的对象就是每次 Young GC 后存活的对象,同时还可以看出每次 Young GC 大概有多少对象进入老年代,推测出老年代增长速率
  • Full GC的触发频率和每次耗时 通过老年代增长速率,就可以知道老年代触发 Full GC 的频率,Full GC的每次耗时可以通过 FGCT/FGC(老年代垃圾回收消耗时间/老年代垃圾回收次数)计算。

优化思路: 尽量让每次 Young GC 后的存活对象小于 Survivor 区域的50%,让用过即死的对象尽量都留存在年轻代里。尽量别让对象进入老年代。避免频繁 Full GC。

6.JVM 简单工具调优分析及步骤

image.png

不使用额外工具是应该如何获取 JVM 运行信息然后进行调优呢?

  • 第一步: jps 定位运行查询 pid
  • 第二步: jinfo -flags pid查看 JVM 运行参数信息
  • 第三步: jstat -gc 3356 10000 10采集 GC 样本,由于 jstate 解析的 GC 使用情况是从项目启动时开始计算的,所以根据以上数据可以预估程序自启动到现在为止,期间发生Young GC 和 Full GC 的次数和耗时。

假设采集GC数据如下:

  • 系统配置:双核4G
  • JVM 内存大小:2G
  • 系统运行时间 7 天
  • 运行期间发生 Full GC 次数和耗时:500+ 次,200+ 秒
  • 运行期间发生 Young GC 次数和耗时:10000+ 次,500+ 秒

根据分析得出如下信息:

    • 平均每 20 分钟发生一次 Full GC,每次耗时大约 400 毫秒
    • 平均每 60 秒发生一次 Young GC,每次耗时大约 50 毫秒
  • 第四步: 根据 JVM 参数及采集的 GC 信息绘制大致的内存对象流转模型图

假设样例中的 JVM 参数如下:

-Xms1536M -Xmx1536M -Xmn512M -Xss256K 
-XX:SurvivorRatio=6 -XX:MetaspaceSize=256M 
-XX:MaxMetaspaceSize=256M -XX:+UseParNewGC 
-XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 
-XX:+UseCMSInitiatingOccupancyOnly

结合以上信息可以大概推断出: 堆内存空间 1.5 G年轻代空间 512M,方法区 256M,栈256K SurvivorRatio=6:Eden区,384M S0、S1各64M CMSInitiatingOccupancyFraction=75,UseCMSInitiatingOccupancyOnly:老年代空间达到 768M 触发 Full GC 平均每 20 分钟发生一次 Full GC:每 20 分钟有700多M对象被添加到老年代 平均每 60 秒发生一次 Young GC:每 60 秒 Eden 区满,假设程序运行平稳,即线程大概每秒产生 6M 左右的对象

image.png

  • 第五步: 根据对象流转规则,推理程序可能出现的问题
  • 第六步: 根据可疑问题进行优化,优化完成进行测试

一些建议

新生代特点:

  • 所有的new操作的内存分配都非常廉价
  • 死亡对象的回收代价是零(复制算法)
  • 大部分对象用过即死
  • Minor GC的时间远远低于FullGC

老年代调优建议,以CMS为例:

  • CMS的老年代内存越大越好
  • 先尝试不做调优,如果没有FULL GC那么说明老年代空间非常丰裕,否则先尝试调优新生代
  • 观察发生FULL GC是老年代内存占用,将老年代内存预设调大1/4~1/3

新生代推荐设置

  • 新生代容量大小:  新生代能容纳所有【并发量*(请求-响应)】的数据
  • 幸存区大小:  幸存区大到能保留【当前活跃对象+需要晋升对象】
  • 晋升阈值配置 晋升阈值配置得当,让长时间存活对象尽快晋升 (减少长时间存活对象在幸存区的复制)

下面是一些常见的问题:

  • Full GC 和 Young GC 频繁

可能原因:如果是年轻代空间紧张,业务高峰期,大量对象被创建,年轻代空间被塞满,Survivor 区空间紧张,对象晋升阈值降低,导致生存周期很短的对象也会被复制到老年代,导致老年代频繁发生Full GC

解决方案:增大新生代内存,使 Young GC 更少,并且 Survivor 区增大,对象晋升阈值上升,原本生命周期不长的对象不会进入老年代,从而减少老年代 Full GC发生

  • Full GC 频繁,甚至多于 Young GC

可能原因:

    • 1.元空间不足导致的多余 Full GC
    • 2.显示调用System.gc() ,造成多余的Full GC,这种一般线上通过­XX:+DisableExplicitGC参数禁用,如果加上了这个JVM启动参数,那么代码中调用System.gc()将失效
    • 3.默认的老年代空间担保机制,可能使Full GC次数是Young GC次数的两倍。因为老年代空间担保机制使得 Young GC 时先判断老年代空间是否足够,如果不够,先进行一次Full GC。然后再进行Young GC,很有可能Young GC后,对象被挪入老年代又触发了-XX:CMSInitiatingOccupancyFraction = 75 配置的老年代 Full GC 比例,再次触发 Full GC,造成非常频繁的 Full GC,甚至是 Young GC 的两倍!
    • 4.大对象直接进入老年代(可能是很大一批对象) ,可以使用 jmap 或者 jvisualvm 等工具查看对象的实例个数,如果发现某个对象个数异常,问题很有可能就在这个对象上。此问题如何解决:分析下占用 CPU 较高的线程,一般有大量对象不断产生,对应的方法代码肯定会被频繁调用,占用的 CPU 必然较高(jstack 和 jvisualvm 可以定位问题代码的位置)
  • 请求高峰期发生Full GC,单次暂停时间特别长

    可能原因:如果使用CMS做老年代回收器,CMS做重新标记时会扫描整个堆内存,在业务高峰时,年轻代对象个数较多,扫描标记时间会变得非常多(需要根据对象找引用)

    解决方案:重新标记前对新生代对象先做一次垃圾清理,重新标记阶段需要扫描和标记的对象就会变少(-XX:+CMSScavengeBeforeRemark)

  • 老年代充裕的情况下,发生 Full GC(CMS JDK1.7)

    可能原因:JDK1.7 之前使用的是永久代,永久代空间不足导致

    解决方案:增大永久代大小

7.内存泄漏与内存溢出

内存泄露:已申请的内存无法释放。比如:使用HashMap作为静态缓存对象,不断的往里面put数据,这些数据就会一直占用老年代的空间,这个map也可能会随着程序的运行不断的变大,时间一长就会导致频繁的 Full GC 甚至发生OOM。 这种情况完全可以考虑采用一些成熟的 JVM 级缓存框架来解决,比如 ehcache 等自带一些LRU数据淘汰算法的框架来作为JVM级的缓存。

  • 常犯错误:static Map map = ... 一直往 map 中添加数据,不进行移除(不建议直接使用 java 实现,可以使用 Redis 等第三方缓存实现,还可以使用考虑软,弱引用)

内存溢出:无法申请到足够的内存。当需要存储的数据超出了指定空间的大小时数据就会发生越界。举例来说,常见的溢出,是指在栈空间里,分配了超过数组长度的数据,导致多出来的数据覆盖了栈空间其他位置的数据,这种情况发生时,可能会导致程序出现各种难排查的异常行为,或是被有心人利用,修改特定位置的变量数据达到溢出攻击的目的。而Java中的内存溢出,一般指OOM。

8.GC日志详解

在生产环境中我们一般不能使用jvisualvm等工具,但是可以配置一些参数置把程序运行过程中的 GC 日志全部打印出来,然后分析 GC 日志得到关键性指标,分析 GC 原因,调优JVM参数。 打印 GC 日志方法,在 JVM 参数里增加参数。如果是 Tomcat 直接添加在 JAVA_OPTS 变量里。

‐Xloggc:./gc‐%t.log ‐XX:+PrintGCDetails ‐XX:+PrintGCDateStamps ‐XX:+PrintGCTimeStamps ‐XX:+PrintGCCause ‐XX:+UseGCLogFileRotation ‐XX:NumberOfGCLogFiles=10 ‐XX:GCLogFileSize=100M ./ 打印在当前目录(可以指定路径) %t时间 PrintGCDateStamps日期 PrintGCTimeStamps时间戳 流动打印 保留最后打印的10个文件 每个100M作为一个文件

如何分析GC日志 下图是 JVM 刚刚启动时打印的一段 JDK1.8默认Parallel + ParallelOld的GC 日志: 

在这里插入图片描述

从日志可以前面的几次 Full GC 都是由于元空间不够导致的,所以我们一般将元空间初始值和最大值设置一样,这样可以减少 Full GC 导致的项目启动过慢。 ‐Xloggc:./gc‐adjust‐%t.log ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:+PrintGCDetails ‐XX:+Print GCDateStamps ‐XX:+PrintGCTimeStamps ‐XX:+PrintGCCause ‐XX:+UseGCLogFileRotation ‐XX:NumberOfGCLogFiles=10 ‐XX:GCLogFileSize=100M

不同的垃圾回收器,打印的 GC 日志也会不同,通过打印 GC 日志,可以发现日志中的垃圾回收步骤和我们上篇文章介绍的不同垃圾回收器回收过程对应(如初始标记->并发标记->重新标记->并发清理->并发重置或使用SerialOld)。 CMS日志   CMS 打印日志设置的参数 ‐Xloggc:d:/gc‐cms‐%t.log ‐Xms50M ‐Xmx50M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:+PrintGCDetails ‐XX:+P rintGCDateStamps ‐XX:+PrintGCTimeStamps ‐XX:+PrintGCCause ‐XX:+UseGCLogFileRotation ‐XX:NumberOfGCLogFiles=10 ‐XX:GCLogFileSize=100M ‐XX:+UseParNewGC ‐XX:+UseConcMarkSweepGC

G1 打印日志设置的参数 ‐Xloggc:d:/gc‐g1‐%t.log ‐Xms50M ‐Xmx50M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:+PrintGCDetails ‐XX:+Pr intGCDateStamps‐XX:+PrintGCTimeStamps ‐XX:+PrintGCCause ‐XX:+UseGCLogFileRotation ‐XX:NumberOfGCLogFiles=10 ‐XX:GCLogFileSize=100M ‐XX:+UseG1GC

在生产中我们一般都会通过一些工具区自动化的解析 GC 日志。比如: gceasy,可以 上传gc文件,然后他会利用可视化的界面来展现GC情况。具体下图所示: 在这里插入图片描述 由图可以看出年轻代,老年代,以及永久代的内存分配,和最大使用情况。 在这里插入图片描述 堆内存在GC之前和之后的变化,以及其他信息 甚至还会提供一些JVM的优化建议: 在这里插入图片描述 最后附上查看JVM参数汇总的命令: java -XX:+PrintFlagsInitial 打印所有参数选项的默认值 java -XX:+PrintFlagsFinal 打印所有参数选项在运行程序时生效的值

9.阿里巴巴Arthas

在生产上一般使用一些开源的工具,比如Arthas(阿尔萨斯)、Prometheus(普罗米修斯)等。Arthas 是 Alibaba 在 2018 年 9 月开源的 Java 诊断工具。支持 JDK6+, 采用命令行交互模式,可以方便的定位和诊断线上程序运行问题。Arthas 官方文档:链接: alibaba.github.io/arthas.