奔走相告(秋,风吹过)穿搭集锦|秋风它吹过我的脸,
81 2026-01-29
磁盘不足排查其实,磁盘不足排查算是系统、程序层面的问题排查,并不算是JVM,但是另一方面考虑过来就是,系统磁盘的不足,也会导致JVM的运行异常,所以也把磁盘不足算进来了并且排查磁盘不足,是比较简单,就是几个命令,然后就是逐层的排查,首先第一个命令就是。
df -h,查询磁盘的状态:
从上面的显示中其中第一行使用的2.8G最大,然后是挂载在 / 目录下,我们直接cd /然后通过执行:du -sh *查看各个文件的大小,找到其中最大的,或者说存储量级差不多的并且都非常大的文件,把那些没用的大文件删除就好。
然后,就是直接cd到对应的目录也是执行:du -sh *,就这样一层一层的执行,找到对应的没用的,然后文件又比较大的,可以直接删除CPU过高排查排查过程使用top查找进程id使用top -Hp 。
查找进程中耗cpu比较高的线程id使用printf %x 将线程id十进制转十六进制使用 jstack -pid | grep -A 20 过滤出线程id锁关联的栈信息根据栈信息中的调用链定位业务代码
案例代码如下:publicclassCPUSoaring { publicstaticvoidmain(String[] args) { Thread thread1 =
new Thread(new Runnable(){ @Override publicvoidrun() {
for (;;){ System.out.println("I am children-thread1"); } } },
"children-thread1"); Thread thread2 = new Thread(new Runnable(){ @
Override publicvoidrun() { for (;;){ System.
out.println("I am children-thread2"); } } },
"children-thread2"); thread1.start(); thread2.start(); System.err.println(
"I am is main thread!!!!!!!!"); } }第一步:首先通过top命令可以查看到id为3806的进程所占的CPU最高:第二步:然后通过top -Hp pid命令,找到占用CPU最高的线程:
第三步:接着通过:printf %x\n tid命令将线程的tid转换为十六进制:xid:第四步:最后通过:jstack pid|grep xid -A 30命令就是输出线程的堆栈信息,线程所在的位置:
第五步:还可以通过jstack -l pid > 文件名称.txt 命令将线程堆栈信息输出到文件,线下查看这就是一个CPU飙高的排查过程,目的就是要找到占用CPU最高的线程所在的位置,然后就是review。
你的代码,定位到问题的所在使用Arthas的工具排查也是一样的,首先要使用top命令找到占用CPU最高的Java进程,然后使用Arthas进入该进程内,使用dashboard命令排查占用CPU最高的线程。
,最后通过thread命令线程的信息内存打满排查排查过程查找进程id:top -d 2 -c查看JVM堆内存分配情况:jmap -heap pid查看占用内存比较多的对象:jmap -histo pid | head -n 100。
查看占用内存比较多的存活对象:jmap -histo:live pid | head -n 100示例第一步:top -d 2 -c第二步:jmap -heap 8338第三步:定位占用内存比价多的对象
这里就能看到对象个数以及对象大小……这里看到一个自定义的类,这样我们就定位到具体对象,看看这个对象在那些地方有使用、为何会有大量的对象存在OOM异常排查OOM的异常排查也比较简单,首先服务上线的时候,要先设置这两个参数:。
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${目录}指定项目出现OOM异常的时候自动导出堆转储文件,然后通过内存分析工具(Visual VM)来进行线下的分析。
首先我们来聊一聊,哪些原因会导致OOM异常,站在JVM的分区的角度:Java堆方法区虚拟机栈本地方法栈程序计数器直接内存只有程序计数器区域不会出现OOM,在Java 8及以上的元空间(本地内存)都会出现OOM。
而站在程序代码的角度来看,总结了大概有以下几点原因会导致OOM异常:内存泄露对象过大、过多方法过长过度使用代理框架,生成大量的类信息接下来我们屋来看看OOM的排查,出现OOM异常后dump出了堆转储文件,然后打开jdk自带的Visual VM工具,导入堆转储文件,首先我使用的OOM异常代码如下:
import java.util.ArrayList; import java.util.List; classOOM {staticclassUser{private String name;
privateint age; publicUser(String name, int age){ this.name = name;
this.age = age; } } publicstaticvoidmain(String[] args) throws InterruptedException
{ List list = new ArrayList<>(); for (int i = 0; i < Integer.MAX_VALUE; i++) { Thread.sleep(
1000); User user = new User("zhangsan"+i,i); list.add(user); } } }
代码很简单,就是往集合里面不断地add对象,带入堆转储文件后,在类和实例那栏就可以看到实例最多的类:
这样就找到导致OOM异常的类,还可以通过下面的方法查看导致OOM异常的线程堆栈信息,找到对应异常的代码段。

上面的方法是排查已经出现了OOM异常的方法,肯定是防线的最后一步,那么在此之前怎么防止出现OOM异常呢?一般大厂都会有自己的监控平台,能够实施的监控测试环境、预览环境、线上实施的服务健康状况(CPU、内存)
等信息,对于频繁GC,并且GC后内存的回收率很差的,就要引起我们的注意了因为一般方法的长度合理,95%以上的对象都是朝生夕死,在Minor GC后只剩少量的存活对象,所以在代码层面上应该避免方法过长、大对象。
的现象每次自己写完代码,自己检查后,都可以提交给比自己高级别的工程师review自己的代码,就能及时的发现代码的问题,基本上代码没问题,百分之九十以上的问题都能避免,这也是大厂注重代码质量,并且时刻review
代码的习惯。Jvisualvm项目频繁YGC 、FGC问题排查内存问题对象内存占用、实例个数监控
对象内存占用、年龄值监控
通过上面两张图发现这些对象占用内存比较大而且存活时间也是比较常,所以survivor 中的空间被这些对象占用,而如果缓存再次刷新则会创建同样大小对象来替换老数据,这时发现eden内存空间不足,就会触发yonggc 如果yonggc 结束后发现eden空间还是不够则会直接放到老年代,所以这样就产生了大对象的提前晋升,导致fgc增加……
优化办法:优化两个缓存对象,将缓存对象大小减小。优化一下两个对象,缓存关键信息!CPU耗时问题排查Cpu使用耗时监控:
耗时、调用次数监控:
从上面监控图可以看到主要耗时还是在网络请求,没有看到具体业务代码消耗过错cpu……调优堆内存分配初始堆空间大小设置使用系统默认配置在系统稳定运行一段时间后查看记录内存使用情况:Eden、survivor0 、survivor1 、old、metaspace
按照通用法则通过gc信息分配调整大小,整个堆大小是Full GC后老年代空间占用大小的3-4倍老年代大小为Full GC后老年代空间占用大小的2-3倍新生代大小为Full GC后老年代空间占用大小的1-1.5倍
元数据空间大小为Full GC后元数据空间占用大小的1.2-1.5倍活跃数大小是应用程序运行在稳定态时,长期存活的对象在java堆中占用的空间大小也就是在应用趋于稳太时FullGC之后Java堆中存活对象占用空间大小。
(注意在jdk8中将jdk7中的永久代改为元数据区,metaspace 使用的物理内存,不占用堆内存)堆大小调整的着手点、分析点统计Minor GC 持续时间统计Minor GC 的次数统计Full GC的最长持续时间
统计最差情况下Full GC频率统计GC持续时间和频率对优化堆的大小是主要着手点,我们按照业务系统对延迟和吞吐量的需求,在按照这些分析我们可以进行各个区大小的调整年轻代调优年轻代调优规则老年代空间大小不应该小于活跃数大小1.5倍。
老年代空间大小应为老年代活跃数2-3倍新生代空间至少为java堆内存大小的10% 新生代空间大小应为1-1.5倍的老年代活跃数在调小年轻代空间时应保持老年代空间不变MinorGC是收集eden+from survivor 区域的,当业务系统匀速生成对象的时候如果年轻带分配内存偏小会发生频繁的MinorGC,如果分配内存过大则会导致MinorGC停顿时间过长,无法满足业务延迟性要求。
所以按照堆分配空间分配之后分析gc日志,看看MinorGC的频率和停顿时间是否满足业务要求MinorGC频繁原因 MinorGC 比较频繁说明eden内存分配过小,在恒定的对象产出的情况下很快无空闲空间来存放新对象所以产生了MinorGC,所以eden区间需要调大。
年轻代大小调整 Eden调整到多大那,我们可以查看GC日志查看业务多长时间填满了eden空间,然后按照业务能承受的收集频率来计算eden空间大小比如eden空间大小为128M每5秒收集一次,则我们为了达到10秒收集一次则可以调大eden空间为256M这样能降低收集频率。
年轻代调大的同时相应的也要调大老年代,否则有可能会出现频繁的concurrent model failed 从而导致Full GC MinorGC停顿时间过长 MinorGC 收集过程是要产生STW的如果年轻代空间太大,则gc收集时耗时比较大,所以我们按业务对停顿的要求减少内存,比如现在一次MinorGC 耗时12.8毫秒,eden内存大小192M ,我们为了减少MinorGC 耗时我们要减少内存。
比如我们MinorGC 耗时标准为10毫秒,这样耗时减少16.6% 同样年轻代内存也要减少16.6% 即192*0.1661 = 31.89M 年轻代内存为192-31.89=160.11M,在减少年轻代大小,而要保持老年代大小不变则要减少堆内存大小至512-31.89=480.11M。
堆内存:512M 年轻代: 192M 收集11次耗时141毫秒 12.82毫秒/次
堆内存:512M 年轻代:192M 收集12次耗时151毫秒 12.85毫秒/次
按照上面计算调优 堆内存: 480M 年轻带: 160M 收集14次 耗时154毫秒 11毫秒/次 相比之前的 12.82毫秒/次 停顿时间减少1.82毫秒
但是还没达到10毫秒的要求,继续按照这样的逻辑进行 11-10=1 ;1/11= 0.909 即 0.09 所以耗时还要降低9% 年轻代减少:160*0.09 = 14.545=14.55 M; 160-14.55 =145.45=145M 堆大小: 480-14.55 = 465.45=465M 但是在这样调整后使用jmap -heap 查看的时候年轻代大小和实际配置数据有出入(年轻代大小为150M大于配置的145M),这是因为-XX:NewRatio 默认2 即年轻代和老年代1:2的关系,所以这里将-XX:NewRatio 设置为3 即年轻代、老年大小比为1:3 ,最终堆内存大小为:。
MinorGC耗时 159/16=9.93毫秒
MinorGC耗时 185/18=10.277=10.28毫秒
MinorGC耗时 205/20=10.25毫秒
Ok 这样MinorGC停顿时间过长问题解决,MinorGC要么比较频繁要么停顿时间比较长,解决这个问题就是调整年轻代大小,但是调整的时候还是要遵守这些规则老年代调优按照同样的思路对老年代进行调优,同样分析FullGC 频率和停顿时间,按照优化设定的目标进行老年代大小调整。
老年代调优流程分析每次MinorGC 之后老年代空间占用变化,计算每次MinorGC之后晋升到老年代的对象大小按照MinorGC频率和晋升老年代对象大小计算提升率即每秒钟能有多少对象晋升到老年代FullGC之后统计老年代空间被占用大小计算老年带空闲空间,再按照第2部计算的晋升率计算该老年代空闲空间多久会被填满而再次发生FullGC,同样观察FullGC 日志信息,计算FullGC频率,如果频率过高则可以增大老年代空间大小老解决,增大老年代空间大小应保持年轻代空间大小不变
如果在FullGC 频率满足优化目标而停顿时间比较长的情况下可以考虑使用CMS、G1收集器并发收集减少停顿时间栈溢出栈溢出异常的排查(包括虚拟机栈、本地方法栈)基本和OOM的一场排查是一样的,导出异常的堆栈信息,然后使用mat或者Visual VM工具进行线下分析,找到出现异常的代码或者方法。
当线程请求的栈深度大于虚拟机栈所允许的大小时,就会出现StackOverflowError异常,二从代码的角度来看,导致线程请求的深度过大的原因可能有:方法栈中对象过大,或者过多,方法过长从而导致局部变量表过大,超过了-Xss参数的设置
死锁排查死锁的案例演示的代码如下:publicclassDeadLock{ publicstatic Object lock1 = new Object(); publicstatic Object lock2 = 。
new Object(); publicstaticvoidmain(String[] args){ Thread a = new Thread(new Lock1(),"DeadLock1"); Thread b =
new Thread(new Lock2(),"DeadLock2"); a.start(); b.start(); } } classLock1implementsRunnable{ @Override
publicvoidrun(){ try{ while(true){ synchronized(DeadLock.lock1){ System.out.println("Waiting for lock2"
); Thread.sleep(3000); synchronized(DeadLock.lock2){ System.out.println("Lock1 acquired lock1 and lock2 "
); } } } }catch(Exception e){ e.printStackTrace(); } } } classLock2implementsRunnable
{ @Overridepublicvoidrun(){ try{ while(true){ synchronized(DeadLock.lock2){ System.out.println(
"Waiting for lock1"); Thread.sleep(3000); synchronized(DeadLock.lock1){ System.out.println(
"Lock2 acquired lock1 and lock2"); } } } }catch(Exception e){ e.printStackTrace(); } } }
上面的代码非常的简单,就是两个类的实例作为锁资源,然后分别开启两个线程,不同顺序的对锁资源资源进行加锁,并且获取一个锁资源后,等待三秒,是为了让另一个线程有足够的时间获取另一个锁对象运行上面的代码后,就会陷入死锁的僵局:。
对于死锁的排查,若是在测试环境或者本地,直接就可以使用Visual VM连接到该进程,如下界面就会自动检测到死锁的存在
并且查看线程的堆栈信息。就能看到具体的死锁的线程:
线上的话可以上用Arthas也可以使用原始的命令进行排查,原始命令可以先使用jps查看具体的Java进程的ID,然后再通过jstack ID查看进程的线程堆栈信息,他也会自动给你提示有死锁的存在:
Arthas工具可以使用thread命令排查死锁,要关注的是BLOCKED状态的线程,如下图所示:
具体thread的详细参数可以参考如下图所示:
如何避免死锁上面我们聊了如何排查死锁,下面我们来聊一聊如何避免死锁的发生,从上面的案例中可以发现,死锁的发生两个线程同时都持有对方不释放的资源进入僵局所以,在代码层面,要避免死锁的发生,主要可以从下面的四个方面进行入手:。
首先避免线程的对于资源的加锁顺序要保持一致并且要避免同一个线程对多个资源进行资源的争抢另外的话,对于已经获取到的锁资源,尽量设置失效时间,避免异常,没有释放锁资源,可以使用acquire() 方法加锁时可指定 timeout 参数
最后,就是使用第三方的工具检测死锁,预防线上死锁的发生死锁的排查已经说完了,上面的基本就是问题的排查,也可以算是调优的一部分吧,但是对于JVM调优来说,重头戏应该是在Java堆,这部分的调优才是重中之重。
调优实战上面说完了调优的目的和调优的指标,那么我们就来实战调优,首先准备我的案例代码,如下:import java.util.ArrayList; import java.util.List; class
OOM {staticclassUser{private String name; privateint age; publicUser(String name, int age){ this
.name = name; this.age = age; } } publicstaticvoidmain(String[] args) throws InterruptedException
{ List list = new ArrayList<>(); for (int i = 0; i < Integer.MAX_VALUE; i++) { Tread.sleep(
1000); System.err.println(Thread.currentThread().getName()); User user = new User("zhangsan"+i,i);
list.add(user); } } }案例代码很简单,就是不断的往一个集合里里面添加对象,首先初次我们启动的命令为:java -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseGCLogFileRotation -XX:+PrintHeapAtGC -XX:NumberOfGCLogFiles=
5 -XX:GCLogFileSize=50M -Xloggc:./logs/emps-gc-%t.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./logs/emps-heap.
dump OOM就是纯粹的设置了一些GC的打印日志,然后通过Visual VM来看GC的显示如下:
可以看到一段时间后出现4次Minor GC,使用的时间是29.648ms,发生一次Full GC使用的时间是41.944msMinor GC非常频繁,Full GC也是,在短时间内就发生了几次,观察输出的日志发现以及Visual VM的显示来看,都是因为内存没有设置,太小,导致Minor GC频繁。
因此,我们第二次适当的增大Java堆的大小,调优设置的参数为:java -Xmx2048m -Xms2048m -Xmn1024m -Xss256k -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseGCLogFileRotation -XX:+PrintHeapAtGC -XX:NumberOfGCLogFiles=
5 -XX:GCLogFileSize=50M -Xloggc:./logs/emps-gc-%t.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./logs/emps-heap.
dump OOM观察一段时间后,结果如下图所示:
可以发现Minor GC次数明显下降,但是还是发生了Full GC,根据打印的日志来看,是因为元空间的内存不足,看了上面的Visual VM元空间的内存图,也是一样,基本都到顶了:
因此第三次对于元空间的区域设置大一些,并且将GC回收器换成是CMS的,设置的参数如下:java -Xmx2048m -Xms2048m -Xmn1024m -Xss256k -XX:MetaspaceSize=
100m -XX:MaxMetaspaceSize=100m -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseGCLogFileRotation -XX:+PrintHeapAtGC -XX:NumberOfGCLogFiles=
5 -XX:GCLogFileSize=50M -Xloggc:./logs/emps-gc-%t.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./logs/emps-heap.
dump OOM观察相同的时间后,Visual VM的显示图如下:
同样的时间,一次Minor GC和Full GC都没有发生,所以这样我觉得也算是已经调优了但是调优并不是一味的调大内存,是要在各个区域之间取得平衡,可以适当的调大内存,以及更换GC种类,举个例子,当把上面的案例代码的Thread.sleep(1000)给去掉。
然后再来看Visual VM的图,如下:
可以看到Minor GC也是非常频繁的,因为这段代码本身就是不断的增大内存,直到OOM异常,真正的实际并不会这样,可能当内存增大到一定两级后,就会在一段范围平衡当我们将上面的情况,再适当的增大内存,JVM参数如下:。
java -Xmx4048m -Xms4048m -Xmn2024m -XX:SurvivorRatio=7 -Xss256k -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=
100m -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseGCLogFileRotation -XX:+PrintHeapAtGC -XX:NumberOfGCLogFiles=
5 -XX:GCLogFileSize=50M -Xloggc:./logs/emps-gc-%t.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./logs/emps-heap.
dump OOM可以看到相同时间内,确实Minor GC减少了,但是时间增大了,因为复制算法,基本都是存活的,复制需要耗费大量的性能和时间:
所以,调优要有取舍,取得一个平衡点,性能、状态达到佳就OK了,并没最佳的状态,这就是调优的基本法则,而且调优也是一个细活,所谓慢工出细活,需要耗费大量的时间,慢慢调,不断的做对比调优参数堆-Xms1024m 设置堆的初始大小。
-Xmx1024m 设置堆的最大大小-XX:NewSize=1024m 设置年轻代的初始大小-XX:MaxNewSize=1024m 设置年轻代的最大值-XX:SurvivorRatio=8 Eden和S区的比例
-XX:NewRatio=4 设置老年代和新生代的比例-XX:MaxTenuringThreshold=10 设置晋升老年代的年龄条件栈-Xss128k元空间-XX:MetasapceSize=200m 设置初始元空间大小
-XX:MaxMatespaceSize=200m 设置最大元空间大小 默认无限制直接内存-XX:MaxDirectMemorySize 设置直接内存的容量,默认与堆最大值一样日志-Xloggc:/opt/app/ard-user/ard-user-gc-%t.log 设置日志目录和日志名称
-XX:+UseGCLogFileRotation 开启滚动生成日志-XX:NumberOfGCLogFiles=5 滚动GC日志文件数,默认0,不滚动-XX:GCLogFileSize=20M GC文件滚动大小,需开 UseGCLogFileRotation
-XX:+PrintGCDetails 开启记录GC日志详细信息(包括GC类型、各个操作使用的时间),并且在程序运行结束打印出JVM的内存占用情况-XX:+ PrintGCDateStamps 记录系统的GC时间
-XX:+PrintGCCause 产生GC的原因(默认开启)GCSerial垃圾收集器(新生代)开启-XX:+UseSerialGC关闭:-XX:-UseSerialGC //新生代使用Serial 老年代则使用SerialOld
Parallel Scavenge收集器(新生代)开启-XX:+UseParallelOldGC 关闭-XX:-UseParallelOldGC 新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器
ParallelOl垃圾收集器(老年代)开启-XX:+UseParallelGC 关闭-XX:-UseParallelGC 新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器
ParNew垃圾收集器(新生代)开启-XX:+UseParNewGC 关闭-XX:-UseParNewGC //新生代使用功能ParNew 老年代则使用功能CMSCMS垃圾收集器(老年代)开启-XX:+UseConcMarkSweepGC 关闭
-XX:-UseConcMarkSweepGC-XX:MaxGCPauseMillis GC停顿时间,垃圾收集器会尝试用各种手段达到这个时间,比如减小年轻代-XX:+UseCMSCompactAtFullCollection 用于在CMS收集器不得不进行FullGC时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,(在Shenandoah和ZGC出现前)是无法并发的
-XX:CMSFullGCsBefore-Compaction 多少次FullGC之后压缩一次,默认值为0,表示每次进入FullGC时都进行碎片整理)-XX:CMSInitiatingOccupancyFraction 当老年代使用达到该比例时会触发FullGC,默认是92
-XX:+UseCMSInitiatingOccupancyOnly 这个参数搭配上面那个用,表示是不是要一直使用上面的比例触发FullGC,如果设置则只会在第一次FullGC的时候使用-XX:CMSInitiatingOccupancyFraction的值,之后会进行自动调整
-XX:+CMSScavengeBeforeRemark 在FullGC前启动一次MinorGC,目的在于减少老年代对年轻代的引用,降低CMSGC的标记阶段时的开销,一般CMS的GC耗时80%都在标记阶段
-XX:+CMSParallellnitialMarkEnabled 默认情况下初始标记是单线程的,这个参数可以让他多线程执行,可以减少STW-XX:+CMSParallelRemarkEnabled 使用多线程进行重新标记,目的也是为了减少STW
G1垃圾收集器开启-XX:+UseG1GC 关闭-XX:-UseG1GC-XX:G1HeapRegionSize 设置每个Region的大小,取值范围为1MB~32MB-XX:MaxGCPauseMillis 设置垃圾收集器的停顿时间,默认值是200毫秒,通常把期望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的
JDK Toolsjps用于显示当前用户下的所有java进程信息:# jps[options][hostid] # q:仅输出VM标识符, m: 输出mainmethod的参数,l:输出完全的包名,
v:输出jvm参数 [root@localhost ~]# jps-l 28729 sun.tools.jps.Jps 23789 cn.ms.test.DemoMain 23651 cn.ms.test
.TestMainjstat用于监视虚拟机运行时状态信息(类装载、内存、垃圾收集、JIT编译等运行数据):-gc:垃圾回收统计(大小)# 每隔2000ms输出进程的gc情况,一共输出2次[root@localhost
~]#jstat-gc20002# 每隔2s输出进程的gc情况,每个3条记录就打印隐藏列标题[root@localhost~]#jstat-gc-t-h32sTimestamp
S0CS1CS0US1U...YGCYGCTFGCFGCTGCT1021.61024.01024.00.01024.0...10.01200.0000.0121023.71024.01024.00.01024.0
...10.01200.0000.0121025.71024.01024.00.01024.0...10.01200.0000.012TimestampS0CS1CS0US1U...YGCYGCTFGC
FGCTGCT1027.71024.01024.00.01024.0...10.01200.0000.0121029.71024.01024.00.01024.0...10.01200.0000.012
# 结果说明: C即Capacity 总容量,U即Used 已使用的容量########################### S0C:年轻代中第一个survivor(幸存区)的容量 (kb)# S1C:年轻代中第二个survivor(幸存区)的容量 (kb)
# S0U:年轻代中第一个survivor(幸存区)目前已使用空间 (kb)# S1U:年轻代中第二个survivor(幸存区)目前已使用空间 (kb)# EC:年轻代中Eden(伊甸园)的容量 (kb)
# EU:年轻代中Eden(伊甸园)目前已使用空间 (kb)# OC:Old代的容量 (kb)# OU:Old代目前已使用空间 (kb)# PC:Perm(持久代)的容量 (kb)# PU:Perm(持久代)目前已使用空间 (kb)
# YGC:从应用程序启动到采样时年轻代中gc次数# YGCT:从应用程序启动到采样时年轻代中gc所用时间(s)# FGC:从应用程序启动到采样时old代(全gc)gc次数# FGCT:从应用程序启动到采样时old代(全gc)gc所用时间(s)
# GCT:从应用程序启动到采样时gc用的总时间(s)-gcutil:垃圾回收统计(百分比)[root@localhostbin]#jstat-gcutilS0S1EOMCCSYGCYGCTFGC
FGCTGCT0.0099.8016.2126.1893.3490.7490.05620.0450.102# 结果说明########################### S0:年轻代中第一个survivor(幸存区)已使用的占当前容量百分比
# S1:年轻代中第二个survivor(幸存区)已使用的占当前容量百分比# E:年轻代中Eden(伊甸园)已使用的占当前容量百分比# O:old代已使用的占当前容量百分比# P:perm代已使用的占当前容量百分比
# YGC:从应用程序启动到采样时年轻代中gc次数# YGCT:从应用程序启动到采样时年轻代中gc所用时间(s)# FGC:从应用程序启动到采样时old代(全gc)gc次数# FGCT:从应用程序启动到采样时old代(全gc)gc所用时间(s)
# GCT:从应用程序启动到采样时gc用的总时间(s)-gccapacity:堆内存统计[root@localhost~]#jstat-gccapacityNGCMNNGCMXNGCS0CS1C
ECOGCMNOGCMXOGCOCPGCMNPGCMXPGCPCYGCFGC84480.01349632.0913408.054272.051200.0502784.0168448.02699264.0
168448.0168448.021504.083968.051712.051712.090# 结果说明########################### NGCMN:年轻代(young)中初始化(最小)的大小 (kb)
# NGCMX:年轻代(young)的最大容量 (kb)# NGC:年轻代(young)中当前的容量 (kb)# S0C:年轻代中第一个survivor(幸存区)的容量 (kb)# S1C:年轻代中第二个survivor(幸存区)的容量 (kb)
# EC:年轻代中Eden(伊甸园)的容量 (kb)# OGCMN:old代中初始化(最小)的大小 (kb)# OGCMX:old代的最大容量 (kb)# OGC:old代当前新生成的容量 (kb)# OC:Old代的容量 (kb)
# PGCMN:perm代中初始化(最小)的大小 (kb)# PGCMX:perm代的最大容量 (kb)# PGC:perm代当前新生成的容量 (kb)# PC:Perm(持久代)的容量 (kb)# YGC:从应用程序启动到采样时年轻代中gc次数
# GCT:从应用程序启动到采样时gc用的总时间(s)-gccause:垃圾收集统计概述(同-gcutil),附加最近两次垃圾回收事件的原因[root@localhost~]#jstat-gccause
S0S1EOPYGCYGCTFGCFGCTGCTLGCCGCC0.0079.2339.3739.9299.7490.19800.0000.198AllocationFailureNoGC# 结果说明
########################### LGCC:最近垃圾回收的原因# GCC:当前垃圾回收的原因jstackjstack(Java Stack Trace)主要用于打印线程的堆栈信息,是JDK自带的很强大的线程分析工具,可以帮助我们排查程序运行时的线程状态、死锁状态等。
# dump出进程的线程堆栈快照至/data/1.log文件中jstack -l >/data/1.log # 参数说明:# -F:如果正常执行jstack命令没有响应(比如进程hung住了),可以加上此参数强制执行thread dump
# -m:除了打印Java的方法调用栈之外,还会输出native方法的栈帧# -l:打印与锁有关的附加信息使用此参数会导致JVM停止时间变长,在生产环境需慎用jstack dump文件中值得关注的线程状态有:。
死锁(Deadlock) —— 重点关注执行中(Runnable)等待资源(Waiting on condition) —— 重点关注等待某个资源或条件发生来唤醒自己具体需结合jstacktrace来分析,如线程正在sleep,网络读写繁忙而等待如果大量线程在“waiting on condition”,并且在等待网络资源,可能是网络瓶颈的征兆。
等待获取监视器(Waiting on monitor entry) —— 重点关注如果大量线程在“waiting for monitor entry”,可能是一个全局锁阻塞住了大量线程暂停(Suspended)
对象等待中(Object.wait() 或 TIMED_WAITING)阻塞(Blocked) —— 重点关注停止(Parked)注意:如果某个相同的call stack经常出现, 我们有80%的以上的理由确定这个代码存在性能问题(读网络的部分除外)。
场景一:分析BLOCKED问题"RMI TCP Connection(267865)-172.16.5.25" daemon prio=10 tid=0x00007fd508371000 nid=0x55ae
waiting for monitor entry [0x00007fd4f8684000] java.lang.Thread.State: BLOCKED (onobject monitor) at org.apache.log4j.Category.callAppenders(Category.java:
201) - waiting to lock (a org.apache.log4j.Logger) at org.apache.log4j.Category.forcedLog(Category.java:
388) at org.apache.log4j.Category.log(Category.java:853) at org.apache.commons.logging.impl.Log4JLogger.warn(Log4JLogger.java:
234) at com.tuan.core.common.lang.cache.remote.SpyMemcachedClient.get(SpyMemcachedClient.java:110)线程状态是 Blocked,阻塞状态。
说明线程等待资源超时“ waiting to lock ”指,线程在等待给这个 0x00000000acf4d0c0 地址上锁(英文可描述为:trying to obtain 0x00000000acf4d0c0 lock)
在 dump 日志里查找字符串 0x00000000acf4d0c0,发现有大量线程都在等待给这个地址上锁如果能在日志里找到谁获得了这个锁(如locked ),就可以顺藤摸瓜了。
“waiting for monitor entry”说明此线程通过 synchronized(obj) {……} 申请进入了临界区,从而进入了下图1中的“Entry Set”队列,但该 obj 对应的 monitor 被其他线程拥有,所以本线程在 Entry Set 队列中等待
第一行里,"RMI TCP Connection(267865)-172.16.5.25"是 Thread Name tid指Java Thread idnid指native线程的idprio是线程优先级。
[0x00007fd4f8684000]是线程栈起始地址场景二:分析CPU过高问题1.top命令找出最高占用的进程(Shift+P)2.查看高负载进程下的高负载线程(top -Hp 或ps -mp -o THREAD,tid,time)
3.找出最高占用的线程并记录thread_id,把线程号进行换算成16进制编号(printf "%X\n" thread_id)4.(可选)执行查看高负载的线程名称(jstack 16143 | grep 3fb6)
5.导出进程的堆栈日志,找到3fb6 这个线程号(jstack 16143 >/home/16143.log)6.根据找到的堆栈信息关联到代码进行定位分析即可jmapjmap(Java Memory Map)主要用于打印内存映射。
常用命令:jmap -dump:live,format=b,file=xxx.hprof 查看JVM堆栈的使用情况[root@localhost~]#jmap-heap7243Attaching
toprocessID27900,pleasewait...Debuggerattachedsuccessfully.Clientcompilerdetected.JVMversionis20.45-b01
usingthread-localobjectallocation.MarkSweepCompactGCHeap Configuration:#堆内存初始化配置MinHeapFreeRatio=40#-XX:MinHeapFreeRatio设置JVM堆最小空闲比率
MaxHeapFreeRatio=70#-XX:MaxHeapFreeRatio设置JVM堆最大空闲比率 MaxHeapSize=100663296(96.0MB)#-XX:MaxHeapSize=设置JVM堆的最大大小
NewSize=1048576(1.0MB)#-XX:NewSize=设置JVM堆的‘新生代’的默认大小MaxNewSize=4294901760(4095.9375MB)#-XX:MaxNewSize=设置JVM堆的‘新生代’的最大大小
OldSize=4194304(4.0MB)#-XX:OldSize=设置JVM堆的‘老生代’的大小NewRatio=2#-XX:NewRatio=:‘新生代’和‘老生代’的大小比率SurvivorRatio
=8#-XX:SurvivorRatio=设置年轻代中Eden区与Survivor区的大小比值PermSize=12582912(12.0MB)#-XX:PermSize=:设置JVM堆的‘持久代’的初始大小
MaxPermSize=67108864(64.0MB)#-XX:MaxPermSize=:设置JVM堆的‘持久代’的最大大小 Heap Usage:NewGeneration(Eden
+1SurvivorSpace):#新生代区内存分布,包含伊甸园区+1个Survivor区capacity=30212096(28.8125MB)used=27103784(25.848182678222656MB)
free=3108312(2.9643173217773438MB)89.71169693092462%usedEden Space:#Eden区内存分布capacity=26869760(25.625MB)
used=26869760(25.625MB)free=0(0.0MB)100.0%usedFrom Space:#其中一个Survivor区的内存分布capacity=3342336(3.1875MB)
used=234024(0.22318267822265625MB)free=3108312(2.9643173217773438MB)7.001809512867647%usedTo Space:#另一个Survivor区的内存分布
capacity=3342336(3.1875MB)used=0(0.0MB)free=3342336(3.1875MB)0.0%usedPS Old Generation:#当前的Old区内存分布capacity
=67108864(64.0MB)used=67108816(63.99995422363281MB)free=48(4.57763671875E-5MB)99.99992847442627%usedPS Perm Generation:
#当前的 “持久代” 内存分布capacity=14417920(13.75MB)used=14339216(13.674942016601562MB)free=78704(0.0750579833984375MB)
99.45412375710227%used新生代内存回收就是采用空间换时间方式;如果from区使用率一直是100% 说明程序创建大量的短生命周期的实例,使用jstat统计jvm在内存回收中发生的频率耗时以及是否有full gc,使用这个数据来评估一内存配置参数、gc参数是否合理。
统计一【jmap -histo】:统计所有类的实例数量和所占用的内存容量[root@localhost~]#jmap-histo7243num#instances #bytes class name
----------------------------------------------1:896919781168[B2:18352296720[I3:197352050688[C4:3448385608
java.lang.Class5:3829371456[Ljava.lang.Object;6:14634351216java.lang.String7:6695214240java.util.concurrent.ConcurrentHashMap$Node
8:6257100112java.lang.Object9:215568960java.util.HashMap$Node10:72363624java.lang.reflect.Method11:49
56368[Ljava.util.concurrent.ConcurrentHashMap$Node;12:83046480java.util.zip.ZipFile$ZipFileInputStream
13:114645840java.lang.ref.Finalizer......统计二【jmap -histo】:查看对象数最多的对象,并过滤Map关键词,然后按降序排序输出[root@localhost
~]#jmap-histo7243|grepMap|sort-k2-g-r|lessTotal96237268755607:6695214240java.util.concurrent.ConcurrentHashMap$Node
9:215568960java.util.HashMap$Node18:56327024java.util.HashMap21:50520200java.util.LinkedHashMap$Entry
16:33734880[Ljava.util.HashMap$Node;27:33616128gnu.trove.THashMap56:1636520java.util.WeakHashMap$Entry
60:1276096java.util.WeakHashMap38:12710144[Ljava.util.WeakHashMap$Entry;53:1267056java.util.LinkedHashMap
......统计三【jmap -histo】:统计实例数量最多的前10个类[root@localhost~]#jmap-histo7243|sort-n-r-k2|head-10num#instances #bytes class name
----------------------------------------------Total96237268755603:197352050688[C6:14634351216java.lang.String
1:896919781168[B7:6695214240java.util.concurrent.ConcurrentHashMap$Node8:6257100112java.lang.Object5:
3829371456[Ljava.lang.Object;4:3448385608java.lang.Class9:215568960java.util.HashMap$Node2:18352296720
[I统计四【jmap -histo】:统计合计容量最多的前10个类[root@localhost~]#jmap-histo7243|sort-n-r-k3|head-10num#instances #bytes class name
----------------------------------------------Total96237268755601:896919781168[B2:18352296720[I3:19735
2050688[C4:3448385608java.lang.Class5:3829371456[Ljava.lang.Object;6:14634351216java.lang.String7:6695
214240java.util.concurrent.ConcurrentHashMap$Node8:6257100112java.lang.Object9:215568960java.util.HashMap$Node
dump注意事项在应用快要发生FGC的时候把堆数据导出来 老年代或新生代used接近100%时,就表示即将发生GC,也可以再JVM参数中指定触发GC的阈值查看快要发生FGC使用命令:jmap -heap 。
数据导出:jmap -dump:format=b,file=heap.bin 通过命令查看大对象:jmap -histo |less使用总结如果程序内存不足或者频繁GC,很有可能存在内存泄露情况,这时候就要借助Java堆Dump查看对象的情况
要制作堆Dump可以直接使用jvm自带的jmap命令可以先使用jmap -heap命令查看堆的使用情况,看一下各个堆空间的占用情况使用jmap -histo:[live]查看堆内存中的对象的情况如果有大量对象在持续被引用,并没有被释放掉,那就产生了内存泄露,就要结合代码,把不用的对象释放掉。
也可以使用 jmap -dump:format=b,file=命令将堆信息保存到一个文件中,再借助jhat命令查看详细内容在内存出现泄露、溢出或者其它前提条件下,建议多dump几次内存,把内存文件进行编号归档,便于后续内存整理分析
在用cms gc的情况下,执行jmap -heap有些时候会导致进程变T,因此强烈建议别执行这个命令,如果想获取内存目前每个区域的使用状况,可通过jstat -gc或jstat -gccapacity来拿到
jhatjhat(JVM Heap Analysis Tool)命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看。
在此要注意,一般不会直接在服务器上进行分析,因为jhat是一个耗时并且耗费硬件资源的过程,一般把服务器生成的dump文件复制到本地或其他机器上进行分析# 解析Java堆转储文件,并启动一个 web server
jhat heapDump.dumpjconsolejconsole(Java Monitoring and Management Console)是一个Java GUI监视工具,可以以图表化的形式显示各种数据,并可通过远程连接监视远程的服务器VM。
用java写的GUI程序,用来监控VM,并可监控远程的VM,非常易用,而且功能非常强命令行里打jconsole,选则进程就可以了第一步:在远程机的tomcat的catalina.sh中加入配置:JAVA_OPTS
="$JAVA_OPTS -Djava.rmi.server.hostname=192.168.202.121 -Dcom.sun.management.jmxremote"JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.port=12345"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.authenticate=true"JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.ssl=false"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.pwd.file=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.101-3.b13.el7_2.x86_64/jre/lib/management/jmxremote.password"
第二步:配置权限文件[root@localhost bin]# cd /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.101-3.b13.el7_2.x86_64/jre/lib/management/
[root@localhost management]# cp jmxremote.password.template jmxremote.password [root@localhost management]
# vi jmxremote.passwordmonitorRole QED controlRole chenqimiao第三步:配置权限文件为600[root@localhost management]
# chmod 600 jmxremote.passwordjmxremote.access这样基本配置就结束了,下面说两个坑,第一个就是防火墙的问题,要开放指定端口的防火墙,我这里配置的是12345端口,第二个是hostname的问题:
请将127.0.0.1修改为本地真实的IP,我的服务器IP是192.168.202.121:
第四步:查看JConsole

jvisualvmjvisualvm(JVM Monitoring/Troubleshooting/Profiling Tool)同jconsole都是一个基于图形化界面的、可以查看本地及远程的JAVA GUI监控工具,Jvisualvm同jconsole的使用方式一样,直接在命令行打入Jvisualvm即可启动,不过Jvisualvm相比,界面更美观一些,数据更实时。
jvisualvm的使用VisualVM进行远程连接的配置和JConsole是一摸一样的,最终效果图如下
Visual GC(监控垃圾回收器)Java VisualVM默认没有安装Visual GC插件,需要手动安装,JDK的安装目录的bin目露下双击 jvisualvm.sh,即可打开Java VisualVM,点击菜单栏:
工具->插件 安装Visual GC,最终效果如下图所示:
大dump文件从服务器dump堆内存后文件比较大(5.5G左右),加载文件、查看实例对象都很慢,还提示配置xmx大小表明给VisualVM分配的堆内存不够,找到$JAVA_HOME/lib/visualvm}/etc/visualvm.conf这个文件,修改:。
default_options="-J-Xms24m -J-Xmx192m"再重启VisualVM就行了jmcjmc(Java Mission Control)是JDK自带的一个图形界面监控工具,监控信息非常全面。
JMC打开性能日志后,主要包括一般信息、内存、代码、线程、I/O、系统、事件 功能。
JMC的最主要的特征就是JFR(Java Flight Recorder),是基于JAVA的飞行记录器,JFR的数据是一些列JVM事件的历史纪录,可以用来诊断JVM的性能和操作,收集后的数据可以使用JMC来分析。
启动JFR在商业版本上面,JFR默认是关闭的,可以通过在启动时增加参数 -XX:+UnlockCommercialFeatures -XX:+FlightRecorder 来启动应用启动之后,也只是开启了JFR特性,但是还没有开始进行事件记录。
这就要通过GUI和命令行了通过Java Mission Control启动JFR打开Java Mission Control点击对应的JVM启动即可,事件记录有两种模式(如果选择第2种模式,那么JVM会使用一个循环使用的缓存来存放事件数据):
记录固定一段时间的事件(比如:1分钟)持续进行记录通过命令行启动JFR通过在启动的时候,增加参数:-XX:+FlightRecorderOptions=string 来启动真正地事件记录,这里的 string
可以是以下值(下列参数都可以使用jcmd命令,在JVM运行的时候进行动态调整,参考地址):name=name:标识recording的名字(一个进程可以有多个recording存在,它们使用名字来进行区分)
defaultrecording=:是否启动recording,默认是false,我们要分析,必须要设置为truesetting=paths:包含JFR配置的文件名字delay=time
:启动之后,经过多长时间(比如:30s,1h)开始进行recordingduration=time:做多长时间的recordingfilename=path:recordding记录到那个文件里面compress=
:是否对recording进行压缩(gzip),默认为falsemaxage=time:在循环使用的缓存中,事件数据保存的最大时长maxsize=size:事件数据缓存的最大大小(比如:1024k,1M)
常用JFR命令启动recording命令格式:jcmd process_id JFR.start [options_list],其中options_list就是上述的参数值dump出循环缓存中的数据命令格式:
jcmd process_id JFR.dump [options_list],其中options_list参数的可选值如下:name=name:recording的名字recording=n:JFR recording的数字(一个标识recording的随机数)
filename=path:dump文件的保存路径查看进程中所有recording命令格式: jcmd process_id JFR.check [verbose],不同recording使用名字进行区分,同时JVM还为它分配一个随机数。
停止recording命令格式: jcmd process_id JFR.stop [options_list],其中options_list参数的可选值如下:name=name:要停止的recording名字
recording=n:要停止的recording的标识数字discard=boolean:如果为true,数据被丢弃,而不是写入下面指定的文件当中filename=path:写入数据的文件名称命令启动JFR案例
第一步:创建一个包含了你自己配置的JFR模板文件(template.jfc)运行jmc, 然后Window->Flight Recording Template Manage菜单准备好档案后,就可以导出文件,并移动到要排查问题的环境中。
第二步:由于JFR需要JDK的商业证书,这一步需要解锁jdk的商业特性[root@localhostbin]# jcmd 12234 VM.unlock_commercial_features12234
:CommercialFeaturesalreadyunlocked.第三步:最后你就可以启动JFR,命令格式如下:jcmdJFR.startname=testduration=60s[settings=template.jfc]
filename=output.jfr 上述命令立即启动JFR并开始使用 template.jfc(在 $JAVA_HOME/jre/lib/jfr 下有 default.jfc 和 profile.jfc
模板)的配置收集 60s 的JVM信息,输出到 output.jfr 中一旦记录完成之后,就可以复制.jfr文件到你的工作环境使用jmc GUI来分析它几乎包含了排查jvm问题需要的所有信息,包括堆dump时的异常信息。
使用案例如下:[root@localhostbin]# jcmd 12234 JFR.start name=test duration=60s filename=output.jfr12234:Started
recording6.Theresultwillbewrittento:/root/zookeeper-3.4.12/bin/output.jfr [root@localhostbin]# ls -l-rw-r--r
--1rootroot2985856月2911:09output.jfrJFR(Java Flight Recorder)Java Mission Control的最主要的特征就是Java Flight Recorder。
正如它的名字所示,JFR的数据是一些列JVM事件的历史纪录,可以用来诊断JVM的性能和操作JFR的基本操作就是开启哪些事件(比如:线程由于等待锁而阻塞的事件)当开启的事件发生了,事件相关的数据会记录到内存或磁盘文件上。
记录事件数据的缓存是循环使用的,只有最近发生的事件才能够从缓存中找到,之前的都因为缓存的限制被删除了Java Mission Control能够对这些事件在界面上进行展示(从JVM的内存中读取或从事件数据文件中读取),我们可以通过这些事件来对JVM的性能进行诊断。
事件的类型、缓存的大小、事件数据的存储方式等等都是通过JVM参数、Java Mission Control的GUI界面、jcmd命令来控制的JFR默认是编译进程序的,因为它的开销很小,一般来说对应用的影响小于1%。
不过,如果我们增加了事件的数目、修改了记录事件的阈值,都有可能增加JFR的开销JFR概况 下面对GlassFish web服务器进行JFR记录的例子,在这个服务器上面运行着在第2章介绍的股票servlet。
Java Mission Control加载完JFR获取的事件之后,大概是下面这个样子:
我们可以看到,通过上图可以看到:CPU使用率,Heap使用率,JVM信息,System Properties,JFR的记录情况等等JFR内存视图Java Mission Control 可以看到非常多的信息,下图只显示了一个标签的内容。
下图显示了JVM 的内存波动非常频繁,因为新生代经常被清除(有意思的是,head的大小并没有增长)下面左边的面板显示了最近一段时间的垃圾回收情况,包括:GC的时长和垃圾回收的类型如果我们点击一个事件,右边的面板会展示这个事件的具体情况,包括:垃圾垃圾回收的各个阶段及其统计信息。
从面板的标签可以看到,还有很多其它信息,比如:有多少对象被清除了,花了多长时间;GC算法的配置;分配的对象信息等等在第5章和第6章中,我们会详细介绍
JFR 代码视图这张图也有很多tab,可以看到各个包的使用频率和类的使用情况、异常、编译、代码缓存、类加载情况等等:
JFR事件视图下图显示了事件的概述视图:
EclipseMAT虽然Java虚拟机可以帮我们对内存进行回收,但是其回收的是Java虚拟机不再引用的对象很多时候我们使用系统的IO流、Cursor、Receiver如果不及时释放,就会导致内存泄漏(OOM)。
但是,很多时候内存泄漏的现象不是很明显,比如内部类、Handler相关的使用导致的内存泄漏,或者你使用了第三方library的一些引用,比较消耗资源,但又不是像系统资源那样会引起你足够的注意去手动释放它们。
以下通过内存泄漏分析、集合使用率、Hash性能分析和OQL快读定位空集合来使用MATGC RootsJAVA虚拟机通过可达性(Reachability)来判断对象是否存活,基本思想:以”GC Roots”的对象作为起始点向下搜索,搜索形成的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即不可达的),则该对象被判定为可以被回收的对象,反之不能被回收
GC Roots可以是以下任意对象一个在current thread(当前线程)的call stack(调用栈)上的对象(如方法参数和局部变量)线程自身或者system class loader(系统类加载器)加载的类。
native code(本地代码)保留的活动对象内存泄漏当对象无用了,但仍然可达(未释放),垃圾回收器无法回收Java四种引用类型Strong References(强引用)普通的java引用,我们通常new的对象就是:。
StringBuffer buffer = new StringBuffer(); 如果一个对象通过一串强引用链可达,那么它就不会被垃圾回收你肯定不希望自己正在使用的引用被垃圾回收器回收吧但对于集合中的对象,应在不使用的时候移除掉,否则会占用更多的内存,导致内存泄漏。
Soft Reference(软引用)当对象是Soft Reference可达时,gc会向系统申请更多内存,而不是直接回收它,当内存不足的时候才回收它因此Soft Reference适合用于构建一些缓存系统,比如图片缓存。
Weak Reference(弱引用)WeakReference不会强制对象保存在内存中它拥有比较短暂的生命周期,允许你使用垃圾回收器的能力去权衡一个对象的可达性在垃圾回收器扫描它所管辖的内存区域过程中,一旦gc发现对象是Weak Reference可达,就会把它放到 。
Reference Queue 中,等下次gc时回收它系统为我们提供了WeakHashMap,和HashMap类似,只是其key使用了weak reference如果WeakHashMap的某个key被垃圾回收器回收,那么entity也会自动被remove。
由于WeakReference被GC回收的可能性较大,因此,在使用它之前,你需要通过weakObj.get()去判断目的对象引用是否已经被回收一旦WeakReference.get()返回null,它指向的对象就会被垃圾回收,那么WeakReference对象就没有用了,意味着你应该进行一些清理。
比如在WeakHashMap中要把回收过的key从Map中删除掉,避免无用的的weakReference不断增长ReferenceQueue可以让你很容易地跟踪dead referencesWeakReference类的构造函数有一个ReferenceQueue参数,当指向的对象被垃圾回收时,会把WeakReference对象放到ReferenceQueue中。
这样,遍历ReferenceQueue可以得到所有回收过的WeakReferencePhantom Reference(虚引用)其余Soft/Weak Reference区别较大是它的get()方法总是返回null。
这意味着你只能用Phantom Reference本身,而得不到它指向的对象当Weak Reference指向的对象变得弱可达(weakly reachable)时会立即被放到ReferenceQueue中,这在finalization、garbage collection之前发生。
理论上,你可以在finalize()方法中使对象“复活”(使一个强引用指向它就行了,gc不会回收它)但没法复活PhantomReference指向的对象而PhantomReference是在garbage collection之后被放到ReferenceQueue中的,没法复活。
MAT视图与概念Shallow HeapShallow Size就是对象本身占用内存的大小,不包含其引用的对象内存,实际分析中作用不大 常规对象(非数组)的Shallow Size由其成员变量的数量和类型决定。
数组的Shallow Size有数组元素的类型(对象类型、基本类型)和数组长度决定案例如下:publicclassString {publicfinalclassString {8 Bytes header
privatechar value[]; 4 Bytes privateint offset; 4 Bytes privateint count; 4 Bytes private
int hash = 0; 4 Bytes // ...... } // "Shallow size“ of a String ==24 Bytes12345678Java的对象成员都是些引用真正的内存都在堆上,看起来是一堆原生的byte[]、char[]、int[],对象本身的内存都很小。
所以我们可以看到以Shallow Heap进行排序的Histogram图中,排在第一位第二位的是byte和charRetained HeapRetained Heap值的计算方式是将Retained Set中的所有对象大小叠加。
或者说,由于X被释放,导致其它所有被释放对象(包括被递归释放的)所占的Heap大小当X被回收时哪些将被GC回收的对象集合比如:一个ArrayList持有100000个对象,每一个占用16 bytes,移除这些ArrayList可以释放16×100000+X,X代表ArrayList的Shallow大小。
相对于Shallow Heap,Retained Heap可以更精确的反映一个对象实际占用的大小(因为如果该对象释放,Retained Heap都可以被释放)Histogram可列出每一个类的实例数支持正则表达式查找,也可以计算出该类所有对象的Retained Size。
Dominator Tree对象之间dominator关系树如果从GC Root到达Y的的所有path都经过X,那么我们称X dominates Y,或者X是Y的Dominator Dominator Tree由系统中复杂的对象图计算而来。
从MAT的dominator tree中可以看到占用内存最大的对象以及每个对象的dominator 我们也可以右键选择Immediate Dominator”来查看某个对象的dominator
Path to GC Roots 查看一个对象到RC Roots的引用链通常在排查内存泄漏的时候,我们会选择exclude all phantom/weak/soft etc.references, 意思是查看排除虚引用/弱引用/软引用等的引用链,因为被虚引用/弱引用/软引用的对象可以直接被GC给回收,我们要看的就是某个对象否还存在Strong 引用链(在导出HeapDump之前要手动出发GC来保证),如果有,则说明存在内存泄漏,然后再去排查具体引用。
查看当前Object所有引用,被引用的对象:List objects with (以Dominator Tree的方式查看)incoming references 引用到该对象的对象outcoming references 被该对象引用的对象
Show objects by class (以class的方式查看)incoming references 引用到该对象的对象outcoming references 被该对象引用的对象OQL(Object Query Language)
类似SQL查询语言:Classes:Table、Objects:Rows、Fileds:Colsselect * from com.example.mat.Listener # 查找size=0并且未使用过的ArrayList
select * from java.util.ArrayList wheresize=0and modCount=01# 查找所有的Activity select * from instanceof android.app.Activity
内存快照对比方式一:Compare To Another Heap Dump(直接进行比较)


方式二:Compare Baseket(更全面,可以直接给出百分比)



MAT内存分析实战实战一:内存泄漏分析查找导致内存泄漏的类既然环境已经搭好,heap dump也成功倒入,接下来就去分析问题查找目标类 如果在开发过程中,你的目标很明确,比如就是查找自己负责的服务,那么通过包名或者Class筛选,OQL搜索都可以快速定位到。
点击OQL图标,在窗口输入,并按Ctrl + F5或者!按钮执行:select * from instanceof android.app.ActivityPaths to GC Roots:exclude all phantom/weak/soft etc.references
查看一个对象到RC Roots是否存在引用链要将虚引用/弱引用/软引用等排除,因为被虚引用/弱引用/软引用的对象可以直接被GC给回收分析具体的引用为何没有被释放,并进行修复小技巧:当目的不明确时,可以直接定位到RetainedHeap最大的Object,Select incoming references,查看引用链,定位到可疑的对象然后Path to GC Roots进行引用链分析。
如果大对象筛选看不出区别,可以试试按照class分组,再寻找可疑对象进行GC引用链分析直接按照包名直接查看GC引用链,可以一次性筛选多个类,但是如下图所示,选项是 Merge Shortest Path to GCRoots,这个选项具体不是很明白,不过也能筛选出存在GC引用链的类,这种方式的准确性还待验证
所以有时候进行MAT分析还是需要一些经验,能够帮你更快更准确的定位实战二:集合使用率分析集合在开发中会经常使用到,如何选择合适的数据结构的集合,初始容量是多少(太小,可能导致频繁扩容),太大,又会开销跟多内存。
当这些问题不是很明确时或者想查看集合的使用情况时,可以通过MAT来进行分析筛选目标对象Show Retained Set(查找当X被回收时那些将被GC回收的对象集合)筛选指定的Object(Hash Map,ArrayList)并按照大小进行分组
查看指定类的Immediate dominatorsCollections fill ratio这种方式只能查看那些具有预分配内存能力的集合,比如HashMap,ArrayList计算方式:”size / capacity”。

实战三:Hash相关性能分析当Hash集合中过多的对象返回相同Hash值的时候,会严重影响性能(Hash算法原理自行搜索),这里来查找导致Hash集合的碰撞率较高的罪魁祸首Map Collision Ratio。
检测每一个HashMap或者HashTable实例并按照碰撞率排序:碰撞率 = 碰撞的实体/Hash表中所有实体查看Immediate dominators通过HashEntries查看key value
Array等其它集合分析方法类似实战四:通过OQL快速定位未使用的集合通过OQL查询empty并且未修改过的集合:select * from java.util.ArrayList where size=0 and modCount=01 select * from java.util.HashMap where size=0 and modCount=0 select * from java.util.Hashtable where count=0 and modCount=012
Immediate dominators(查看引用者)计算空集合的Retained Size值,查看浪费了多少内存火焰图火焰图是用来分析程序运行瓶颈的工具火焰图也可以用来分析 Java 应用环境安装确认你的机器已经安装了
git、jdk、perl、c++编译器安装Perlwgethttp://www.cpan.org/src/5.0/perl-5.26.1.tar.gztarzxvf perl-5.26.1.tar.gz。
cdperl-5.26.1./Configure-demakemaketestmakeinstallwget后面的路径可以按需更改安装过程比较耗时间,安装完成后可通过perl -version查看是否安装成功。
C++编译器apt-get install g++一般用于编译c++程序,缺少这个编译器进行make编译c++代码时,会报“g++: not found”的错误clone相关项目下载下来所需要的两个项目(这里建议放到data目录下):。
git clone https://github.com/jvm-profiling-tools/async-profiler git clone https://github.com/brendangregg/FlameGraph
编译项目下载好以后,需要打开async-profiler文件,输入make指令进行编译:cd async-profiler make生成文件生成火焰图数据可以从 github 上下载 async-profiler 的压缩包进行相关操作。
进入async-profiler项目的目录下,然后输入如下指令:./profiler.sh -d 60 -o collapsed -f /tmp/test_01.txt ${pid}上面的-d表示的是持续时长,后面60代表持续采集时间60s,-o表示的是采集规范,这里用的是collapsed,-f后面的路径,表示的是数据采集后生成的数据存放的文件路径(这里放在了/tmp/test_01.txt),${pid}表示的是采集目标进程的pid,回车运行,运行期间阻塞,知道约定时间完成。
运行完成后去tmp下看看有没有对应文件生成svg文件上一步产生的文件里的内容,肉眼是很难看懂的,所以现在FlameGraph的作用就体现出来了,它可以读取该文件并生成直观的火焰图,现在进入该项目目录下面,执行如下语句:
perl flamegraph.pl --colors=java /tmp/test_01.txt > test_01.svg因为是perl文件,这里使用perl指令运行该文件,后面--colors表示着色风格,这里是java,后面的是数据文件的路径,这里是刚刚上面生成的那个文件/tmp/test_01.txt,最后的test_01.svg就是最终生成的火焰图文件存放的路径和文件命名,这里是命名为test_01.svg并保存在当前路径,运行后看到该文件已经存在于当前目录下。
展示火焰图现在下载下来该文件,使用浏览器打开,效果如下:
火焰图案例生成的火焰图案例如下:
瓶颈点1CoohuaAnalytics$KafkaConsumer:::send方法中Gzip压缩占比较高。已经定位到方法级别,再看代码就快速很多,直接找到具体位置,找到第一个消耗大户:Gzip压缩
瓶颈点2展开2这个波峰,查看到这个getOurStackTrace方法占用了大比例的CPU,怀疑代码里面频繁用丢异常的方式获取当前代码栈:
直接看代码:
果然如推断,找到第二个CPU消耗大户:new Exception().getStackTrace()。瓶颈点3展开波峰3,可以看到是这个Gzip解压缩:
定位到具体的代码,可以看到对每个请求的参数进行了gzip解压缩:
