专栏介绍
学习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是可能的根本原因时,这一点非常有用,因此您可以对已找到的线程数据/模式进行开箱即用的相关性。
内存回收日志
Heap
PSYoungGen 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 (self�tuning)'" 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已经用爆了!
Heap
PSYoungGen 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程序,简单地执行一些类方法调用并抛出一个异常。所生成的程序输出如下所述:
JavaStrackTraceSimulator
Author: Pierre-Hugues Charbonneau
http://javaeesupportpatterns.blogspot.com
Exception 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线程。
- 一些线程将等待一些工作要做(不做任何工作的线程通常处于等待状态)
- 一些线程可能正在等待其他线程的完成工作,例如,线程等待获取某些对象上的监视器锁(同步块{})。
线程堆栈跟踪为您提供了其当前执行情况的快照。第一行通常包括线程的本机信息,如其名称、状态、地址等。必须从自下而上开始读取当前的执行堆栈跟踪。请遵循下面的分析过程。你使用线程转储分析的经验越多,你就能越快地阅读和识别每个线程所执行的工作:
开始从底部读取线程堆栈跟踪
首先,识别发起者(Java EE容器线程、自定义线程、GC线程、JVM内部线程、独立的Java程序“主”线程等)。
下一步是确定线程正在执行的请求的类型(WebApp、Web服务、JMS、Remote EJB(RMI)、内部Java EE容器等)。
下一步是从执行堆栈中跟踪您的应用程序模块涉及线程尝试执行的实际核心工作。分析的复杂性将取决于中间件环境和应用程序的抽象层。
下一步是查看在第一行之前的最后一个~10-20行。识别线程所涉及的协议或工作,例如HTTP调用、套接字通信、JDBC或原始计算任务,如磁盘访问、类加载等。
下一步是看第一行。第一行通常告诉线程状态上的LOT,因为它是在您拍摄快照时执行的当前代码片段。
最后两个步骤的结合将为您提供信息的核心,以总结线程所涉及的工作和/或挂起条件。
现在,使用从JBoss生产环境捕获的线程转储线程堆栈跟踪的真实示例,找到上述步骤的可视化细分。在本例中,许多线程在创建JAX-WS服务实例的新实例时都显示了类似的过度IO问题模式。
正如您所看到的,最后10行和第一行将告诉我们线程涉及什么挂起或慢状态,如果有的话。从底部开始的行将给我们提供发起者和请求类型的详细信息。