分类 Troubleshooting 相关 下的文章

如何生成 thread dump

在分析应用响应变慢, CPU 使用率比较高, 死锁等问题时, thread dump 都非常有用. 那么怎么样才能获取一个Java 应用的 thread dump 呢?

  1. 如果有 JDK, 那么 JDK 再带的小工具能非常快的做出 thread dump.
    首先, 使用 JDK bin 目录下的 jps 命令, 能非常快的确认正在运行的 java 进程, 打印进程 id.

    33205 MyApp
    93946 Jps
    

    上面的输出当中, 93946是 jps 命令本身的进程 id, 因为它也是一个 java 进程;
    得到进程 ID 之后, 有下面这2种方法, 可以获取 thread dump:

    1. 使用 jstack 命令
      jstack -l > <file-path>
      jstack -l 33205 > /tmp/threadDump.txt

    2. 使用 jcmd 命令
      jcmd Thread.print > <file-path>
      jcmd 33205 Thread.print /tmp/threadDump.txt

  2. 在某些生产环境, 可能只安装 JRE, 没有安装 JDK, 那么可以使用 kill -3 命令, 给Java 进程发信号, 让它产生 thread dump.

  3. 如果你是在一个桌面系统, 可以使用JDK 自带的 GUI 工具: Java Mission Control 或者 JVisualVM.

  4. 如果想使用编程的方式, 可以使用 ThreadMXBean, 它能 dump 出 Java 进程的所有线程.

    ThreadMXBean threadMxBean = ManagementFactory.getThreadMXBean();
        for (ThreadInfo ti : threadMxBean.dumpAllThreads(true, true)) {
            System.out.print(ti.getThreadId());
            System.out.print(ti.getThreadName());
            System.out.print(ti.getThreadState());
            System.out.print(ti.getStackTrace());
            System.out.print(ti.getLockInfo());
        }
}
  1. 在某些 Windows 桌面环境, 如果你是通过命令行启动, 可以通过 Ctrl + Break 来生成 thread dump

jstat

jstat 用来显示 JVM 的某些指标的统计信息, 可以每隔固定时间采集一次,设置共采集多少次. 主要用来采集 GC 和运行时编译相关的统计信息.

比如下面诊断为什么系统一直 full GC, 最后发现是人为的调用 System.gc().

jstat -gccause 332 3s 5 //332 是进程id, 每 3s 采集一次, 共采集 5 次z

image1.png

下面这个例子采集 GC 的统计信息, S0, S1 分别代表 survivor 0 和 survivor 1, E 代表 Eden, O 代表 Old, P 代表 Permermanent, C 代表 Capacity, U 代表 Used. YGC 和 FGC 分别代表 Young generation 和 Full generation GC, 后面的 T 代表花费的的时间 (Time)

jstat -gc 332 3s 5

image2.png

另外 -options 显示所有可以采集的项目

$ ./jstat -options
-class
-compiler
-gc
-gccapacity
-gccause
-gcnew
-gcnewcapacity
-gcold
-gcoldcapacity
-gcpermcapacity
-gcutil
-printcompilation

jps

jps 相当于 Linux ps命令, 不过它只显示 java 进程. 通常我们我们为了做 thread dump, heap dump 的第一步就是找到这个进程 id, jps 能很快的帮我们找到这个进程 id. 默认情况下 jps 只会显示当前用户的 java 进程, 如果要查看的应用程序是属于另外一个用户的进程, 那么要切换到另外一个用户, 或者使用 sudo jps.

workstation:~ xiatian$ jps
10824 Jps
95833 gcviewer-1.36-SNAPSHOT.jar

// -v 显示启动 jvm 时候的参数

workstation:~ xiatian$ jps -v 
10833 Jps -Dapplication.home=/Library/Java/JavaVirtualMachines/jdk-11.0.3.jdk/Contents/Home -Xms8m -Djdk.module.main=jdk.jcmd
95833 gcviewer-1.36-SNAPSHOT.jar

//显示 main 函数的全包名, 如果是 jar 的话, 显示全路径

workstation:~ xiatian$ jps -l
10836 jdk.jcmd/sun.tools.jps.Jps
95833 /Users/xiatian/Desktop/gcviewer-1.29/gcviewer-1.36-SNAPSHOT.jar

基于 Java 的互联网应用 应该监控那些指标

  • 操作系统层面
  1. cpu 使用率;
  2. 内存使用率;
  3. 磁盘使用情况;
  4. 网络使用情况;
  • JVM 层面
  1. Jvm cpu 使用情况;
  2. gc overhead;
  3. gc count;
  4. gc 每次回收后的使用情况;
  5. jfr 的重要指标;
  6. Jvm memory available;
  7. oom error count;
  • 应用框架层面
  1. 作为 service client 调用下游时候的 成功/失败次数, 平均时间;
  2. 调用外部数据存储的 成功/失败次数, 平均时间;
  • 应用层面
  1. tps/ops;
  2. transaction time;
  3. 5xx count;
  4. 4xx count;
  5. app busy threads;
  6. app total thread count;
  7. error count;

Java thread 线程的6种状态及转换

Java 线程共有6种状态:

  1. NEW 一个线程刚创建, 尚未开始运行. 这种状态不会出现在 thread dump 种. 比如代码中 new 了一个 Thread(), 还没调用 start() 方法, 就是 NEW 这个状态.
  2. RUNNABLE 线程正在运行或可运行状态,但是被挂起. 线程运行都是基于时间片的, 只要线程属于可运行状态(RUNNABLE), 就会给它分配时间片, 一旦时间片分配到该线程, 它就立马能运行.
  3. BLOCKED 线程正在等待一个 monitor 锁. 它处于该锁的 block 队列里面. 这个完全是跟 java 的 synchronize 关键字有关, 在线程等待进入 synchronize 里面的同步区之前, 尚未获得锁的时候, 就处于 BLOCKED 状态. 也只有这种情况, 线程才能进入 BLOCKED 状态;
  4. WAITING 线程正在等待其它线程触发某种操作, 在其它线程没有触发某种操作之前, 它只能在这个 WAITING 状态等待;
  5. TIMED_WAITING 同上面的 WAITING 状态, 只不过它有最长等待时间, 一旦时间到, 它就退出这种 TIMED_WAITING 状态;
  6. TERMINATED 线程以及结束. 这种状态也不可能出现在 thread dump 当中.

其中 NEW 和 TERMINATED 都很容易理解, 一个是开始之前, 一个是结束之后, 都不会出现在 thread dump 中. RUNNABLE 表示线程处在正在运行或随时可运行状态, 只要给它分配到时间片, 它立马就能运行. 所以 RUNNABLE 状态并不代表它正在运行.

只有当一个线程想要进入 Synchronize 保护的代码区时候, 才有可能进入 BLOCKED 状态. 如果它获得了 monitor 锁, 那么它直接进入被保护的代码块, 还是 RUNNABLE 状态. 如果没获得,那么进入该 monitor 的 block 队列(Entry Set), 线程则变成 BLOCKED 状态. 另外一个很容易忽略的地方是,当一个线程曾经拥有 monitor 锁, 然后因为某种原因需要挂起(已经进入被 synchronize 保护的代码区, 由于某种条件不满足, 不能退出被保护的代码区, 只能挂起当前线程), 这时该线程调用 Object.wait() 方法, 线程变成 WAITING 状态, 这时线程进入该 monitor 的 wait 队列 (Wait Set), 如果有其它线程调用该对象的 notify/notifyAll 方法, 那么这些线程被从 wait 队列 移入 block 队列( Entry Set), 这时该线程又要重新竞争 monitor 锁, 这时线程状态变化为 BLOCKED 状态. 所以不论是直接竞争锁未得到锁, 还是因为之前由于 wait 进入 Wait Set, 后来被 notify 之后, 重新进入 Entry Set 等待竞争monitor 锁, 只有这2种情况, 线程会进入 BLOCKED 状态.

对于 WAITING 状态, 只有下面这几种情况, 线程才可能进入 WAITING 状态:
1. Object.wait with no timeout
2. Thread.join with no timeout
3. LockSupport.park

通过 Object.wait 进入的线程, 进入该 monitor 锁的 Wait Set, 等待该 Object 的 notify/notifyAll 事件;

通过 Thread.join 进入的线程, 等待另外一个线程的结束. 实际的实现还是通过 synchronize 的 monitor 锁和 wait 方法实现的. 假如有线程 threadA 和 threadB 两个线程, threadA 通过调用 threadB.join() 方法等待 threadB 的结束. 实际实现是 threadB 对象的 join 方法是 synchronized, 它先是获得了 threadB 对象的 monitor 锁, 然后又进入了该 monitor 的 wait() 方法, 进入 Wait Set, 进入等待状态. 当 threadB 结束运行之前, 它会调用 threadB 对象的 notifyAll() 方法, 那么所有在 Wait Set 的线程进入 Entry Set, 之后竞争 threadB 对象的 monitor 锁, 然后释放锁, 继续执行, 最终通过这种方式实现对其它线程的终止状态的等待.

通过 LockSupport.park 进入的线程, 一般都是通过 AQS 进入的, 它们都在等待 LockSupport.unpark() 方法.

对于 TIMED_WAITING 状态, 下面这几种情况会进入该状态:
1. Thread.sleep
2. Object.wait with timeout
3. Thread.join with timeout
4. LockSupport.parkNanos
5. LockSupport.parkUntil