JVM调优常用指令与JConsole/JvisualVM可视化调优工具
文章目录
常用指令
jps
jps 是(java process Status Tool), Java版的ps命令,查看java进程及其相关的信息,如果你想找到一个java进程的pid,那可以用jps命令替代linux中的ps命令了,简单而方便。
命令格式:
jps [options] [hostid]
options参数解释:
- -l : 显示进程id,显示主类全名或jar路径
- -q : 显示进程id
- -m : 显示进程id, 显示JVM启动时传递给main()的参数
- -v : 显示进程id,显示JVM启动时显示指定的JVM参数
hostid : 主机或其他服务器ip
最常用示例:
jps -l 输出jar包路径,类全名
jps -m 输出main参数
jps -v 输出JVM参数
参考代码
/**
* jps -q 显示进程id
* jps -l 输出jar包路径,类全名
* jps -m 输出主类名,及传入main方法的参数
* jps -v 输出主类名,及输出JVM参数
*/
public class JPSTest {
public static void main(String[] args) {
System.out.println("jps 指令");
try {
Thread.sleep(200000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
从图中可以看到,jps命令输出了我们的JPSTest任务,进程id是12652。
其他的参数也可以一一去尝试
jinfo
jinfo是用来查看JVM参数和动态修改部分JVM参数的命令
命令格式
jinfo [option]
options参数解释:
- no options 输出所有的系统属性和参数
- -flag 打印指定名称的参数
- -flag [+|-] 打开或关闭参数
- -flag = 设置参数
- -flags 打印所有参数
- -sysprops 打印系统配置
参考代码
/**
jinfo [option] <pid>
options参数解释:
- no options 输出所有的系统属性和参数
- -flag <name> 打印指定名称的参数
- -flag [+|-]<name> 打开或关闭参数
- -flag <name>=<value> 设置参数
- -flags 打印所有参数
- -sysprops 打印系统配置
**/
public class Demo2_jinfo {
public static void main(String[] args) {
System.out.println("jinfo 指令");
try {
Thread.sleep(2000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
首先先用jps命令,找到当前类运行的进程pid,我这里是pid=27980
然后利用该pid,执行jinfo命令
查看JVM参数和系统配置
jinfo 27980
jinfo -flags 27980
jinfo -sysprops 27980
查看打印GC日志参数
jinfo -flag PrintGC 27980
jinfo -flag PrintGCDetails 27980
打开GC日志参数
jinfo -flag +PrintGC 27980
jinfo -flag +PrintGCDetails 27980
关闭GC日志参数
jinfo -flag -PrintGC 27980
jinfo -flag -PrintGCDetails 27980
还可以使用下面的命令查看那些参数可以使用jinfo命令来管理:
常用JVM参数
- -Xms: 初始堆大小,默认为物理内存的1/64(<1GB);默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制
- -Xmx: 最大堆大小,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制
- -Xmn: 新生代的内存空间大小,注意:此处的大小是(eden+ 2 survivor space)。与jmap -heap中显示的New gen是不同的。整个堆大小=新生代大小 + 老生代大小 + 永久代大小。
- 在保证堆大小不变的情况下,增大新生代后,将会减小老生代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
- -XX:SurvivorRatio: 新生代中Eden区域与Survivor区域的容量比值,默认值为8。两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10。
- -Xss: 每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。应根据应用的线程所需内存大小
进行适当调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。一般小的应用, 如果栈不是很深, 应该是128k够用的,大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:"-Xssis translated
in a VM flag named ThreadStackSize”一般设置这个值就可以了。 - -XX:PermSize: 设置永久代(perm gen)初始值。默认值为物理内存的1/64。
- -XX:MaxPermSize: 设置持久代最大值。物理内存的1/4。
jstat
jstat命令是使用频率比较高的命令,主要用来查看JVM运行时的状态信息,包括内存状态、垃圾回收等。
命令格式
jstat [option] VMID [interval] [count]
其中VMID是进程id,interval是打印间隔时间(毫秒),count是打印次数(默认一直打印)
option参数解释
- -class class loader的行为统计
- -compiler HotSpt JIT编译器行为统计
- -gc 垃圾回收堆的行为统计
- -gccapacity 各个垃圾回收代容量(young,old,perm)和他们相应的空间统计
- -gcutil 垃圾回收统计概述
- -gccause 垃圾收集统计概述(同-gcutil),附加最近两次垃圾回收事件的原因
- -gcnew 新生代行为统计
- -gcnewcapacity 新生代与其相应的内存空间的统计
- -gcold 年老代和永生代行为统计
- -gcoldcapacity 年老代行为统计
- -printcompilation HotSpot编译方法统计
常用示例及打印字段解释
参考代码
public class Demo3_jstat {
public static void main(String[] args) {
System.out.println("jstat 指令");
try {
Thread.sleep(2000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
同样需要先利用jps命令找到当前类运行的进程pid,这里是pid=4856
jstat -gcutil 4856 1000 3
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 0.00 8.00 0.00 17.35 19.94 0 0.000 0 0.000 0.000
0.00 0.00 8.00 0.00 17.35 19.94 0 0.000 0 0.000 0.000
0.00 0.00 8.00 0.00 17.35 19.94 0 0.000 0 0.000 0.000
上述命令中,4856为pid,每隔1000毫秒打印一次,打印3次。
字段解释:
- S0 survivor0使用百分比
- S1 survivor1使用百分比
- E Eden区使用百分比
- O 老年代使用百分比
- M 元数据区使用百分比
- CCS 压缩使用百分比
- YGC 年轻代垃圾回收次数
- YGCT 年轻代垃圾回收消耗时间
- FGC Full GC垃圾回收次数
- FGCT Full GC垃圾回收消耗时间
- GCT 垃圾回收消耗总时间
jstat -gc 4856 1000 3
-gc和-gcutil参数类似,只不过输出字段不是百分比,而是实际的值。
输出
jstat -gc 4856 1000 3
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
15872.0 15872.0 0.0 0.0 98304.0 7864.4 261120.0 0.0 4480.0 777.2 384.0 76.6 0 0.000 0 0.000 0.000
15872.0 15872.0 0.0 0.0 98304.0 7864.4 261120.0 0.0 4480.0 777.2 384.0 76.6 0 0.000 0 0.000 0.000
15872.0 15872.0 0.0 0.0 98304.0 7864.4 261120.0 0.0 4480.0 777.2 384.0 76.6 0 0.000 0 0.000 0.000
字段解释:
- S0C survivor0大小
- S1C survivor1大小
- S0U survivor0已使用大小
- S1U survivor1已使用大小
- EC Eden区大小
- EU Eden区已使用大小
- OC 老年代大小
- OU 老年代已使用大小
- MC 方法区大小
- MU 方法区已使用大小
- CCSC 压缩类空间大小
- CCSU 压缩类空间已使用大小
- YGC 年轻代垃圾回收次数
- YGCT 年轻代垃圾回收消耗时间
- FGC Full GC垃圾回收次数
- FGCT Full GC垃圾回收消耗时间
- GCT 垃圾回收消耗总时间
jstack
jstack是用来查看JVM线程快照的命令,线程快照是当前JVM线程正在执行的方法堆栈集合。使用jstack命令可以定位线程出现长时间卡顿的原因,例如死锁,死循环等。jstack还可以查看程序崩溃时生成的core文件中的stack信息
命令格式
jstack [options]
option参数解释:
- -F 当使用jstack 无响应时,强制输出线程堆栈。
- -m 同时输出java堆栈和c/c++堆栈信息(混合模式)
- -l 除了输出堆栈信息外,还显示关于锁的附加信息
cpu占用过高问题
- 使用Process Explorer工具找到cpu占用率较高的线程
- 在thread卡中找到cpu占用高的线程id
- 线程id转换成16进制
- 使用jstack -l 查看进程的线程快照
- 线程快照中找到指定线程,并分析代码
jstack检查死锁问题
public class DeadLock {
private static Object obj1 = new Object();
private static Object obj2 = new Object();
public static void main(String[] args) {
new Thread(new Thread1()).start();
new Thread(new Thread2()).start();
}
private static class Thread1 implements Runnable{
public void run() {
synchronized (obj1){
System.out.println("Thread1 拿到了 obj1 的锁!");
try {
// 停顿2秒的意义在于,让Thread2线程拿到obj2的锁
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2){
System.out.println("Thread1 拿到了 obj2 的锁!");
}
}
}
}
private static class Thread2 implements Runnable{
public void run() {
synchronized (obj2){
System.out.println("Thread2 拿到了 obj2 的锁!");
try {
// 停顿2秒的意义在于,让Thread1线程拿到obj1的锁
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj1){
System.out.println("Thread2 拿到了 obj1 的锁!");
}
}
}
}
}
上图代码是一个非常明显的死锁演示代码,其中Thread1首先获取对象obj1的锁,然后休眠的时候Thread2获取到对象obj2的锁,而Thread1和Thread2又同时在尝试获取对方持有的锁,符合死锁的条件。
执行指令
jstack -l 28032
打印结果
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00000267fbaa2d48 (object 0x0000000741349460, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00000267fbaa55d8 (object 0x0000000741349470, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at com.lagou.concurrent.demo.test.DeadLock$Thread2.run(DeadLock.java:39)
- waiting to lock <0x0000000741349460> (a java.lang.Object)
- locked <0x0000000741349470> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
"Thread-0":
at com.lagou.concurrent.demo.test.DeadLock$Thread1.run(DeadLock.java:22)
- waiting to lock <0x0000000741349470> (a java.lang.Object)
- locked <0x0000000741349460> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
jmap
jmap可以生成 java 程序的 dump 文件, 也可以查看堆内对象示例的统计信息、查看 ClassLoader 的信息以及finalizer 队列
命令格式
jmap [option] (连接正在执行的进程)
option参数解释
- 如果使用不带选项参数的jmap打印共享对象映射,将会打印目标虚拟机中加载的每个共享对象的起始地址、映射大小以及共享对象文件的路径全称。
- -heap 打印java heap摘要
- -histo[:live] 打印堆中的java对象统计信息
- -clstats 打印类加载器统计信息
- -finalizerinfo 打印在f-queue中等待执行finalizer方法的对象
- -dump: 生成java堆的dump文件
- dump-options:
- live 只转储存活的对象,如果没有指定则转储所有对象
- format=b 二进制格式
- file= 转储文件到
常用示例
jmap -dump:live,format=b,file=dump.bin 15084
输出:
Dumping heap to /dump.bin ...
Heap dump file created
这个命令是要把java堆中的存活对象信息转储到dump.bin文件
jmap -finalizerinfo 15084
输出:
Attaching to process ID 15084, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.281-b09
Number of objects pending for finalization: 0
输出结果的含义为当前没有在等待执行finalizer方法的对象
jmap -heap 15084
输出堆的详细信息
Attaching to process ID 15084, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.281-b09
using thread-local object allocation.
Parallel GC with 10 thread(s)
Heap Configuration: //堆内存初始化配置
MinHeapFreeRatio = 0 //对应jvm启动参数-XX:MinHeapFreeRatio设置JVM堆最小空闲比率(default 40)
MaxHeapFreeRatio = 100 //对应jvm启动参数 -XX:MaxHeapFreeRatio设置JVM堆最大空闲比率(default 70)
MaxHeapSize = 6385827840 (6090.0MB) //对应jvm启动参数-XX:MaxHeapSize=设置JVM堆的最大大小
NewSize = 133169152 (127.0MB) //对应jvm启动参数-XX:NewSize=设置JVM堆的新生代的默认大小
MaxNewSize = 2128609280 (2030.0MB) //对应jvm启动参数-XX:MaxNewSize=设置JVM堆的新生代的最大大小
OldSize = 267386880 (255.0MB) //对应jvm启动参数-XX:OldSize=<value>:设置JVM堆的老年代的大小
NewRatio = 2 //对应jvm启动参数-XX:NewRatio=:新生代和老生代的大小比率
SurvivorRatio = 8 //对应jvm启动参数-XX:SurvivorRatio=设置新生代中Eden区与Survivor区的大小比值
MetaspaceSize = 21807104 (20.796875MB) // 元数据区大小
CompressedClassSpaceSize = 1073741824 (1024.0MB) //类压缩空间大小
MaxMetaspaceSize = 17592186044415 MB /元数据区最大大小
G1HeapRegionSize = 0 (0.0MB) //G1垃圾收集器每个Region大小
Heap Usage: //堆内存使用情况
PS Young Generation
Eden Space: //Eden区内存分布
capacity = 100663296 (96.0MB) //Eden区总容量
used = 0 (0.0MB) //Eden区已使用
free = 100663296 (96.0MB) //Eden区剩余容量
0.0% used //Eden区使用比率
From Space: //其中一个Survivor区的内存分布
capacity = 16252928 (15.5MB)
used = 0 (0.0MB)
free = 16252928 (15.5MB)
0.0% used
To Space: //另一个Survivor区的内存分布
capacity = 16252928 (15.5MB)
used = 0 (0.0MB)
free = 16252928 (15.5MB)
0.0% used
PS Old Generation
capacity = 101711872 (97.0MB) //老年代容量
used = 981064 (0.9356155395507812MB) //老年代已使用
free = 100730808 (96.06438446044922MB) //老年代空闲
0.9645521026296714% used //老年代使用比率
3156 interned Strings occupying 259152 bytes.
jmap -histo:live 15084 | more
输出存活对象统计信息
num #instances #bytes class name
----------------------------------------------
1: 4563 425096 [C
2: 415 134688 [B
3: 4414 105936 java.lang.String
4: 706 80888 java.lang.Class
5: 632 41512 [Ljava.lang.Object;
6: 791 31640 java.util.TreeMap$Entry
7: 628 25120 java.util.LinkedHashMap$Entry
8: 426 19072 [Ljava.lang.String;
9: 372 11904 java.util.HashMap$Node
10: 24 8640 [Ljava.util.HashMap$Node;
....
jhat
jhat是用来分析jmap生成dump文件的命令,jhat内置了应用服务器,可以通过网页查看dump文件分析结果,jhat一般是用在离线分析上。
命令格式
jhat [option] [dumpfile]
option参数解释 :
- -stack false: 关闭对象分配调用堆栈的跟踪
- -refs false: 关闭对象引用的跟踪
- -port : HTTP服务器端口,默认是7000
- -debug : debug级别
- -version 分析报告版本
常用实例
jhat dump.bin
JVM常用工具
Jconsole 监控管理工具
Jconsole(Java Monitoring and Management Console)是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存,线程和类等的监控,是一个基于JMX(java management extensions)的GUI性能监测工具。jconsole使用jvm的扩展机制获取并展示虚拟机中运行的应用程序的性能和资源消耗等信息。
直接在jdk/bin目录下点击jconsole.exe即可启动
内存监控
代码准备
public class JConsoleDemo {
static class OOMObject {
// 每个字节数组占8K内存
public byte[] placeholder = new byte[8 * 1024];
}
public static void fillHeap(int num) throws InterruptedException {
List<OOMObject> list = new ArrayList<OOMObject>();
for (int i = 0; i < num; i++) {
Thread.sleep(50);
list.add(new OOMObject());
}
System.gc();
}
public static void main(String[] args) throws Exception {
fillHeap(1000);
//System.gc();
Thread.sleep(10000);
}
}
编译运行JConsoleDemo类, 运行时设置的虚拟机参数为 -Xms100m -Xmx100m -XX:+UseSerialGC ,在%JAVA_HOME%\bin目录下, 启动jconsole.exe , 将自动搜索出本机运行的所有虚拟机进程, 这里我们选择JConsoleDemo对应的进程24036。
启动后主界面如下:
“概览”页签显示的是整个虚拟机主要运行数据的概览,其中包括“堆内存使用情况”、“线程”、“类”、“CPU使用情况”4种信息的曲线图。这些曲线图是后面“内存” 、“线程”、 ‘类”页签的信息汇总,具体内容将在后面介绍。
在"内存"页签, 查看堆内存Eden区的运行趋势如下:
从图中详细信息可以看出, Eden区的内存大小为27.328KB, 所以折线图中显示每次到27Mb左右时系统就会进行一次GC。当1000次循环结束后, 执行System.gc(), 柱状图中显示Eden区和Survivor区基本被清空, 但老年代的对应柱状图仍保持峰值状态, 这是因为System.gc()是在fillHeap()方法内执行, 所以list对象在System.gc()执行时仍然是存活的( 处于作用域之内、被引用)。 如果将System.gc()移动到fillHeap()方法外执行, 如下图所示, 则会回收包括老年代的所有内存。
线程监控
查看CPU使用率及活锁阻塞线程
代码准备
public class Demo7_JConsole02 {
/**
* 线程死循环演示
*/
public static void createBusyThread() {
Thread thread = new Thread(new Runnable() {
// 无限循环
public void run() {
while (true);
}
}, "testBusyThread");
System.out.println("启动testBusyThread 线程完毕..");
thread.start();
}
/
**
* 线程锁等待演示
*/
public static void createLockThread(final Object lock) {
Thread thread = new Thread(new Runnable() {
// 创建一个活锁线程
public void run() {
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "testLockThread");
thread.start();
System.out.println("启动testLockThread 线程完毕..");
}
public static void main(String[] args) throws Exception {
System.out.println("main 线程..");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
System.out.println("redLine阻塞");
br.readLine();
createBusyThread();
System.out.println("redLine阻塞");
br.readLine();
Object obj = new Object();
createLockThread(obj);
System.out.println("main 线程结束..");
}
}
启动代码,在控制台执行JConsole命令观察
首先执行BusyThread线程
可以看到,在执行BusyThread线程时,右下角的CPU占用率从一开始比较低的值一下升高,再趋于平稳。原因就是我们新启动了一个线程,占用了一定的CPU,同时由于该线程无限循环,所以CPU占用率再提升到一定值后会趋于平稳,而不是执行结束后回落。
接下来,再执行LockThread线程
在”线程“的菜单栏中可以看到,testLockThread线程的状态被标记为WAITING,而且根据堆栈追踪可知,线程WAITING在代码的第27行。
通过上述分析可以看出,JConsole工具为我们提供了丰富的观察JVM某一进程执行情况的各项指标,帮助我们追踪代码运行性能和问题回溯。
VisualVM 可视化优化工具
简介
VisualVM 是一个工具,它提供了一个可视界面,用于查看 Java 虚拟机 (Java Virtual Machine, JVM) 上运行的基于Java 技术的应用程序(Java 应用程序)的详细信息。VisualVM 对 Java Development Kit (JDK) 工具所检索的 JVM 软件相关数据进行组织,并通过一种使您可以快速查看有关多个 Java 应用程序的数据的方式提供该信息。您可以查看本地应用程序以及远程主机上运行的应用程序的相关数据。此外,还可以捕获有关 JVM 软件实例的数据,并将该数据保存到本地系统,以供后期查看或与其他用户共享。
概述与插件安装
VisualVM基于NetBeans平台开发, 因此它一开始就具备了插件扩展的特性, 通过插件支持, VisualVM可以做许多事情。例如:
- 显示虚拟机进程和进程的配置、环境信息(jps、jinfo)
- 监视应用程序的CPU、GC、堆、方法区及线程的信息(jstat、jstack)
- dump及分析堆转储快照(jmap、jhat)
- 方法级的程序运行性能分析, 找出被调用最多、运行时间最长的方法
- 离线程序快照: 收集程序的运行时配置、线程dump、内存dump等信息建立一个快照, 可以将快照发送开发者处进行bug反馈等等
在%JAVA_HOME%\bin目录下, 启动jvisualvm.exe进入主界面, 点击"工具"→"插件"→"可用插件"选项, 选择所需的插件安装。
安装好插件后, 选择一个正在运行的java程序就可以查看程序监控的主界面了
测试代码:
public class Demo8_JVisualVM {
public static void main(String[] args) throws IOException, InterruptedException {
System.in.read(); // 阻塞,只要输入回车,就会向下执行代码
fun();
System.in.read();
}
private static void fun() throws InterruptedException {
List<Capacity> list = new ArrayList<>();
for(int i = 0; i < 10000; i++){
Thread.sleep(400);
list.add(new Capacity());
}
}
}
class Capacity{
private byte[] big = new byte[8 * 1024 * 1024]; // 8M
}
执行测试程序,在Terminal中输入jvisualVM,观察jvisualVM工具对进程运行各项指标的监控展示。
在启动页面可以看到我们执行的进程:Demo8_JVisualVM,pid=18684
双击点开对应进程,可以看到该进程执行情况的相应概述:
接下来,在控制台中输入回车,即会执行fun
方法,我们通过Visual GC标签栏,来观察程序运行时的GC过程。(Visual GC标签栏可以通过导航栏的 工具->插件->可用插件 里下载):
从上图中我们可以看到,JVM各区域的内存使用情况。在fun
方法中,每隔400ms就会往队列中添加一个8k大小的数组。可以看到Eden Space区的图形成锯齿状,原因是每次到达峰值时,就会进行一次Young GC。由于fun
方法中list
引用一直持有这些数据,因此这些数据不会被GC回收,多次未被回收会就被放入老年代中。这也是老年代内存空间持续增加的原因。最终,老年代一直增加达到阈值就会报错Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
其他标签栏中的性能监控展示,跟JConsole里的类似,这里就不重复介绍了,有兴趣的同学可以自己动手试一试,利用一些测试代码,然后通过JConsole或JvisualVM可视化工具,来观察程序运行过程中各项指标的情况,以及GC过程各区域的变化情况,也有利于自己对GC过程的深入理解。