思维导图:点击查看思维导图.
前言:阅读此文章前,需要了解的知识
链接: JVM-03.对象创建与内存分配.
链接: JVM-04.垃圾回收机制看着一篇就够了.
来查看内存信息,实例个数以及占用内存大小
文件较长,可以输出为对应的文件,也可以直接查看
num:序号
instances:实例数量
bytes:占用空间大小
class name:类名称,[C is a char[],[S is a short[],[I is a int[],[B is a byte[],[[I is a int[][]
查看堆内存占用情况(jmap -heap +进程id)
堆内存dump
生成堆dump文件,该文件可以使用各种JVM工具打开装载,例如 jvisualvm,查看堆dump信息,分析内存溢出原因。
注意: 生产环境谨慎使用,需要导出当时的堆区对象快照,期间会引起程序暂停
jmap ‐dump:format=b,file=xxx.hprof 33980
也可以设置内存溢出自动导出dump文件(内存很大的时候,可能会导不出来)
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信息:
可以从对象实例数和大小进行分析(系统突然内存飙升/CPU飙升,查看是否有大量类生成)
从图中可以看出byte[]占堆内存 86%
进入分析发现都属于TestJVM类
jstack加进程id查找死锁:
E:\>jps
12160 Jps
26960 TestJstackDeadLock
E:\>jstack 26960
jvisualvm也能自动检查死锁: 线程的堆dump,点击可以查看到和jstack一样的信息
jstack找出占用cpu高的线程堆栈信息:
1.使用top 命令查看哪个进程占用 CPU 高
2.使用 top -p 命令 精确到该进程,显示该JAVA进程的内存情况,例如 top -p 21919
2.按H(大写 shift + h),获取该进程下所有线程的 CPU 情况
3. 找到内存和 CPU 占用最高的线程tid,比如 21920
4. 转为十六进制得到 55a0 (jstack对应的 nid=0x55a0 ),此为线程id的十六进制表示
5. 执行 jstack 21919 |grep -A 10 55a0,得到线程堆栈信息中 55a0 这个线程所在行的后面10行,从堆栈中可以发现导致cpu飙升的调用方法
查看正在运行的Java应用程序的扩展参数
查看jvm的参数: jinfo -flags
查看系统属性:jinfo -sysprops 34160
jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。命令的格式如下:
jstat [-命令选项] [vmid] [间隔时间(毫秒)] [查询次数]
例如jstat -gc 23924 10000 10 每隔十秒运行一次,运行十次
注意:使用的 JDK 版本是 JDK8
垃圾回收统计
jstat -gc pid 是 最常用 的 jstat 命令,可以评估程序内存使用及GC压力整体情况
标识 | 含义 | 标识 | 含义 |
---|---|---|---|
S0C | 第一个幸存区大小 (KB) | S1C | 第二个幸存区大小 |
S0U | 第一个幸存区已使用大小 | S1U | 第二个幸存区已使用大小 |
EC | 伊甸园区大小 | EU | 伊甸园区已使用大小 |
OC | 老年代大小 | OU | 老年代已使用大小 |
MC | 方法区(元空间)大小 | MU | 方法区(元空间)已使用大小 |
CCSC | 压缩类空间大小 | CCSU | 压缩类空间已使用大小 |
YGC | 年轻代垃圾回收次数 | YGCT | 年轻代垃圾回收消耗时间(s) |
FGC | 老年代垃圾回收次数 | FGCT | 老年代垃圾回收消耗时间(s) |
GCT | 垃圾回收消耗总时间(s) |
标识 | 含义 | 标识 | 含义 |
---|---|---|---|
NGCMN | 新生代最小容量 | NGCMX | 新生代最大容量 |
NGC | 当前新生代容量 | S0C | 第一个幸存区大小 |
S1C | 第二个幸存区的大小 | EC | 伊甸园区的大小 |
OGCMN | 老年代最小容量 | OGCMX | 老年代最大容量 |
OGC | 当前老年代大小 | OC | 当前老年代大小 |
MCMN | 最小元数据容量 | MCMX | 最大元数据容 |
MC | 当前元数据空间大小 | CCSMN | 最小压缩类空间大小 |
CCSMX | 最大压缩类空间大小 | CCSC | 当前压缩类空间大小 |
YGC | 年轻代gc次数) | FGC | 老年代GC次数 |
标识 | 含义 | 标识 | 含义 |
---|---|---|---|
S0C | 第一个幸存区的大小 | S1C | 第二个幸存区的大小 |
S0U | 第一个幸存区的使用大小 | S1U | 第二个幸存区的使用大小 |
TT | 对象在新生代存活的次数 | MTT | 对象在新生代存活的最大次数 |
DSS | 期望的幸存区大小 | EC | 伊甸园区的大小 |
EU | 伊甸园区的使用大小 | YGC | 年轻代垃圾回收次数 |
YGCT | 年轻代垃圾回收消耗时间 |
标识 | 含义 | 标识 | 含义 |
---|---|---|---|
NGCMN | 新生代最小容量 | NGCMX | 新生代最大容量 |
NGC | 当前新生代容量 | S0CMX | 最大幸存1区大小 |
S0C | 当前幸存1区大小 | S1CMX | 最大幸存2区大小 |
S1C | 当前幸存2区大小 | ECMX | 最大伊甸园区大小 |
EC | 当前伊甸园区大小 | YGC | 年轻代垃圾回收次数 |
FGC | 老年代回收次数 |
标识 | 含义 | 标识 | 含义 |
---|---|---|---|
MC | 方法区大小 | MU | 方法区使用大小 |
CCSC | 压缩类空间大小 | CCSU | 压缩类空间使用大小 |
OC | 老年代大小 | OU | 老年代使用大小 |
YGC | 年轻代垃圾回收次数 | FGC | 老年代垃圾回收次数 |
FGCT | 老年代垃圾回收消耗时间 | GCT | 垃圾回收消耗总时间 |
标识 | 含义 | 标识 | 含义 |
---|---|---|---|
OGCMN | 老年代最小容量 | OGCMX | 老年代最大容量 |
OGC | 当前老年代大小 | OC | 老年代大小 |
YGC | 年轻代垃圾回收次数 | FGC | 老年代垃圾回收次数 |
FGCT | 老年代垃圾回收消耗时间 | GCT | 垃圾回收消耗总时间 |
标识 | 含义 | 标识 | 含义 |
---|---|---|---|
MCMN | 最小元数据容量 | MCMX | 最大元数据容量 |
MC | 当前元数据空间大小 | CCSMN | 最小压缩类空间大小 |
CCSMX | 最大压缩类空间大小 | CCSC | 当前压缩类空间大小 |
YGC | 年轻代垃圾回收次数 | FGC | 老年代垃圾回收次数 |
FGCT | 老年代垃圾回收消耗时间 | GCT | 垃圾回收消耗总时间 |
标识 | 含义 | 标识 | 含义 |
---|---|---|---|
S0 | 幸存1区当前使用比例 | S1 | 幸存2区当前使用比例 |
E | 伊甸园区使用比例 | O | 老年代使用比例 |
M | 元数据区使用比例 | CCS | 压缩使用比例 |
YGC | 年轻代垃圾回收次数 | FGC | 老年代垃圾回收次数 |
FGCT | 老年代垃圾回收消耗时间 GCT | 垃圾回收消耗总时间 |
标识 | 含义 | 标识 | 含义 |
---|---|---|---|
Compiled | 最近编译方法的数量 | Size | 最近编译方法的字节码数量 |
Type | 最近编译方法的编译类型 | Method | 方法名标识。 |
在进行 JVM 调优之前,必须先获取JVM的运行情况。使用 jstat gc -pid
命令可以计算出一些至关重要的信息,有了这些信息就可以对 JVM 进行一些优化了,首先可以进行 JVM 参数的设置。例如:JVM堆内存大小,年轻代大小,Eden 和 Survivor 的比例,老年代的大小,大对象的阈值,大龄对象进入老年代的阈值等,在后期的测试中,也可以使用这些信息,定位问题之所在。
年轻代对象增长的速率
可以执行命令 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。
不使用额外工具是应该如何获取 JVM 运行信息然后进行调优呢?
第一步:jps
定位运行查询pid
第二步:jinfo -flags pid
查看 JVM 运行参数信息
第三步:jstat -gc 3356 10000 10
采集 GC 样本,由于 jstate 解析的 GC 使用情况是从项目启动时开始计算的,所以根据以上数据可以预估程序自启动到现在为止,期间发生Young GC 和 Full GC 的次数和耗时。
假设采集GC数据如下:
根据分析得出如下信息:
第四步: 根据 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 左右的对象
第五步: 根据对象流转规则,推理程序可能出现的问题
对象流转规则链接: JVM-03.对象创建与内存分配.
第六步: 根据可疑问题进行优化,优化完成进行测试
下面是一些常见的问题:
可能原因:
如果是年轻代空间紧张,业务高峰期,大量对象被创建,年轻代空间被塞满,Survivor 区空间紧张,对象晋升阈值降低,导致生存周期很短的对象也会被复制到老年代,导致老年代频繁发生Full GC解决方案:
增大新生代内存,使 Young GC 更少,并且 Survivor 区增大,对象晋升阈值上升,原本生命周期不长的对象不会进入老年代,从而减少老年代 Full GC发生可能原因:
1.元空间不足
导致的多余 Full GC2.显示调用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 可以定位问题代码的位置)可能原因:
如果使用CMS做老年代回收器,CMS做重新标记时会扫描整个堆内存,在业务高峰时,年轻代对象个数较多,扫描标记时间会变得非常多(需要根据对象找引用)解决方案:
重新标记前对新生代对象先做一次垃圾清理,重新标记阶段需要扫描和标记的对象就会变短(-XX:+CMSScavengeBeforeRemark)可能原因:
JDK1.7 之前使用的是永久代,永久代空间不足导致解决方案:
增大永久代大小内存泄露:
已申请的内存无法释放。比如:使用HashMap作为静态缓存对象,不断的往里面put数据,这些数据就会一直占用老年代的空间,这个map也可能会随着程序的运行不断的变大,时间一长就会导致频繁的 Full GC 甚至发生OOM。 这种情况完全可以考虑采用一些成熟的 JVM 级缓存框架来解决,比如 ehcache 等自带一些LRU数据淘汰算法的框架来作为JVM级的缓存。
内存溢出:
无法申请到足够的内存。当需要存储的数据超出了指定空间的大小时数据就会发生越界。举例来说,常见的溢出,是指在栈空间里,分配了超过数组长度的数据,导致多出来的数据覆盖了栈空间其他位置的数据,这种情况发生时,可能会导致程序出现各种难排查的异常行为,或是被有心人利用,修改特定位置的变量数据达到溢出攻击的目的。而Java中的内存溢出,一般指OOM。
在生产环境中我们一般不能使用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 打印所有参数选项在运行程序时生效的值
在生产上一般使用一些开源的工具,比如Arthas(阿尔萨斯)、Prometheus(普罗米修斯)等。Arthas 是 Alibaba 在 2018 年 9 月开源的 Java 诊断工具。支持 JDK6+, 采用命令行交互模式,可以方便的定位和诊断线上程序运行问题。Arthas 官方文档:链接: https://alibaba.github.io/arthas.