一、调优工具
先通过jps命令显示Java应用程序的进程id
1、jmap
查看堆实例个数及占用内存大小,把这些信息生成到当前目录下的log.txt文件
jmap -histo 21932 > ./log.txt #查看历史生成的实例 jmap -histo:live 14660 #查看当前存活的实例,执行过程中可能会触发一次full gc
- 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 21932
在当前目录下生成堆内存dump文件
jmap -dump:format=b,file=eureka.hprof 21932
也可以设置内存溢出时自动导出dump文件(内存很大的时候,可能会导不出来-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./ (路径))示例代码,堆空间设置小一点,触发OOM
public class OOMTest { public static List<Object> list = new ArrayList<>(); // JVM设置 // -Xms5M -Xmx5M -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\jvm.dump public static void main(String[] args) { List<Object> list = new ArrayList<>(); int i = 0; int j = 0; while (true) { list.add(new User(i++, UUID.randomUUID().toString())); new User(j--, UUID.randomUUID().toString()); } } }
生成的dump文件可以通过jvisualvm工具导入查看,在命令行输入jvisualvm,具体操作如下
选择文件打开
看到实例使用情况
2、jstack
jstack加进程id查找死锁
public class DeadLockTest { private static Object lock1 = new Object(); private static Object lock2 = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (lock1) { try { System.out.println("lock1 object is locked"); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2) { System.out.println("try acquire lock2"); } } }).start(); new Thread(() -> { synchronized (lock2) { try { System.out.println("lock2 object is locked"); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock1) { System.out.println("try acquire lock1"); } } }).start(); System.out.println("main thread end"); } }
"Thread-1" 线程名
prio 优先级=5
tid=0x000000002076a000 线程id
nid=0x38b8 线程对应的本地线程标识nid
java.lang.Thread.State: BLOCKED 线程为阻塞状态
jvisualvm也可以检测死锁
jstack找出占用cpu最高的线程
public class Test { public void acquire() { } public static void main(String[] args) { Test math = new Test(); while (true){ math.acquire(); } } }
在Linux环境运行Test.class文件,假如文件目录是/usr/workspace/com/gaorufeng/jvm/Test.class,要在/usr/workspace目录运行命令java com.gaorufeng.jvm.Test,然后Linux命令行输入top查看到cpu占用最高的进程号
找到java相关占用cpu较高的PID,运行top -p <PID>,找到之后按H,获取每个线程的内存情况
找到内存和cpu占用最高的线程tid,比如4551
转为十六进制得到 0x11c7,此为线程id的十六进制表示(十六进制要小写形式输入)
执行 jstack 4550|grep -A 10 11c7,得到线程堆栈信息中 11c7 这个线程所在行的后面10行,从堆栈中可以发现导致cpu飙高的调用方法
查看对应的堆栈信息找出可能存在问题的代码
3、jinfo
查看正在运行的Java应用程序的扩展参数
查看jvm参数
jinfo -flags 进程id
查看java系统参数
jinfo -sysprops 进程id
4、jstat
垃圾回收统计
jstat -gc <进程id> <多少毫秒运行一次> <运行多少次>
jstat -gc 4550
- S0C:第一个幸存区的大小,单位KB
- S1C:第二个幸存区的大小
- S0U:第一个幸存区的使用大小
- S1U:第二个幸存区的使用大小
- EC:伊甸园区的大小
- EU:伊甸园区的使用大小
- OC:老年代大小
- OU:老年代使用大小
- MC:方法区大小(元空间)
- MU:方法区使用大小
- CCSC:压缩类空间大小
- CCSU:压缩类空间使用大小
- YGC:年轻代垃圾回收次数
- YGCT:年轻代垃圾回收消耗时间,单位s
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间,单位s
- GCT:垃圾回收消耗总时间,单位s、
堆内存统计
jstat -gccapacity 4550
- NGCMN:新生代最小容量
- NGCMX:新生代最大容量
- NGC:当前新生代容量
- S0C:第一个幸存区大小
- S1C:第二个幸存区的大小
- EC:伊甸园区的大小
- OGCMN:老年代最小容量
- OGCMX:老年代最大容量
- OGC:当前老年代大小
- OC:当前老年代大小
- MCMN:最小元数据容量
- MCMX:最大元数据容量
- MC:当前元数据空间大小
- CCSMN:最小压缩类空间大小
- CCSMX:最大压缩类空间大小
- CCSC:当前压缩类空间大小
- YGC:年轻代gc次数
- FGC:老年代GC次数
新生代垃圾回收统计
jstat -gcnew 4550
- S0C:第一个幸存区的大小
- S1C:第二个幸存区的大小
- S0U:第一个幸存区的使用大小
- S1U:第二个幸存区的使用大小
- TT:对象在新生代存活的次数
- MTT:对象在新生代存活的最大次数
- DSS:期望的幸存区大小
- EC:伊甸园区的大小
- EU:伊甸园区的使用大小
- YGC:年轻代垃圾回收次数
- YGCT:年轻代垃圾回收消耗时间
新生代内存统计
jstat -gcnewcapacity 4550
- NGCMN:新生代最小容量
- NGCMX:新生代最大容量
- NGC:当前新生代容量
- S0CMX:最大幸存1区大小
- S0C:当前幸存1区大小
- S1CMX:最大幸存2区大小
- S1C:当前幸存2区大小
- ECMX:最大伊甸园区大小
- EC:当前伊甸园区大小
- YGC:年轻代垃圾回收次数
- FGC:老年代回收次数
老年代垃圾回收统计
jstat -gcold 4550
- MC:方法区大小
- MU:方法区使用大小
- CCSC:压缩类空间大小
- CCSU:压缩类空间使用大小
- OC:老年代大小
- OU:老年代使用大小
- YGC:年轻代垃圾回收次数
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
老年代内存统计
jstat -gcoldcapacity 4550
- OGCMN:老年代最小容量
- OGCMX:老年代最大容量
- OGC:当前老年代大小
- OC:老年代大小
- YGC:年轻代垃圾回收次数
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
元数据空间统计
jstat -gcmetacapacity 4550
- MCMN:最小元数据容量
- MCMX:最大元数据容量
- MC:当前元数据空间大小
- CCSMN:最小压缩类空间大小
- CCSMX:最大压缩类空间大小
- CCSC:当前压缩类空间大小
- YGC:年轻代垃圾回收次数
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
jstat -gcutil 4550
- S0:幸存1区当前使用比例
- S1:幸存2区当前使用比例
- E:伊甸园区使用比例
- O:老年代使用比例
- M:元数据区使用比例
- CCS:压缩使用比例
- YGC:年轻代垃圾回收次数
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
5、jvisualvm
远程连接jvisualvm
- 启动普通的jar程序JMX端口配置:
java -Dcom.sun.management.jmxremote.port=8888 -Djava.rmi.server.hostname=192.168.65.60 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -jar xxx.jar
-Dcom.sun.management.jmxremote.port 为远程机器的JMX端口
-Djava.rmi.server.hostname 为远程机器IP
- tomcat的JMX配置:在catalina.sh文件里的最后一个JAVA_OPTS的赋值语句下一行增加如下配置行
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.port=8888 -Djava.rmi.server.hostname=192.168.50.60 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false"
二、调优思路
1、调优指标
- 年轻代对象增长的速率
通过执行jstat -gc pid 1000 10(每隔1秒执行1次命令,共执行10次),通过观察EU(eden区的使用)来估算每秒eden大概新增多少对象,如果系统负载不高,可以把频率1秒换成1分钟,甚至10分钟来观察整体情况。注意,一般系统可能有高峰期和日常期,所以需要在不同的时间分别估算不同情况下对象增长速率。
- Young GC触发频率和每次耗时
知道年轻代对象增长速率我们就能推根据eden区的大小推算出Young GC大概多久触发一次,Young GC的平均耗时可以通过 YGCT/YGC 公式算出,根据结果我们大概就能知道系统大概多久会因为Young GC的执行而卡顿多久
- 每次Young GC后有多少对象存活进入老年代
这个因为之前已经大概知道Young GC的频率,假设是每5分钟一次,那么可以执行命令 jstat -gc pid 300000 10 ,观察每次结果eden,survivor和老年代使用的变化情况,在每次gc后eden区使用一般会大幅减少,survivor和老年代都有可能增长,这些增长的对象就是每次Young GC后存活的对象,同时还可以看出每次Young GC后进去老年代大概多少对象,从而可以推算出老年代对象增长速率。
- Full GC触发频率和每次耗时
知道了老年代对象的增长速率就可以推算出Full GC的触发频率了,Full GC的每次耗时可以用公式 FGCT/FGC 计算得出。
2、调优思路(主要调优full gc)
如果full gc比较频繁的情况下,使用jstat -gc pid命令和设置的内存参数计算出eden区的多长时间被占满,可以推算多久一次minor gc,占满最后一秒存活的对象是否占survivor区的50%,考虑minor gc后老年代是否够放,如果不够那就扩大老年代,如果够了那就再回到是不是有大量对象往老年代里面放
元空间不够触发full gc,一般启动时会扩容,这个一般不会影响太大
一般大对象和长期存活的对象都不会对full gc有太大影响
对象动态年龄判断机制有可能,每隔一段进来的对象经过minor gc发现survivor区放不下触发full gc
老年代空间担保机制一般都发生在老年代比较小的情况下触发full gc
如果还有full gc发生,通过jmap命令找到对象个数比较多的
如果不确定创建大量对象是在哪里就可以通过jstack命令找到,创建大量对象肯定cpu占用比较高