学习JVM需要一定的编程经验和计算机基础知识,适用于从事Java开发、系统架构设计、性能优化、研究学习等领域的专业人士和技术爱好者。
学习本专栏以及本章内容的前提和适用人群如下:
每位Java开发者都了解到Java字节码是在Java运行时环境(JRE)上执行的。JRE包含了最为关键的组成部分:Java虚拟机(JVM),它负责分析和执行Java字节码。通常情况下,大多数Java开发者无需深入了解虚拟机的内部运行原理。即使对虚拟机的运行机制不甚了解,也不会对开发工作产生太多影响。然而,对JVM有一定了解的话,将更有助于深入理解Java语言,并解决一些看似困难的问题。
本专栏全面系统地剖析了特定虚拟机产品(即HotSpot,Oracle官方虚拟机)的实现,本人不仅深刻地讲解了看似深奥的原理,还提供了大量易于上手的实践案例,下面是总体的JVM相关的知识拓扑架构。
tips:当然还有一些最新的JVM特性未在这张图并非展示本专栏的全部内容,另外还包含了最新的JVM特性。
本文将教您如何分析JVM线程转储,并确定问题的根本原因。从我的角度来看,线程转储分析是任何参与Java EE生产支持的个人都需要掌握的最重要技能集。您可以从线程转储快照中获得的信息量通常远远超出了您的想象。
在深入研究线程转储分析和问题模式之前,了解基本原理是至关重要的。
“Java虚拟机是Java EE平台的基石,它为您的中间件和应用提供了运行环境。在这里,您的程序会被部署并得以运行。”
JVM是一种中间件软件,为Java/Java EE程序提供运行环境。它支持字节码格式的运行时,并具备多种特性,如IO设施、数据结构、线程管理、安全和监控。此外,JVM还通过垃圾收集器实现动态内存分配和管理。
下面的图表显示了JVM、中间件和应用程序之间的高级交互视图。
这是一个展示JVM、中间件和应用程序之间典型而简单交互的图示。在标准的Java EE应用程序中,线程的分配主要在中间件内核和JVM之间完成(尽管在应用程序本身或某些API直接创建线程时可能会有一些例外,但这并不常见,需要非常小心地处理)。
注意,JVM本身会管理一些线程,例如垃圾收集(GC)线程,用于处理并发的垃圾收集任务。
由于大多数线程分配是由Java EE容器完成的,因此理解和识别线程堆栈跟踪,并正确地从线程转储数据中识别它们非常重要。这将帮助您快速了解Java EE容器试图执行的请求类型。
从线程转储分析的角度来看,您可以学习如何区分不同的线程池,并识别请求类型。这将帮助您更好地理解JVM中的线程池,并进行有效的分析。
JVM线程转储器是在特定时间生成的一个快照,它提供了所有已创建的Java线程的完整列表。这个转储器可以帮助您获取关于线程的详细信息,以便进行分析和调试。
找到的每个单独的Java线程都为您提供以下信息:
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):
这部分是线程转储的核心,您通常将在这里花费大部分的分析时间。找到的线程的数量将取决于您使用的中间件软件、第三方库(可能有自己的线程)和您的应用程序(如果创建任何自定义线程,这通常不是最佳实践)。
在我们的示例线程转储中,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管理的内部线程,以便执行内部本机操作。通常,您不应该担心这个问题,除非您看到高CPU(通过线程转储和prstat /本地线程id相关性)。
"VM Periodic Task Thread" prio=3 tid=0x0000000101238800 nid=0x19 waiting on condition
当使用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(Java本机接口)全局引用基本上是从本地代码到由Java垃圾收集器管理的Java对象的对象引用。它的作用是防止收集本机代码仍在使用的对象,但在技术上讲,在Java代码中没有“实时”引用。
为了检测与JNI相关的泄漏,密切关注JNI的参考文献也很重要。如果程序直接使用JNI或使用第三方工具,如监控工具,容易导致本机内存泄漏,就会发生这种情况
JNI global references: 1925
这些数据被添加到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.lang.NoClassDefFound错误,java.lang.Nullpointer异常等。这样的代码执行路径使我们能够看到最终导致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程序员来说都应该是非常熟悉的。接下来您将看到的是,线程转储线程堆栈跟踪分析过程与上面的Java堆栈跟踪分析非常相似。
从JVM生成的线程转储为您提供了整个JVM进程中所有“已创建的”线程的代码级执行快照。已创建的线程并不意味着所有这些线程实际上都在做一些事情。在从Java EE容器JVM生成的典型线程转储快照中:
线程堆栈跟踪为您提供了其当前执行情况的快照。第一行通常包括线程的本机信息,如其名称、状态、地址等。必须从自下而上开始读取当前的执行堆栈跟踪。请遵循下面的分析过程。你使用线程转储分析的经验越多,你就能越快地阅读和识别每个线程所执行的工作:
开始从底部读取线程堆栈跟踪
首先,识别发起者(Java EE容器线程、自定义线程、GC线程、JVM内部线程、独立的Java程序“主”线程等)。
下一步是确定线程正在执行的请求的类型(WebApp、Web服务、JMS、Remote EJB(RMI)、内部Java EE容器等)。
下一步是从执行堆栈中跟踪您的应用程序模块涉及线程尝试执行的实际核心工作。分析的复杂性将取决于中间件环境和应用程序的抽象层。
下一步是查看在第一行之前的最后一个~10-20行。识别线程所涉及的协议或工作,例如HTTP调用、套接字通信、JDBC或原始计算任务,如磁盘访问、类加载等。
下一步是看第一行。第一行通常告诉线程状态上的LOT,因为它是在您拍摄快照时执行的当前代码片段。
最后两个步骤的结合将为您提供信息的核心,以总结线程所涉及的工作和/或挂起条件。
现在,使用从JBoss生产环境捕获的线程转储线程堆栈跟踪的真实示例,找到上述步骤的可视化细分。在本例中,许多线程在创建JAX-WS服务实例的新实例时都显示了类似的过度IO问题模式。
正如您所看到的,最后10行和第一行将告诉我们线程涉及什么挂起或慢状态,如果有的话。从底部开始的行将给我们提供发起者和请求类型的详细信息。
全方位带你深度剖析Java线程转储分析的开发指南