Java程序员面试JVM几乎必问,对于JVM监控,线上OOM,CPU负载100%等问题也是经常被问到,尽管在企业中不一定轮得到我们去处理线上问题,但是不管是为了面试还是为了应对开发那么对于JVM线上问题处理都是必须要去了解的。
相对而言,解决故障问题也好,处理性能瓶颈也罢,通常思路大致都是相同的,即:分析数据(日志) , 分析排查,问题定位,解决问题 ,如果我们连程序执行的数据或日志都拿不到,那么我们是没办法去定位问题的。
庆幸的是Java提送了JVM监视工具以及相关指令来帮助我们获取JVM相关数据来帮助我们进行问题排查。
我们只知道有JVM的存在,但它的运行对于我们来说感觉像是摸不着看不见的,所以我们需要借助工具来监控它的一个实时状态,就像Windows的性能监视器一样,JDK也有自己的可视化工具.Java提供了2个监视工具:
D:\opensource\jdk1.8\bin\jconsole.exe
D:\opensource\jdk1.8\bin\jvisualvm.exe
通过cmd命令行输入 jconsole 弹出如下界面
选择java进行后进入,进入后可以看到内存的情况,类加载情况,线程情况等等。
我们以运行cmd ,输入jvisualvm,将Java VisualVM启动
左边本地菜单下是java进程,选择一个进程后右边可以看到堆,类加载情况,现成情况等
自带的jvisualvm没有监视GC垃圾回收功能,我们需要额外安装插件:
打开工具 -> 插件 -> 选择“可用插件”页 : 我们在这里安装一个Visual GC,方便我们看到内存回收以及各个分代的情况 . 打上勾之后点击安装,就是常规的next以及同意协议等 ,网络不是很稳定,有时候可能需要多尝试几次。可以在设置中修改插件中心地址:
根据如下步骤修改地址:找到插件中心
VisualVM: Plugins Centers
找到对应的JDK版本:
Update Center documentation
复制插件地址:
安装插件:
然后再 可用插件中 找到 Visual GC
安装完成后我们将当前监控页关掉,再次打开,就可以看到Profiler后面多了一个Visual GC页。
在这里我们可以看到JIT活动时间,类加载活动时间,GC活动时间以及各个分代的情况。
需要注意的是,当前课件使用的JDK版本为1.8,仍然自带了VisualVM,从1.9开始的版本是没有自带的,需要额外下载,下载的github地址:
https://visualvm.github.io/download.html
另外,如果开发工具使用的是Intellij IDEA的话,可以下载一个插件,VisualVM Launcher,通过插件启动可以直接到上述页面,不用在左边的条目中寻找自己的项目.
当然也有其他的工具,但这个在可预见的未来都会是主力发展的多合一故障处理工具.所以我们后面将会使用这个工具来分析我们的JVM运行情况,进而优化.而需要优化我们还需要对JVM的组成有进一步的了解
在生产环境中,经常会遇到各种各样奇葩的性能问题,我们可以通过Java提供的JVM监控命令来达到监控和查看效果,相关命令如下
名称 | 主要作用 |
---|---|
jps | 查看正在运行的Java进程 |
jstack | 打印线程快照 |
jmap | 导出堆内存映像文件 |
jstat | 查看jvm统计信息 |
jinfo | 实时查看和修改jvm配置参数 |
jhat | 用于分析heapdump文件 |
jps可以列出正在运行的Java进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及进程id ,通过 jps -help 可以查看参数
选项 | 作用 |
---|---|
-q | 只输出进程id |
-m | 输出传递给主类main函数的参数 |
-l | 输出主类全类名,如果进程执行的是Jar包,输出jar包名字 |
-v | 程序启动时指定的jvm参数 |
案例演示:
一般在生产环境中发生了长时间停顿,卡死,死锁,请求时间长等问题就可以通过打印线程快照来分析定位问题. 下面是一段死锁的代码:
public class DeadlockExample {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread() {
public void run() {
synchronized (lock1) {
System.out.println("Thread 1 acquired lock 1");
try {
Thread.sleep(1000);
synchronized (lock2) {
System.out.println("Thread 1 acquired lock 2");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread t2 = new Thread() {
public void run() {
synchronized (lock2) {
System.out.println("Thread 2 acquired lock 2");
try {
Thread.sleep(1000);
synchronized (lock1) {
System.out.println("Thread 2 acquired lock 1");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
拿到进程id,使用jstack查看每个线程快照 : jstack 27928
从线程快照中可以看到当前进程中的所有线程。其中包括我们的代码的线程,状态是 locked 锁住状态。同时它提示 found 1 deadlock 发现死锁,并给出了死锁出现的位置。
执行jmap -histo pid
可以打印出当前堆中所有每个类的实例数量和内存占用,如下,class name是每个类的类名([B是byte类型,[C是char类型,[I是int类型),bytes是这个类的所有示例占用内存大小,instances是这个类的实例数量:
执行jmap -dump 可以转储堆内存快照到指定文件,比如执行
jmap -dump:format=b,file=/data/jvm/dumpfile_jmap.hprof PID ,可以把当前堆内存的快照转储到dumpfile_jmap.hprof文件中,然后可以对内存快照进行分析。
生产环境我们一般会配置,让虚拟机在OOM异常出现之后自动生成dump文件
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/Users
比如现在有一个死循环的代码然后运行到一定时间会导致内存溢出
public class Main {
public static void main(String[] args) {
ArrayList arrayList = new ArrayList();
int i = 0;
while(true){
arrayList.add(new Main());
System.out.println(i++);
}
}
}
为了效果明显,我们把堆设置小一些,拉后把HeapDumpOnOutOfMemoryError设置上
-Xms2m
-Xmx2m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=d:\
然后我们找到hprof 堆快照文件,可以通过 jvisualvm工具去装载分析
jstat -gc pid 500 10
:pid是线程ID,每500毫秒打印一次Java堆状况(各个区的容量、使用容量、gc时间等信息),打印10次
jstat还可以以其他角度监视各区内存大小、监视类装载信息等,具体可以google jstat的详细用法。下面是结果对照
S0C:第一个幸存区的大小
S1C:第二个幸存区的大小
S0U:第一个幸存区的使用大小
S1U:第二个幸存区的使用大小
EC:伊甸园区的大小
EU:伊甸园区的使用大小
OC:老年代大小
OU:老年代使用大小
MC:方法区大小
MU:方法区使用大小
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
单位:KB
jinfo(Configuration Info for Java) 查看虚拟机配置参数信思,也可用于调整虚拟机的配置参数。
在很多情况下,Java应用程序不会指定所有的Java虚拟机参数。而此时,开发人员可能不知道某一个具体的Java虚拟机参数的默认值。在这种情况下,可能需要通过查找文档获取某个参数的默认值。这个查找过程可能是非常艰难的。但有了 jinfo工具,开发人员可以很方便地找到Java虚拟机参数的当前值。
jinfo不仅可以查看运行时某一个Java虚拟机参数的实际取值, 甚至可以在运行时修改部分参 数,并使之立即生效。 但是,并非所有参数都支持动态修改。参数只有被标记 manageable的flag可以被实时修改。其实,这个修改能力是 极其有限的。
VM Flags:
Non-default VM flags: -XX:CICompilerCount=12 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=null -XX:InitialHeapSize=2097152 -XX:MaxHeapSize=209715200 -XX:MaxNewSize=69730304 -XX:MinHeapDeltaBytes=524288 -XX:OldSize=524288 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
Command line: -Xms2m -Xmx200m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:\ -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2022.2.1\lib\idea_rt.jar=54817:D:\Program Files\JetBrains\IntelliJ IDEA 2022.2.1\bin -Dfile.encoding=UTF-8
通过 jinfo -flags pid: 查看曾经赋过值的参数值
jinfo不仅可以查看运行时某一个Java虚拟机参数的实际取值, 甚至可以在运行时修改部分参 数,并使之立即生效。 但是,并非所有参数都支持动态修改。参数只有被标记 manageable的flag可以被实时修改。可以通过使用java -XX:+PrintFlagsInitial | grep manageable命令来查看manageable
修改格式如下:
比如修改打印GC日志演示如下 : jinfo -flag +PrintGCDetails PID
//查看进程
C:\Users\Administrator>jps -l
20924 org.example.Main
...
//查看是否设置PrintGCDetails参数配置
C:\Users\Administrator>jinfo -flag PrintGCDetails 20924
//增加jvm参数:打印GC详情
C:\Users\Administrator>jinfo -flag +PrintGCDetails 20924
//查看是否设置PrintGCDetails参数配置
C:\Users\Administrator>jinfo -flag PrintGCDetails 20924
-XX:+PrintGCDetails
...
一般 CPU 100%,基本都是代码死循环造成的。排查的核心思路是 找到对应服务器,定位是哪个进程的哪个线程中的哪些代码引发的问题,可以简单介绍当时的异常代码示例。
第一步找到最耗CPU的进程 : 使用top -c 查看进程,然后输入大 P,按照 CPU 的使用率进行排序。
第二步找到进程中最耗CPU的线程 :找到CPU最高的进程,找到进程ID (PID) , 通过命令 top -Hp PID 找到这个进程对应的线程,然后输入大 P ,按照 CPU 的使用率进行排序。
拿到排第一的PID就是最耗时的线程ID,然后使用 printf “%x\n” PID 把PID由十进制转换为十六进制(之所以要转化为16进制,是因为堆栈里,线程id是用16进制表示的。)
[root@VM-4-2-centos ~]# printf "%x\n" 13759
35bf
接着,我们需要使用 jstack 打印进程的堆栈信息,再通过 grep 查看对应线程相关的东西。jstack 进程ID | grep “线程ID” -C5 --color
jstack 30979 | grep "35bf" -C5 --color
这个时候就可以打印出代码,然后从打印的线程快照中匹配nid,就可以定位到哪个线程耗时了,同时可以快速定位到代码,可以看到是哪个类中的哪个方法导致此次 CPU 100% 的原因了。
如果你不是很熟悉命令,使用命令监控JVM是一件痛苦的事情,JVisualvm提供了jmx远程功能。默认是通过localhost的ip地址提供RMI服务,它需要我们在远程配置JVM参数来开启远程连接
-Xms256m -Xmx512m -Xss256m -XX:PermSize=512m -XX:MaxPermSize=1024m -Dcom.sun.management.jmxremote -Djava.rmi.server.hostname=服务器IP #远程服务的端口: -Dcom.sun.management.jmxremote.port=9015 #客户端 rmi通信端口 -Dcom.sun.management.jmxremote.rmi.port=9015 #关闭ssl功能 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false
然后我们在本地Jvisualvm添加远程主机
制定远程连接参数,取消ssl连接
关于文章中提到的Jvm资料 可以通过关注公众号《编程乐学》获取对应资料,同时,公众号还有更多有趣的项目以及关于学习编程的笔记资料大家可以看看。