主要内容:
- JVM参数类型
- 查看运行时JVM参数
- 查看JVM统计信息
- jmap + MAT 实战内存溢出
- jstack实战死循环与死锁
1.JVM参数类型
1.1 参数分类:
- 标准参数
- X参数
- XX参数
1.2 标准参数
常用标准参数如下:
- -help
- server -clien
- -version -showversion
- -cp -classpath
以上参数在JVM各版本中属于相对比较稳定的,很少变动。
1.3 X参数
X参数,也就是非标准化参数。主要有以下几种:
- -Xint: 解释执行
- -Xcomp: 第一次使用就编译成本地代码
- -Xmixed: 混合模式,JVM自己来决定是否编译成本地代码
1.4 XX参数
(1)说明:该中参数也属于非标准化参数,且相对不稳定。
(2)作用:主要用于JVM调优和Debug。
(3)XX参数分类
- Boolean类型
格式: -XX:[+-]<name>表示启用或者禁用name属性
比如:-XX:+UseConcMarkSweepGC
-XX:+UseG1GC (开启G1垃圾收集器)
- 非Boolean类型
格式:-XX:<name>=<value>表示name属性的值是value
比如:-XX:MaxGCPauseMillis=500 (表示GC的最大停顿时间为500ms)
XX:GCTimeRatio=19
(4)关于 -Xmx -Xms 参数
-Xmx -Xms,不是X参数,而是XX参数。
- -Xms等价于 -XX:InitialHeapSize ,表示初始化堆的大小
- -Xmx等价于 -XX:MaxHeapSize ,表示最大的对大小
2.查看JVM运行时参数
(1)相关参数
- -XX:+PrintFlagsInitial (查看初始值)
- -XX:+PrintFlagsFinal (查看最终的值,初始值是可以被改变的)
- -XX:+uNLOCKeXPERIMENTALvmoPTIONS 解锁实验参数
- JVM中的参数并不都是可以直接赋值的,部分需要先执行解锁操作后才能使用
- -XX:+UnlockDiagnosticVMOptions解锁诊断参数
- -XX:+PrintCommandLineFlag打印命令行参数
(2)参数是否被赋值的说明:
- = 表示默认值
- := 被用户或者JVM修改后的值
(3)演示——打印版本号 -version
java -client -XX:+PrintFlagsFinal Benchmark
打印出来的参数结果:
从上图可知,其UseG1GC是没有启用过的。
上图中的每一行包括五列,来表示一个XX参数。第一列表示参数的数据类型,第二列是名称,第四列为值,第五列是参数的类别。第三列”=”表示第四列是参数的默认值,而”:=” 表明了参数被用户或者JVM赋值了。
(4)官方文档地址
(5)jps
功能: 显示当前所有java进程pid的命令。
常用指令:
jps
:显示当前用户的所有java进程的PIDjps -v 3331
:显示虚拟机参数jps -m 3331
:显示传递给main()函数的参数jps -l 3331
:显示主类的全路径
案例:
(6)jinfo
功能:查看运行中的JVM内的参数值
使用方式:
-flag <name>
用于打印虚拟机标记参数的值,name表示虚拟机标记参数的名称。
案例:通过 jinfo 来查看对应进程的参数信息
上图中案例分析:jinfo -flag MaxHeapSize 22684
- jinfo —— 查看命令
- -flag —— 查看方式
- MaxHeapSize —— 查看的参数
- 22684 —— 被查看参数的那个进程(JVM的Bootstrap)
3.jstat查看JVM统计信息
(1)定义:Jstat是JDK自带的一个轻量级小工具。全称“Java Virtual Machine statistics monitoring tool”,它位于java的bin目录下,主要利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控,包括了对Heap size和垃圾回收状况的监控。
(2)jstat 可查看的JVM统计信息有如下几种:
- 类装载
- 垃圾收集
- JIT编译
(3)jstat 用法
- option: 参数选项
- -t: 可以在打印的列加上Timestamp列,用于显示系统运行的时间
- -h: 可以在周期性数据数据的时候,可以在指定输出多少行以后输出一次表头
- vmid: Virtual Machine ID( 进程的 pid)
- interval: 执行每次的间隔时间,单位为毫秒
- count: 用于指定输出多少次记录,缺省则会一直打印
option 可以从下面参数中选择
- -class 显示ClassLoad的相关信息;
- -compiler 显示JIT编译的相关信息;
- -gc 显示和gc相关的堆信息;
- -gccapacity 显示各个代的容量以及使用情况;
- -gcmetacapacity 显示metaspace的大小
- -gcnew 显示新生代信息;
- -gcnewcapacity 显示新生代大小和使用情况;
- -gcold 显示老年代和永久代的信息;
- -gcoldcapacity 显示老年代的大小;
- -gcutil 显示垃圾收集信息;
- -gccause 显示垃圾回收的相关信息(通-gcutil),同时显示最后一次或当前正在发生的垃圾回收的诱因;
- -printcompilation 输出JIT编译的方法信息;
(4)查看类装载的信息 : -class
显示加载class的数量,及所占空间等信息。
jstat -class <pid>
案例:
jstat -class 3176 1000 10
- 3176: 为PID
- 1000:表示每个1s输出
- 10: 表示输出次数
结果参数说明:
- Loaded : 已经装载的类的数量
- Bytes : 装载类所占用的字节数
- Unloaded:已经卸载类的数量
- Bytes:卸载类的字节数
- Time:装载和卸载类所花费的时间
(5)查看垃圾回收信息:-gc
显示gc相关的堆信息,查看gc的次数,及时间。
jstat –gc <pid>
案例:
参数值说明:
- S0C:年轻代中第一个survivor(幸存区)的总容量 (字节)
- S1C:年轻代中第二个survivor(幸存区)的总容量 (字节)
- S0U :年轻代中第一个survivor(幸存区)目前已使用空间 (字节)
- S1U :年轻代中第二个survivor(幸存区)目前已使用空间 (字节)
- EC :年轻代中Eden(伊甸园)的总容量 (字节)
- EU :年轻代中Eden(伊甸园)目前已使用空间 (字节)
- OC :Old代的总容量 (字节)
- OU :Old代目前已使用空间 (字节)
- MC:metaspace(元空间)的总容量 (字节)
- MU:metaspace(元空间)目前已使用空间 (字节)
- YGC :从应用程序启动到采样时年轻代中gc次数
- YGCT :从应用程序启动到采样时年轻代中gc所用时间(s)
- FGC :从应用程序启动到采样时old代(全gc)gc次数
- FGCT :从应用程序启动到采样时old代(全gc)gc所用时间(s)
- GCT:从应用程序启动到采样时gc用的总时间(s)
(6)查看JIT编译信息 :-compiler
显示VM实时编译(JIT)的数量等信息。
jstat -compiler <pid>
案例:
参数说明:
- Compiled:编译任务执行数量
- Failed:编译任务执行失败数量
- Invalid :编译任务执行失效数量
- Time :编译任务消耗时间
- FailedType:最后一个编译失败任务的类型
- FailedMethod:最后一个编译失败任务所在的类及方法
4.jmap+MAT实战内存溢出
重点:如何定位内存溢出的位置?
4.1 JVM的内存结构
JVM 的内存分为两大块:堆区、非堆区。
堆区又分为两大块:Young、Old.
Young区分为两大块:Eden、S0+S1
- S0和S1大小一样,同一时间只使用其中一个,另一个是空的。
4.2 代码案例 -- 演示堆内存溢出
(1)创建SpringBoot工程,并定义接口 “/heap” ,访问接口无限写入数据,知道堆内存占满。
// 定义内存对象,在该对象循环存入User实例数据知道堆内存占满
private List<User> userList = new ArrayList<User>();
/**
* -Xmx32M -Xms32M
* */
@GetMapping("/heap")
public String heap() {
int i=0;
while(true) {
userList.add(new User(i++, UUID.randomUUID().toString()));
}
}
(2)关于User对象,提供 id、name 两个参数以及set/get方法即可。
(3)结果分析:
访问 localhost:8080/heap 路径后开始写入数据到堆中。
- 为提高效率看到堆内存溢出结果。这里将堆内存大小进行修改为 “-Xmx32M -Xms32M” 。
启动程序,并访问接口后结果如下:
如上图,当死循环写入的数据超过堆内存容量后,就发生堆内存异常的情况。
4.3 代码案例 -- 演示非堆内存溢出
(1)定义 Metaspace.java 类用于创建 class 类对象
package com.imooc.monitor_tuning.chapter2;//package com.imooc.monitor_tuning.chapter2;
import java.util.ArrayList;
import java.util.List;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
/*
* https://blog.csdn.net/bolg_hero/article/details/78189621
* 继承ClassLoader是为了方便调用defineClass方法,因为该方法的定义为protected
* */
public class Metaspace extends ClassLoader {
public static List<Class<?>> createClasses() {
// 类持有
List<Class<?>> classes = new ArrayList<Class<?>>();
// 循环1000w次生成1000w个不同的类。
for (int i = 0; i < 10000000; ++i) {
ClassWriter cw = new ClassWriter(0);
// 定义一个类名称为Class{i},它的访问域为public,父类为java.lang.Object,不实现任何接口
cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null,
"java/lang/Object", null);
// 定义构造函数<init>方法
MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>",
"()V", null, null);
// 第一个指令为加载this
mw.visitVarInsn(Opcodes.ALOAD, 0);
// 第二个指令为调用父类Object的构造函数
mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object",
"<init>", "()V");
// 第三条指令为return
mw.visitInsn(Opcodes.RETURN);
mw.visitMaxs(1, 1);
mw.visitEnd();
Metaspace test = new Metaspace();
byte[] code = cw.toByteArray();
// 定义类
Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length);
classes.add(exampleClass);
}
return classes;
}
}
上述代码中的类,主要用于动态生产并返回多个class类对象。
(2)存储class对象避免被回收
private List<Class<?>> classList = new ArrayList<Class<?>>();
/**
* -XX:MetaspaceSize=32M -XX:MaxMetaspaceSize=32M
* */
@GetMapping("/nonheap")
public String nonheap() {
while(true) {
classList.addAll(Metaspace.createClasses());
}
}
在上述代码的 nonheap() 接口中,由于将生产的class都存入到了 classList 中,会导致该 class 对象一直是可用的,同时其不断添加 class 对象进入,导致其在非堆区所占领的内存越来越大,知道溢出。
说明:每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法 的代码都在非堆内存中。
(3)测试结果并分析:
访问: http://localhost:8080/nonheap 路径触发非堆溢出
分析: 从上图可知,该接口调用后由于 class 不断动态生成并未被及时回收,导致存放class文件的非堆区内存被占满,最终溢出。
5.导出内存映像文件
5.1 文件的作用
用于在内存溢出的情况下,分析到底是哪些类一直占用内存不释放。
5.2 如何导出内存映像文件
- 内存溢出自动导出
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./
- 使用 jmap 命令手动导出文件
5.3 案例一:内存溢出自动导出
在 "VM options" 中添加 “ -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./ ” 配置,然后重新访问接口触发溢出:
导出来的映像文件:
5.4 案例二:使用 jmap 命令手动导出文件
(1)jmap功能
命令jmap是一个多功能的命令。它可以生成 java 程序的 dump 文件, 也可以查看堆内对象示例的统计信息、查看 ClassLoader 的信息以及 finalizer 队列。
(2)jmap 用法
参数:
- option: 选项参数。
- pid: 需要打印配置信息的进程ID。
- executable: 产生核心dump的Java可执行文件。
- core: 需要打印配置信息的核心文件。
- server-id 可选的唯一id,如果相同的远程主机上运行了多台调试服务器,用此选项参数标识服务器。
- remote server IP or hostname 远程调试服务器的IP地址或主机名。
option
- no option: 查看进程的内存映像信息,类似 Solaris pmap 命令。
- heap: 显示Java堆详细信息
- histo[:live]: 显示堆中对象的统计信息
- clstats:打印类加载器信息
- finalizerinfo: 显示在F-Queue队列等待Finalizer线程执行finalizer方法的对象
- dump:<dump-options>:生成堆转储快照,也就是导出映像文件。
- F: 当-dump没有响应时,使用-dump或者-histo参数. 在这个模式下,live子参数无效.
- help:打印帮助信息
- J<flag>:指定传递给运行jmap的JVM的参数
(3)导出映像文件 --- dump 命令
C:\Users\wushaopei\Desktop>jmap -dump:format=b,file=heap.hprof 20204
Dumping heap to C:\Users\wushaopei\Desktop\heap.hprof ...
Heap dump file created
说明:jmap -dump:format=b,file=heap.hprof 20204
- jmap 导出命令
- -dump 导出映像文件命令
- format=b 导出格式
- file=heap.hprof 导出到 heap.hprof 文件中
导出结果:
6. MAT分析内存溢出
(1)下载MAT工具 —— MemoryAnalyzer-1.11.0.20201202-win32.win32.x86_64
(2)解压MAT工具后,打开文件夹 mat/ 目录,双击 eclipsec.exe 启动工具
(3)导入映像文件,File - Open File - 选择本地已导出的映像文件。
(4)工具自动解析,并生成分析结果。如下:
上图中,除了灰色部分以外,其他的都是发生内存溢出的可能区域。
上述三个 Problem Suspect1、2、3分别为内存溢出区域的分析结果。
- Problem Suspect1 —— 应用加载器,也就是我们自己创建的类所占的引用内存
- Problem Suspect2 —— JVM 启动时class所占用的引用内存,这是正常的
- Problem Suspect3 —— JVM 启动时String所占用的引用内存,这是正常的
根据以上的分析结果,发生溢出的原因和区域应该是 Problem Suspect1。
(5)查看对象的数量
模糊查询过滤
(6)查看对象的引用
查看引用
(7)在上述的(5)、(6)中通过查看分析最终定位到内存溢出的对象、引用。并从而获取到溢出的原因。
7.jstack与线程的状态
7.1 jstack 功能
jstack一般用来查看指定线程(比如CPU较高、内存占用较高)的堆栈、查看死锁的原因。
打印对指定进程的堆栈信息:
jstack 进程号
7.2 用法与参数
/opt/java8/bin/jstack
Usage:
jstack [-l] <pid>
(to connect to running process) 连接活动线程
jstack -F [-m] [-l] <pid>
(to connect to a hung process) 连接阻塞线程
jstack [-m] [-l] <executable> <core>
(to connect to a core file) 连接dump的文件
jstack [-m] [-l] [server_id@]<remote server IP or hostname>
(to connect to a remote debug server) 连接远程服务器
Options:
-F to force a thread dump. Use when jstack <pid> does not respond (process is hung)
-m to print both java and native frames (mixed mode)
-l long listing. Prints additional information about locks
-h or -help to print this help message
7.3 案例-jstack查看输出
/opt/java8/bin/jstack -l 28367
2021-04-28 15:04:46
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.77-b03 mixed mode):
"Attach Listener" #453 daemon prio=9 os_prio=0 tid=0x00007f9f94001000 nid=0xf30 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"grpc-default-executor-263" #452 daemon prio=5 os_prio=0 tid=0x00007f9f4c01f800 nid=0x9aa waiting on condition [0x00007f9f398bd000]
java.lang.Thread.State: TIMED_WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000007400243f0> (a java.util.concurrent.SynchronousQueue$TransferStack)
at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
at java.util.concurrent.SynchronousQueue$TransferStack.awaitFulfill(SynchronousQueue.java:460)
at java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:362)
at java.util.concurrent.SynchronousQueue.poll(SynchronousQueue.java:941)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1066)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
Locked ownable synchronizers:
- None
"http-bio-8080-exec-10" #235 daemon prio=5 os_prio=0 tid=0x0000000001bcc800 nid=0x3c13 waiting on condition [0x00007f9f384a9000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x0000000743d26638> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:104)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:32)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
Locked ownable synchronizers:
- None
堆栈信息说明:
- http-apr-xxxx-exec-** : 工作线程,没有请求进入时为 WAITING 状态
- C1 CompilerThread2 : JIT编译使用的线程
- JDWP ** : 调试对象用的
- main : main 主函数线程
- G1 Concurrent Refinement Thread#*: GC 的线程
- java.lang.Thread.State : 当前线程的状态
8.jstack实战死循环与死锁
8.1 死循环导致的CPU飙升
该案例引用自: 若鱼1919 - 多么痛的领悟-代码优化导致的BUG
(1)案例代码:
public static List<Long> getPartneridsFromJson(String data){
//{\"data\":[{\"partnerid\":982,\"count\":\"10000\",\"cityid\":\"11\"},{\"partnerid\":983,\"count\":\"10000\",\"cityid\":\"11\"},{\"partnerid\":984,\"count\":\"10000\",\"cityid\":\"11\"}]}
//上面是正常的数据
List<Long> list = new ArrayList<Long>(2);
if(data == null || data.length() <= 0){
return list;
}
int datapos = data.indexOf("data");
if(datapos < 0){
return list;
}
int leftBracket = data.indexOf("[",datapos);
int rightBracket= data.indexOf("]",datapos);
if(leftBracket < 0 || rightBracket < 0){
return list;
}
String partners = data.substring(leftBracket+1,rightBracket);
if(partners == null || partners.length() <= 0){
return list;
}
while(partners!=null && partners.length() > 0){
int idpos = partners.indexOf("partnerid");
if(idpos < 0){
break;
}
int colonpos = partners.indexOf(":",idpos);
int commapos = partners.indexOf(",",idpos);
if(colonpos < 0 || commapos < 0){
//partners = partners.substring(idpos+"partnerid".length());//1
continue;
}
String pid = partners.substring(colonpos+1,commapos);
if(pid == null || pid.length() <= 0){
//partners = partners.substring(idpos+"partnerid".length());//2
continue;
}
try{
list.add(Long.parseLong(pid));
}catch(Exception e){
//do nothing
}
partners = partners.substring(commapos);
}
return list;
}
(2)打包部署到服务器
打包:
mvn clean package -Dmaven.test.skip #打包
上传至服务器,后台启动 jar包:
nohup java -jar monitor_tuning-0.0.1-SNAPSHOT.jar &
(3)访问接口,触发已经配置好的脏数据写入逻辑层造成死循环
/**
* 死循环
* */
@RequestMapping("/loop")
public List<Long> loop(){
String data = "{\"data\":[{\"partnerid\":]";
return getPartneridsFromJson(data);
}
访问地址: localhost:12345/loop
(4)top 命令查看 CPU 负载
上图中划线部分为当前服务器的CPU负载值。
(5)top -p 7930 -H 打印按CPU占用率排序
(6)导出死循环导致的堆栈信息文件,并分析:
这里只列出其中一个执行 getPartneridsFromJson(CpuController.java:71) 任务的线程,在映像文件中一共发现了五个这样的线程,这正好对应了前面 (5)中CPU占用率最高的五个线程。
定位问题:结合 (5)(6)中的分析,我们可以得出结论,造成当前服务器CPU飙升的原因是因为 getPartneridsFromJson(CpuController.java:71) 方法。
8.2 死锁导致的CPU飙升
(1)案例代码
private Object lock1 = new Object();
private Object lock2 = new Object();
/**
* 死锁
* */
@RequestMapping("/deadlock")
public String deadlock(){
new Thread(()->{
synchronized(lock1) {
try {Thread.sleep(1000);}catch(Exception e) {}
synchronized(lock2) {
System.out.println("Thread1 over");
}
}
}) .start();
new Thread(()->{
synchronized(lock2) {
try {Thread.sleep(1000);}catch(Exception e) {}
synchronized(lock1) {
System.out.println("Thread2 over");
}
}
}) .start();
return "deadlock";
}
上述方法为死锁程序,lock1 、lock2竞争对方的锁。求而不得,最终将导致线程卡死、CPU飙升。
(2)部署至服务器后启动jar包
流程参照死循环的打包和启动步骤。
(3)访问接口,触发死锁
访问路径:localhost:12345/deadlock
(4)访问结果
访问接口,触发死锁的时候,直接就返回了最后一行的 “deadlock” 结果。但这不代表着死锁失败了。而是因为造成死锁所在主线程执行比较快。在主线程内部的两个子线程依旧是陷入在死锁的情境中。
(5)根据进程id(PID)用 jstack 命令导出堆栈信息文件
jstack 23674 > 23674.txt
(6)定位问题——死锁
查看堆栈文件,根据 deadlock() 最后会返回的 “deadlock” 可以定位到我们定义的死锁的位置。
从上图中,我们可以知道,在主线程 “deadlock” 之前就是死锁的线程问题所在,经过分析:
- Thread-5\Thread-6都处于 WAITING 状态
- Thread-5\Thread-6要解锁需要获取对方的 locked
- 导致这两个线程死锁的类是 CpuController.java,分别对应下图中 lock1\lock2.