写点什么

【深入浅出 JVM 原理及调优】「搭建理论知识框架」全方位带你深度剖析 Java 线程转储分析的开发指南

作者:洛神灬殇
  • 2024-01-21
    江苏
  • 本文字数:5908 字

    阅读完需:约 19 分钟

【深入浅出JVM原理及调优】「搭建理论知识框架」全方位带你深度剖析Java线程转储分析的开发指南

专栏介绍

学习 JVM 需要一定的编程经验和计算机基础知识,适用于从事 Java 开发、系统架构设计、性能优化、研究学习等领域的专业人士和技术爱好者。

前提准备

  • 编程基础:具备良好的编程基础,理解面向对象编程(OOP)的基本概念,熟悉 Java 编程语言。

  • 数据结构与算法:对基本的数据结构和算法有一定了解,理解内存管理、线程操作等基本概念。

面向人群

学习本专栏以及本章内容的前提和适用人群如下:


  • Java 开发人员:JVM 是 Java 程序的核心执行引擎,因此 Java 开发人员需要深入了解 JVM 的工作原理和运行机制,以优化程序性能并解决相关问题。

  • 系统架构师和高级工程师:对系统整体性能、稳定性有较高要求的人群,有必要深入理解 JVM 以优化系统性能。

  • Java 程序员和技术爱好者:具备一定 Java 编程经验,有意向深入了解 JVM 内部工作原理的人群。

  • 研究人员和学生:从事计算机科学相关研究或学习的人群,有兴趣深入研究 JVM 内部原理和优化方法。

  • JVM 运维工程师:负责 JVM 性能优化、故障排查和调优的专业人员,需要对 JVM 有深入的理解。

知识脉络

每位 Java 开发者都了解到 Java 字节码是在 Java 运行时环境(JRE)上执行的。JRE 包含了最为关键的组成部分:Java 虚拟机(JVM),它负责分析和执行 Java 字节码。通常情况下,大多数 Java 开发者无需深入了解虚拟机的内部运行原理。即使对虚拟机的运行机制不甚了解,也不会对开发工作产生太多影响。然而,对 JVM 有一定了解的话,将更有助于深入理解 Java 语言,并解决一些看似困难的问题。


本专栏全面系统地剖析了特定虚拟机产品(即 HotSpot,Oracle 官方虚拟机)的实现,本人不仅深刻地讲解了看似深奥的原理,还提供了大量易于上手的实践案例,下面是总体的 JVM 相关的知识拓扑架构。



tips:当然还有一些最新的 JVM 特性未在这张图并非展示本专栏的全部内容,另外还包含了最新的 JVM 特性。



分析线程转储

本文将教您如何分析 JVM 线程转储,并确定问题的根本原因。从我的角度来看,线程转储分析是任何参与 Java EE 生产支持的个人都需要掌握的最重要技能集。您可以从线程转储快照中获得的信息量通常远远超出了您的想象。

线程转储分析的介绍

在深入研究线程转储分析和问题模式之前,了解基本原理是至关重要的。

JVM 和线程运行机制

"Java 虚拟机是 Java EE 平台的基石,它为您的中间件和应用提供了运行环境。在这里,您的程序会被部署并得以运行。"



JVM 是一种中间件软件,为 Java/Java EE 程序提供运行环境。它支持字节码格式的运行时,并具备多种特性,如 IO 设施、数据结构、线程管理、安全和监控。此外,JVM 还通过垃圾收集器实现动态内存分配和管理。

JVM 和中间件之间的软件交互

下面的图表显示了 JVM、中间件和应用程序之间的高级交互视图。



这是一个展示 JVM、中间件和应用程序之间典型而简单交互的图示。在标准的 Java EE 应用程序中,线程的分配主要在中间件内核和 JVM 之间完成(尽管在应用程序本身或某些 API 直接创建线程时可能会有一些例外,但这并不常见,需要非常小心地处理)。



注意,JVM 本身会管理一些线程,例如垃圾收集(GC)线程,用于处理并发的垃圾收集任务。


由于大多数线程分配是由 Java EE 容器完成的,因此理解和识别线程堆栈跟踪,并正确地从线程转储数据中识别它们非常重要。这将帮助您快速了解 Java EE 容器试图执行的请求类型。


从线程转储分析的角度来看,您可以学习如何区分不同的线程池,并识别请求类型。这将帮助您更好地理解 JVM 中的线程池,并进行有效的分析。

JVM 线程转储

JVM 线程转储器是在特定时间生成的一个快照,它提供了所有已创建的 Java 线程的完整列表。这个转储器可以帮助您获取关于线程的详细信息,以便进行分析和调试。

Java 快照的基本信息

找到的每个单独的 Java 线程都为您提供以下信息:



  • 线程名称:通常被中间件供应商用于识别线程 Id 及其关联的线程池名称和状态(正在运行、被卡住等)。

  • 线程类型和优先级等:守护进程 prio=3 中间件软件通常创建它们的线程作为守护进程,这意味着它们的线程在后台运行;为其用户提供服务,例如您的 Java EE 应用程序

  • Java 线程 ID ex: tid=0x000000011e52a800 这是通过 java.lang 获得的 Java 线程 ID。Thread.getId(),通常实现为一个自动增量的长 1..n

  • 本地线程 ID ex: nid=0x251c 关键信息,因为这个本地线程 ID 允许您相关,例如哪些线程从操作系统的角度使用最多的 CPU 在您的 JVM 等。

  • Java 线程状态和细节等:等待监视器输入[0x fffffff ea5afb000]java.lang.thread。状态:已阻塞(在对象监视器上)允许快速了解线程状态及其潜在的当前阻塞条件

  • Java 线程堆栈跟踪:这是迄今为止您将从线程转储文件中找到的最重要的数据。这也是您将花费大部分分析时间的地方,因为 Java 堆栈跟踪为您提供了 90%的信息,以便查明许多问题模式类型的根本原因。

  • Java 堆分解:从 HotSpot VM 1.6 开始,您还将在线程转储快照的底部发现 HotSpot 内存空间利用率的细分,如 Java 堆(YoungGen,OldGen)和 PermGen/metaspace。当怀疑过多的 GC 是可能的根本原因时,这一点非常有用,因此您可以对已找到的线程数据/模式进行开箱即用的相关性。

内存回收日志

HeapPSYoungGen total 466944K, used 178734K [0xffffffff45c00000, 0xffffffff70800000,0xffffffff70800000)eden space 233472K, 76% used [0xffffffff45c00000,0xffffffff50ab7c50,0xffffffff54000000)from space 233472K, 0% used [0xffffffff62400000,0xffffffff62400000,0xffffffff70800000)to space 233472K, 0% used [0xffffffff54000000,0xffffffff54000000,0xffffffff62400000)PSOldGen total 1400832K, used 1400831K [0xfffffffef0400000, 0xffffffff45c00000, 0xffffffff45c00000)object space 1400832K, 99% used [0xfffffffef0400000,0xffffffff45bfffb8,0xffffffff45c00000)PSPermGen total 262144K, used 248475K [0xfffffffed0400000, 0xfffffffee0400000, 0xfffffffef0400000)object space 262144K, 94% used [0xfffffffed0400000,0xfffffffedf6a6f08,0xfffffffee0400000)
复制代码

线程转储分解概述

为了让你更好地理解,找到下面的图表,显示一个 HotSpot VM 线程转储及其常见的线程池的可视化分解发现:



您可以从 HotSpot VM 线程转储文件中找到一些信息。根据您的问题模式,其中的一些将比其他的更重要,现在,根据我们的示例 Hotspot 线程转储,在下面找到每个线程转储部分的详细说明。

全线程转储标识符

这基本上是唯一的关键字,一旦你生成线程转储(例如:通过 UNIX,你可以在<PID>中找到。这是线程转储快照数据的开头。


Full thread dump Java HotSpot(TM) 64-Bit Server VM (20.0-b11 mixed mode):
复制代码

Java EE 中间件,第三方和自定义应用程序线程

这部分是线程转储的核心,您通常将在这里花费大部分的分析时间。找到的线程的数量将取决于您使用的中间件软件、第三方库(可能有自己的线程)和您的应用程序(如果创建任何自定义线程,这通常不是最佳实践)。


在我们的示例线程转储中,Weblogic 是所使用的中间件。从 Weblogic9.2 开始,使用一个自调优线程池和唯一标识符“Weblogic.内核”。默认值(自调)。


"[STANDBY] ExecuteThread: '414' for queue: 'weblogic.kernel.Default (selftuning)'" daemon prio=3 tid=0x000000010916a800 nid=0x2613 in Object.wait() [0xfffffffe9edff000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0xffffffff27d44de0> (a weblogic.work.ExecuteThread) at java.lang.Object.wait(Object.java:485) at weblogic.work.ExecuteThread.waitForRequest(ExecuteThread.java:160) - locked <0xffffffff27d44de0> (a weblogic.work.ExecuteThread) at weblogic.work.ExecuteThread.run(ExecuteThread.java:181)
复制代码
HotSpot VM Thread

这是一个由 Hotspot 管理的内部线程,以便执行内部本机操作。通常,您不应该担心这个问题,除非您看到高 CPU(通过线程转储和 prstat /本地线程 id 相关性)。


"VM Periodic Task Thread" prio=3 tid=0x0000000101238800 nid=0x19 waiting on condition
复制代码

HotSpot GC Thread

当使用 HotSpot 并行 GC 时(现在在使用多物理核硬件时很常见),HotSpot VM 会默认创建或根据 JVM 调优特定 #的 GC 线程。这些 GC 线程允许 VM 以并行的方式执行其定期的 GC 清理,从而导致 GC 时间的总体减少;以增加 CPU 利用率为代价。


"GC task thread#0 (ParallelGC)" prio=3 tid=0x0000000100120000 nid=0x3 runnable "GC task thread#1 (ParallelGC)" prio=3 tid=0x0000000100131000 nid=0x4 runnable
复制代码


这也是至关重要的数据,因为当遇到与 GC 相关的问题时,如 GC 过多、内存泄漏过多等,您将能够使用本地 id 值(nid=0x3)将从 OS / Java 进程中观察到的任何高 CPU 与这些线程关联起来。

JNI 全局引用计数

JNI(Java 本机接口)全局引用基本上是从本地代码到由 Java 垃圾收集器管理的 Java 对象的对象引用。它的作用是防止收集本机代码仍在使用的对象,但在技术上讲,在 Java 代码中没有“实时”引用。


为了检测与 JNI 相关的泄漏,密切关注 JNI 的参考文献也很重要。如果程序直接使用 JNI 或使用第三方工具,如监控工具,容易导致本机内存泄漏,就会发生这种情况


JNI global references: 1925
复制代码

Java 堆利用率视图

这些数据被添加到 JDK 中,并为您提供了 Hotspot 堆的一个简短和快速的视图。我发现它很有用当故障排除 GC 相关问题以及高 CPU 因为你得到线程转储和 Java 堆在一个快照允许你确定(或排除)任何压力点在一个特定的 Java 堆内存空间以及当前线程计算目前正在完成。正如您在我们的示例线程转储中看到的,Java 堆 OldGen 已经用爆了!


HeapPSYoungGen total 466944K, used 178734K [0xffffffff45c00000, 0xffffffff70800000, 0xffffffff70800000) eden space 233472K, 76% used [0xffffffff45c00000,0xffffffff50ab7c50,0xffffffff54000000) from space 233472K, 0% used [0xffffffff62400000,0xffffffff62400000,0xffffffff70800000) to space 233472K, 0% used [0xffffffff54000000,0xffffffff54000000,0xffffffff62400000)PSOldGen total 1400832K, used 1400831K [0xfffffffef0400000, 0xffffffff45c00000, 0xffffffff45c00000) object space 1400832K, 99% used[0xfffffffef0400000,0xffffffff45bfffb8,0xffffffff45c00000)PSPermGen total 262144K, used 248475K [0xfffffffed0400000, 0xfffffffee0400000, 0xfffffffef0400000) object space 262144K, 94% used [0xfffffffed0400000,0xfffffffedf6a6f08,0xfffffffee0400000)
复制代码


为了让您快速从线程转储中识别出问题模式,您首先需要了解如何读取线程堆栈跟踪以及如何正确地获取“故事”。这意味着,如果我让你告诉我线程 #38 在做什么;你应该能够精确地回答;包括线程堆栈跟踪是否显示了一个健康(正常)和挂起条件。

Java 堆栈跟踪重新访问

你们大多数人都熟悉 Java 堆栈跟踪。这是我们在抛出 Java 异常时从服务器和应用程序日志文件中找到的典型数据。在此上下文中,Java 堆栈跟踪为我们提供了触发 Java 异常的线程的代码执行路径,例如,一个 java.lang.NoClassDefFound 错误,java.lang.Nullpointer 异常等。这样的代码执行路径使我们能够看到最终导致 Java 异常的不同代码层。

Java 堆栈跟踪必须始终从自下而上读取
  • 底部的一行将公开请求的发起者,例如 Java / Java EE 容器线程。

  • 堆栈跟踪顶部的第一行将显示最后一个异常被触发的 Java 类。


让我们通过一个简单的例子来介绍一下这个过程。我们创建了一个示例 Java 程序,简单地执行一些类方法调用并抛出一个异常。所生成的程序输出如下所述:


JavaStrackTraceSimulatorAuthor: Pierre-Hugues Charbonneauhttp://javaeesupportpatterns.blogspot.comException in thread "main" java.lang.IllegalArgumentException:  at org.ph.javaee.training.td.Class2.call(Class2.java:12) at org.ph.javaee.training.td.Class1.call(Class1.java:14) at org.ph.javaee.training.td.JavaSTSimulator.main(JavaSTSimulator.java:20)
复制代码


  • Java program JavaSTSimulator is invoked (via the "main" Thread)

  • The simulator then invokes method call() from Class1

  • Class1 method call() then invokes Class2 method call()

  • Class2 method call()throws a Java Exception: java.lang.IllegalArgumentException

  • The Java Exception is then displayed in the log / standard output


如您所示,导致此异常的代码执行路径总是从下向上显示。上面的分析过程对于任何 Java 程序员来说都应该是非常熟悉的。接下来您将看到的是,线程转储线程堆栈跟踪分析过程与上面的 Java 堆栈跟踪分析非常相似。

线程转储:线程堆栈跟踪分析

从 JVM 生成的线程转储为您提供了整个 JVM 进程中所有“已创建的”线程的代码级执行快照。已创建的线程并不意味着所有这些线程实际上都在做一些事情。在从 Java EE 容器 JVM 生成的典型线程转储快照中:


  • 一些线程可以执行原始的计算任务,如 XML 解析、IO /磁盘访问等。

  • 一些线程可能正在等待一些阻塞的 IO 调用,比如远程 Web 服务调用,DB/JDBC 查询等。

  • 一些线程可能涉及到垃圾收集,例如 GC 线程。

  • 一些线程将等待一些工作要做(不做任何工作的线程通常处于等待状态)

  • 一些线程可能正在等待其他线程的完成工作,例如,线程等待获取某些对象上的监视器锁(同步块{})。


线程堆栈跟踪为您提供了其当前执行情况的快照。第一行通常包括线程的本机信息,如其名称、状态、地址等。必须从自下而上开始读取当前的执行堆栈跟踪。请遵循下面的分析过程。你使用线程转储分析的经验越多,你就能越快地阅读和识别每个线程所执行的工作:


  1. 开始从底部读取线程堆栈跟踪

  2. 首先,识别发起者(Java EE 容器线程、自定义线程、GC 线程、JVM 内部线程、独立的 Java 程序“主”线程等)。

  3. 下一步是确定线程正在执行的请求的类型(WebApp、Web 服务、JMS、Remote EJB(RMI)、内部 Java EE 容器等)。

  4. 下一步是从执行堆栈中跟踪您的应用程序模块涉及线程尝试执行的实际核心工作。分析的复杂性将取决于中间件环境和应用程序的抽象层。

  5. 下一步是查看在第一行之前的最后一个~10-20 行。识别线程所涉及的协议或工作,例如 HTTP 调用、套接字通信、JDBC 或原始计算任务,如磁盘访问、类加载等。

  6. 下一步是看第一行。第一行通常告诉线程状态上的 LOT,因为它是在您拍摄快照时执行的当前代码片段。

  7. 最后两个步骤的结合将为您提供信息的核心,以总结线程所涉及的工作和/或挂起条件。


现在,使用从 JBoss 生产环境捕获的线程转储线程堆栈跟踪的真实示例,找到上述步骤的可视化细分。在本例中,许多线程在创建 JAX-WS 服务实例的新实例时都显示了类似的过度 IO 问题模式。



正如您所看到的,最后 10 行和第一行将告诉我们线程涉及什么挂起或慢状态,如果有的话。从底部开始的行将给我们提供发起者和请求类型的详细信息。

发布于: 18 小时前阅读数: 10
用户头像

洛神灬殇

关注

🏆 InfoQ写作平台-签约作者 🏆 2020-03-25 加入

👑 后端技术架构师,前优酷资深工程师 📕 个人著作《深入浅出Java虚拟机—JVM原理与实战》 💻 10年开发经验,参与过多个大型互联网项目,定期分享技术干货和项目经验

评论

发布
暂无评论
【深入浅出JVM原理及调优】「搭建理论知识框架」全方位带你深度剖析Java线程转储分析的开发指南_Java_洛神灬殇_InfoQ写作社区