Java性能调优工具实践
本文结合深入理解 Java 虚拟机对 JDK 中涉及的若干性能调优及监测工具进行简单使用。
测试代码
首先给出用于测试的 .java 文件
主函数里启动两个线程,每个线程都执行死循环。循环内部对一个 Point 对象的坐标进行随机更新,当循环指定次数后向 List 中追加新的对象,使得堆空间不断增长。
Main.java
public class Main { public static void main(String[] args) throws RuntimeException { Thread t1 = new Thread(new MyThread()); t1.start(); Thread t2 = new Thread(new MyThread2()); t2.start(); } }
MyThread.java
import java.util.ArrayList; import java.util.List; import java.util.Random; /* 一个占用空间很大的类,模拟实际业务 */ class BigClass{ List<Double> list; BigClass() { list = new ArrayList<>(); for (int i=0; i<100000; i++) { list.add(i*1.0); } } } class MyThread implements Runnable{ @Override public void run() { Point p = new Point(0, 0, "Thread-1"); Random rt = new Random(); int cnt = 0; while (true) { int dx = rt.nextInt(10); int dy = rt.nextInt(10); if ((dx&1)==1) { p.add(dx, dy); } else { p.plus(dx, dy); } if (cnt%3==0) { Main.l1.add(new BigClass()); System.out.println("Generated by 1"); } p.print(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } cnt++; } } } class MyThread2 implements Runnable{ @Override public void run() { Point p = new Point(0, 0, "Thread-2"); Random rt = new Random(); int cnt = 0; while (true) { int dx = rt.nextInt(10); int dy = rt.nextInt(10); if ((dx&1)==1) { p.add(dx, dy); } else { p.plus(dx, dy); } if (cnt%3==0) { Main.l2.add(new BigClass()); System.out.println("Generated by 2"); } p.print(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } cnt++; } } }
Point.java
class Point { private int x; private int y; private String name; Point(int x, int y, String name) { this.x = x; this.y = y; this.name = name; } public void add(int dx, int dy) { x += dx; y += dy; } public void plus(int dx, int dy) { x -= dx; y -= dy; } public void print() { System.out.println(x+ " "+y+" "+" "+name); } }
jps
jps 命令类似与 linux 的 ps 命令,但是它只列出系统中所有的 Java 应用程序。 通过 jps 命令可以方便地查看 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息。
选项 | 作用 |
---|---|
-q | 只输出LVMID,省略主类的名称 |
-m | 输出虚拟机进程启动时传递给主类 main() 函数的参数 |
-l | 输出主类的全名,如果进程执行的是 jar 包,输出 jar 路径 |
-v | 输出虚拟机进程启动时 JVM 参数 |
运行 Main 类后在命令行中输入如下命令并观看执行结果:
jstat
jstat 是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,在没有 GUI 图形界面只提供纯文本控制台环境的服务器上,它是运行期定位虚拟机性能问题的首选工具。jstat 主要工具选项如下:
- -class:监视类装载、卸载数量、总空间以及类装载所耗费的时间
- -gc:监视 java 堆状态,包括 Eden, Survivor, 老年代, 永久代的容量、已用空间、GC 时间等信息
- -gccapacity:同 -gc ,但是更关注堆空间使用的最大最小空间
- -gcutil:同 -gc ,但是更关注已使用空间占总空间的百分比
- -gccause:同 -gcutil ,但是更关注上一次 GC 产生的原因
- -gcnew:监视新生代 GC 情况
- -gcnewcapacity:同 -gcnew ,但是更关注使用到的最大、最小空间
- -gcold:监视老年代 GC 情况
- -gcoldcapacity:同 -gcold,但是更关注使用到的最大、最小空间
- -gcpermcapacity:监视永久代使用的最大、最小空间
运行 Main 类后在命令行中输入如下命令(jstat -参数 pid 时间间隔 采样次数)并观看执行结果:
信息统计中各列含义说明如下:
- S0C:年轻代中第一个survivor(幸存区)的容量 (字节)
- S1C:年轻代中第二个survivor(幸存区)的容量 (字节)
- S0U:年轻代中第一个survivor(幸存区)目前已使用空间 (字节)
- S1U:年轻代中第二个survivor(幸存区)目前已使用空间 (字节)
- EC:年轻代中Eden(伊甸园)的容量 (字节)
- EU:年轻代中Eden(伊甸园)目前已使用空间 (字节)
- OC:Old代的容量 (字节)
- OU:Old代目前已使用空间 (字节)
- PC:Perm(持久代)的容量 (字节)
- PU:Perm(持久代)目前已使用空间 (字节)
- CCSC:压缩类空间大小
- CCSU:压缩类空间使用大小
- YGC:从应用程序启动到采样时年轻代中gc次数
- YGCT:从应用程序启动到采样时年轻代中gc所用时间(s)
- FGC:从应用程序启动到采样时old代(全gc)gc次数
- FGCT:从应用程序启动到采样时old代(全gc)gc所用时间(s)
- GCT:从应用程序启动到采样时gc用的总时间(s)
- NGCMN:年轻代(young)中初始化(最小)的大小 (字节)
- NGCMX:年轻代(young)的最大容量 (字节)
- NGC:年轻代(young)中当前的容量 (字节)
- OGCMN:old代中初始化(最小)的大小 (字节)
- OGCMX:old代的最大容量 (字节)
- OGC:old代当前新生成的容量 (字节)
- PGCMN:perm代中初始化(最小)的大小 (字节)
- PGCMX:perm代的最大容量 (字节)
- PGC:perm代当前新生成的容量 (字节)
- S0:年轻代中第一个survivor(幸存区)已使用的占当前容量百分比
- S1:年轻代中第二个survivor(幸存区)已使用的占当前容量百分比
- E:年轻代中Eden(伊甸园)已使用的占当前容量百分比
- O:old代已使用的占当前容量百分比
- P:perm代已使用的占当前容量百分比
- S0CMX:年轻代中第一个survivor(幸存区)的最大容量 (字节)
- S1CMX :年轻代中第二个survivor(幸存区)的最大容量 (字节)
- ECMX:年轻代中Eden(伊甸园)的最大容量 (字节)
- DSS:当前需要survivor(幸存区)的容量 (字节)(Eden区已满)
- TT: 持有次数限制
- MTT : 最大持有次数限制
jinfo
jinfo 的作用是实时查看和调整虚拟机各项参数。当系统崩溃时,jinfo 可以从 core 文件里面知道崩溃的 Java 应用程序的配置信息。选项说明如下:
- no option 输出全部的参数和系统属性
- -flag name 输出对应名称的参数
- -flag [+|-]name 开启或者关闭对应名称的参数
- -flag name=value 设定对应名称的参数
- -flags 输出全部的参数
- -sysprops 输出系统属性
运行 Main 类后在命令行中输入如下命令并观看执行结果:
jmap
jmap 命令用于生成堆存储快照,还可以查询 finalize 执行队列、 Java 对和永久代的详细信息,如空间使用率、当前用的是哪种收集器等。主要选项如下:
- -dump:生成 Java 堆存储快照
- -finalizerinfo:显示在 F-Queue 中等待 Finalizer 线程执行 finalize 方法的对象
- -heap:显示 Java 堆详细信息,比如使用哪种回收器、参数配置、分代状况等
- -histo:显示堆中对象统计信息,包括类、实例数量、合计数量
- -permstat:显示永久代内存状态
- -F:强制选项
运行 Main 类后在命令行中输入如下命令并观看执行结果:
jhat
jhat 是 JDK 内置的工具之一。主要是用来分析 Java 堆的命令,可以将堆中的对象以 Html 的形式显示出来,包括对象的数量,大小等等,并支持对象查询语言。运行 Main 类后在命令行中输入如下命令并观看执行结果:
随后在浏览器中访问 http://localhost:7000/ 即可。
jstack
jstack 命令用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈集合,生成线程快照的主要目的在于定位线程长时间停顿的原因,比如线程死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的原因。主要选项如下:
- -F:强制
- -l:除堆栈外,显示关于锁的附加信息
- -m:如果调用本地方法,可以显示C/C++的堆栈
运行 Main 类后在命令行中输入如下命令并观看执行结果:
下面给出一个检测死锁的例子,代码如下:
public class DeadLock { private static Lock lock1 = new ReentrantLock(); private static Lock lock2 = new ReentrantLock(); public static void deathLock() { Thread t1 = new Thread() { @Override public void run() { try { lock1.lock(); TimeUnit.SECONDS.sleep(1); lock2.lock(); } catch (InterruptedException e) { e.printStackTrace(); } } }; Thread t2 = new Thread() { @Override public void run() { try { lock2.lock(); TimeUnit.SECONDS.sleep(1); lock1.lock(); } catch (InterruptedException e) { e.printStackTrace(); } } }; t1.setName("mythread1"); t2.setName("mythread2"); t1.start(); t2.start(); } public static void main(String[] args) { deathLock(); }
运行这个类后,我们在命令行中观看其输出如下:
首先,使用 -l 参数输出了关于死锁的信息。接下来定位到两个线程的信息输出位置: