前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用 JDK 自带工具进行 JVM 诊断调优实战

使用 JDK 自带工具进行 JVM 诊断调优实战

作者头像
用户3587585
发布2023-12-18 15:56:27
3130
发布2023-12-18 15:56:27
举报
文章被收录于专栏:阿福谈Web编程阿福谈Web编程

前言

最近参加面试多次被面试官问到JVM调 优方面的问题,即时自己面试前也重点复习了这一块的面试题,但是发现还是回答地不太好,浪费了好多次面试机会,真是让自己很抓狂。归根结底是自己以前一直只注重业务,而忽略了JVM调优这一块,对JVM这一块的实践太少了。这几天自己也重点观看了马士兵老师的JVM调优视频课, 看完之后自己也在本机和腾讯云服务器上进行了一番实践,感觉还是很有收获的。

1 使用 JDK 自带诊断工具命令

我们知道JDK安装目录的bin目录下有一系列的命令可以用来诊断和分析服务器CPU和内存占用过高的问题。JDK 安装目录的bin目录下主要有一下帮助我们进行JVM调优的命令和工具

常用JDK性能分析命令

  • jps: 进程监控命令
  • jstat:性能监控命令
  • jinfo: 参数配置监控命令
  • jstack:线程监控命令
  • jmap: 内存监控命令

JDK自带的几款在线监控工具(jps、jstat、jstack、jmap),用户实时监控JVM运行装填

名称

主要作用

jps

JVM Process Status Tool, 显示指定系统内所有 Hotspot 虚拟机进程

jstat

JVM Statics Monitoring Tool, 用于手机 Hotspot 各方面的运行数据

jinfo

Configuration Info for Java,显示虚拟机配置信息

jstack

Stack Trace for Java,显示虚拟机的线程快照

jmap

Memory Map for Java,生成虚拟机的内存转储快照(heapdump 文件)

jhat

JVM Heap Analysis Tool,用于分析dump 文件(它会建立一个Http/HTML服务器,让用户可以在游览器上查看分析结果)

其中 jhat命令从JDK9开始在JDK 的bin目录已经被移除,笔者下面就不对jhat命令做过多介绍了。

2 jps 命令

使用场景 :查看当前机器的所有Java进程信息(可追踪到应用进程ID 、启动类名、文件路径。)

指令格式 : jps 【options 】 [hostid]

[hostid] 远程地址,可选参数,指定特定主机的IP或者域名,也可以指定具体协议端口,不指定则查看当前机器的相关信息,hostid所指机器必须开启jstatd服务

选项

作用

-q

只输出进程id

-m

输出虚拟机启动时传递给主类main函数的参数

-l

输出主类的全名,如果进程执行的是jar包,则输出jar包路径

-v

输出虚拟机进程启动时的JVM参数

在IDEA 中启动一个Java项目,然后点击下图所示的Terminal图标按钮打开一个命令终端

1)我们下面所有执行 jvm 有关的命令都以这种方式进行,输入jsp 命令可以看到控制台中输出进程相关信息:

代码语言:javascript
复制
PS D:\giteeProjects\blogserver> jps
22464 
21412 BlogserverApplication
21400 Jps
6280 Launcher

左边的数字代表进程ID, 右边的字符串表示进程main函数的类

2)输入jps -l

代码语言:javascript
复制
PS D:\giteeProjects\blogserver> jps -l
22464 
29232 sun.tools.jps.Jps
21412 org.sang.BlogserverApplication
6280 org.jetbrains.jps.cmdline.Launcher

左边的数字代表进程ID, 右边的字符串表示进程main函数的类的全类名

3)输入 jps -v

代码语言:javascript
复制
PS D:\giteeProjects\blogserver> jps -v
22464  exit -XX:ErrorFile=C:\Users\xeawp\\java_error_in_idea64_%p.log -XX:HeapDumpPath=C:\Users\xeawp\\java_error_in_idea64.hprof -Xms128m -Xmx1024m -XX:ReservedCodeCac
heSize=512m -XX:+IgnoreUnrecognizedVMOptions -XX:+UseG1GC -XX:SoftRefLRUPolicyMSPerMB=50 -XX:CICompilerCount=2 -XX:+HeapDumpOnOutOfMemoryError -XX:-OmitStackTraceInFast
Throw -ea -Dsun.io.useCanonCaches=false -Djdk.http.auth.tunneling.disabledSchemes="" -Djdk.attach.allowAttachSelf=true -Djdk.module.illegalAccess.silent=true -Dkotlinx.
coroutines.debug=off -XX:ErrorFile=$USER_HOME/java_error_in_idea_%p.log -XX:HeapDumpPath=$USER_HOME/java_error_in_idea.hprof --add-opens=java.base/jdk.internal.org.obje
ctweb.asm=ALL-UNNAMED --add-opens=java.base/jdk.internal.org.objectweb.asm.tree=ALL-UNNAMED -javaagent:D:\jetbrain_tools\jetbra\ja-netfilter.jar=jetbrains -Djb.vmOption
sFile=D:\jetbrain_tools\jetbra\vmoptions\idea.vmoptions -Djava.system.class.loader=com.intellij.util.lang.PathClassLoader -Didea.vendor.name=JetBrains -Didea.paths.selector=IntelliJIdea2023.2
13268 Jps -Dapplication.home=C:\Program Files\Java\jdk1.8.0_191 -Xms8m
21412 BlogserverApplication -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:55645,suspend=y,server=n -Xms50m -Xmx50m -XX:TieredStopAtLevel=1 -Xverify:none -Dspring.profiles.active=dev -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dsp
ring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -Dmanagement.endpoints.jmx.exposure.include=* -javaagent:C:\Users\xeawp\AppData\Local\JetBrains\IntelliJIdea2023.2\captureAgent\debugger-agent.jar -Dfile.encoding=UTF-8
6280 Launcher -Xmx700m -Djava.awt.headless=true -Djna.boot.library.path=D:\Program Files\JetBrains\IntelliJ IDEA 2023.2.2/lib/jna/amd64 -Djna.nosys=true -Djna.noclasspa
th=true --add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tool
s.javac.code=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-opens=jdk.com
piler/com.sun.tools.javac.main=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAME
D --add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-opens=jdk.compiler/com.sun.too
ls.javac.jvm=ALL-UNNAMED -Dpreload.project.path=D:/giteeProjects/blogserver -Dpreload.config.path=C:/Users/xeawp/AppData/Roaming/JetBrains/IntelliJIdea2023.2/options -Dexternal.project.config=C:\Use

每行左边的数字代表进程ID,右边的文本表示该进程启动时的JVM参数, 以-XX:开头的参数都代表JVM启动参数

3 jstat 命令

jstat命令可以查看Java程序运行时相关信息,可以通过它查看堆信息的相关情况

jstat命令的格式:jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]

options 参数表示可选项; vmid 参数表示进程ID;interval 参数表示统计间隔时间,单位为秒或者毫秒,默认为毫秒;count 参数表示统计次数。

options 参数由下列可选值构成

选项

作用

-class

显示类加载、卸载数量、总空间及类装载所耗费的时间

-gc

监视 java 堆状况,包括 Eden区、Survivor区、老年代、永久代等的容量、已使用空间、垃圾收集时间等信息

-gccapacity

监视内容基本与-gc 相同,但输出主要关注 Java 堆各个区域使用到的最大、最小空间

-gcutil

监视内容基本与-gc 相同,但输出主要关注已使用空间占总空间的百分比

-gccause

与 -gcutil 功能一样,但会额外输出导致上一次垃圾收集产生的原因

-gcnew

监视新生代垃圾收集状况

-gcnewcapacity

监视内容与 -gcnew 基本相同,输出主要关注使用到的最大、最小空间

-gcold

监视老年队垃圾收集状况

-gcoldcapacity

监视内容与 -gcold 基本相同,输出主要关注使用到的最大、最小空间

-gcpermcapacity

输出永久代使用的最大、最小空间

-complier

输出即时编译器编译过的方法、耗时等

-printcompilation

输出已经被即时编译的方法

1)获取21412进程的 GC 统计信息:

代码语言:javascript
复制
PS D:\giteeProjects\blogserver> jstat -gc 21412
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
3072.0 3072.0 766.6   0.0   10752.0   4459.4   34304.0    27245.3   53160.0 49412.8 7336.0 6683.0     68    0.100   2      0.085    0.185

参数说明:

  • S0C: S0区总内存大小(kb);
  • S1C:S1区总内存大小(kb);
  • S0U:S0区已使用内存(kb);
  • S1U: S1区已使用内存(kb);
  • EC: Eden 区总内存大小(kb);
  • EU:Eden 区已使用内存大小(kb);
  • OC: 老年代总内存大小(kb);
  • OU:老年代已使用内存大小(kb);
  • MC:元空间总内存大小(kb);
  • MU:元空间已使用内存大小(kb);
  • CCSC:压缩类空间总内存大小(kb);
  • CCSU:压缩类空间已使用内存大小(kb);
  • YGCT:Yong GC 总耗时(单位秒);
  • YGC:Yong GC 总次数;
  • FGC:Full Gc的总次数;
  • FGCT:Full GC 总耗时(单位秒);
  • GCT :Full GC 总次数

2)每间隔一段时间获取固定次数的GC统计信息:

代码语言:javascript
复制
PS D:\giteeProjects\blogserver> jstat -gc 21412 1000 5
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
3072.0 3072.0 766.6   0.0   10752.0   5676.0   34304.0    27245.3   53160.0 49412.8 7336.0 6683.0     68    0.100   2      0.085    0.185
3072.0 3072.0 766.6   0.0   10752.0   5676.0   34304.0    27245.3   53160.0 49412.8 7336.0 6683.0     68    0.100   2      0.085    0.185
3072.0 3072.0 766.6   0.0   10752.0   5676.0   34304.0    27245.3   53160.0 49412.8 7336.0 6683.0     68    0.100   2      0.085    0.185
3072.0 3072.0 766.6   0.0   10752.0   5676.0   34304.0    27245.3   53160.0 49412.8 7336.0 6683.0     68    0.100   2      0.085    0.185
3072.0 3072.0 766.6   0.0   10752.0   5676.0   34304.0    27245.3   53160.0 49412.8 7336.0 6683.0     68    0.100   2      0.085    0.185

这里由于笔者本地的21412进程服务没有使用,所以每次垃圾回收后各个区的内存使用没有发生什么变化,而一个线上不断有用户使用的服务进程通过这个命令是可以看到明显的拉进回收堆内存变化的。

3)获取21412 进程垃圾回收后各个区已使用内存占其对应区的总内存大小百分比数据:

代码语言:javascript
复制
PS D:\giteeProjects\blogserver> jstat -gcutil 21412
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   
 24.95   0.00  55.81  79.42  92.95  91.10     68    0.100     2    0.085    0.185

这里的S0、S1、E、O、M等参数对应的值为各个区对应的已使用内存大小占它们各自总内存大小的百分比。

4 jinfo 命令

这个命令主要用来查看和修改JVM参数

jinfo命令格式:jinfo [option]

option 为选项参数;pid 代表进程id

option 有以下这些选项参数

  • -flag : 打印 指定名称的 jvm 参数值;
  • -flag [+|-] : 启动或禁用指定名称的 jvm参数;
  • -flag =: 设置指定名称的 jvm 参数值;
  • -sysprops: 打印 java 系统属性
  • -h | -help: 打印 jinfo 命令帮助信息

1)获取16462 进程 jvm 的全部参数和系统属性:

代码语言:javascript
复制
PS D:\giteeProjects\blogserver> jinfo 21412
Attaching to process ID 21412, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.191-b12
Java System Properties:

spring.output.ansi.enabled = always
java.runtime.name = Java(TM) SE Runtime Environment
java.vm.version = 25.191-b12
sun.boot.library.path = C:\Program Files\Java\jdk1.8.0_191\jre\bin
java.vendor.url = http://java.oracle.com/
java.vm.vendor = Oracle Corporation
path.separator = ;
java.rmi.server.randomIDs = true
file.encoding.pkg = sun.io
java.vm.name = Java HotSpot(TM) 64-Bit Server VM
sun.os.patch.level =
sun.java.launcher = SUN_STANDARD
user.script =
user.country = CN
management.endpoints.jmx.exposure.include = *
user.dir = D:\giteeProjects\blogserver
java.vm.specification.name = Java Virtual Machine Specification
PID = 21412
intellij.debug.agent = true
java.runtime.version = 1.8.0_191-b12
java.awt.graphicsenv = sun.awt.Win32GraphicsEnvironment
os.arch = amd64
java.endorsed.dirs = C:\Program Files\Java\jdk1.8.0_191\jre\lib\endorsed
spring.profiles.active = dev
line.separator =java.io.tmpdir = C:\Users\xeawp\AppData\Local\Temp\java.vm.specification.vendor = Oracle Corporation
user.variant =
os.name = Windows 10
sun.jnu.encoding = GBK

VM Flags:
Non-default VM flags: -XX:-BytecodeVerificationLocal -XX:-BytecodeVerificationRemote -XX:CICompilerCount=12 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=null -XX:InitialHeapSize=52428800 -XX:+ManagementServer -XX:MaxHeapSize=52428800 -XX:MaxNewSize=17301504 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=17301504 -XX:OldSize=35127296 -XX:TieredStopAtLevel=1 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC   
Command line:  -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:55645,suspend=y,server=n -Xms50m -Xmx50m -XX:TieredStopAtLevel=1 -Xverify:none -Dspring.profiles.active=dev -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -Dmanagement.endpoints.jmx.exposure.include=* -javaagent:C:\Users\xeawp\AppData\Local\JetBrains\IntelliJIdea2023.2\captureAgent\debugger-agent.jar -Dfile.encoding=UTF-8

2)获取新生代大小 JVM 参数值

代码语言:javascript
复制
PS D:\giteeProjects\blogserver> jinfo -flag NewSize 21412
-XX:NewSize=17301504 #单位为byte

3)开启GC打印日志

代码语言:javascript
复制
PS D:\giteeProjects\blogserver> jinfo -flag +PrintGCDetails 21412

4)开启堆内存溢出日志打印(默认是关闭的)

代码语言:javascript
复制
PS D:\giteeProjects\blogserver> jinfo -flag +HeapDumpOnOutOfMemoryError 21412

5)设置堆内存溢出时的堆转储文件路径

代码语言:javascript
复制
PS D:\giteeProjects\blogserver> jinfo -flag :HeapDumpPath=D:\heapdump\blogDump.hprof

6)修改当堆内存对象所占空间超过80%时进行扩容

代码语言:javascript
复制
PS D:\giteeProjects\blogserver> jinfo -flag MaxHeapFreeRatio=80 21412

7)最后我们再来使用jinfo -flags PID 命令来查看21412进程的JVM参数信息

代码语言:javascript
复制
PS D:\giteeProjects\blogserver> jinfo -flags 21412
Attaching to process ID 21412, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.191-b12
Non-default VM flags: -XX:-BytecodeVerificationLocal -XX:-BytecodeVerificationRemote -XX:CICompilerCount=12 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=null -XX:In
itialHeapSize=52428800 -XX:+ManagementServer -XX:MaxHeapFreeRatio=80 -XX:MaxHeapSize=52428800 -XX:MaxNewSize=17301504 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=17301504 
-XX:OldSize=35127296 -XX:+PrintGCDetails -XX:TieredStopAtLevel=1 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
Command line:  -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:55645,suspend=y,server=n -Xms50m -Xmx50m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\heapdum
p\blogDump.hprof -XX:TieredStopAtLevel=1 -Xverify:none -Dspring.profiles.active=dev -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enab
led=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -Dmanagement.endpoints.jmx.exposure.include=* -javaagent:C:\Users\xeawp\AppData\Local\JetBrains\IntelliJIdea2023.2\captureAgent\debugger-agent.jar -Dfile.encoding=UTF-8

可以看到明显多了-XX:MaxHeapFreeRatio=80-XX:+PrintGCDetails-XX:HeapDumpPath=D:\heapdum p\blogDump.hprofJVM启动参数值。

5 jstack 命令

使用场景:查看JVM线程信息和生成线程快照

jstack 命令格式:jstack [ 选项 ] [进程ID] 远程IP

选项

作用

-F

当正常输出的请求不被响应时,强制输出线程堆栈

-l

除堆栈外,显示关于锁的附加信息

-m

如果调用了本地方法的话,可以显示C/C++的堆栈

1)在21412进程的项目源码中写一个线程死锁的接口

代码语言:javascript
复制
@RestController
@RequestMapping("/jvm")
public class JVMTestController {
    Object lock1 = new Object();
        Object lock2 = new Object();
        Thread thread1 = new Thread(() -> {
           while (true){
               synchronized (lock1){
                   System.out.println("thread1 get lock1");
                   synchronized (lock2){
                       System.out.println("thread1 get lock2");
                   }
               }
           }

        }, "thread1");

        Thread thread2 = new Thread(()->{
            while (true){
                synchronized (lock2){
                    System.out.println("thread2 get lock2");
                    synchronized (lock1){
                        System.out.println("thread2 get lock1");
                    }
                }
            }

        }, "thread2");
        thread1.start();
        thread2.start();
        RespBean<String> respBean = new RespBean<>();
        respBean.setStatus(200);
        respBean.setMsg("success");
        respBean.setData("启动了两个线程");
        return respBean;
}

1)然后重启服务(服务重启后服务进程ID会发生改变)并在ApiPost中先调用登录接口获取token认证信息(笔者这里是在服务启动之前就已经有这段代码了,所以进程ID没有发生变化)

3)在ApiPost中新建一个测试死锁的http接口,并将登录接口返回的authToken字段值放到测试死锁接口的Authorization请求头参数中

在日志控制台中可以看到 thread1thread2都只拿到了一把锁

代码语言:javascript
复制
thread1 get lock1
thread2 get lock2

4)在Terminal 控制台中输入 jstack -l 21412 可以看到 thread1thread2 两个线程都出现了阻塞

代码语言:javascript
复制
PS D:\giteeProjects\blogserver> jstack -l 21412
2023-12-13 13:21:13
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.191-b12 mixed mode):

"thread2" #64 daemon prio=5 os_prio=0 tid=0x000000001c708000 nid=0x7e94 waiting for monitor entry [0x000000001aa8f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at org.sang.controller.JVMTestController.lambda$deadLockTest$1(JVMTestController.java:61)
        - waiting to lock <0x00000000ff16b240> (a java.lang.Object)
        - locked <0x00000000ff16b250> (a java.lang.Object)
        at org.sang.controller.JVMTestController$$Lambda$1155/562657671.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
        - None

"thread1" #63 daemon prio=5 os_prio=0 tid=0x000000001c705000 nid=0x5968 waiting for monitor entry [0x000000000173f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at org.sang.controller.JVMTestController.lambda$deadLockTest$0(JVMTestController.java:49)
        - waiting to lock <0x00000000ff16b250> (a java.lang.Object)
        - locked <0x00000000ff16b240> (a java.lang.Object)
        at org.sang.controller.JVMTestController$$Lambda$1154/754784134.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
        - None

"DestroyJavaVM" #57 prio=5 os_prio=0 tid=0x000000001c709800 nid=0xf80 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
        - None

5)在Terminal 控制台中输入jstack -F 21412 回车后可以看到具体的线程死锁信息

代码语言:javascript
复制
PS D:\giteeProjects\blogserver> jstack -F 21412
Attaching to process ID 21412, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.191-b12
Deadlock Detection:

Found one Java-level deadlock:
=============================

"thread1":
  waiting to lock Monitor@0x00000000197efc68 (Object@0x00000000ff16b250, a java/lang/Object),
  which is held by "thread2"
"thread2":
  waiting to lock Monitor@0x00000000197ed538 (Object@0x00000000ff16b240, a java/lang/Object),
  which is held by "thread1"

Found a total of 1 deadlock.

可以看到thread1线程等待获取thread2持有的锁,而thread2线程也在等待获取thread1持有的锁,但是它们彼此都没有释放自己持有的锁 ,于是造成了死锁。

6 jmap 命令

jmap 命令是JVM内存信息监控和 Java 内存映像工具

使用场景:监控堆内存使用情况和对象占用情况, 生成堆内存快照文件,查看堆内存区域配置信息。

jmp 命令格式:jmap [option]

option选项值如下:

选项

作用

-dump

生成java堆转储快照,格式为dump:[live,]format=b,file=,其中live子参数指定是否只dump出存活对象。

-finalizerinfo

显示在F-Queue中等待Finerlizer线程执行 finalize方法的对象,只在linux/Solaries平台下有效

-heap

显示java堆详细信息,包括使用哪种垃圾收集器、参数配置、分代状况等,只在linux/Solaries平台下有效

-permstat

以ClassLoader 为统计口径显示永久代内存状态,只在linux/Solaries平台下有效

-histo

显示堆中对象统计信息,包括类、实例数量、合计容量

-F

当虚拟机进程对 -dump 选项没有响应时,可使用这个选项强制生成dump快照,只在linux/Solaries平台下有效

1)在终端命令控制台中输入jmap -heap 21412 查看堆详细信息

代码语言:javascript
复制
PS D:\giteeProjects\blogserver> jmap -heap 21412
Attaching to process ID 21412, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.191-b12

using thread-local object allocation.
Parallel GC with 13 thread(s) #使用并行垃圾收集器收集垃圾,启动了13个垃圾收集线程

Heap Configuration:
   MinHeapFreeRatio         = 0   #JVM堆缩减空间比率,低于则进行内存缩减
   MaxHeapFreeRatio         = 80  #JVM堆扩大内存空闲比例,高于则进行内存扩张
   MaxHeapSize              = 52428800 (50.0MB) #堆最大内存大小
   NewSize                  = 17301504 (16.5MB) #新生代初始化内存大小
   MaxNewSize               = 17301504 (16.5MB) #新生代最大内存大小
   OldSize                  = 35127296 (33.5MB) #老年代内存大小
   NewRatio                 = 2 #新生代和老年代占堆内存比率
   SurvivorRatio            = 8 #survivor区和Eden区占新生代内存比率
   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 = 7864320 (7.5MB)  #Eden区总容量
   used     = 1097912 (1.0470504760742188MB) #Eden区已使用容量
   free     = 6766408 (6.452949523925781MB) #Eden区剩余容量
   13.960673014322916% used #Eden区使用比例
From Space: #From区(也就是Survivor中的S1区)
   capacity = 4718592 (4.5MB)  #S1区总容量大小
   used     = 1014520 (0.9675216674804688MB) #S1区已使用大小
   free     = 3704072 (3.5324783325195312MB) #S1区剩余大小
   21.50048149956597% used #S1使用比例
To Space: #To区 (也就是Survivor中的S2区)
   capacity = 4718592 (4.5MB) #S2区总容量大小
   used     = 0 (0.0MB) #S2区已使用大小
   free     = 4718592 (4.5MB) #S2区剩余大小
   0.0% used #/S2区使用比率
PS Old Generation #老年代
   capacity = 35127296 (33.5MB) #老年代总容量大小
   used     = 22521464 (21.47814178466797MB) #老年代已使用大小
   free     = 12605832 (12.021858215332031MB) #老年代剩余大小
   64.11385607363573% used #老年代使用功能比例

2)在终端命令控制台中输入jmap -histo <Pid> 获取堆中对象统计信息

代码语言:javascript
复制
PS D:\giteeProjects\blogserver> jmap -histo 21412

 num     #instances         #bytes  class name
----------------------------------------------
   1:         68887        7308488  [C
   2:          9412        2218296  [B
   3:         17735        1560680  java.lang.reflect.Method
   4:         62646        1503504  java.lang.String
   5:         11883        1313912  java.lang.Class
   6:         35207        1126624  java.util.concurrent.ConcurrentHashMap$Node
   7:         15513         817600  [Ljava.lang.Object;
   8:          5887         671776  [I
   9:         16506         660240  java.util.LinkedHashMap$Entry
  10:          7365         601184  [Ljava.util.HashMap$Node;
  11:         15330         490560  java.util.HashMap$Node
  12:          7616         426496  java.util.LinkedHashMap
  13:           223         391552  [Ljava.util.concurrent.ConcurrentHashMap$Node;
  14:          8620         275840  java.util.concurrent.locks.AbstractQueuedSynchronizer$Node
  15:         12060         273648  [Ljava.lang.Class;
  16:         16972         271552  java.lang.Object
  17:          7105         227360  java.util.ArrayList$Itr
  18:          4525         217200  java.util.HashMap
  19:          8529         204696  java.util.ArrayList
  20:          7490         179760  org.springframework.core.MethodClassKey
 # 其他省略

num: 序号,数字越小占用内存越大;#instances: 类实例数;class name:全类名

以上为21412进程中最占用堆内存的前20个类。

3)在终端命令控制台中输入jmap -dump:live,format=b,file=<filename> <PID>

输出堆转储文件

例如笔者在自己的云服务器上对nacos服务进程4461堆转储操作

代码语言:javascript
复制
[root@VM-8-16-centos local]# jmap -dump:live,format=b,file=nacosDump.hprof 4461
Heap dump file created

然后把这个堆转储文件从云服务器上下载到本地磁盘

选中文件后鼠标右键->下载(默认的下载目录为:C:\Users${username}\Desktop\fsdownload)

7 jvisualvm 工具

jvisualvm 是JDK自带的具有图形界面操作功能的JVM性能监控和诊断工具,它不仅能分析和诊断堆转储文件,在线实时监控本地JVM进程,还能监控远程服务器上的JVM进程。

7.1 分析服务器下载dump文件

1)在我们在安装JDK的bin目录双击jvisaulvm.exe程序,程序启动成功后会进入Java VisualVM界面

2)依次点击 文件-> 装入-> 选择文件名和文件类型,文件类型选择【堆Dump】导入我们从服务器上下载的dump文件

3)然后点击【打开】后 Java VisualVM UI 的右边主界面会新增一个Tab页显示导入的dump文件的概要信息

4)点击下面的【显示线程】链接可以看到线程信息

5)点击【概要】右边的【类】可以看到主要占内存的类名、实例数以及占内存大小

可以看到这个堆中占内存前三位的分别是byte[]StringConcurrentHashMap$Node三个类

注意:生产环境一般不能使用jmap -dump 命令生成堆转储文件,因为执行这个命令的时候会导致STW(应用主线程停顿),影响应用的正常使用。而一般通过设置两个jvm启动参数:-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=<filename> 在堆内存溢出时实现自动生成堆转储文件。然后我们从服务器上下载堆转储文件后通过Java VisualVM或者Eclipse Memory Analyzer内存分析工具导入我们下载的dump文件进行分析诊断。

7.2 分析内存溢出dump文件

首先需要我们设置JVM启动参数测试堆内存溢出时自动导出堆转储文件

1)在我们的本地启动的Java服务中通过IDEA设置启动类参数

四个JVM启动参数分别如下:

代码语言:javascript
复制
-Xms50m
-Xmx50m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=D:\heapdump\blogDump.hprof

然后点击下面的【ok】使之生效

2)在JVMTestController类中新增一个能导致内存溢出的接口

代码语言:javascript
复制
@GetMapping("/testOutMemoryError")
    public RespBean<List<UserDTO>> outMemoryErrorTest(){
        List<UserDTO> userDTOList = new ArrayList<>();
        for(int i=0;i<1000000;i++){
            UserDTO userDTO = new UserDTO();
            userDTO.setId(i+1);
            userDTO.setUsername("username"+i);
            userDTO.setNickname("张三"+i);
            userDTO.setPhoneNum(18975623568L);
            userDTO.setEmail("ZhangSan"+i+"@163.com");
            userDTO.setEnabled(1);
            userDTO.setRegTime(new Timestamp(System.currentTimeMillis()));
            userDTO.setUpdateTime(new Timestamp(System.currentTimeMillis()));
            userDTOList.add(userDTO);
        }
        RespBean<List<UserDTO>> respBean = new RespBean<>();
        respBean.setStatus(200);
        respBean.setMsg("success");
        respBean.setData(userDTOList);
        return respBean;
    }

3)重启服务后在ApiPost中新建一个http接口,并在请求头中带上用户登录成功后返回的authToken认证信息

点击【发送按钮】一段时间后会返回内存溢出异常信息,同时在D:\heapdump目录下生成了堆转储文件blogDump.hprof

4)我们同样在 Java VisualVM中导入这个dump文件可以看到它的概要信息和类实例信息

内存溢出堆转储概要信息

内存溢出堆转储类实例信息

从内存溢出堆转储实例信息中我们可以看到UserDTO类产生了47899个实例对象占用了4406708字节的堆内存。

7.3 本地JVM监控

本地监控很简单,打开VisualVM 就可以从左侧栏目里看到本机的应用,点击对应的应用就可以看到对应的内存、线程、GC信息

如果应用出现线程死锁也能通过【线程】tab页看到

点击右上方的【线程 Dump】可以看到线程死锁的具体原因

7.5 远程JVM监控

VisualVM不仅能监控本地的应用程序,还可以监控远程服务器上的应用,虽然远程监控一般不会用于生产环境的,但是在测试环境做一些压力测试做一些性能的预调优,这个时候使用VisualVM来远程监控测试服务器的JVM使用情况,这样有助于我们了解到JVM的实时运行状态而进行优化和调整。

1)应用配置jmx支持

需要使用VisualVM监控某个远程服务器的JVM应用,那么首先要对需要配置远程监控应用对MX的支持

配置方式:

  • jar包启动:直接 java -jar <jmx相关参数>;
  • tomcat启动:编辑tomcat的catalina.sh配置文件增加JAVA_OPTS配置

jar 包启动配置案例

代码语言:javascript
复制
 nohup java -Dcom.sun.management.jmxremote 
 -Dcom.sun.management.jmxremote.port=8999 
 -Dcom.sun.management.jmxremote.local.only=true 
 -Dcom.sun.management.jmxremote.authenticate=true 
 -Dcom.sun.management.jmxremote.ssl=false 
 -Dcom.sun.management.jmxremote.rmi.port=8999 
 -Dcom.sun.management.jmxremote.access.file=jmx.access 
 -Dcom.sun.management.jmxremote.password.file=jmx.password 
 -Djava.rmi.server.hostname=159.138.47.89 
  -jar jstx-server.jar&

需要注意的参数:

  • Dcom.sun.management.jmxremote.port:指定jms通讯端口,这个随意只要不与其他应用冲突即可
  • Djava.rmi.server.hostname:连接IP,填写当前服务器的外网IP
  • Dcom.sun.management.jmxremote.ssl:是否位加密连接
  • Dcom.sun.management.jmxremote.authenticate:是否进行权限连接认证,flase 不需要,ture的话就需要指定用户名,密码配置。
  • management.jmxremote.access.file: 用户名 和对应用户的权限配置
  • Dcom.sun.management.jmxremote.password.file:用户名对应的密码
  • 创建用户名密码
  • 如果Dcom.sun.management.jmxremote.authenticate 配置为true则需要进行此步骤

编辑jmx.access文件添加用户名 并指定权限

执行命令:vim jmx.access

添加内容:admin readwrite

jmx.password文件里指定用户名和对应密码

执行命令:vim jmx.access

添加内容:admin 123456

修改password,access文件访问权限:

代码语言:javascript
复制
 Chmod –R 600 jmx.password
 Chmod –R 600 jmx.access

2)建立内网公网IP映射

如果是云服务器则需要建立局域网与公网IP映射

代码语言:javascript
复制
 vim /etc/hosts

3)开放端口访问限制

  • 防火墙:Dcom.sun.management.jmxremote.port对应端口
  • 云服务器安全组策略开放Dcom.sun.management.jmxremote.port对应的端口

4)VisualVM进行远程连接

添加远程服务器:

建立JMX连接:

连接成功:

远程jmx连接成功之后就可以像监视本地JVM进程一样监视远程服务器上的JVM进程了。

不过生产环境远程服务器上的JMV 进程指标的监控大多使用阿里开源的Arthas 中间件, 这里限于文章篇幅笔者改天再新写一篇文章来介绍如何使用Arthas 中间件进行线上JVM调优。

总结

本文主要对JDK自带的JVM诊断和调优命令和工具的用法做了详细的介绍,包括jpsjstatjinfojstackjmap等五个命令及Java VisualVM图形话界面的具体使用。总结一下它们各自的用处与使用场景:

  • jps: 查询 JVM 进程及其对应的启动类信息,主要用法:jpsjps -l
  • jstat:可以查看Java程序运行时相关信息,也可以通过它查看堆信息的相关情况,尤其可以监视垃圾回收信息,主要用法:jstat -gc <PID> [interval] [times]
  • jinfo: 获取JVM参数信息、修改JVM参数值或使 JVM 参数生效或失效,主要用法:jinfo -flag <name> <PID>jinfo -flags <PID>jinfo -flag <name>=<value> <PID>jinfo -flag +<name> <PID>等;
  • jstack:查看JVM线程信息及生成线程快照,主要用法:jstack -l <PID>jstack -F等;
  • jmap:监控堆内存使用情况和对象占用情况, 生成堆内存快照文件,查看堆内存区域配置信息,主要用法:jmap -histo <PID>jmap -dump:live,format=b,file=<filename> <PID>,注意切忌在生产环境使用jmap -dump导致应用长时间停顿的操作。
  • Java VisualVM:图形化界面应用可用来装入dump文件进行诊断分析以及实时监控本地和远程JVM的内存使用、线程和GC信息。
本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-12-14,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 阿福谈Web编程 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 1 使用 JDK 自带诊断工具命令
  • 2 jps 命令
  • 3 jstat 命令
  • 4 jinfo 命令
  • 5 jstack 命令
  • 6 jmap 命令
  • 7 jvisualvm 工具
    • 7.2 分析内存溢出dump文件
      • 7.3 本地JVM监控
        • 7.5 远程JVM监控
        • 总结
        相关产品与服务
        消息队列 TDMQ
        消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
        http://www.vxiaotou.com