一、监控java程序运行状态

举例用以下代码为例介绍如何使用常见的JVM命令行工具监控程序运行状态,程序运行环境为Ubuntu 14.04、jdk 1.8.0_181:

package com.cn.test; import java.util.ArrayList; import java.util.List; public class HeapOOM { private static class TestObject{ private long[] data = new long[1024]; } public static void main(String[] args) { List<TestObject> objects = new ArrayList<>(); while (true){ objects.add(new TestObject()); try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } } }

上面的代码逻辑很简单:死循环向List中add对象,最终会造成OOM,下面设置堆内存为200M运行程序:

java -verbose:gc -Xms200M -Xmx200M com.cn.test.HeapOOM

1.1 获取java进程pid

要用命令行工具监控java程序运行情况的第一步是需要获取java程序的pid,因为使用其他命令行工具需要java进程的pid作为参数,有两种方式获取pid。

1.1.1 linux的ps命令

运行程序的pid为22719

jvm监控显示线程驻留(互联网一线大厂常用的JVM性能监控与故障处理工具)(1)

1.1.2 jdk自带的命令行工具jps

只列出所有java进程的pid和主类名称,-l参数列出全类名,-m参数列出给main函数的入参,-v参数列出传递给JVM的启动参数,如图可以看到jps获取的pid和linux的ps命令获取结果相同

jvm监控显示线程驻留(互联网一线大厂常用的JVM性能监控与故障处理工具)(2)

1.2 Java应用运行过程中查看配置信息

可能在程序运行中需要查看虚拟机参数、系统属性,便于我们确定参数配置是否有问题,可能还需要调整一些虚拟机参数,使用jinfo(Configuration Info for Java)命令可以达到这个目的,-flags参数列出虚拟机参数,-sysprops参数列出系统属性,-flag参数可以查看单个属性或者修改属性值,下面添加让程序运行中打印GC日志的虚拟机参数-XX: PrintGCDetails。

jvm监控显示线程驻留(互联网一线大厂常用的JVM性能监控与故障处理工具)(3)

1.3 监控运行中JVM各个分区内存使用情况及垃圾回收情况

jstat(JVM Statistics Monitoring Tool)作为虚拟机统计信息监视工具可以获取JVM各个分区内存使用、垃圾回收、类加载、JIT编译等详细信息,并且通过设置参数可以做到实时打印监控数据,下图为监控pid为23222的java进程,每1000ms打印一次JVM各个分区内存使用情况和gc的统计信息,连续打印10次。

jvm监控显示线程驻留(互联网一线大厂常用的JVM性能监控与故障处理工具)(4)

1.4 监控堆中对象统计信息

通过监控堆空间中类、实例数量以及合计容量的统计信息可以统计出应用程序中哪些对象被创建的最多,哪些对象占用的内存量最大,哪些对象数量在持续增长,进而可以分析出应用中存在的隐患,例如内存溢出和内存泄漏问题,jmap(Memory Map for Java)是jdk自带的Java内存映像工具可以完成这些工作,使用-histo参数可以按照占用内存的量倒序列出堆中对象的类名、实例数、占用内存量,如下图所示两次统计结果发现test.HeapOOM$TestObject,也就是HeapOOM的内部类TestObject的数量在增加,这种情况持续下去可能造成OOM,实际分析程序可以证明这一点。

jvm监控显示线程驻留(互联网一线大厂常用的JVM性能监控与故障处理工具)(5)

jmap另一种更普遍的使用场景是用于生成堆转储快照(称为heapdump或dump文件),当然也可以在启动时通过设置

-XX: HeapDumpOnOutOfMemoryError

参数让虚拟机在OOM异常出现之后自动生成dump文件。用于生成堆转储快照的命令格式为

jmap -dump:[live,]format=b,file=<filename> pid

live表示只dump出存活的对象。生成的堆转储快照可以用jdk自带的jhat(JVM Heap Analysis Tool)虚拟机堆转储快照分析工具进行分析或者使用更强大更专业的工具如Eclipse Memory Analyzer Tool(MAT)等。

二、分析OutOfMemoryError原因

上面示例程序运行下去最终会造成OOM,为了让虚拟机在OOM异常出现之后自动生成dump文件用于分析原因,在运行之前添加

-XX: HeapDumpOnOutOfMemoryError

参数:

java -verbose:gc -Xms100M -Xmx100M -XX: HeapDumpOnOutOfMemoryError com.cntest.HeapOOM

运行后经过几次垃圾回收最终发生OOM,同时生成了dump文件,将生成的dump文件导入MAT(https://www.eclipse.org/mat/documentation/) 中,使用Histogram进行Class维度展示每个Class类的实例存在的个数,可以发现com.cn.test.HeapOOM$

TestObject和long数组的Shawllow Heap占比最大,所以可以确定发生OOM的原因是创建了大量com.cn.test.HeapOOM$TestObject将内存撑爆。

jvm监控显示线程驻留(互联网一线大厂常用的JVM性能监控与故障处理工具)(6)

jvm监控显示线程驻留(互联网一线大厂常用的JVM性能监控与故障处理工具)(7)

三、死锁分析

运行下面的死锁程序:

package test; public class DeadLock { public static void main(String[] args) { /** * 创建并启动两个线程t1、t2。两个线程都要共享o1、o2两个对象 */ Object o1 = new Object(); Object o2 = new Object(); Thread t1 = new Thread(new T1(o1, o2) , "线程1"); Thread t2 = new Thread(new T2(o1, o2) , "线程2"); t1.start(); t2.start(); } } class T1 implements Runnable { Object o1; Object o2; public T1(Object o1, Object o2) { this.o1 = o1; this.o2 = o2; } public void run() { //锁o1和o2 synchronized (o1) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (o2) { System.out.println("o2"); } } } } class T2 implements Runnable { Object o1; Object o2; public T2(Object o1, Object o2) { this.o1 = o1; this.o2 = o2; } public void run() { //锁o2和o1 synchronized (o2) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (o1) { System.out.println("o1"); } } } }

jstack(Stack Trace for Java)Java堆栈跟踪工具用于生成虚拟机当前时刻的线程快照(一般称为threaddump或javacore文件)。线程快照就是当前虚拟机内每条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致长时间等待都是导致线程长时间停顿的常见原因。使用:

jastck –l pid

查看线程堆栈发现“线程1”和“线程2”都处于阻塞状态,相互持有对方的锁,这就是造成死锁的原因。

四、在线分析诊断工具Arthas

Arthas是Alibaba开源的Java诊断工具,通过全局视角实时查看应用load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率。

项目github地址:https://github.com/alibaba/arthas

项目文档地址:https://alibaba.github.io/arthas/index.html

详细安装、使用等方面的问题在文档中有完整的描述,这里只通过几个使用案例大致了解一下这款工具的功能,运行下面的实例程序:

package test; import java.util.UUID; public class TestCase { public static String uuid(){ return UUID.randomUUID().toString().replaceAll("-", ""); } public static void main(String[] args) { while(true){ System.out.println("uuid = " uuid()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }

4.1 运行arthas

运行as.sh脚本然后选择要attach的java进程

jvm监控显示线程驻留(互联网一线大厂常用的JVM性能监控与故障处理工具)(8)

4.2 大屏展示

线程、内存、GC、运行时信息等,直接键入dashboard命令:

4.3 监控方法调用

test.TestCase的uuid方法调用的返回值

jvm监控显示线程驻留(互联网一线大厂常用的JVM性能监控与故障处理工具)(9)

4.4 监控方法调用耗时

test.TestCase的uuid方法调用耗时

jvm监控显示线程驻留(互联网一线大厂常用的JVM性能监控与故障处理工具)(10)

4.5 对方法的执行情况进行监控

对test.TestCase的uuid方法执行方法监控,返回结果包括:时间戳、类名、方法名、调用次数、成功次数、失败次数、平均RT、失败率

jvm监控显示线程驻留(互联网一线大厂常用的JVM性能监控与故障处理工具)(11)

4.6 打印类的信息

jvm监控显示线程驻留(互联网一线大厂常用的JVM性能监控与故障处理工具)(12)

4.7 反编译

反编译test.TestCase类的uuid方法

jvm监控显示线程驻留(互联网一线大厂常用的JVM性能监控与故障处理工具)(13)

4.8 方法内部调用

输出方法内部调用路径,并输出方法路径上的每个节点上耗时

jvm监控显示线程驻留(互联网一线大厂常用的JVM性能监控与故障处理工具)(14)

,