跳至主要內容

⑥JVM JVM故障问题排查

holic-x...大约 58 分钟JAVA基础

⑥JVM JVM故障问题排查

学习核心

  • 内存泄漏问题排查
    • GC引起CPU飙升问题排查(5个步骤一步步定位)
    • 现场保留(瞬时信息和历史信息的记录保留,后续进行跟踪分析)
      • 常用工具:jinfo、jstat、jstack、jhsdb(jmap)等工具,尤其是jmap(分析处理内存泄漏的利器)
    • 一个卡顿实例分析(结合卡顿实例,掌握分析思路和实践过程)
      • 高并发业务场景中启用swap造成的服务卡顿
    • 内存泄漏 VS 内存溢出
      • 区分内存泄漏(原因)和内存溢出(结果)概念
      • 内存泄漏现象
      • 内存泄漏案例(HashMap使用不当、文件操作资源未及时释放、JavaAPI使用不当、ThrealLocal使用误区)

学习资料

内存泄漏排查

​ JVM 是运行在操作系统之上的,操作系统的一些限制,会严重影响 JVM 的行为,例如最常见的系统发生 OOM。故障排查是一个综合性的技术问题,在日常项目开发中要增加自己的知识广度。多总结、多思考、多记录,这才是正确的晋级方式。

​ 现在的互联网服务,一般都做了负载均衡。如果一个实例发生了问题,不要着急去重启。万能的重启会暂时缓解问题,但如果不保留现场,可能就错失了解决问题的根本,担心的事情还会到来。

​ 所以,当实例发生问题的时候,第一步是隔离,第二步才是问题排查。所谓隔离,就是把你的这台机器从请求列表里摘除,比如把 nginx 相关的权重设成零。在微服务中,也有相应的隔离机制

掌握JVM 故障排查的涉及到的 Linux 命令并逐个击破

✨常用的监控和诊断内存工具

​ Java 是基于 JVM 上运行的,大部分内存都是在 JVM 的用户内存中创建的。一方面是需要监控整个服务器的内存使用情况,另一方面是要掌握JVM中的内存分配和使用情况

  • 基于服务器的内存使用情况监控:使用Linux命令行工具

    • top:实时显示正在执行进程的 CPU 使用率、内存使用率以及系统负载等信息;还可进一步查看指定进程下具体线程的资源占用情况
    • vmstat:一款指定采样周期和次数的功能性监测工具,可查看内存情况,常用作观察进程上下文切换
    • pidstat:除了监测进程内存,还可深入到监测线程级别的内存使用情况
  • 基于JVM的内存分配和使用情况监控(使用JDK自带的命令工具)

    • jstat:监测Java应用程序的实时运行情况(包括堆内存信息、垃圾回收信息)
    • jstack:线程堆栈分析工具(查看线程状态、堆栈信息,常用于排查死锁异常)
    • jamp:查看堆内存初始化配置信息、堆内存的使用情况(可确认堆内存的对象信息、产生什么对象、对象数量多少)

JDK 中就自带了很多命令工具可以监测到 JVM 的内存分配以及使用情况

top命令(Linux命令行工具)

top 命令可以实时显示正在执行进程的 CPU 使用率、内存使用率以及系统负载等信息。其中上半部分显示的是系统的统计信息,下半部分显示的是进程的使用率统计信息

image-20240602102116766

​ 还可通过top -Hp $pid查看具体线程使用系统资源的情况

image-20240602102242779

vmstat命令(Linux命令行工具)

​ vmstat 是一款指定采样周期和次数的功能性监测工具,它不仅可以统计内存的使用情况,还可以观测到 CPU 的使用率、swap 的使用情况。但 vmstat 一般很少用来查看内存的使用情况,而是经常被用来观察进程的上下文切换

vmstatvmstat 1 5(每隔1s,采样次数为5)

image-20240602102820261

列名
r:等待运行的进程数
b:处于非中断睡眠状态的进程数
swpd:虚拟内存使用情况
free:空闲的内存
buff:用来作为缓冲的内存数
si:从磁盘交换到内存的交换页数量
so:从内存交换到磁盘的交换页数量
bi:发送到块设备的块数
bo:从块设备接收到的块数
in:每秒中断数
cs:每秒上下文切换次数
us:用户 CPU 使用时间
sy:内核 CPU 系统使用时间
id:空闲时间
wa:等待 I/O 时间
st:运行虚拟机窃取的时间

pidstat命令(Linux命令行工具)

​ pidstat 是 sysstat 中的一个组件,也是一款功能强大的性能监测工具

​ 通过命令:yum install sysstat 安装该监控组件(sudo yum install sysstat

# 腾讯云宝塔安装pidstat工具
sudo apt-get update
sudo apt-get install -y sysstat

​ top 和 vmstat 两个命令都是监测进程的内存、CPU 以及 I/O 使用情况,而 pidstat 命令则是深入到线程级别。通过 pidstat -help 命令,查看到有以下几个常用的参数来监测线程的性能

pidstat --help
用法:pidstat [ 选项 ] [ <时间间隔> [ <计数> ] ] [ -e <程序> <参数> ]
选项:
[ -d ] [ -H ] [ -h ] [ -I ] [ -l ] [ -R ] [ -r ] [ -s ] [ -t ] [ -U [ <用户名> ] ]
[ -u ] [ -V ] [ -v ] [ -w ] [ -C <命令> ] [ -G <进程名> ]
[ -p { <pid> [,...] | SELF | ALL } ] [ -T { TASK | CHILD | ALL } ]
[ --dec={ 0 | 1 | 2 } ] [ --human ]
参数
-u:默认的参数,显示各个进程的 cpu 使用情况
-r:显示各个进程的内存使用情况
-d:显示各个进程的 I/O 使用情况
-w:显示每个进程的上下文切换情况
-p:指定进程号
-t:显示进程中线程的统计信息
# 案例分析

# 步骤1:查看相关进程ID
noob@noob-virtual-machine:~$ jps
53200 Jps
45530 OjCodeSandboxApplication

# 步骤2:监测进程的内存使用情况
noob@noob-virtual-machine:~$ pidstat -p 45530 -r 2 3
Linux 6.5.0-28-generic (noob-virtual-machine) 	2024年06月02日 	_x86_64_	(4 CPU)
11时05分19秒   UID       PID  minflt/s  majflt/s     VSZ     RSS   %MEM  Command
11时05分21秒  1000     45530      0.00      0.00 5555300  220628   2.73  java
11时05分23秒  1000     45530      0.00      0.00 5555300  220628   2.73  java
11时05分25秒  1000     45530      0.00      0.00 5555300  220628   2.73  java
平均时间:  1000     45530      0.00      0.00 5555300  220628   2.73  java

# 步骤3:进一步查看进程下的线程内存使用率,添加-t选项
noob@noob-virtual-machine:~$ pidstat -p 45530 -r 2 3 -t
Linux 6.5.0-28-generic (noob-virtual-machine) 	2024年06月02日 	_x86_64_	(4 CPU)

11时11分40秒   UID      TGID       TID  minflt/s  majflt/s     VSZ     RSS   %MEM  Command
11时11分42秒  1000     45530         -      1.00      0.00 5555300  220628   2.73  java
11时11分42秒  1000         -     45530      0.00      0.00 5555300  220628   2.73  |__java
11时11分42秒  1000         -     45532      0.00      0.00 5555300  220628   2.73  |__java
11时11分42秒  1000         -     45533      0.00      0.00 5555300  220628   2.73  |__GC task thread#
11时11分42秒  1000         -     45542      0.00      0.00 5555300  220628   2.73  |__VM Thread
11时11分42秒  1000         -     45543      0.00      0.00 5555300  220628   2.73  |__Reference Handl
11时11分42秒  1000         -     45544      0.00      0.00 5555300  220628   2.73  |__Finalizer
11时11分42秒  1000         -     45545      0.00      0.00 5555300  220628   2.73  |__Signal Dispatch
11时11分42秒  1000         -     45548      0.00      0.00 5555300  220628   2.73  |__JDWP Event Help

【步骤1】使用psjps:查看相关进程ID

【步骤2】使用pidstat:监测该进程的内存使用情况

  • 参数说明:-p指定进程ID、-r监控内存的使用情况、2:每隔2s、3:采样3次

  • 关键指标说明:

    • Minflt/s:任务每秒发生的次要错误,不需要从磁盘中加载页
    • Majflt/s:任务每秒发生的主要错误,需要从磁盘中加载页
    • VSZ:虚拟地址大小,虚拟内存使用 KB
    • RSS:常驻集合大小,非交换区内存使用 KB

jps(JDK工具)

​ jps:打印所有正在运行的Java进程

noob@noob-virtual-machine:~/logs/oomDemo$ jps
58761 Jps
45530 OjCodeSandboxApplication
55533 jar

jinfo(JDK工具)

​ jinfo:打印目标Java进程的配置参数,且可以改动其中的manageabe参数

noob@noob-virtual-machine:~/logs/oomDemo$ sudo jinfo 55533
Attaching to process ID 55533, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.402-b06
Java System Properties:

java.runtime.name = OpenJDK Runtime Environment
java.vm.version = 25.402-b06
sun.boot.library.path = /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/amd64
java.protocol.handler.pkgs = org.springframework.boot.loader
java.vendor.url = http://java.oracle.com/
java.vm.vendor = Private Build
path.separator = :
file.encoding.pkg = sun.io
java.vm.name = OpenJDK 64-Bit Server VM
sun.os.patch.level = unknown
sun.java.launcher = SUN_STANDARD
user.country = CN
user.dir = /home/noob/code/jars/target
java.vm.specification.name = Java Virtual Machine Specification
PID = 55533
java.runtime.version = 1.8.0_402-8u402-ga-2ubuntu1~22.04-b06
java.awt.graphicsenv = sun.awt.X11GraphicsEnvironment
os.arch = amd64
java.endorsed.dirs = /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/endorsed
CONSOLE_LOG_CHARSET = UTF-8
line.separator = 

java.io.tmpdir = /tmp
java.vm.specification.vendor = Oracle Corporation
os.name = Linux
FILE_LOG_CHARSET = UTF-8
sun.jnu.encoding = UTF-8
java.library.path = /usr/java/packages/lib/amd64:/usr/lib/x86_64-linux-gnu/jni:/lib/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu:/usr/lib/jni:/lib:/usr/lib
spring.beaninfo.ignore = true
java.specification.name = Java Platform API Specification
java.class.version = 52.0
sun.management.compiler = HotSpot 64-Bit Tiered Compilers
os.version = 6.5.0-28-generic
user.home = /home/noob
user.timezone = Asia/Shanghai
catalina.useNaming = false
java.awt.printerjob = sun.print.PSPrinterJob
file.encoding = UTF-8
java.specification.version = 1.8
catalina.home = /tmp/tomcat.8080.8994288621376893718
user.name = noob
java.class.path = OomDemo-0.0.1-SNAPSHOT.jar
java.vm.specification.version = 1.8
sun.arch.data.model = 64
sun.java.command = OomDemo-0.0.1-SNAPSHOT.jar
java.home = /usr/lib/jvm/java-8-openjdk-amd64/jre
user.language = zh
java.specification.vendor = Oracle Corporation
awt.toolkit = sun.awt.X11.XToolkit
java.vm.info = mixed mode
java.version = 1.8.0_402
java.ext.dirs = /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/ext:/usr/java/packages/lib/ext
sun.boot.class.path = /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/resources.jar:/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/rt.jar:/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/sunrsasign.jar:/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/jsse.jar:/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/jce.jar:/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/charsets.jar:/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/jfr.jar:/usr/lib/jvm/java-8-openjdk-amd64/jre/classes
java.awt.headless = true
java.vendor = Private Build
catalina.base = /tmp/tomcat.8080.8994288621376893718
java.specification.maintenance.version = 5
file.separator = /
java.vendor.url.bug = http://bugreport.sun.com/bugreport/
sun.io.unicode.encoding = UnicodeLittle
sun.cpu.endian = little
sun.desktop = gnome
sun.cpu.isalist = 

VM Flags:
Non-default VM flags: -XX:CICompilerCount=3 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=null -XX:InitialHeapSize=1048576000 -XX:MaxHeapSize=4194304000 -XX:MaxNewSize=1397751808 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=349175808 -XX:OldSize=699400192 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseParallelGC 
Command line:  -Xms1000m -Xmx4000m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/noob/logs/oomDemo/heapdump.hprof -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/home/noob/logs/oomDemo/heapTest.log

​ 还可通过ps指令查看进程信息(例如此处通过关键字定位进程信息,并得到这个java进程的启动配置信息)

noob@noob-virtual-machine:~/logs/oomDemo$ ps -ef | grep OomDemo
noob       55533    5432 25 15:28 pts/0    01:11:45 java -jar -Xms1000m -Xmx4000m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/noob/logs/oomDemo/heapdump.hprof -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/home/noob/logs/oomDemo/heapTest.log OomDemo-0.0.1-SNAPSHOT.jar
noob       58824   56459  0 20:10 pts/3    00:00:00 grep --color=auto OomDemo

jstat(JDK工具)

​ jstat 可以监测 Java 应用程序的实时运行情况,包括堆内存信息以及垃圾回收信息

# 查看关键参数信息(-help)
noob@noob-virtual-machine:~$ jstat -help
Usage: jstat -help|-options
       jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]

Definitions:
  <option>      An option reported by the -options option
  <vmid>        Virtual Machine Identifier. A vmid takes the following form:
                     <lvmid>[@<hostname>[:<port>]]
                Where <lvmid> is the local vm identifier for the target
                Java virtual machine, typically a process id; <hostname> is
                the name of the host running the target Java virtual machine;
                and <port> is the port number for the rmiregistry on the
                target host. See the jvmstat documentation for a more complete
                description of the Virtual Machine Identifier.
  <lines>       Number of samples between header lines.
  <interval>    Sampling interval. The following forms are allowed:
                    <n>["ms"|"s"]
                Where <n> is an integer and the suffix specifies the units as 
                milliseconds("ms") or seconds("s"). The default units are "ms".
  <count>       Number of samples to take before terminating.
  -J<flag>      Pass <flag> directly to the runtime system.


# 查看jstat有哪些操作(-options)
noob@noob-virtual-machine:~$ jstat -options
-class
-compiler
-gc
-gccapacity
-gccause
-gcmetacapacity
-gcnew
-gcnewcapacity
-gcold
-gcoldcapacity
-gcutil
-printcompilation
参数说明
-class:显示 ClassLoad 的相关信息;
-compiler:显示 JIT 编译的相关信息;
-gc:显示和 gc 相关的堆信息;
-gccapacity:显示各个代的容量以及使用情况;
-gcmetacapacity:显示 Metaspace 的大小;
-gcnew:显示新生代信息;
-gcnewcapacity:显示新生代大小和使用情况;
-gcold:显示老年代和永久代的信息;
-gcoldcapacity :显示老年代的大小;
-gcutil:显示垃圾收集信息;
-gccause:显示垃圾回收的相关信息(通 -gcutil),同时显示最后一次或当前正在发生的垃圾回收的诱因;
-printcompilation:输出 JIT 编译的方法信息

✨常用功能:使用jstat查看堆内存的使用情况

jstat -gc pid

# 步骤1:获取到指定进程ID
noob@noob-virtual-machine:~$ jps
45530 OjCodeSandboxApplication
53500 Jps

# 步骤2:查看堆内存的使用情况
noob@noob-virtual-machine:~$ jstat -gc 45530
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
12288.0 12288.0  0.0    0.0   144896.0 37732.0   90112.0    17517.4   35416.0 33015.1 4864.0 4299.8      6    0.061   2      0.072    0.133

参数说明
S0C:年轻代中 To Survivor 的容量(单位 KB)
S1C:年轻代中 From Survivor 的容量(单位 KB)
S0U:年轻代中 To Survivor 目前已使用空间(单位 KB)
S1U:年轻代中 From Survivor 目前已使用空间(单位 KB)
EC:年轻代中 Eden 的容量(单位 KB)
EU:年轻代中 Eden 目前已使用空间(单位 KB)
OC:Old 代的容量(单位 KB)
OU:Old 代目前已使用空间(单位 KB)
MC:Metaspace 的容量(单位 KB)
MU:Metaspace 目前已使用空间(单位 KB)
YGC:从应用程序启动到采样时年轻代中 gc 次数
YGCT:从应用程序启动到采样时年轻代中 gc 所用时间 (s)
FGC:从应用程序启动到采样时 old 代(全 gc)gc 次数
FGCT:从应用程序启动到采样时 old 代(全 gc)gc 所用时间 (s)
GCT:从应用程序启动到采样时 gc 用的总时间 (s)

jstack(JDK工具)

jstack是一种线程堆栈分析工具,最常用的功能就是使用 jstack pid 命令查看线程的堆栈信息

​ 通常会结合 top -Hp pidpidstat -p pid -t 一起查看具体线程的状态,也经常用来排查一些死锁的异常

# jps:列出当前系统中正在运行的Java进程
noob@noob-virtual-machine:~$ jps
53625 Jps
45530 OjCodeSandboxApplication

# jstack pid
noob@noob-virtual-machine:~$ jstack 45530
2024-06-02 12:08:20
Full thread dump OpenJDK 64-Bit Server VM (25.402-b06 mixed mode):

"DestroyJavaVM" #39 prio=5 os_prio=0 tid=0x00007d0e10013000 nid=0xb1dc waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"http-nio-8090-Acceptor" #38 daemon prio=5 os_prio=0 tid=0x00007d0e10f4d800 nid=0xb227 runnable [0x00007d0db5fcf000]
   java.lang.Thread.State: RUNNABLE
	at sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method)
	at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:421)
	at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:249)
	- locked <0x00000000854281d8> (a java.lang.Object)
	at org.apache.tomcat.util.net.NioEndpoint.serverSocketAccept(NioEndpoint.java:547)
	at org.apache.tomcat.util.net.NioEndpoint.serverSocketAccept(NioEndpoint.java:79)
	at org.apache.tomcat.util.net.Acceptor.run(Acceptor.java:129)
	at java.lang.Thread.run(Thread.java:750)

​ 可跟踪每个线程的堆栈信息,查看到线程ID、线程的状态(wait、sleep、running中等状态)、是否持有锁

jmap(JDK工具)

使用`jmap`查看堆内存初始化配置信息以及堆内存的使用情况。还可以使用 `jmap` 输出堆内存中的对象信息,包括产生了哪些对象,对象数量多少等
# jamp $pid (如果操作不允许,则添加sudo执行)
noob@noob-virtual-machine:~$ jmap -heap 45530
Attaching to process ID 45530, please wait...
ERROR: ptrace(PTRACE_ATTACH, ..) failed for 45530: 不允许的操作
Error attaching to process: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process: ptrace(PTRACE_ATTACH, ..) failed for 45530: 不允许的操作
sun.jvm.hotspot.debugger.DebuggerException: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process: ptrace(PTRACE_ATTACH, ..) failed for 45530: 不允许的操作
	at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$LinuxDebuggerLocalWorkerThread.execute(LinuxDebuggerLocal.java:163)
	at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal.attach(LinuxDebuggerLocal.java:278)
	at sun.jvm.hotspot.HotSpotAgent.attachDebugger(HotSpotAgent.java:671)
	at sun.jvm.hotspot.HotSpotAgent.setupDebuggerLinux(HotSpotAgent.java:611)
	at sun.jvm.hotspot.HotSpotAgent.setupDebugger(HotSpotAgent.java:337)


# sudo jamp $pid
noob@noob-virtual-machine:~$ sudo jmap -heap 45530
[sudo] noob 的密码: 
Attaching to process ID 45530, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.402-b06

using thread-local object allocation.
Parallel GC with 4 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 2071986176 (1976.0MB)
   NewSize                  = 42991616 (41.0MB)
   MaxNewSize               = 690487296 (658.5MB)
   OldSize                  = 87031808 (83.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at sun.tools.jmap.JMap.runTool(JMap.java:201)
	at sun.tools.jmap.JMap.main(JMap.java:130)
Caused by: java.lang.RuntimeException: unknown CollectedHeap type : class sun.jvm.hotspot.gc_interface.CollectedHeap
	at sun.jvm.hotspot.tools.HeapSummary.run(HeapSummary.java:144)
	at sun.jvm.hotspot.tools.Tool.startInternal(Tool.java:260)
	at sun.jvm.hotspot.tools.Tool.start(Tool.java:223)
	at sun.jvm.hotspot.tools.Tool.execute(Tool.java:118)
	at sun.jvm.hotspot.tools.HeapSummary.main(HeapSummary.java:49)
	... 6 more

​ 使用 jmap -histo[:live] pid 查看堆内存中的对象数目、大小统计直方图,如果带上 live 则只统计活对象

noob@noob-virtual-machine:~$ jmap -histo:live 45530 | more

 num     #instances         #bytes  class name
----------------------------------------------
   1:         33340        2903960  [C
   2:         33273         798552  java.lang.String
   3:          7148         791648  java.lang.Class
   4:         14304         457728  java.util.concurrent.ConcurrentHashMap$Node
   5:          7695         373312  [Ljava.lang.Object;
   6:          1190         291480  [B
   7:          6120         244800  java.util.LinkedHashMap$Entry
   8:          2549         224312  java.lang.reflect.Method
   9:          2410         222416  [Ljava.util.HashMap$Node;
  10:          6912         221184  java.util.HashMap$Node
  11:           122         177696  [Ljava.util.concurrent.ConcurrentHashMap$Node;
  12:          9130         146080  java.lang.Object
  13:          2500         140000  java.util.LinkedHashMap
  14:          2804         131272  [I
  15:          1381          77336  java.lang.invoke.MemberName
  16:          2772          64360  [Ljava.lang.Class;
  17:          1556          62240  java.lang.ref.SoftReference
  18:          2348          56352  java.util.ArrayList
  19:          1248          49920  java.lang.invoke.MethodType
  20:           953          49096  [Ljava.lang.String;

​ 通过jmap命令把堆内存的使用情况dump到文件中,然后将文件下载下来,使用MAT工具(EclipseMemoryAnalyzerTool)打开文件进行分析

1.GC引起CPU飙升

CPU占用过高排查思路

问题场景:线上应用:单节点在运行一段时间后,CPU 的使用会飙升

​ 一旦飙升,一般怀疑某个业务逻辑的计算量太大,或者是触发了死循环(比如HashMap 高并发引起的死循环)但也排除是GC 的问题

​ 在 Linux 上,分析哪个线程引起的 CPU 问题,通常有一个固定的步骤

步骤指令说明
1top查找CPU最多的某个进程,记录pid
使用shift+P快捷键可按CPU使用率进行排序
2top -Hp $pid查看某个进程中使用CPU最多的某个线程,记录线程ID(tid)
3printf %x &tid使用printf函数,将10进制的tid转化为16进制
4jstack $pid>$pid.log使用jstack命令,查看Java进程的线程栈(输出到指定文件)
5less $pid.log使用less命令查看生成的文件,并查找步骤3中获取到的16进制的tid,找到发生问题的线程上下文

其中步骤4、步骤5可以调整为直接查看堆栈信息

  • 查看堆栈信息定位jstack pid |grep tid的十六进制 -A 30

​ 参考jstack日志分析:

image-20240602074954957

​ 结合上述日志分析:问题发生的根源,是堆已经满了,但是又没有发生 OOM,于是 GC 进程就一直在那里回收,回收的效果又非常一般,造成 CPU 升高应用假死

​ 接下来的具体问题排查,就需要把内存 dump 一份下来,使用 MAT 等工具分析具体原因

GC问题排查分析

​ 结合jstack日志,如果发现CPU占用过高的线程在进行大量的GC,则需进行GC问题排查,排查思路参考如下:

步骤指令说明
1top查看占用cpu高的进程信息
2jstat -gcutil pid查看gc状况
3jmap -dump:format=b,file=name.dump pid导出dump文件
4visualVMMAT工具分析用visualVM分析dump文件

2.现场保留

​ 现场保留过程是繁杂而冗长的,需要记忆很多内容。现场保留可以使用自动化方式将必要的信息保存下来,需要了解一般在线上系统会保留哪些信息。

瞬时态和历史态

​ 此处引入瞬时态和历史态概念:瞬时态是指当时发生的、快照类型的元素;历史态是指按照频率抓取的,有固定监控项的资源变动图

​ 有很多信息,比如 CPU、系统内存等,瞬时态的价值就不如历史态来的直观一些。因为瞬时状态无法体现一个趋势性问题(比如斜率、求导等),而这些信息的获取一般依靠监控系统的协作。

​ 但对于 lsof、heap 等,这种没有时间序列概念的混杂信息,体积都比较大,无法进入监控系统产生有用价值,就只能通过瞬时态进行分析。在这种情况下,瞬时态的价值反而更大一些。常见的堆快照,就属于瞬时状态。

​ 问题不是凭空产生的,在分析时,一般要收集系统的整体变更集合,比如代码变更、网络变更,甚至数据量的变化

image-20240602075313367

保留信息

(1)系统当前网络连接

# ss 命令将系统的所有网络连接输出到 ss.dump 文件中
ss -antp > $DUMP_DIR/ss.dump 2>&1

​ 使用 ss 命令而不是 netstat 的原因:netstat 在网络连接非常多的情况下,执行非常缓慢

​ 排查分析:可通过查看各种网络连接状态的梳理,来排查 TIME_WAIT 或者 CLOSE_WAIT,或者其他连接过高的问题

​ 案例分析:线上有个系统更新之后,监控到 CLOSE_WAIT 的状态突增,最后整个 JVM 都无法响应。CLOSE_WAIT 状态的产生一般都是代码问题,使用 jstack 最终定位到是因为 HttpClient 的不当使用而引起的,多个连接不完全主动关闭

(2)网络状态统计

# netstat 命令将网络统计状态输出到 netstat-s.dump 文件中(能够按照各个协议进行统计输出,对把握当时整个网络状态有非常大的作用)
netstat -s > $DUMP_DIR/netstat-s.dump 2>&1
# 使用 sar 输出当前的网络流量
sar -n DEV 1 2 > $DUMP_DIR/sar-traffic.dump 2>&1

​ 在一些速度非常高的模块上,比如 Redis、Kafka,就经常发生跑满网卡的情况。如果Java 程序和它们在一起运行,资源则会被挤占,表现形式就是网络通信非常缓慢

(3)进程资源

# 通过lsof指令查看进程(可跟踪打开了哪些文件)
lsof -p $PID > $DUMP_DIR/lsof-$PID.dump

​ 通过lsof指令查看进程(可跟踪打开了哪些文件),可以以进程的维度来查看整个资源的使用情况,包括每条网络连接、每个打开的文件句柄、连接到哪些服务、使用了什么资源。这个命令在资源非常多的情况下,输出稍慢,请耐心等待。

(4)CPU 资源

#  输出当前系统的 CPU 和负载,便于事后排查。这几个命令的功能,有不少重合,使用时要注意甄别
mpstat > $DUMP_DIR/mpstat.dump 2>&1
vmstat 1 3 > $DUMP_DIR/vmstat.dump 2>&1
sar -p ALL  > $DUMP_DIR/sar-cpu.dump  2>&1
uptime > $DUMP_DIR/uptime.dump 2>&1

(5)I/O 资源

# 使用iostat命令输出每块磁盘的基本性能信息,用来排查 I/O 问题
iostat -x > $DUMP_DIR/iostat.dump 2>&1

​ 一般情况下如果是以计算为主的服务节点,I/O 资源会比较正常,但有时也会发生问题,比如日志输出过多,或者磁盘问题等

(6)内存问题

# free 命令能够大体展现操作系统的内存概况
free -h > $DUMP_DIR/free.dump 2>&1

​ free 命令能够大体展现操作系统的内存概况,这是故障排查中一个非常重要的点,比如 SWAP 影响了 GC,SLAB 区挤占了 JVM 的内存

(7)其他全局

ps -ef > $DUMP_DIR/ps.dump 2>&1
dmesg > $DUMP_DIR/dmesg.dump 2>&1
sysctl -a > $DUMP_DIR/sysctl.dump 2>&1

​ dmesg 是许多静悄悄死掉的服务留下的最后一点线索。ps 作为执行频率最高的一个命令,它当时的输出信息,也必然有一些可以参考的价值

​ 由于内核的配置参数,会对系统和 JVM 产生影响,因此也输出了一份用作排查

(8)进程快照,最后的遗言(jinfo)

# 输出 Java 的基本进程信息,包括环境变量和参数配置
${JDK_BIN}jinfo $PID > $DUMP_DIR/jinfo.dump 2>&1

​ 此命令将输出 Java 的基本进程信息,包括环境变量和参数配置,可以查看是否因为一些错误的配置造成了 JVM 问题

(9)dump 堆信息

${JDK_BIN}jstat -gcutil $PID > $DUMP_DIR/jstat-gcutil.dump 2>&1
${JDK_BIN}jstat -gccapacity $PID > $DUMP_DIR/jstat-gccapacity.dump 2>&1

​ jstat 将输出当前的 gc 信息(一般,基本能大体看出一个端倪),如果不能分析出来,可将借助 jmap 来进行分析

(10)堆信息

${JDK_BIN}jmap $PID > $DUMP_DIR/jmap.dump 2>&1
${JDK_BIN}jmap -heap $PID > $DUMP_DIR/jmap-heap.dump 2>&1
${JDK_BIN}jmap -histo $PID > $DUMP_DIR/jmap-histo.dump 2>&1
${JDK_BIN}jmap -dump:format=b,file=$DUMP_DIR/heap.bin $PID > /dev/null  2>&1

​ jmap 将会得到当前 Java 进程的 dump 信息。如上所示,其实最有用的就是第 4 个命令,但是前面三个能够让你初步对系统概况进行大体判断。

​ 因为,第 4 个命令产生的文件,一般都非常的大。而且,需要下载下来,导入 MAT 这样的工具进行深入分析,才能获取结果。这是分析内存泄漏一个必经的过程

(11)JVM 执行栈

# jstack 将会获取当时的执行栈(一般会多次取值,此处取一次),能够还原 Java 进程中的线程情况
${JDK_BIN}jstack $PID > $DUMP_DIR/jstack.dump 2>&1
# 使用top 命令,来获取进程中所有线程的 CPU 信息(得到更加精细的信息),据此可以可能到资源到底消耗在什么地方
top -Hp $PID -b -n 1 -c >  $DUMP_DIR/top-$PID.dump 2>&1

(12)高级替补

​ 有时候,jstack 并不能够运行(有很多原因,比如 Java 进程几乎不响应了等之类的情况)。尝试向进程发送 kill -3 信号,这个信号将会打印 jstack 的 trace 信息到日志文件中,是 jstack 的一个替补方案

# jstack的替补方案,打印jstack的trace信息到日志文件
kill -3 $PID

​ 对于 jmap 无法执行的问题,也有替补,那就是 GDB 组件中的 gcore,将会生成一个 core 文件

gcore -o $DUMP_DIR/core $PID

​ 可以使用如下的命令去生成 dump

${JDK_BIN}jhsdb jmap --exe ${JDK}java  --core $DUMP_DIR/core --binaryheap

3.一个卡顿实例

卡顿问题场景:

  • 有一个关于服务的某个实例,经常发生服务卡顿,由于服务的并发量是比较高的,所以表现也非常明显,每多停顿 1 秒钟,几万用户的请求就会感到延迟

问题排查过程分析:

  • 统计、类比了此服务其他实例的 CPU、内存、网络、I/O 资源,区别并不是很大,所以一度怀疑是机器硬件的问题

  • 随后对比了节点的 GC 日志,发现无论是 Minor GC,还是 Major GC,这个节点所花费的时间,都比其他实例长得多

  • 通过仔细观察,发现在 GC 发生的时候,vmstat 的 si、so 飙升的非常严重,这和其他实例有着明显的不同

  • 使用 free 命令再次确认,发现 SWAP 分区,使用的比例非常高,引起的具体原因是什么呢?

  • 查看更详细的操作系统内存分布,从 /proc/meminfo 文件中可以看到具体的逻辑内存块大小,有多达 40 项的内存信息,这些信息都可以通过遍历 /proc 目录的一些文件获取。进一步注意到 slabtop 命令显示的有一些异常,dentry(目录高速缓冲)占用非常高

  • 问题最终定位到是由于某个运维工程师执行了一句命令:find / | grep "x",其目的是想找一个叫做 x 的文件,看看在哪台服务器上,结果因为这些老服务器由于文件太多,扫描后这些文件信息都缓存到了 slab 区上。而服务器开了 swap,操作系统发现物理内存占满后,并没有立即释放 cache,导致每次 GC 都要和硬盘打一次交道

解决方案

​ 解决方式就是关闭 SWAP 分区

swap 是很多性能场景的万恶之源,建议禁用。当应用真正高并发了,SWAP 绝对能让你体验到它魔鬼性的一面:进程倒是死不了了,但 GC 时间长的却让人无法忍受

4.内存泄漏 VS 内存溢出

内存泄漏 VS 内存溢出

​ 内存泄漏:申请内存资源并使用完成后无法正常释放资源导致的情况(例如不再被使用的对象、没有被回收、没有及时切断与 GC Roots 的联系,其主要是因为一些错误的编程方式,或者过多的无用对象创建引起的),内存泄漏最终会导致内存溢出

​ 内存溢出:内存不够用了,当无法为新对象分配足够的内存的情况。(内存溢出的原因有内存空间不足、配置错误等因素)

内存溢出是一个结果,而内存泄漏是一个原因

内存泄漏现象

​ jmap 命令在 9 版本里被干掉了,取而代之的是 jhsdb,可以像下面的命令一样使用

jhsdb jmap  --heap --pid  37340
jhsdb jmap  --pid  37288
jhsdb jmap  --histo --pid  37340
jhsdb jmap  --binaryheap --pid  37340

​ heap 参数能够体现大体的内存布局,以及每一个年代中的内存使用情况,且由于它是命令行,所以使用更加广泛。

image-20240602093509355

​ histo 能够大概的看到系统中每一种类型占用的空间大小,用于初步判断问题。比如某个对象 instances 数量很小,但占用的空间很大,这就说明存在大对象。但它也只能看大概的问题,要找到具体原因,还是要 dump 出当前 live 的对象

image-20240602093547583

​ 一般内存溢出,表现形式就是 Old 区的占用持续上升,即使经过了多轮 GC 也没有明显改善。我们在前面提到了 GC Roots,内存泄漏的根本就是,有些对象并没有切断和 GC Roots 的关系,可通过一些工具,能够看到它们的联系

内存泄漏案例

案例1:(由HashMap引起的内存泄漏)未限定HashMap缓存策略

​ 例如项目中使用了 HashMap 做缓存,但是并没有设置超时时间或者 LRU 策略,造成了放入 Map 对象的数据越来越多,从而产生了内存泄漏

案例2:(由HashMap引起的内存泄漏)使用自定义对象作为HashMap的key值,但是没有重写自定义对象的hashCode、equals方法

就算重写了hashCode、equals方法,也应尽量避免使用自定义对象作为key值

​ 例如此处由于没有重写 Key 类的 hashCode 和 equals 方法,造成了放入 HashMap 的所有对象都无法被取出来(和外界失联了)

import java.util.HashMap;
import java.util.Map;

public class HashMapLeakDemo {
    public static class Key {
        String title;

        public Key(String title) {
            this.title = title;
        }
    }

    public static void main(String[] args) {
        Map<Key, Integer> map = new HashMap<>();

        map.put(new Key("1"), 1);
        map.put(new Key("2"), 2);
        map.put(new Key("3"), 2);

        Integer integer = map.get(new Key("2"));
        System.out.println(integer);
    }
}

// output
null

案例3:文件操作异常没有正确地在finally块关闭资源,造成了文件句柄的泄漏

​ 关于文件处理器的应用,在读取或者写入一些文件之后,由于发生了一些异常,close 方法又没有放在 finally 块里面,进而造成了文件句柄的泄漏。由于文件处理十分频繁,产生了严重的内存泄漏问题

案例4:对Java API的使用不当,也会造成内存泄漏

String 的 intern 方法,但如果字符串本身是一个非常长的字符串,而且创建之后不再被使用,则会造成内存泄漏

public class InternDemo {
    static String getLongStr() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 100000; i++) {
            sb.append(UUID.randomUUID().toString());
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        while (true) {
            getLongStr().intern();
        }
    }
}

案例5:ThreadLocal使用不当,导致内存泄漏,进而触发OOM

​ 模拟对每个线程设置一个本地变量。运行以下代码,访问接口,系统一会儿就发送了内存溢出异常

@RequestMapping(value = "/testThreadLocalOom")
public String testThreadLocalOom(HttpServletRequest request) {
    ThreadLocal<Byte[]> localVariable = new ThreadLocal<Byte[]>();
    localVariable.set(new Byte[4096*1024]);// 为线程添加变量
    return "success";
}

​ 设置启动参数,开启堆内存异常日志

java -jar -Xms1000m -Xmx4000m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/tmp/heapTest.log heapTest-0.0.1-SNAPSHOT.jar

​ 模拟请求接口,然后查看接口请求是否出现异常:

  • 步骤1:创建Springboot项目,然后编写接口测试本地可否正常访问
  • 步骤2:导出jar并上传到指定的服务器运行
  • 步骤3:JMeter压测工具模拟多次请求,查看接口请求是否异常

​ 具体分析步骤参考实践应用

GC问题排查

JVM问题排查和性能调优主要基于监控数据来进行

  • 指标监控
  • 指定JVM启动内存(结合实际场景调整内存大小)
  • 指定垃圾收集器(JDK8默认ParallelGC,可指定G1)
  • 开启GC日志进行排查,打印并分析GC日志

结合GC性能维度进行分析(确认性能指标是否基本满足、保持资源占用在合理范围)

  • STW:停顿时间(暂停用户线程,如果STW时间过长则可能导致用户体验极差),GC中影响延迟的主要因素就是STW
    • GC回收速率和存活对象的多少有关,例如CMS追求的是STW尽可能短,会导致吞吐量相对较低(结合打扫教室案例分析)
  • 吞吐量:看业务线程消耗的CPU资源百分比
  • 系统容量:硬件配置、服务能力等

​ 一般场景下会借助一些操作指令来排查JVM故障,尤其是一些GC问题(内存泄露、内存溢出等),但很多时候可能无法实时跟踪线上的情况,而是需要事后通过日志去分析相关的内容,因此可以通过设置JVM参数来完成“现场保留”操作。(但需注意的是,不同JDK版本的JVM参数配置不同,在实际应用的场景下一定要选择兼容自己版本的参数配置,否则会出现各种意想不到的问题)

​ 一般情况下不用去强制死记硬背这些参数信息,可以说明会配置哪些GC日志信息,用于跟踪什么:例如,一般在项目中输出详细的 GC 日志,并加上可读性强的 GC 日志的时间戳。特别情况下还会追加一些反映对象晋升情况和堆详细信息的日志,用来排查问题。另外,OOM 时自动 Dump 堆栈,也会进行配置

​ ==简化记忆:==输出GC日志的详细信息,跟踪堆栈信息等,也会根据dump的文件信息,借助GC日志分析工具可视化跟踪GC情况

1.启动日志配置

Java8版本

#!/bin/sh
LOG_DIR="/tmp/logs"
JAVA_OPT_LOG=" -verbose:gc"
JAVA_OPT_LOG="${JAVA_OPT_LOG} -XX:+PrintGCDetails"
JAVA_OPT_LOG="${JAVA_OPT_LOG} -XX:+PrintGCDateStamps"
JAVA_OPT_LOG="${JAVA_OPT_LOG} -XX:+PrintGCApplicationStoppedTime"
JAVA_OPT_LOG="${JAVA_OPT_LOG} -XX:+PrintTenuringDistribution"
JAVA_OPT_LOG="${JAVA_OPT_LOG} -Xloggc:${LOG_DIR}/gc_%p.log"
JAVA_OPT_OOM=" -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOG_DIR} -XX:ErrorFile=${LOG_DIR}/hs_error_pid%p.log "
JAVA_OPT="${JAVA_OPT_LOG} ${JAVA_OPT_OOM}"
JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow"


# 合并一行展示
-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps 
-XX:+PrintGCApplicationStoppedTime -XX:+PrintTenuringDistribution 
-Xloggc:/tmp/logs/gc_%p.log -XX:+HeapDumpOnOutOfMemoryError 
-XX:HeapDumpPath=/tmp/logs -XX:ErrorFile=/tmp/logs/hs_error_pid%p.log 
-XX:-OmitStackTraceInFastThrow
参数意义
-verbose:gc打印 GC 日志
PrintGCDetails打印详细 GC 日志
PrintGCDateStamps系统时间,更加可读,PrintGCTimeStamps 是 JVM 启动时间
PrintGCApplicationStoppedTime打印 STW 时间
PrintTenuringDistribution打印对象年龄分布,对调优 MaxTenuringThreshold 参数帮助很大
loggc将以上 GC 内容输出到文件中
参数配置说明意义
HeapDumpOnOutOfMemoryErrorOOM 时 Dump 信息,非常有用
HeapDumpPathDump 文件保存路径
ErrorFile错误日志存放路径
OmitStackTraceInFastThrow缩减日志输出

​ 其中OmitStackTraceInFastThrow用于缩减日志输出,例如如果发生了多次空指针异常,就会打印如下类型信息

java.lang.NullPointerException
java.lang.NullPointerException
java.lang.NullPointerException
java.lang.NullPointerException

​ 在实际生产环境中默认开启,就会导致排查问题非常不方便。如果关闭的话就会输出所有的异常堆栈,日志会特别的多。

Java13版本

​ 从 Java 9 开始,移除了 40 多个 GC 日志相关的参数。具体参见 JEP 158。所以这部分的日志配置有很大的变化。

#!/bin/sh
LOG_DIR="/tmp/logs"
JAVA_OPT_LOG=" -verbose:gc"
JAVA_OPT_LOG="${JAVA_OPT_LOG} -Xlog:gc,gc+ref=debug,gc+heap=debug,gc+age=trace:file=${LOG_DIR}/gc_%p.log:tags,uptime,time,level"
JAVA_OPT_LOG="${JAVA_OPT_LOG} -Xlog:safepoint:file=${LOG_DIR}/safepoint_%p.log:tags,uptime,time,level"
JAVA_OPT_OOM=" -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOG_DIR} -XX:ErrorFile=${LOG_DIR}/hs_error_pid%p.log "
JAVA_OPT="${JAVA_OPT_LOG} ${JAVA_OPT_OOM}"
JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow"
echo $JAVA_OPT

# 合并一行展示
-verbose:gc -Xlog:gc,gc+ref=debug,gc+heap=debug,gc+age=trace:file
=/tmp/logs/gc_%p.log:tags,uptime,time,level -Xlog:safepoint:file=/tmp
/logs/safepoint_%p.log:tags,uptime,time,level -XX:+HeapDumpOnOutOfMemoryError 
-XX:HeapDumpPath=/tmp/logs -XX:ErrorFile=/tmp/logs/hs_error_pid%p.log 
-XX:-OmitStackTraceInFastThrow

​ 对比以前的版本,GC日志的打印方式参数规整了许多。除了GC日志的输出,也输出了safepoint日志信息

​ safepoint 是 JVM 中非常重要的一个概念,指的是可以安全地暂停线程的点。当发生 GC 时,用户线程必须全部停下来,才可以进行垃圾回收,这个状态可以认为 JVM 是安全的(safe),整个堆的状态是稳定的

image-20240603203210861

​ 如果在 GC 前,有线程迟迟进入不了 safepoint,那么整个 JVM 都在等待这个阻塞的线程,会造成了整体 GC 的时间变长。

​ 所以呢,并不是只有 GC 会挂起 JVM,进入 safepoint 的过程也会。

2.GC日志的含义(todo)

image-20240603203723029

  • 1 表示 GC 发生的时间,一般使用可读的方式打印;
  • 2 表示日志表明是 G1 的“转移暂停: 混合模式”,停顿了约 223ms;
  • 3 表明由 8 个 Worker 线程并行执行,消耗了 214ms;
  • 4 表示 Diff 越小越好,说明每个工作线程的速度都很均匀;
  • 5 表示外部根区扫描,外部根是堆外区。JNI 引用,JVM 系统目录,Classloaders 等;
  • 6 表示更新 RSet 的时间信息;
  • 7 表示该任务主要是对 CSet 中存活对象进行转移(复制);
  • 8 表示花在 GC 之外的工作线程的时间;
  • 9 表示并行阶段的 GC 总时间;
  • 10 表示其他清理活动;
  • 11表示收集结果统计;
  • 12 表示时间花费统计

GC日志描述了垃圾回收器过程中的几乎每一个阶段。但即使了解了这些数值的意义,在分析问题时也会感到吃力,一般使用图形化的分析工具进行分析

结合最后一行数据来看,GC消耗的时间涉及到3个数值(可以在linux环境输入指令time ls /查看结果进行分析)

  • real 实际花费的时间,指的是从开始到结束所花费的时间。比如进程在等待 I/O 完成,这个阻塞时间也会被计算在内;
  • user 指的是进程在用户态(User Mode)所花费的时间,只统计本进程所使用的时间,注意是指多核;
  • sys 指的是进程在核心态(Kernel Mode)花费的 CPU 时间量,指的是内核中的系统调用所花费的时间,只统计本进程所使用的时间

结合上述GC日志分析,会发现real < user + sys:因为使用了多核进行垃圾收集,所以实际发生的时间会比(user+sys)少很多,在多核机器上这是很常见的

如果是串行垃圾收集器收集的GC示例(参考:[Times: user=0.29 sys=0.00, real=0.29 secs] ),因为串行垃圾收集器始终只有一个线程在做回收工作,因此实际使用时间=用户时间+系统时间

​ ==思考:统计GC以哪个时间为准?==一般情况下用户只关心系统停顿了多少秒,对实际的影响时间非常感兴趣。至于背后是怎么实现的,是多核还是单核,是用户态还是内核态,它们都不关心,因此选择real字段作为参考

3.GC日志分析

方式1:借助可视化工具(需要经历日志导出、上传、分析,不便于实时跟踪)

​ todo:可以借助一些在线分析平台(例如gceasy)

方式2:借助JDK命令行工具(jstat指令)

# 指定Java进程ID,指定间隔时间(毫秒)
jstat -gcutil $pid 1000
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 0.00 72.03 0.35 54.12 55.72 11122 16.019 0 0.000 16.019
0.00 0.00 95.39 0.35 54.12 55.72 11123 16.024 0 0.000 16.024
0.00 0.00 25.32 0.35 54.12 55.72 11125 16.025 0 0.000 16.025
0.00 0.00 37.00 0.35 54.12 55.72 11126 16.028 0 0.000 16.028
0.00 0.00 60.35 0.35 54.12 55.72 11127 16.028 0 0.000 16.028

​ 如果直接看上面的内容可能会比较迷惑,结合垃圾回收机制图示去理解这些参数所代表的含义

image-20240603205101649

  • 年轻代:S0(Survivor0:to)、S1(Survivor1:from)、E:Eden(伊甸空间)、O:老年代、M(元空间:Metaspace)
  • YGC:年轻代回收次数;YGCT:年轻代回收耗时
  • FGC:Full GC次数;FGCT:Full GC回收耗时

jstat配置说明:常用的有 gc、gcutil、gccause、gcnew 等,其他的可简单了解

  • gc: 显示和 GC 相关的 堆信息
  • gcutil: 显示 垃圾回收信息
  • gccause: 显示垃圾回收 的相关信息(同 -gcutil),同时显示 最后一次当前 正在发生的垃圾回收的 诱因
  • gcnew: 显示 新生代 信息;
  • gccapacity: 显示 各个代容量 以及 使用情况
  • gcmetacapacity: 显示 元空间 metaspace 的大小;
  • gcnewcapacity: 显示 新生代大小使用情况
  • gcold: 显示 老年代永久代 的信息;
  • gcoldcapacity: 显示 老年代 的大小;
  • printcompilation: 输出 JIT 编译 的方法信息;
  • class: 显示 类加载 ClassLoader 的相关信息;
  • compiler: 显示 JIT 编译 的相关信息;

​ 如果GC的问题特别明显,可以通过jstat快速发现,例如在命令行中指定参数-t,则可输出从程序启动到现在的时间。如果 FGC 和启动时间的比值太大,就证明系统的吞吐量比较小,GC 花费的时间太多了。另外,如果老年代在 Full GC 之后,没有明显的下降,那可能内存已经达到了瓶颈,或者有内存泄漏问题

​ 参考命令:在基命令基础上追加GC时间的增量和GC时间比率

jstat -gcutil -t 90542 1000 | awk 'BEGIN{pre=0}{if(NR>1) {print $0 "\t" ($12-pre) "\t" $12*100/$1 ; pre=$12 } else { print $0 "\tGCT_INC\tRate"} }' 
Timestamp         S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT    GCT_INC Rate
           18.7   0.00 100.00   6.02   1.45  84.81  76.09      1    0.002     0    0.000    0.002 0.002 0.0106952
           19.7   0.00 100.00   6.02   1.45  84.81  76.09      1    0.002     0    0.000    0.002 0 0.0101523

4.GC日志"捣蛋"场景

​ ElasticSearch 的速度是非常快的,我们为了压榨它的性能,对磁盘的读写几乎是全速的。它在后台做了很多 Merge 动作,将小块的索引合并成大块的索引。还有 TransLog 等预写动作,都是 I/O 大户。

​ 使用 iostat -x 1 可以看到具体的 I/O 使用状况。

​ 现有场景:有一套 ES 集群,在访问高峰时,有多个 ES 节点发生了严重的 STW 问题。有的节点竟停顿了足足有 7~8 秒。

[Times: user=0.42 sys=0.03, real=7.62 secs]

​ 从日志可以看到在 GC 时用户态只停顿了 420ms,但真实的停顿时间却有 7.62 秒。盘点一下资源,唯一超额利用的可能就是 I/O 资源了(%util 保持在 90 以上),GC 可能在等待 I/O

​ 这种情况是真实存在的,其原因就在于,写 GC 日志的 write 动作,是统计在 STW 的时间里的。在已有的场景中,由于 ES 的索引数据,和 GC 日志放在了一个磁盘,GC 时写日志的动作,就和写数据文件的动作产生了资源争用。

image-20240603210250760

解决方案:把 ES 的日志文件,单独放在一块普通 HDD 磁盘上就可以了

CPU 100%问题排查

1.命令行方式

​ CPU飙升问题排查一般有相应的步骤,按照这个步骤去定位相应的问题(案例可参考**【JVM-JVM排查实践之GC篇】**,分析现实场景中是如何一步步排查定位JVM问题)

👻参考步骤说明(todo:具体案例分析)

步骤说明
【1】top查看每个进程的CPU占用情况
【2】top -Hp $pid查看指定进程中CPU占用最高的线程(确认CPU占用最高的线程PID)
【3】jstack pid > out.log保存线程的堆栈信息(通过jstack指令将占用CPU最高的线程的堆栈信息并进行保存、分析)
该指令将某个进程的堆栈信息输出到out.log文件中
【4】分析线程栈信息转化:先将CPU占用最高的线程的PID转化为16进制
检索:随后在步骤3中输出的堆栈信息日志文件之进行检索,分析其堆栈信息
分析:跟踪线程名称、线程状态

​ jstack用于生成 Java 进程的线程快照(thread dump)。线程快照是一个关于 Java 进程中所有线程当前状态的快照,包括每个线程的堆栈信息。通过分析线程快照,可以了解 Java 进程中各个线程的运行状态、锁信息等

问题排查定位思路

  • 如果cpu使用率上涨,用jstack查看cpu占用最高的线程,确认线程在跑什么代码
  • 如果是GC线程,则进一步跟踪GC曲线是否正常(进行GC排查)
    • 正常情况:看堆内存曲线,正常的曲线是锯齿形
    • 异常情况:一次full GC之后内存没有明显下降,基本可以推断发生内存泄漏了
      • 如果怀疑是内存泄漏的问题,可通过jmap进行分析:
        • 分析1:jmap -heap $pid查看堆内存占用情况
        • 分析2:jmap -histo[:live] pid)查看具体的堆内存对象(分析存活对象数量和内存占用情况)
        • 分析3:使用jamp dump文件,并拉到MAT等工具进行分析

2.Arthas(todo)

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

​ 用 Arthas 查找占用 CPU 最高的方法只是一个开胃小菜,除此之外,它最大的用途是在不改代码、不重启服务的情况下对程序进行动态监控。如果你碰到了线上诡异问题,可以用 Arthas 尝试找一找问题,开阔眼界。(用 Arthas 达到前面用 jstack 同样的目的)

Arthas安装(jar:下载、启动)

# 下载jar
curl -O https://arthas.aliyun.com/arthas-boot.jar

# 启动
java -jar arthas-boot.jar

​ 启动之后,会列出当前这台服务器上的所有 Java 进程,然后选择要排查的那个服务即可

系统排查

​ 参考学习资料:https://mp.weixin.qq.com/s/Ue1HMTAWD5YaV7l5g1g5-w

​ https://afghl.github.io/2017/09/16/production-cpu-100-tracing.html

死锁问题排查

1.死锁概念

​ 死锁是一种特定的程序状态,在实体之间,由于循环依赖导致彼此一直处于等待之中,没有任何个体可以继续前进。死锁不仅仅是在线程之间会发生,存在资源独占的进程之间同样也可能出现死锁。通常来说,大多是聚焦在多线程场景中的死锁,指两个或多个线程之间,由于互相持有对方需要的锁,而永久处于阻塞的状态。

image-20240604075030049

定位死锁最常见的方式就是利用jstack等工具获取线程栈,然后定位互相之间的依赖关系,进而找到死锁。如果是比较明显的死锁,往往jstack等就能直接定位,类似JConsole甚至可以在图形界面进行有限的死锁检测。

​ 如果程序运行时发生了死锁,绝大多数情况下都是无法在线解决的,只能重启、修正程序本身问题。所以,代码开发阶段互相审查,或者利用工具进行预防性排查,往往也是很重要的

​ 大部分死锁本身并不难定位,掌握基本思路和工具使用,理解线程相关的基本概念(各种线程状态和同步、锁、Latch等并发工具)来应对死锁场景。常见的死锁考察思路:

  • 抛开字面上的概念,手撕一个可能死锁的程序:死锁概念、基本的线程编程
  • 诊断死锁有哪些工具,如果是分布式环境,可能更关心能否用API实现吗?
  • 如何在编程中尽量避免一些典型场景的死锁,有其他工具辅助吗?

案例分析1:死锁程序(两个嵌套的synchronized获取锁)

(1)死锁场景模拟
/**
 * 死锁案例场景
 */
public class DeadLockDemo extends Thread {

    private String first;
    private String second;
    public DeadLockDemo(String threadName,String first, String second) {
        super(threadName);
        this.first = first;
        this.second = second;
    }

    // 重写run方法
    @Override
    public void run() {
        // 使用synchronized嵌套获取锁
        synchronized (first){
            System.out.println(this.getName() + "obtained:" + first);
            // 模拟等待时长
            try {
                Thread.sleep(1000L);
                // 嵌套Synchronized获取锁
                synchronized (second){
                    System.out.println(this.getName() + "obtained:" + second );
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    // 死锁测试
    public static void main(String[] args) throws InterruptedException {
        String lockA = "lockA";
        String lockB = "lockB";
        Thread threadA = new DeadLockDemo("threadA",lockA,lockB);
        Thread threadB = new DeadLockDemo("threadB",lockB,lockA);
        // 启动线程
        threadA.start();
        threadB.start();
        threadA.join();
        threadB.join();
    }
}

// output
threadBobtained:lockB
threadAobtained:lockA

结合上述结果分析如下:

  • 虽然线程A优于线程B先启动,但是线程B先执行。这是因为线程的执行依赖于操作系统调度(可设定优先级优先抢占调度资源),但是具体什么时候执行由操作系统决定
  • 发生死锁时,程序不会报异常,但是线程会一直处于阻塞状态,都在等待对方释放资源
    • 例如此处B先占据锁B,A又占据了锁A,线程B在等到锁A被释放,而线程A又在等到锁B被释放,双方陷入僵持,都希望对方先释放资源自己才能执行完成(即互相持有对方需要的锁而无法释放,线程永久阻塞)
(2)死锁排查:jstack

排查思路:选择jstack进行排查

  • 【步骤1】利用jps或者系统的ps命令、任务管理器等工具确认进程ID
# 使用jps获取当前正在执行的java进程
24291 Main
25627 Jps
25230 Launcher
25231 DeadLockDemo
  • 【步骤2】使用jstack获取线程栈信息(treminal对应系统环境JDK为17)
# 使用jstack获取线程栈信息(jstack $pid)
jstack 25321

// output
Full thread dump OpenJDK 64-Bit Server VM (25.412-b08 mixed mode):

"Attach Listener" #15 daemon prio=9 os_prio=31 tid=0x00007f7dd782c800 nid=0x6c03 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"threadB" #14 prio=5 os_prio=31 tid=0x00007f7db6814000 nid=0x7003 waiting for monitor entry [0x000000030f483000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.noob.jvm.DeadLockDemo.run(DeadLockDemo.java:27)
        - waiting to lock <0x000000071573d680> (a java.lang.String)
        - locked <0x000000071573d6b8> (a java.lang.String)

"threadA" #13 prio=5 os_prio=31 tid=0x00007f7db6813000 nid=0x714b waiting for monitor entry [0x000000030f380000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.noob.jvm.DeadLockDemo.run(DeadLockDemo.java:27)
        - waiting to lock <0x000000071573d6b8> (a java.lang.String)
        - locked <0x000000071573d680> (a java.lang.String)

"Service Thread" #12 daemon prio=9 os_prio=31 tid=0x00007f7dd6818800 nid=0x7507 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread3" #11 daemon prio=9 os_prio=31 tid=0x00007f7dd6817800 nid=0x6103 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread2" #10 daemon prio=9 os_prio=31 tid=0x00007f7dd681c800 nid=0x6003 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #9 daemon prio=9 os_prio=31 tid=0x00007f7d97012000 nid=0x7a03 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #8 daemon prio=9 os_prio=31 tid=0x00007f7d97010800 nid=0x5e0f waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"JDWP Command Reader" #7 daemon prio=10 os_prio=31 tid=0x00007f7dd6817000 nid=0x5c03 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"JDWP Event Helper Thread" #6 daemon prio=10 os_prio=31 tid=0x00007f7da6831000 nid=0x5903 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"JDWP Transport Listener: dt_socket" #5 daemon prio=10 os_prio=31 tid=0x00007f7dd883c800 nid=0x7d0b runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=31 tid=0x00007f7dd781a000 nid=0x7e03 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=31 tid=0x00007f7dd8008800 nid=0x511f in Object.wait() [0x000000030e751000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x0000000715588f00> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
        - locked <0x0000000715588f00> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:188)

"Reference Handler" #2 daemon prio=10 os_prio=31 tid=0x00007f7d9700a800 nid=0x4323 in Object.wait() [0x000000030e64e000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x0000000715586b98> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
        - locked <0x0000000715586b98> (a java.lang.ref.Reference$Lock)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

"main" #1 prio=5 os_prio=31 tid=0x00007f7dd7809800 nid=0x2303 in Object.wait() [0x000000030db2d000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000071573d6f0> (a com.noob.jvm.DeadLockDemo)
        at java.lang.Thread.join(Thread.java:1257)
        - locked <0x000000071573d6f0> (a com.noob.jvm.DeadLockDemo)
        at java.lang.Thread.join(Thread.java:1331)
        at com.noob.jvm.DeadLockDemo.main(DeadLockDemo.java:44)

"VM Thread" os_prio=31 tid=0x00007f7da6830000 nid=0x4203 runnable 

"GC task thread#0 (ParallelGC)" os_prio=31 tid=0x00007f7dd880c000 nid=0x3a0b runnable 

"GC task thread#1 (ParallelGC)" os_prio=31 tid=0x00007f7dd880d000 nid=0x3903 runnable 

"GC task thread#2 (ParallelGC)" os_prio=31 tid=0x00007f7dd880d800 nid=0x3703 runnable 

"GC task thread#3 (ParallelGC)" os_prio=31 tid=0x00007f7dd880e800 nid=0x2e03 runnable 

"GC task thread#4 (ParallelGC)" os_prio=31 tid=0x00007f7dd880f000 nid=0x3003 runnable 

"GC task thread#5 (ParallelGC)" os_prio=31 tid=0x00007f7dd8810000 nid=0x3203 runnable 

"GC task thread#6 (ParallelGC)" os_prio=31 tid=0x00007f7dd8810800 nid=0x3303 runnable 

"GC task thread#7 (ParallelGC)" os_prio=31 tid=0x00007f7dd8811800 nid=0x4003 runnable 

"GC task thread#8 (ParallelGC)" os_prio=31 tid=0x00007f7dd8812000 nid=0x4103 runnable 

"VM Periodic Task Thread" os_prio=31 tid=0x00007f7dd6819800 nid=0x7403 waiting on condition 

JNI global references: 1496


Found one Java-level deadlock:
=============================
"threadB":
  waiting to lock monitor 0x00007f7d9680f748 (object 0x000000071573d680, a java.lang.String),
  which is held by "threadA"
"threadA":
  waiting to lock monitor 0x00007f7d9680f8a8 (object 0x000000071573d6b8, a java.lang.String),
  which is held by "threadB"

Java stack information for the threads listed above:
===================================================
"threadB":
        at com.noob.jvm.DeadLockDemo.run(DeadLockDemo.java:27)
        - waiting to lock <0x000000071573d680> (a java.lang.String)
        - locked <0x000000071573d6b8> (a java.lang.String)
"threadA":
        at com.noob.jvm.DeadLockDemo.run(DeadLockDemo.java:27)
        - waiting to lock <0x000000071573d6b8> (a java.lang.String)
        - locked <0x000000071573d680> (a java.lang.String)

Found 1 deadlock.

​ 从显示结果分析,堆栈信息很明显指出线程信息、线程状态、代码文件定位等信息。参考图示相同颜色的圈注进行分析:

  • 线程B持有锁0x000000071573d6b8并等待0x000000071573d680锁,线程A持有锁0x000000071573d680并等待0x000000071573d6b8,双方都持有对方需要的锁

image-20240604084747651

​ 结合上述案例可以通过jstack快速定位死锁问题,但是在实际场景应用中类死锁未必有如此清晰的输出(场景复杂度不一样),但是整体分析思路可以梳理为:区分线程状态=》查看等待目标=》对比Monitor等持有状态。理解线程基本状态和并发相关元素是定位问题的关键,然后配合程序调用栈结构,基本就可以定位到具体的问题代码

(3)死锁排查:程序化方式

​ 如果是开发自己的管理工具,需要用更加程序化的方式扫描服务进程、定位死锁,可以考虑使用Java提供的标准管理API:ThreadMXBeanopen in new window,其直接就提供了findDeadlockedThreads()方法用于定位。在DeadLockDemo基础上构建案例(main中定义方法)如下所示:

// 死锁测试
public static void main(String[] args) throws InterruptedException {
  ThreadMXBean mbean = ManagementFactory.getThreadMXBean();
  Runnable dlCheck = new Runnable() {
    @Override
    public void run() {
      long[] threadIds = mbean.findDeadlockedThreads();
      if (threadIds != null) {
        ThreadInfo[] threadInfos = mbean.getThreadInfo(threadIds);
        System.out.println("Detected deadlock threads:");
        for (ThreadInfo threadInfo : threadInfos) {
          System.out.println(threadInfo.getThreadName());
        }
      }
    }
  };

  ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
  // 稍等5秒,然后每10秒进行一次死锁扫描
  scheduler.scheduleAtFixedRate(dlCheck, 5L, 10L, TimeUnit.SECONDS);
  
  // ---------- 死锁样例代码 ----------
  String lockA = "lockA";
  String lockB = "lockB";
  Thread threadA = new DeadLockDemo("threadA",lockA,lockB);
  Thread threadB = new DeadLockDemo("threadB",lockB,lockA);
  // 启动线程
  threadA.start();
  threadB.start();
  threadA.join();
  threadB.join();
}

// output
threadAobtained:lockA
threadBobtained:lockB
Detected deadlock threads:
threadB
threadA
Detected deadlock threads:
threadB
threadA
Detected deadlock threads:
threadB
threadA

​ 重新编译执行,周期性地进行死锁扫描,可以看到死锁被定位并输出。在实际应用中,就可以据此收集进一步的信息,然后进行预警等后续处理。但是要注意的是,对线程进行快照本身是一个相对重量级的操作,还是要慎重选择频度和时机

2.死锁预防

​ 首先分析例子中死锁的产生包含哪些基本元素。基本上死锁的发生是因为:

  • 互斥条件,类似Java中Monitor都是独占的,要么是我用,要么是你用
  • 互斥条件是长期持有的,在使用结束之前,自己不会释放,也不能被其他线程抢占
  • 循环依赖关系,两个或者多个个体之间出现了锁的链条环

​ 可以据此分析可能的避免死锁的思路和方法

方法1:尽可能避免使用多个锁,并且只有需要时才持有锁

尽量避免使用多个锁,并且只有需要时才持有锁。否则,即使是非常精通并发编程的工程师,也难免会掉进坑里,嵌套的synchronized或者lock非常容易出问题。

一个JDK改进死锁例子open in new window, Java NIO的实现代码向来以锁多著称,一个原因是,其本身模型就非常复杂,某种程度上是不得不如此;另外是在设计时,考虑到既要支持阻塞模式,又要支持非阻塞模式。直接结果就是,一些基本操作如connect,需要操作三个锁以上,在其中一个JDK改进中,就发生了死锁现象。简化伪代码如下,分析其栈信息

// Thread HttpClient-6-SelectorManager: 持有readLock/writeLock
readLock.lock();
writeLock.lock();
// 调用close()试图获得closeLock
close();

// Thread HttpClient-6-Worker-2 持有closeLock
implCloseSelectableChannel (); //想获得readLock

​ 在close发生时, HttpClient-6-SelectorManager线程持有readLock/writeLock,试图获得closeLock;与此同时,另一个HttpClient-6-Worker-2线程,持有closeLock,试图获得readLock,这就不可避免地进入了死锁。简单来说线程1一开始持有锁A、锁B,在调用close方法的时候试图获取锁C,但是与此同时线程2持有锁C,且试图获取锁A,双方互相占用对方所需要的锁,因此不可避免进入死锁。结合官方案例的线程栈信息

image-20240604094445934

​ 参考线程栈的死锁说明

image-20240604094647642

​ 结合上述线程栈信息提示,可以跟踪到具体代码的实现:查看SocketChannelImplopen in new window的663行,对比implCloseSelectableChannel()方法实现和AbstractInterruptibleChannel.close()open in new window在109行的代码。即调用close方法需要获取到closeLock

image-20240604095103205

​ 从程序设计的角度反思,如果赋予一段程序太多的职责,出现“既要…又要…”的情况时,可能就需要重新审视下设计思路或目的是否合理了。对于类库,因为其基础、共享的定位,比应用开发往往更加令人苦恼,需要仔细斟酌之间的平衡。

方法2:如果必须使用多个锁,必须设计好锁的获取顺序

扩展:理解银行家算法open in new window

​ 如果必须使用多个锁,必须设计好锁的获取顺序,但实践起来往往比较复杂,参考银行家算法。一般情况下,可以考虑才去一些简单的辅助手段,例如以图示的方式进行分析,以前面的DeadLockDemo为例,其分析步骤如下所示

image-20240604191141112

【步骤1】抽离对象(方法)和锁的关系(用图形化的方式进行展示)

【步骤2】根据对象之间组合、调用的关系对比,考虑调用时序

【步骤3】按照可能时序进行合并,发现可能死锁的场景

方法3:超时处理

​ 使用带超时的方法,为程序带来更多可控性。

​ 类似Object.wait(…)或者CountDownLatch.await(…),都支持所谓的timed_wait,完全可以就不假定该锁一定会获得,指定超时时间,并为无法得到锁时准备退出逻辑。

​ 并发Lock实现,如ReentrantLock还支持非阻塞式的获取锁操作tryLock(),这是一个插队行为(barging),并不在乎等待的公平性,如果执行时对象恰好没有被独占,则直接获取锁。有时,如果希望条件允许就尝试插队,不然就按照现有公平性规则等待,一般采用下面的方法:

    if (lock.tryLock() || lock.tryLock(timeout, unit)) {
      	// ...
       }

方法4:其他方式

​ 也有一些其他方面的尝试,比如通过静态代码分析(如FindBugs)去查找固定的模式,进而定位可能的死锁或者竞争情况。实践证明这种方法也有一定作用,请参考相关文档open in new window

​ 除了典型应用中的死锁场景,其实还有一些更令人头疼的死锁,比如类加载过程发生的死锁,尤其是在框架大量使用自定义类加载时,因为往往不是在应用本身的代码库中,jstack等工具也不见得能够显示全部锁信息,所以处理起来比较棘手。对此,Java有官方文档open in new window进行了详细解释,并针对特定情况提供了相应JVM参数和基本原则

概念扩展

JVM内部线程

​ 启动一个简单的“Hello World”示例程序,也会在 Java 进程中创建几十号线程,这几十个线程主要是 JVM 内部线程,以及 Lib 相关的线程(例如引用处理器、终结者线程等等)

JVM 内部线程主要分为以下几种:

  • VM 线程:单例的 VMThread 对象,负责执行 VM 操作,下文将对此进行讨论;
  • 定时任务线程:单例的 WatcherThread 对象,模拟在 VM 中执行定时操作的计时器中断;
  • GC 线程:垃圾收集器中,用于支持并行和并发垃圾回收的线程;
  • 编译器线程:将字节码编译为本地机器代码;
  • 信号分发线程:等待进程指示的信号,并将其分配给 Java 级别的信号处理方法。

JVM 中的所有线程都是 Thread 实例,而所有执行 Java 代码的线程都是(Thread 的子类)JavaThread 的实例

JVM 在链表 Threads_list 中跟踪所有线程,并使用 Threads_lock 来保护(这是 JVM 内部使用的一个核心同步锁)

1.线程转储

线程转储(简而言之,线程转储就是线程快照)

​ 线程转储(Thread Dump)是 JVM 中所有线程状态的快照。一般是文本格式,可以将其保存到文本文件中,然后可以人工查看和分析,也可以通过程序自动分析。

​ 每个线程的状态都可以通过调用栈来表示(StackTrace方法调用栈)。线程转储展示了各个线程的行为,对于诊断和排查问题非常有用

JVM 支持多种方式来进行线程转储,包括:

  • JDK 工具,包括:jstack 工具、jcmd 工具、jconsole、jvisualvm、Java Mission Control 等;
  • Shell 命令或者系统控制台, 比如 Linux 的 kill -3、Windows 的 Ctrl + Break 等;
  • JMX 技术,主要是使用 ThreadMxBean,可以在程序中嵌入

(1)jstack

一般使用 JDK 自带的命令行工具来获取 Java 应用程序的线程转储(jstack)

# jstack指令用于执行线程转储的。一般连接本地 JVM:
jstack [-F] [-l] [-m] <pid>

# 参考示例(导出指定线程的线程转储信息到指定文件)
jstack 8848 > ./threandump.txt

pid 是指对应的 Java 进程 id,使用时支持如下的选项:

  • -F 选项强制执行线程转储;有时候 jstack pid 会假死,则可以加上 -F 标志
  • -l 选项,会查找堆内存中拥有的同步器以及资源锁
  • -m 选项,额外打印 native 栈帧(C 和 C++ 的)

(2)jcmd

​ 向JVM发送一串命令

jcmd 8248 Thread.print

(3)JMX方式

​ JMX方式技术支持各种各样的花式操作,可以通过 ThreadMxBean 来线程转储

import java.lang.management.*;
/**
 * 线程转储示例
 */
public class JMXDumpThreadDemo {
    public static void main(String[] args) {
        String threadDump = snapThreadDump();
        System.out.println("=================");
        System.out.println(threadDump);
    }
    public static String snapThreadDump() {
        StringBuffer threadDump = new StringBuffer(System.lineSeparator());
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        for (ThreadInfo threadInfo : threadMXBean.dumpAllThreads(true, true)) {
            threadDump.append(threadInfo.toString());
        }
        return threadDump.toString();
    }
}


// output
=================

"JDWP Command Reader" Id=7 RUNNABLE (in native)

"JDWP Event Helper Thread" Id=6 RUNNABLE

"JDWP Transport Listener: dt_socket" Id=5 RUNNABLE

"Signal Dispatcher" Id=4 RUNNABLE

"Finalizer" Id=3 WAITING on java.lang.ref.ReferenceQueue$Lock@55f96302
	at java.lang.Object.wait(Native Method)
	-  waiting on java.lang.ref.ReferenceQueue$Lock@55f96302
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
	at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:188)

"Reference Handler" Id=2 WAITING on java.lang.ref.Reference$Lock@3d4eac69
	at java.lang.Object.wait(Native Method)
	-  waiting on java.lang.ref.Reference$Lock@3d4eac69
	at java.lang.Object.wait(Object.java:502)
	at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
	at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

"main" Id=1 RUNNABLE
	at sun.management.ThreadImpl.dumpThreads0(Native Method)
	at sun.management.ThreadImpl.dumpAllThreads(ThreadImpl.java:496)
	at sun.management.ThreadImpl.dumpAllThreads(ThreadImpl.java:484)
	at com.noob.jvm.JMXDumpThreadDemo.snapThreadDump(JMXDumpThreadDemo.java:16)
	at com.noob.jvm.JMXDumpThreadDemo.main(JMXDumpThreadDemo.java:9)

线程dump结果:简单分析上述线程栈信息

  • JDWP 相关的线程
  • Signal Dispatcher,将操作系统信号(例如 kill -3 )分发给不同的处理器进行处理,也可以在程序中注册自己的信号处理器
  • Finalizer,终结者线程,处理 finalize 方法进行资源释放
  • Reference Handler,引用处理器
  • main,这是主线程,属于前台线程,本质上和普通线程没什么区别
  • 如果程序运行时间比较长,除了业务线程之外还会有一些GC线程

2.死锁案例

​ 参考前面的死锁案例(和下面的案例类似),实际上实现逻辑就是:两个锁获取的顺序不同、两个线程都在死等对方的资源,可以设计一个死锁案例,思路如下:

  • 定义两个锁(Object)
  • 分别定义两个线程:t1、t2
    • 线程t1的run方法:先获取锁A,然后获取锁B
    • 线程t2的run方法:先获取锁B,然后获取锁A
  • main方法中分别启动这两个线程然后查看死锁情况
/**
 * 线程死锁Demo
 */
public class ThreadDeadLockDemo {

    // 定义锁
    private static Object lockA = new Object();
    private static Object lockB = new Object();

    public static class Thread1 extends Thread{
        @Override
        public void run() {
            synchronized (lockA){
                System.out.println(Thread.currentThread().getName() + "占据了锁A");
                synchronized (lockB){
                    System.out.println(Thread.currentThread().getName() + "占据了锁B");
                }
            }
        }
    }

    public static class Thread2 extends Thread{
        @Override
        public void run() {
            synchronized (lockB){
                System.out.println(Thread.currentThread().getName() + "占据了锁B");
                synchronized (lockA){
                    System.out.println(Thread.currentThread().getName() + "占据了锁A");
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread1 t1 = new Thread1();
        Thread2 t2 = new Thread2();
        t1.start();
        t2.start();
    }
}

// output
Thread-1占据了锁B
Thread-0占据了锁A

​ 启动程序,然后可以通过命令行工具转储线程栈信息

  • jps查看当前Java进程
  • jstack $pid:转储线程栈信息
  • jcmd $pid:转出线程栈信息到指定文件

3.可视化工具

jconsole可视化工具

jconsole参考学习文章open in new window,此处以mac进行演示

(1)死锁案例分析

【1】输入jconsole指令,弹出窗口(本地进程:类似jps效果,获取到当前的Java进程)

image-20240604194857756

【2】选择本地进程,进入分析页面

image-20240604195428585

【3】确认要分析的进程,点击进入,分析内容

​ Tab页面:概述、内存、线程、类、VM概要、Mbean

  • 内存Tab等价于可视化的jstat命令
  • 线程Tab等价于可视化的jstack命令

image-20240604195814414

【4】选择【线程Tab】,检查死锁

image-20240604200225286

(2)案例场景

案例1:模拟按照一定速率向堆中填充数据,借助jconsole工具查看内存变化

image-20240604201302861

案例2:线程监控

线程监控相当于可视化的jstack,遇到线程停顿的时候,可以使用它来监控。 线程长时间停顿的主要原因:等待外部资源(数据库连接、网络资源、设备资源等)、死循环、锁等待。

Java Visual VM

参考学习链接open in new window

image-20240604201834563

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3