我们最先接触的排查问题的方法是DEBUG和异常报错信息,已能解决我们开发时大多数的情况。但有时问题并不是我们代码的问题,定位这些问题则需要jvm提供的工具。
排查工具
jstack
介绍
jstack命令工具可以得到线程堆栈信息,方便分析。
有什么用?
- 可以检测出死锁
- 分析线程的状态,观察那里出现了阻塞
使用方法
前置知识
jstack中线程的状态:
- NEW(新建状态) 新创建了一个线程对象。
- RUNNABLE(运行态) 线程对象创建后,其他线程调用了该对象的start()方法。分就绪态和运行态
- BLOCKED(阻塞状态) 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
- WAITING(等待态) 线程处于等待态表示它需要等待其他线程的指示才能继续运行
- TIMED_WAITING(超时等待态) 当线程调用sleep(time)、wait、join、parkNanos、parkUntil时,就会进入该状态。与等待态的区别:到了超时时间后自动进入阻塞队列,开始竞争锁。
- TERMINATED(终止态) 线程执行结束后的状态。
jstack日志中出现的关键信息
- Deadlock: 表示有死锁
- Wait on condition: 等待某个资源或条件发生来唤醒自己。
- Blocked: 阻塞
- Waiting on monitor entry:在等待获取锁(说明此线程通过 synchronized(obj) {……} 申请进入了临界区,从而进入了下图1中的“Entry Set”队列,但该 obj 对应的 monitor 被其他线程拥有,所以本线程在 Entry Set 队列中等待。)
- in Object.wait():获取锁后又执行obj.wait()放弃锁
- TIMED_WAITING (parking)”中的 timed_waiting 指等待状态,但这里指定了时间,到达指定的时间后自动退出等待状态;parking指线程处于挂起中。
- waiting to lock <0x00000000acf4d0c0>”指,线程在等待给这个 0x00000000acf4d0c0 地址上锁(如果能在日志里找到谁获得了这个锁(如locked < 0x00000000acf4d0c0 >),就可以顺藤摸瓜了)
日志样例
import java.util.concurrent.TimeUnit; public class MainThread { public static void main(String[] args) { final Thread thread2 = new Thread(){ public void run(){ synchronized (this){ try{ System.out.println(Thread.currentThread()); TimeUnit.SECONDS.sleep(60); }catch (InterruptedException e){ e.printStackTrace(); } } } }; thread2.setName("thread2"); thread2.start(); synchronized (thread2){ try{ System.out.println(Thread.currentThread()); TimeUnit.SECONDS.sleep(60); } catch (InterruptedException e) { e.printStackTrace(); } } } }
"main" #1 prio=5 os_prio=0 cpu=93.75ms elapsed=30.25s tid=0x0000027193f7e290 nid=0x4d00 waiting on condition [0x000000fe36fff000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(java.base@17/Native Method)
at java.lang.Thread.sleep(java.base@17/Thread.java:337)
at java.util.concurrent.TimeUnit.sleep(java.base@17/TimeUnit.java:446)
at com.leecode.test.MainThread.main(MainThread.java:25)
- locked <0x0000000711ae4be8> (a com.leecode.test.MainThread$1)
Locked ownable synchronizers:
- None
"thread2" #16 prio=5 os_prio=0 cpu=0.00ms elapsed=30.16s tid=0x00000271b7e8e020 nid=0x534c waiting for monitor entry [0x000000fe383ff000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.leecode.test.MainThread$1.run(MainThread.java:11)
- waiting to lock <0x0000000711ae4be8> (a com.leecode.test.MainThread$1)
Locked ownable synchronizers:
- None
- "thread2"为线程名称,在平时创建线程或线程池时请务必取一个见明之义的线程名称,方便排查问题;
- prio=5:线程优先级,不用关心;
- tid=0x00000271b7e8e020:线程id,不用关心;
- nid=0x534c:操作系统映射的线程id, 非常关键,后面再使用jstack时补充;
- waiting for monitor entry:表示线程正在等待获取锁
- 0x000000fe383ff000:线程栈起始地址
分析:
主线程获取到thread2对象上的锁,因此正在执行sleep操作,状态为TIMED_WAINTING, 而thread2由于未获取到thread2对象上的锁,因此处于BLOCKED状态。
thread2 正在"waiting to lock <0x0000000711ae4be8>",即试图在地址为0x0000000711ae4be8所在的对象获取锁,而该锁却被main线程占有(locked <0x0000000711ae4be8>)。main线程正在"waiting on condition",说明正在等待某个条件触发,由jstacktrace来看,此线程正在sleep。
Linux排查命令:
- top 命令:查看哪个进程占用 CPU 过高。定位到 pid
- top -Hp pid 命令:查看问题进程中的线程情况。
- jstack pid | grep nid -C10 :查看对应的线程前后 10 行的状态信息(注意,先使用
printf '%x\n' nid 或者其他方式,将十进制的 nid 转换为十六进制的 nid
)
在线定位(Arthas 工具)
Arthas 是阿里开源的 Java 诊断工具。
- 先下载 arthas-boot.jar 包,直接通过 java -jar 命令启动,然后会让列出所有正在运行的 java 进程,让用户选择需要监控的进程,之后会进入 Arthas 的操作界面
- 在操作界面输入 dashboard 命令,可以看到所监控的进程的所有线程信息(线程 ID、名称、状态、占用 CPU 情况、占用内存情况、是否为守护线程等等)、内存信息(堆内存、Eden 区、Survivor 区、老年代、方法区)、以及机器情况。
- 通过 sysporp 命令可以查看所有的 System Properties 信息。
- 通过 sysenv 命令可以查看所有的环境变量信息。
- JVM 命令,查看当前的进程使用的 JVM 参数。
- 在知道了线程 id 之后,可以通过 thread thread_id 的方式查看某个线程正在执行的堆栈信息。
- 通过 sc 命令可以查看已经加载过的类的信息。
- 通过 sm 类名的方式可以获取类的所有函数,添加 -d 可以获取详细的函数信息,也可以指定查看某个函数
- 通过 jad 命令可以进行反编译代码,该功能可以帮助我们查看动态代理生成了什么样的类(例如先通过 sc + 通配的方式定位到某个类,再通过 jad 命令反编译得出该类的代码)
- 通过 watch 命令可以查看当前函数的参数/返回值/异常信息
- ognl命令,用于动态执行代码,在当前线程环境中执行代码。(
ognl '@java.lang.System@out.println("hello ognl")'
) - 使用 redefine /path/xxx.class 的方式,能重新加载编译好的类。通常可以很方便的实现一些热修复。其实和 IDEA 中 tomcat 的热部署原理一样,就是用 ClasssLoader 重新 load 一遍修改的类。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。