《深入理解Java虚拟机》学习笔记。
在定位问题的时候,知识和经验是关键基础,数据是依据,而工具是运用知识处理数据的手段…
jstack
命令用于生成虚拟机当前时刻的线程快照堆转储快照(一般称为threaddump或javacore文件),所以jstack
工具主要用于分析线程相关。
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstack.html
jstack: Java堆栈跟踪工具
1 简介
jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或javacore文件)。线程快快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。线程出现停顿的时候通过jstack
来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
2 用法
1 | jstack -help |
Options:
选项 | 说明 |
---|---|
-F | 当正常输出请求不被响应时,强制输出线程堆栈 |
-m | 同时打印java和本地C/C++方法堆栈 |
-l | 长列表,打印锁信息 |
-h or -help | 打印帮助信息 |
jstack的两个主要功能:
- 一是获取当前活着的进程的threaddump文件,使用命令
jstack [-l] <pid>
和jstack -F [-m] [-l] <pid>
; - 二是对java程序崩溃已经生成的core文件解析获取threaddump文件,使用命令
jstack [-m] [-l] <executable> <core>
对core文件分析举例:
1 | jstack /usr/bin/java core.13221 |
远程连接不常用,不做深入讨论。
获取threaddump文件命令很简单很容易,但如何分析threaddump文件,需要了解一些基本概念。
3 玩转threaddump文件
3.1 Java多线程Monitor对象
可以参考:
https://blog.csdn.net/hello_worldee/article/details/77919549
https://blog.csdn.net/boyeleven/article/details/81390738
Java多线程中,monitor是实现线程互斥和协作的主要手段,monitor可以理解为对象或Class的一把锁,一个对象有且仅有一个monitor对象。
**进入区(Entrt Set)**:表示线程通过
synchronized
要求获取对象的锁。
- 如果对象未被锁住,则进入拥有者;否则则在进入区等待。
- 一旦对象锁被其他线程释放,立即参与竞争。
**拥有者(The Owner)**:表示某一线程成功竞争到对象锁。
**等待区(Wait Set)**:表示线程通过对象的wait方法,释放对象的锁,并在等待区等待被唤醒。
一个 Monitor在某个时刻,只能被一个
Active Threa”
线程拥有 ,而其它线程都是Waiting Thread
(在两个队列Entry Set
和Wait Set
里面等候)。
- 在
Entry Set
中等待的线程状态是Waiting for monitor entry
- 在
Wait Set
中等待的线程状态是in Object.wait()
。
Entry Set
里面的线程:我们称被 synchronized保护起来的代码段为临界区。当一个线程申请进入临界区时,它就进入了 “Entry Set”队列。对应的 code:
1
2
3
4 >synchronized(obj) {
.........
>}
3.2 线程状态转换
我们还需要了解线程的状态。Java定义了5中线程状态,在任意一个时间点,一个线程只能有且只有其中一种状态。
可以阅读《深入理解Java虚拟机》12.4.3小节。
新建(NEW):创建后尚未启动的线程处于这种状态。该状态不会出现在threaddump文件中。
运行(RUNNABLE):Runabele包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间。如果含有
locked
,说明它获得了一把锁。无限期等待(WAITING):处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒。通常是被加了锁。以下方法会让线程陷入无限期的等待状态:
- 没有设置Timeout参数的Object.wait()方法
- 没有设置Timeout参数的Thread.join()方法
- LockSupport.park()方法
限期等待(TIMED_WAITING):处于这种状态的线程也不会被分配CPU执行时间,不过无须等待被其他线程显示地唤醒,在一定时间之后他们会由系统自动唤醒。以下方法会让线程进入限期等待状态:
- Thread.sleep()方法。
- 设置了Timeout参数的Object.wait()方法。
- 设置了Timeout参数的Thread.join()方法。
- LockSupport.parkNanos()方法。
- LockSupport.parkUtil()方法。
阻塞(BLOCKED):线程被阻塞了,“阻塞状态”与“等待状态”的区别是:“阻塞状态”在等待着获取到一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。
结束(TERMINATED):已终止线程的线程状态,线程已经结束执行。
3.3 方法调用修饰符
在threaddump文件中,线程在重要的方法调用时,会加上额外的修饰符,便于我们了解该方法的状态。
3.3.1 locked
使用synchronized申请对象锁成功,监视器的拥有者。
1 | locked <地址> 目标 |
示例:
1 | "hiveserver2-web-46 Acceptor4 SelectChannelConnector@0.0.0.0:10013" #46 daemon prio=5 os_prio=0 tid=0x00007f4e6117c800 nid=0xe817 runnable [0x00007f4e26458000] |
说明当前线程获得了锁 <0x00000005c0029328> (a java.lang.Object) ,当前线程拥有监视器。
3.3.2 waiting to lock
使用synchronized申请对象锁未成功,在进入区等待。
示例:
1 | "hiveserver2-web-45 Acceptor3 SelectChannelConnector@0.0.0.0:10013" #45 daemon prio=5 os_prio=0 tid=0x00007f4e6117b000 nid=0xe816 waiting for monitor entry [0x00007f4e264d9000] |
在进入区等待锁 <0x00000005c0029328> (a java.lang.Object)
3.3.3 waiting on
使用synchronized申请对象锁成功后,释放锁并在等待区等待。
示例:
1 | "HiveServer2-Handler-Pool: Thread-55" #55 prio=5 os_prio=0 tid=0x00007f4e4c42c800 nid=0xe999 waiting on condition [0x00007f4e27363000] |
线程HiveServer2-Handler-Pool释放锁之后,在等待区等待。
3.3.4 parking to wait for
park
是基本的线程阻塞原语,不通过监视器在对象上阻塞。
示例:
1 | "HiveServer2-Handler-Pool: Thread-7365" #7365 prio=5 os_prio=0 tid=0x00007f4e4c404800 nid=0x2456e waiting on condition [0x00007f4e24d15000] |
3.3.5 死锁deadlocak
如果发生死锁,JDK5+版本的dump中会提供报告。可以搜索deadlock
来判断是否存在死锁。
3.4 线程的动作及其状态间的关系
通常线程动作和线程状态是有对应关系的,之所以我们说线程动作,因为一个线程动作可能对应多种线程状态。
3.4.1 runnable
运行中,该状态通常是RUNNABLE。
示例:
1 | "hiveserver2-web-46 Acceptor4 SelectChannelConnector@0.0.0.0:10013" #46 daemon prio=5 os_prio=0 tid=0x00007f4e6117c800 nid=0xe817 runnable [0x00007f4e26458000] |
3.4.2 in Object.wait()
等待区等待状态,该状态是WAITING或TIMED_WAITING。
示例:
1 | "org.apache.hadoop.fs.FileSystem$Statistics$StatisticsDataReferenceCleaner" #23 daemon prio=5 os_prio=0 tid=0x00007f4e61970800 nid=0xe3d0 in Object.wait() [0x00007f4e2bd36000] |
3.4.3 waiting for monitor entry
进入区等待,该状态为BLOCKED。
示例:
1 | "hiveserver2-web-49 Acceptor5 SelectChannelConnector@0.0.0.0:10013" #49 daemon prio=5 os_prio=0 tid=0x00007f4e62343000 nid=0xe819 waiting for monitor entry [0x00007f4e26356000] |
3.4.4 waiting on condition
该状态出现在线程等待某个条件的发生。具体是什么原因,需要结合 stacktrace 来分析。
示例:
1 | "hiveserver2-web-7399" #7399 daemon prio=5 os_prio=0 tid=0x00000000019ac000 nid=0x11a8 waiting on condition [0x00007f4e24511000] |
最常见的情况就是线程处于sleep状态,等待被唤醒。
常见的情况还有等待网络IO:在java引入nio之前,对于每个网络连接,都有一个对应的线程来处理网络的读写操作,即使没有可读写的数据,线程仍然阻塞在读写操作上,这样有可能造成资源浪费,而且给操作系统的线程调度也带来压力。在 NewIO里采用了新的机制,编写的服务器程序的性能和可扩展性都得到提高。
如果发现有大量的线程都处在 Wait on condition,从线程 stack看, 正等待网络读写,这可能是一个网络瓶颈的征兆,因为网络阻塞导致线程无法执行。
> 一种情况是网络非常忙,几 乎消耗了所有的带宽,仍然有大量数据等待网络读写;
>
> 另一种情况也可能是网络空闲,但由于路由等问题,导致包无法正常的到达。
所以要结合系统的一些性能观察工具来综合分析,比如 netstat统计单位时间的发送包的数目,如果很明显超过了所在网络带宽的限制 ; 观察 cpu的利用率,如果系统态的 CPU时间,相对于用户态的 CPU时间比例较高;这些都指向由于网络带宽所限导致的网络瓶颈。
参考:http://www.blogjava.net/jzone/articles/303979.html
3.5 JVM线程种类
通常,通过threaddump文件主要包括:jvm自身线程、用户线程等。其中jvm线程会在jvm启动时就会存在。对于用户线程则是在用户访问时才会生成。
3.5.1 两个重要的JVM线程:Attach Listener和Signal Dispatcher
每个JVM都会有Signal Dispatcher
线程,用于处理信号。
Attach Listener
线程用于JVM进程间的通信,但是它不一定会启动。
1 | "Attach Listener" #7400 daemon prio=9 os_prio=0 tid=0x00007f4e4ca40800 nid=0x12c3 waiting on condition [0x0000000000000000] |
1 | "Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007f4e600df800 nid=0xe3b9 runnable [0x0000000000000000] |
3.5.2 HotSpot VM Thread
被HotSpot VM管理的内部线程为了完成内部本地操作,通常不关心,除非CPU很高。
1 | "VM Periodic Task Thread" os_prio=0 tid=0x00007f4e60133000 nid=0xe3c8 waiting on condition |
3.5.3 HotSpot GC Thread
当使用HotSpot parallel GC 时,HotSpot VM默认创建一定数目的GC thread。
1 | "GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007f4e6002a000 nid=0xe3a4 runnable |
在GC频繁和疑似内存泄漏时,可以查看他们的nid是否有关联。
3.5.4 JNI global reference
JNI global reference 是基本的对象引用,从本地代码到被Java GC管理的Java对象的引用。其角色是阻止仍然被本地代码使用的对象集合,但在Java代码中没有引用。在探测JNI相关内存泄露时,关注JNI references很重要。
1 | JNI global references: 299 |
3.5.5 Java Heap utilization view
在程序奔溃后生成的core文件的最后,可以看到奔溃时的内存使用情况。
3.6 读懂 threaddump文件
3.6.1 文件头
1 | 2020-04-06 10:37:02 |
dump时间:2020-04-06 10:37:02
JVM版本信息:Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.77-b03 mixed mode):
3.6.2 线程信息
1 线程头信息
1 | "Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007f4e600df800 nid=0xe3b9 runnable [0x0000000000000000] |
线程名称:Signal Dispatcher
守护线程标记:daemon,可以没有
线程优先级:prio=9
系统优先级:os_prio=0,并不是所有的操作系统都支持线程优先级,所以可能会出现0的情况
JVM中的线程ID:tid=0x00007f4e600df800,也有说法是内存地址(不深究)
本地线程ID:nid=0xe3b9,通过top -Hp
线程动作:runnable ,和线程状态对应,也可以理解为线程状态。
起始栈地址:[0x00007f4e27363000]
2 线程状态
1 | java.lang.Thread.State: WAITING (parking) |
这里表示线程在等待,括号里列出了等待的原因。parking是说调用了LockSupport.park()
方法导致等待。
3 线程栈
除了上面的内容,剩余部分就是线程堆栈了,这里会包含锁信息locked。
一个完整的线程信息:
1 | "HiveServer2-Handler-Pool: Thread-55" #55 prio=5 os_prio=0 tid=0x00007f4e4c42c800 nid=0xe999 waiting on condition [0x00007f4e27363000] |
3.6.3 分析threaddump文件
进入区等待:
线程状态:BLOCKED
线程动作:wait on monitor entry
方法调用修饰符:waiting to lock
如果上述三者总是一起出现。说明代码级别存在冲突的调用,需要避免这种情况。
同步块阻塞:
当大量线程等待同一把锁时,很有可能出现了同步块阻塞。
如果捕获到RUNNABLE状态的IO操作,通常是有问题的。比如发现JDBC调用栈,此时很有可能发生了数据库死锁。
分线程调度的休眠:
通常线程池的线程等待是正常的。
守护线程daemon无限期等待是符合预期,但非守护线程的无限期等待就需要格外注意。
Full GC 的影响:
在Full GC时,会阻塞所有的用户线程,所以,获取到同步锁的线程也有可能被阻塞。在分析threaddump文件时,格外注意Full GC 情况。
4 最佳实践
4.1 服务器CPU持续飙高的排查方法
找到定位长期占用CPU的线程,抓取jstack现场,分析该线程在做什么。
4.1.1 消耗CPU的进程定位
使用top命令:我们定位到进程 149454
1 | top |
现场保留:为方便分析,通常我们会将top
快照截图保存。
4.1.2 消耗CPU的线程定位
使用命令top -Hp <pid>
:
1 | top -Hp 149501 |
或者,进入top
,然后shift + h
切换到线程界面查看。
这里我们看到线程id是 149501。
如何查看一个进程下的所有线程呢?
可以通过命令查看:cat /proc/
/task 有什么用呢?通常我们可以看看某个服务进程的线程数是否打满:
ll /proc/
/task | wc -l
现场保留:为方便分析,通常我们会将top
快照截图保存。
4.1.3 获取threaddump文件
threaddump文件的获取和top快照的查看可以并行。
1 | jps |
4.1.4 查到到线程的栈
在threaddump文件中nid是十六进制表示,所以我们需要将线程id转为十六进制:
1 | printf '%x\n' 149501 |
然后在threaddump文件中搜索247fd找到对应的线程栈,即可分析。
4.2 CPU不高但响应慢的排查方法
此时CPU不高,我们不能通过占用CPU的进程和线程来入手了。
响应缓慢通常是网络IO,访问数据库等。排查思路首先获取threaddump文件,然后重点查看网络IO相关和数据库相关。比如搜索关键字Socket, JDBC等快速定位。
4.3 服务hang住无响应的排查方法
此时,通常CPU不高,load也不高,但服务就是没有响应。
排查思路,多次dump获取不同时间的dump文件,然后对比RUNNABLE状态的线程。
5 总结
至此,JVM性能分析的工具学习告一段落。通过jps可以查看java进程,jstat可以分析内存gc情况,jinfo可以查看设置JVM参数,jmap获取堆转储dump快照文件用于分析内存泄漏等问题,还可以通过histo查看对象数量排名情况,jstack获取线程的dump快照文件,主要用于分析CPU飙高,服务响应慢或hang住的问题。
6 Ths
54686973 20617274 69636C65 20697320 64656469 63617465 6420746F 20526F6E 67657220 77686F20 49206465 65706C79 206C6F76 65642E
本文链接: https://stefanxiepj.github.io/archives/1dec3e60.html
版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!
![知识共享许可协议](https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png)