深入理解 jmap 和 jstack:Java 应用程序诊断的最佳拍档

CPU被java程序打满分析过程记录

线程死循环打满CPU:

创建一个Java程序来“打满”CPU(即让CPU达到100%的使用率)通常用于测试或演示目的,但请谨慎使用,因为它可能会干扰其他正在运行的应用程序。下面是一个示例Java程序,以及如何使用它和可能的排查步骤。

public class CpuLoadTest {
    public static void main(String[] args) {
        while (true) {
            // 无意义的循环,仅用于消耗CPU资源
            for (long i = 0; i < 1000000000L; i++) {
                // 进行一些简单的计算,例如平方根,以消耗更多CPU
                Math.sqrt(i);
            }
        }
    }
}

运行与监控
1、编译程序:
使用命令 javac CpuLoadTest.java 编译上述代码。
2、运行程序:
使用命令 java CpuLoadTest 运行编译后的程序。
3、监控CPU使用率:
在另一个终端窗口中,你可以使用系统自带的任务管理器或者如top, htop(在Linux上),或Activity Monitor(在Mac上)等工具来监控CPU的使用情况。

排查步骤:

如果CPU没有达到预期的使用率:

1、检查多核/多处理器

确认你的系统是单核还是多核。如果有多核,你可能需要为每个核心运行一个实例。使用top或htop确认CPU核心是否均匀负载。
top 是一个实时显示系统中各个进程的资源占用状况的动态视图命令行工具。它能显示系统中 CPU 使用率最高的进程以及内存消耗最大的进程。

如何使用 top

启动 top 命令后,你将看到类似下面的输出:

PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
  • %CPU: 进程的 CPU 使用率
  • %MEM: 进程的内存使用率
  • RES: 进程实际使用的物理内存
  • PID: 进程 ID
    同样你也可以使用ps指令直接进行查看:
如果你想要直接在终端输出中查找,可以使用 ps 命令结合 sort 和 head:
ps -eo pid,%cpu,command --sort=-%cpu | head -n 10

这将显示 CPU 使用率最高的前10个进程。
在获取到top 或者 ps的结果后,需要在top指令内使用 top 命令的 -H 选项查看线程细节:

  • 继续在 top 界面内,输入 1 来启用 -H 选项,这会显示每个进程内部的线程。
  • 再次按 Shift + P,可以看到线程级别的CPU使用率。
  • 记下消耗CPU最多的线程的TID。

一旦你发现了高 IO 或高 CPU 使用率的 Java 进程,下一步就是使用 jmap 和 jstack 来深入了解。

2. 获取进程中消耗资源最多的线程号

一旦确定了 Java 进程的 PID,可以使用jstack 来查看该进程内部所有线程的堆栈信息,并找出哪个线程消耗最多 CPU。你可以使用 -l 参数获取详细的线程信息:

jstack <PID> > thread_dump.txt

在生成的文本文件 thread_dump.txt 中,搜索关键字 “nid=0x” 来找到线程 ID。线程 ID 后面跟着的十六进制数字即为线程号。需要把top获取的进程号

3. 查看线程的 IO 数量

虽然 jstack 本身并不直接提供 IO 统计信息,但是结合 strace 或者 iotop 可以查看进程或线程的 IO 活动。例如,使用 strace 来追踪一个特定的线程:

strace -p <PID> -e trace=open,read,write

这将显示与打开、读取和写入文件相关的系统调用,可以帮助你判断是否有大量的 IO 活动。
如果你只想跟踪特定的线程,可以使用 tasksetstrace 的组合:

taskset -c <CPU_CORE> strace -p <TID> -e trace=open,read,write

这里的 <CPU_CORE> 是线程绑定的核心编号, 是你之前从 jstack 输出中找到的线程 ID。

4. 检查线程的 IO 活动与 CPU 使用

结合 top、jstack 和 strace 的输出,你可以分析哪些线程正在大量消耗 CPU 并产生 IO 活动。如果发现某个线程的 IO 活动与 CPU 使用率高度相关,这可能意味着该线程正在进行密集的 IO 操作,从而导致 CPU 高负载。

5. 使用 jmap 查看堆内存信息

jmap 可以帮助你分析 Java 进程的堆内存使用情况,找出可能的内存泄漏或内存热点。

jmap -histo <PID>

这将列出所有对象的实例数和占用的空间,有助于识别哪些对象可能在大量消耗内存。

补充

jmap和jstack常用命令:

jmap

jmap 是一个强大的工具,用于获取 Java 进程的内存映像,包括堆内存和非堆内存的信息。它可以帮助我们检测内存泄漏、分析内存使用情况以及查看对象实例的分布。

常用命令:
  • 查看堆内存概览:

    jmap -histo <pid>
    

    这个命令显示了堆内存中每个类的实例数量和占用空间。

  • 导出堆内存快照:

    jmap -dump:format=b,file=<filename> <pid>
    

    将堆内存快照保存到文件,可以使用其他工具如 Eclipse Memory Analyzer (MAT) 进一步分析。

  • 查看详细的堆内存信息:

    jmap -heap <pid>
    

    展示了堆内存的详细配置和使用情况。

jstack

jstack 则专注于线程状态的分析,它能够打印出 Java 进程中所有线程的堆栈跟踪,这对于诊断死锁、线程挂起等问题至关重要。

常用命令:
  • 查看所有线程的堆栈信息:

    jstack <pid>
    

    显示所有线程的堆栈信息,帮助识别线程的状态和执行流程。

  • 分析死锁:

    jstack -l <pid>
    

    输出线程的详细信息,包括锁定信息,对于检测死锁特别有用。

  • 特定线程ID的堆栈信息:

    jstack -l <pid> <tid>
    

    可以查看指定线程ID的详细堆栈信息。

使用案例

案例一:检测内存泄漏

假设我们发现应用的内存消耗持续增长,怀疑存在内存泄漏。这时,我们可以使用 jmap-histo 命令来检查对象实例的分布,寻找可能的泄漏源。

案例二:分析死锁

当应用程序出现响应延迟或停止响应时,可能是由于死锁造成的。jstack 可以帮助我们找到死锁的线程,并通过堆栈信息分析死锁的原因。

16进制线程号转换

  • jstack 输出的线程ID通常是十六进制形式,而从 ps 命令得到的TID是十进制的。
  • 如果需要匹配这两个ID,可以使用 printf 或 echo 和 awk 来将十六进制转换为十进制:
printf '%d\n' "0x<TID-in-hex>"

阶段性分析结果:

通过以上分析,此次判断出问题就出在死循环导致的CPU沾满,那么如何解决呢?尤其是针对NIO的死循环等待客户端上线问题。

  • 根据业务判断,如果可以让读之后在开启写操作,写完之后关闭写操作,那也是一种可行的思路
  • 使用Thread.sleep()

Thread.sleep() 是 Java 中 Thread 类的一个静态方法,用于让当前正在执行的线程暂停执行指定的时间(以毫秒为单位),然后进入等待状态。在等待期间,线程不会占用 CPU 时间,从而允许其他线程有机会执行。这是一个阻塞调用,也就是说,调用它的线程会一直等待直到指定的时间过去。会在此期间让出CPU,减少COU占用。

基础用法

Thread.sleep(long millis) 的基本形式接受一个长整型参数,表示线程应该暂停的毫秒数。例如:

try {
    Thread.sleep(1000); // 会让当前线程暂停1秒钟
} catch (InterruptedException e) {
    // 处理中断异常
}

InterruptedException

Thread.sleep() 方法声明抛出 InterruptedException,这是因为如果在睡眠期间,线程被中断(其他线程调用了 interrupt() 方法),则 InterruptedException 会被抛出,线程的中断状态会被清除。这种情况下,Thread.sleep() 会提前终止并抛出异常。

Thread.sleep(0)

Thread.sleep(0) 是一个特殊情况,它并不会使线程真正“睡着”,而是立即放弃当前线程的 CPU 时间片,使线程进入可运行状态(就绪状态),但不保证线程会立即重新获得 CPU 执行时间。这通常用于以下几种情况:

  1. 线程礼让:在高负载的循环中,Thread.sleep(0) 可以使线程暂时让出 CPU,给其他等待的线程机会。这有助于避免一个线程过度消耗 CPU 资源,尤其是在 GUI 应用中,可以使界面响应更流畅。

  2. 调度器提示:在多线程环境中,Thread.sleep(0) 可以作为对操作系统调度器的提示,表明当前线程愿意放弃 CPU,这样调度器可以重新评估线程优先级和分配 CPU 时间。

  3. 触发 GC:在某些情况下,Thread.sleep(0) 可能会触发垃圾回收,因为线程进入了一个安全点(Safepoint),在安全点处,JVM 可以暂停所有线程来进行 GC。

总之,Thread.sleep(0) 主要用于线程间的礼让,以及在循环中避免线程过度占用 CPU,从而改善多线程应用的性能和响应性。然而,它并不是一种精确的控制手段,线程何时能重新获得 CPU 执行时间取决于操作系统调度器的具体实现。但是sleep(0)只针对高优先级的线程。且sleep(x)除了0之外,其余如果小于100的话,优化差异不大,可以任意选取。

补充说明:

高优先级(CPU调度)执行时间之间的关系是正相关的。当一个任务具有高优先级时,它将被赋予更高的执行优先级,从而更快地被调度和执行。相反,当一个任务具有低优先级时,它将会被推迟执行,直到更高优先级的任务完成。因此,高优先级的任务通常具有更短的执行时间,而低优先级任务通常具有更长的执行时间。

  • 8
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值