vlambda博客
学习文章列表

读书笔记《hands-on-high-performance-with-spring-5》JVM内部

Inside JVM

上一章向我们介绍了如何通过了解性能问题的症状来调整应用程序的性能。我们浏览了性能调整生命周期,了解在应用程序性能的哪些阶段可以调整以及如何调整。我们还学习了如何将 JMX 连接到 Spring 应用程序,观察应用程序的瓶颈并对其进行调整。

在本章中,我们将深入了解 Java 虚拟机 (JVM) 并调整 JVM 以实现高性能。 JVM 执行两项主要工作——执行代码和管理内存。 JVM 从 OS 分配内存,设法进行堆压缩,并执行 unreferenced 对象的 垃圾收集 (GC)。 GC 很重要,因为适当的 GC 可以提高应用程序的内存管理和性能。

以下是我们将在本章中讨论的主题:

  • Understanding JVM internals
  • Understanding memory leak
  • Common pitfalls
  • GC
  • GC methods and policies
  • Tools to analyze GC logs

Understanding JVM internals

作为一名 Java 开发人员,我们知道 Java 字节码运行在 Java Runtime Environment (JRE) 中,而 JRE 最重要的部分是 JVM,它分析和执行 Java字节码。当我们创建一个 Java 程序并对其进行编译时,结果是一个带有 .class 扩展名的文件。它包含 Java 字节码。 JVM 将 Java 字节码转换为在我们运行应用程序的硬件平台上执行的机器指令。当 JVM 运行程序时,它需要内存来存储从加载的类文件、实例化对象、方法参数、返回值、局部变量和计算的中间结果中提取的字节码和其他信息。 JVM 将它需要的内存组织成几个运行时数据区域。

JVM由三部分组成:

  • Class loader subsystem
  • Memory areas
  • Execution engine

下图说明了高级 JVM 架构:

读书笔记《hands-on-high-performance-with-spring-5》JVM内部

JVM架构

让我们简单了解一下我们在图中看到的 JVM 的三个不同部分。

Class loader subsystem

类加载器子系统的职责不仅限于定位和导入类的二进制数据。它还验证导入的类是否正确,为类变量分配和初始化内存,并协助解析符号引用。这些活动按严格的顺序执行:

  1. Loading: The class loader reads the .class file and finds and imports binary data for a type.
  2. Linking: It performs verification, preparation, and (optionally) resolution:
  • Verification: Ensures the correctness of the imported type
  • Preparation: Allocates memory to class variables and initializes the memory to default values
  • Resolution: Transforms symbolic references from the type into direct references
  1. Initialization: Assigns values to all static variables defined in the code and executes static block (if any). Execution occurs from top to bottom in a class, and from parent to child in a class hierarchy.

一般来说,有三种类加载器:

  • Bootstrap class loader: This loads core-trusted Java API classes located in the JAVA_HOME/jre/lib directory. These Java APIs are implemented in native languages, such as C or C++.
  • Extension class loader: This inherits the Bootstrap class loader. It loads the classes from extension directories located at JAVA_HOME/jre/lib/ext, or any other directory specified by the java.ext.dirs system property. It is implemented in Java by the sun.misc.Launcher$ExtClassLoader class.
  • System class loader: This inherits the extension class loader. It loads classes from our application classpath. It uses the java.class.path environment variable.
为了加载类,JVM 遵循委托层次原则。系统类加载器将请求委托给扩展类加载器,扩展类加载器将请求委托给 Bootstrap 类加载器。如果在 Bootstrap 路径中找到一个类,则加载该类,否则,请求将被传输到扩展类加载器,然后再传输到系统类加载器。最后,如果系统类加载器加载类失败,那么 java.lang.ClassNotFoundException 异常产生。

下图说明了委托层次结构原则:

读书笔记《hands-on-high-performance-with-spring-5》JVM内部

委托层次原则

Memory areas

Java 运行时内存分为五个不同的区域,如下图所示:

读书笔记《hands-on-high-performance-with-spring-5》JVM内部

存储区

让我们看一下每个组件的简要说明:

  • Method Area: This contains all the class-level information, such as class name, parent class, methods, instance, and static variables. There is only one method area per JVM, and it is a shared resource.
  • Heap Area: This contains the information of all the objects. There is one Heap Area per JVM. It is also a shared resource. As Method Area and Heap Area are shared memory between multiple threads, the data stored is not thread-safe.
  • Stack Memory: JVM creates one runtime stack for every thread in execution and stores it in the stack area. Every block of this stack is called an activation record that stores methods call. All local variables of that method are stored in their corresponding frame. The stack area is thread-safe since it is not a shared resource. The runtime stack will be destroyed by the JVM up on termination of the thread. So, in the case of infinite loops of method calls, we might see StackOverFlowError, which is due to no memory in the stack for storing method calls.
  • PC Registers: These hold the addresses of current instructions under execution. Once the instruction is executed, the PC Registers will be updated with the next instruction. Each thread has a separate PC Registers.
  • Native Method Stacks: For every thread, a separate native stack is created. It stores the native method information. Native information is nothing but native method calls.

Execution engine

执行引擎在运行时数据区执行字节码。它按每一行执行字节码,并使用运行时数据区域中可用的信息。执行引擎可以分为三个部分:

  • Interpreter: This reads, interprets, and executes bytecode by each line. It interprets and executes bytecode quickly; however, it can be very slow in executing interpreted results.
  • Just-In-Time (JIT): In order to overcome the interpreter's slowness in executing interpreted results, the JIT compiler converts the bytecode to native code once the interpreter interprets the code the first time. Execution happens fast with native code; it executes instructions one by one.
  • Garbage collector: This destroys anything that is not referenced. This is very important, so anything not required will be destroyed to create room for new execution.

Understanding memory leak

Java 的最大优势是 JVM,它提供了开箱即用的内存管理。我们可以创建对象,Java 的垃圾收集器负责为我们释放内存。尽管如此,Java 应用程序中仍会发生内存泄漏。在下一节中,我们将看到一些常见的内存泄漏原因,并介绍一些解决方案来检测/避免它们。

Memory leak in Java

当垃圾收集器无法收集应用程序不再使用/引用的对象时,就会发生内存泄漏。如果对象没有被垃圾回收,应用程序会使用更多的内存,一旦整个堆满了,就无法分配对象,这会导致 OutOfMemoryError

堆内存有两种类型的对象——引用对象和未引用对象。垃圾收集器将删除所有未引用的对象。但是,即使应用程序不使用引用的对象,垃圾收集器也无法删除它们。

Common reasons for memory leaks

以下是内存泄漏的最常见原因:

  • Open streams: While working on streams and readers, we often forget to close the streams, which eventually results in the memory leak. There are two types of leaks that result from unclosed streams—low-level resource leak and memory leak. Low-level resource leak includes OS-level resources, such as file descriptor and open connection. As JVM consumes memory to track these resources, it leads to memory leak. To avoid leaks, use the finally block to close the stream or use the autoclose feature of Java 8.
  • Open connections: We often forget to close opened HTTP, database, or FTP connections, which results in the memory leak. Similar to closing streams, close the connections.
  • Static variables referencing instance objects: Any static variable referencing a heavy object could lead to memory leak because even if the variable is not in use, it won't be garbage collected. To prevent this, try not to have heavy static variables; use local variables instead.
  • Missing methods for objects in collection: Adding objects having no implementation of the equals and hashcode methods to HashSet will add the number of duplicate objects in HashSet and we would not be able to remove these objects once added. To prevent this, implement the equals and hashcode methods in the object added to HashSet.

诊断内存泄漏是一个漫长的过程,需要大量的实践经验、调试技能和对应用程序的详细了解。以下是诊断内存泄漏的方法:

  • Enable GC logs and fine-tune GC parameters
  • Profiling
  • Code review

在接下来的章节中,我们将看到 GC 的常见陷阱、GC 方法以及分析 GC 日志的工具。

Common pitfalls

性能调优至关重要,一个小的 JVM 标志就会让事情变得棘手。 JVM 会受到 GC 暂停的影响,其频率和持续时间各不相同。在暂停期间,一切都停止了,各种意想不到的行为开始了。在 JVM 卡住的暂停和不稳定行为期间,性能会受到影响。我们可以看到响应时间慢、CPU 和内存利用率高的症状,或者系统大部分时间运行正常但行为异常,例如执行极其缓慢的事务和断开连接。

大多数时候,我们测量平均交易时间并忽略导致不稳定行为的异常值。大多数情况下,系统运行正常,但在某些时候,系统响应能力会下降。大多数情况下,这种低性能的原因是由于对 GC 开销的认识不足,并且只关注平均响应时间。

在定义性能要求时,我们需要回答的一个重要问题是:对于我们的应用程序而言,与 GC 暂停频率和持续时间相关的可接受标准是什么?要求因应用程序而异,因此根据我们的应用程序和用户体验,我们需要首先定义这些标准。

我们通常有几个常见的误解如下。

Number of garbage collectors

大多数时候,人们并不知道垃圾收集器不止一个,而是四个。四个垃圾收集器是 - Serial、Parallel、Concurrent 和 Garbage First(G1强>)。我们将在下一节中看到它们。有一些第三方垃圾收集器,例如 Shenandoah。 JVM HotSpot 的默认垃圾收集器在 Java 8 之前是 Parallel,而从 Java 9 开始,默认收集器是 Garbage First Garbage Collector (G1 GC)。大多数时候并行垃圾收集器并不是最好的。但是,这取决于我们的应用程序要求。例如,Concurrent Mark Sweep (CMS) 和 G1 收集器导致 GC 暂停的频率较低。但是当它们确实导致暂停时,暂停持续时间很可能会比由 Parallel 收集器引起的暂停更长。另一方面,并​​行收集器通常在相同的堆大小下实现更高的吞吐量。

Wrong garbage collector

GC 问题的一个常见原因是为应用程序类型选择了错误的垃圾收集器。每个收藏家都有自己的意义和好处。我们需要找到应用程序的行为和优先级,并据此选择正确的垃圾收集器。 HotSpot 的默认垃圾收集器是 Parallel/Throughput,而且大多数时候,它并没有被证明是一个好的选择。 CMS 和 G1 收集器是并发的,导致的停顿频率较低,但是当停顿发生时,其持续时间比 Parallel 收集器长。所以收集器的选择是我们经常犯的一个常见错误。

Parallel / Concurrent keywords

GC 可能会导致 stop-the-world (STW) 情况,或者可以在不停止应用程序的情况下同时收集对象。 GC算法可以单线程执行,也可以多线程执行。因此,Concurrent GC 并不意味着它并行执行,而 Serial GC 并不意味着它由于串行执行而导致更多的暂停。 Concurrent 和 Parallel 是不同的,其中 Concurrent 表示 GC 周期,Parallel 表示 GC 算法。

G1 is a problem solver

随着 Java 7 中新的垃圾收集器的引入,很多人认为它是所有以前垃圾收集器的问题解决者。 G1 GC 解决的一个重要问题是碎片问题,这是 CMS 收集器常见的问题。然而,在许多情况下,其他收集器可以胜过 G1 GC。所以这一切都取决于我们应用程序的行为和要求。

Average transaction time

大多数情况下,在测试性能时,我们倾向于测量平均事务时间,并且只这样做,我们就会错过异常值。在某些时候,当 GC 导致长时间暂停时,应用程序的响应时间会急剧增加,从而影响用户访问应用程序。这可能会被忽视,因为我们只关注平均交易时间。当 GC 暂停频率增加时,响应时间成为一个严重的问题,我们可能仅仅通过测量平均响应时间就忽略了它。

Reducing new object allocation rates improves GC behavior

与其关注或降低新对象的分配率,不如关注对象的生命周期。存在三种不同类型的对象:寿命长的对象,我们无能为力;中年物体,这些会导致最大的问题;和短期对象,它们通常会很快被释放和分配,以便在下一个 GC 周期中收集。因此,与其专注于长寿命和短寿命对象,不如关注中期对象分配率可以带来积极的结果。这不仅仅是对象分配率;导致所有麻烦的是游戏中的对象类型。

GC logs cause overhead

GC 日志不会导致开销,尤其是在默认日志设置中。这些数据非常有价值,Java 7 引入了挂钩来控制其日志文件的大小。如果我们不收集带有时间戳的 GC 日志,那么我们就错过了分析和解决暂停问题的关键数据源。 GC 日志是系统中GC 状态的最丰富的数据源。我们可以在我们的应用程序中获取有关所有 GC 事件的数据;比如说,它同时完成或导致 STW 暂停:花了多长时间,消耗了多少 CPU,以及释放了多少内存。从这些数据中,我们将能够了解暂停的频率和持续时间、它们的开销,并继续采取行动减少它们。

通过添加以下参数启用 GC:

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:`date +%F_%H-%M-%S`-gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=10M

GC

Java 最好的成就之一是 GC。 GC 进程自动管理内存和堆分配,以跟踪死对象、删除它们并将内存重新分配给新对象。理论上,由于垃圾收集器自动管理内存,它使开发人员在创建新对象时无需考虑内存的分配和释放,以消除内存泄漏和其他与内存相关的问题。

How GC works

我们通常认为 GC 会收集并移除未引用的对象。相反,Java 中的 GC 跟踪活动对象并将所有未引用的对象标记为垃圾。

内存的堆区域是动态分配对象的地方。我们应该在运行应用程序之前为 JVM 分配堆内存。提前将堆分配给 JVM 有几个后果:

  • Improves object creation rate because JVM doesn't need to communicate with the OS to get memory for each new object. Once the JVM allocates memory to an object, JVM moves the pointer toward the next available memory.
  • Garbage collectors collect the object when there is no object reference and reuse its memory for new object allocation. As the garbage collector doesn't delete the object, no memory is returned to the OS.

在对象被引用之前JVM 认为它们是活动对象。当一个对象不再被引用并且应用程序代码无法访问时,垃圾收集器将其删除并回收其内存。我们脑海中出现一个问题,对象树中的第一个引用是谁,对吗?让我们看看对象树及其根。

GC roots

对象的每一棵树在根处都有一个或多个对象。如果垃圾收集器可以到达根,则树是可达的。任何没有被 GC 根访问或引用的对象都被认为是死的,垃圾收集器将其删除。

以下是 Java 中不同类型的 GC 根:

  • Local variables: Variables or parameters of a Java method.
  • Active threads: A running thread is a live object.
  • Static variables: Classes referencing static variables. When the garbage collector collects classes, it removes references to static variables.
  • JNI references: Object reference created during the JNI call. They are kept alive because JVM is unaware that the native code has references of it.

请看下图:

读书笔记《hands-on-high-performance-with-spring-5》JVM内部

GC 根

GC methods and policies

正如我们在上一节中了解到的,没有一种,而是四种不同的垃圾收集器。每个都有自己的优点和缺点。这些收集器的共同点是它们将托管堆拆分为不同的段,并假设对象是短暂的,应该很快被删除。让我们看看 GC 的四种不同算法。

Serial collector

Serial 收集器是最简单的 GC 实现,主要针对单线程环境和小堆设计。此 GC 实现在其工作时冻结所有应用程序线程。因此,在多线程应用程序(例如服务器环境)中使用它并不是一个好主意。

要启用串行垃圾收集器,请将 -XX:+UseSerialGC 设置为 VM 参数

Parallel/Throughput collector

Parallel 收集器是 JVM 的默认收集器,也称为吞吐量收集器。顾名思义,这个收集器与串行收集器不同,它使用多线程来管理堆内存。并行垃圾收集器在执行次要或完整 GC 时仍会冻结所有应用程序线程。如果我们想使用 Parallel 垃圾收集器,我们应该指定调优参数,例如线程、暂停时间、吞吐量和占用空间。

以下是指定调整参数的参数:

  • Threads: -XX:ParallelGCThreads=<N>
  • Pause time: -XX:MaxGCPauseMillis=<N>
  • Throughput: -XX:GCTimeRatio=<N>
  • Footprint (maximum heap size): -Xmx<N>

要在我们的应用程序中启用并行垃圾收集器,请设置 -XX:+UseParallelGC 选项。

CMS garbage collector

CMS 实现使用多个垃圾收集器线程来扫描(mark)可以删除(清除)的未使用对象。这种垃圾收集器更适合需要短暂 GC 暂停的应用程序,并且可以在应用程序运行时与垃圾收集器共享处理器资源。

CMS 算法仅在两种情况下进入 STW 模式:当旧代中的对象仍然从线程入口点或静态变量引用时,以及当应用程序在 CMS 运行时更改堆的状态时,使算法返回并重复对象树以验证它是否标记了正确的对象。

有了这个收集器,提升失败是最值得关注的问题。当年轻代和老年代的对象集合之间出现竞争条件时,就会发生提升失败。如果收集器需要将对象从年轻代提升到老年代并且没有足够的空间,它必须先STW来创建空间。为了确保在 CMS 收集器的情况下不会发生这种情况,请增加老年代的大小或为收集器分配更多后台线程以与分配率竞争。

为了提供高吞吐量,CMS 使用更多 CPU 来扫描和收集对象。它适用于长时间运行的服务器应用程序,这对应用程序冻结不利。所以,如果我们可以分配更多的 CPU 来避免应用程序暂停,我们可以在我们的应用程序中选择 CMS 收集器进行 GC。要启用 CMS 收集器,请设置 -XX:+UseConcMarkSweepGC 选项。

G1 collector

这是新的收集器,在 JDK 7 更新 4 中引入。G1 收集器专为愿意分配超过 4 GB 堆内存的应用程序而设计。 G1 将堆划分为多个区域,范围从 1 MB 到 32 MB,具体取决于我们配置的堆,并使用多个后台线程来扫描堆区域。将堆划分为多个区域的好处是 G1 将首先扫描存在大量垃圾的区域以满足给定的暂停时间。

G1 在后台线程完成对未使用对象的扫描之前减少低堆可用性的变化。这减少了STW的机会。 G1 在运行中压缩堆,与 CMS 不同,CMS 在 STW 期间执行此操作。

为了在我们的应用程序中启用 G1 垃圾收集器,我们需要在 JVM 参数中设置 -XX:+UseG1GC 选项。

Java 8 update 20 引入了一个新的 JVM 参数, -XX:+UseStringDeduplication,用于 G1 收集器。使用这个参数,G1 识别重复的字符串并创建指向相同积分的指针 char[] 数组以避免同一字符串的多个副本。
从 Java 8 PermGen,部分堆被移除。这是为类元数据、静态变量和内部字符串分配的部分。这种参数调整引起了许多 OutOfMemory 异常,从 Java 8 开始就可以了,JVM 会处理它。

Heap memory

堆内存主要分为两代:年轻一代和老一代。在 Java 7 之前,有一个 PERM GENERATION 是堆内存的一部分,而从 Java 8 开始, PERM GENERATION span>被 METASPACE 取代。 METASPACE 不是堆内存的一部分,而是 Native Memory 的一部分。使用 -XX:MaxMetaspaceSize 选项设置 METASPACE 的大小。在投入生产时考虑此设置至关重要,因为如果 METASPACE 占用过多内存,它会影响应用程序的性能:

读书笔记《hands-on-high-performance-with-spring-5》JVM内部
Java 8 memory management

年轻一代是创建和分配对象的地方;它适用于年轻的物体。 Young Generation又进一步分为Survivor Space。以下是热点堆结构:

读书笔记《hands-on-high-performance-with-spring-5》JVM内部

默认情况下,eden 区域大于 Survivor Space。所有对象都首先在 eden 区域中创建。当eden满时,触发minor GC,快速扫描对象的引用,未引用的对象标记为dead并回收。其中任何一个的幸存者空间区域总是空的。在次要 GC 期间在 eden 中幸存的对象将被移动到空的 Survivor Space。我们可能想知道为什么有两个幸存者空间区域而不是一个。原因是为了避免内存碎片。当 Young Generation 跑过并从 Survivor Space 中移除死亡对象时,它会在内存中留下空洞,需要进行压缩。为了避免压缩,JVM 将幸存的对象从一个 Survivor Space 移动到另一个。这种从 eden 和一个 Survivor Space 到另一个的活动对象的乒乓球发生直到以下情况发生:

  • Objects reach maximum tenuring threshold. This means objects are no longer young.
  • Survivor Space is full and cannot accommodate any new objects.

当上述条件发生时,对象被移动到Old Generation。

JVM flags

以下是应用程序中常用的 JVM 参数/标志,用于调整 JVM 以获得更好的性能。调整值取决于我们的应用程序的行为和生成它的速率。因此,没有明确的指导方针来使用 JVM 标志的特定值来获得更好的性能。

-Xms and -Xmx

-Xms-Xmx 被称为最小和最大堆大小。将 -Xms 设置为等于 -Xmx 可防止堆扩展期间的 GC 暂停并提高性能。

-XX:NewSize and -XX:MaxNewSize

我们可以使用 -XX:MaxNewSize 设置年轻代的大小。 Young Generation 位于总堆内存之下,如果我们将 Young Generation 的大小设置为大,Old Generation 的大小会更小 。出于稳定性原因,年轻代的大小永远不应大于老一代。因此,-Xmx/2 是我们可以为 -XX:MaxNewSize 设置的最大尺寸。

为了获得更好的性能,通过设置 -XX:NewSize 标志来设置年轻代的初始大小。这节省了随着时间的推移年轻一代增长到该规模的一些成本。

-XX:NewRatio

我们可以使用 -XX:NewRatio 选项将年轻代的大小设置为老一代的比例。我们可以使用此选项获得的好处可能是当 JVM 在执行期间调整总堆大小时,年轻代可以增长和缩小。 -XX:NewRatio 表示老一代的比例大于年轻一代。 -XX:NewRatio=2 表示老年代的大小是新生代的两倍,也就是新生代是总堆的1/3。

如果我们为年轻代指定比率和固定大小,那么固定大小将优先。关于指定年轻代大小的哪种方法更可取,没有生成规则。这里的经验法则是,如果您知道我们的应用程序生成的对象的大小,则指定固定大小,否则指定比率。

-XX:SurvivorRatio

-XX:SurvivorRatio 值是伊甸园相对于幸存者空间的比率。 将有两个幸存者空间,每个空间都相等。 如果-XX:SurvivorRatio=8,则eden占3/4,每个Survivor Spaces占Old Generation总大小的1/4。

如果我们设置一个比例使得 Survivor Spaces 很小,那么 eden 将为新对象腾出更多空间。在 Minor GC 期间,未引用的对象将被收集,新对象的 eden 将为空,但是,如果对象仍有引用,则垃圾收集器将它们移动到幸存者空间。如果 Survivor Space 很小,无法容纳新的对象,那么这些对象将被移动到老年代。 Old Generation 中的对象只能在 Full GC 期间收集,这会在应用程序中造成长时间的停顿。而如果幸存者空间足够大,那么更多的物体可以在幸存者空间中生存,但会英年早逝。如果 Survivor Spaces 很大,那么 eden 就会很小,一个小的 eden 会导致频繁的年轻 GC。

-XX:InitialTenuringThreshold, -XX:MaxTenuringThreshold, and -XX:TargetSurvivorRatio

寿命阈值决定何时可以将对象从年轻代提升/移动到老年代。 我们可以使用 -XX:InitialTenuringThreshold-XX:MaxTenuringThreshold JVM 标志来设置使用期限阈值的初始值和最大值。我们还可以使用 -XX:TargetSurvivorRatio 来指定年轻代 GC 结束时 Survivor Space 的目标利用率(以百分比表示)。

-XX:CMSInitiatingOccupancyFraction

使用 CMS 收集器时使用 -XX:CMSInitiatingOccupancyFraction=85 选项 (-XX:+UseConcMarkSweepGC)。如果设置了标志并且 Old Generation 已满 85%,则 CMS 收集器开始收集未引用的对象。 CMS 没有必要在 Old Generation 占用 85% 后才开始收集。如果我们希望 CMS 仅在 85% 时启动,那么我们需要设置 -XX:+UseCMSInitiatingOccupancyOnly-XX:CMSInitiatingOccupancyFraction 标志的默认值为 65%。

-XX:+PrintGCDetails, -XX:+PrintGCDateStamps, and -XX:+PrintTenuringDistribution

设置标志以生成 GC 日志。为了微调 JVM 参数以获得更好的性能,了解 GC 日志和应用程序的行为非常重要。 -XX:+PrintTenuringDistribution 报告对象的统计信息(它们的年龄)以及提升对象时所需的对象阈值。这对于了解我们的应用程序如何保存对象非常重要。

Tools to analyze GC logs

Java GC 日志是我们可以在出现性能问题时开始调试应用程序的地方之一。 GC 日志提供重要信息,例如:

  • The last time the GC ran
  • The number of GC cycles run
  • The interval at which the GC ran
  • The amount of memory freed up after the GC ran
  • The time the GC took to run
  • The amount of time for which the JVM paused when the garbage collector ran
  • The amount of memory allocated to each generation

以下是示例 GC 日志:

2018-05-09T14:02:17.676+0530: 0.315: Total time for which application threads were stopped: 0.0001783 seconds, Stopping threads took: 0.0000239 seconds
2018-05-09T14:02:17.964+0530: 0.603: Application time: 0.2881052 seconds
.....
2018-05-09T14:02:18.940+0530: 1.579: Total time for which application threads were stopped: 0.0003113 seconds, Stopping threads took: 0.0000517 seconds
2018-05-09T14:02:19.028+0530: 1.667: Application time: 0.0877361 seconds
2018-05-09T14:02:19.028+0530: 1.667: [GC (Allocation Failure) [PSYoungGen: 65536K->10723K(76288K)] 65536K->13509K(251392K), 0.0176650 secs] [Times: user=0.05 sys=0.00, real=0.02 secs] 
2018-05-09T14:02:19.045+0530: 1.685: Total time for which application threads were stopped: 0.0179326 seconds, Stopping threads took: 0.0000525 seconds
2018-05-09T14:02:20.045+0530: 2.684: Application time: 0.9992739 seconds
.....
2018-05-09T14:03:54.109+0530: 96.748: Total time for which application threads were stopped: 0.0000498 seconds, Stopping threads took: 0.0000171 seconds
Heap
 PSYoungGen total 76288K, used 39291K [0x000000076b200000, 0x0000000774700000, 0x00000007c0000000)
  eden space 65536K, 43% used [0x000000076b200000,0x000000076cde5e30,0x000000076f200000)
  from space 10752K, 99% used [0x000000076f200000,0x000000076fc78e28,0x000000076fc80000)
  to space 10752K, 0% used [0x0000000773c80000,0x0000000773c80000,0x0000000774700000)
 ParOldGen total 175104K, used 2785K [0x00000006c1600000, 0x00000006cc100000, 0x000000076b200000)
  object space 175104K, 1% used [0x00000006c1600000,0x00000006c18b86c8,0x00000006cc100000)
 Metaspace used 18365K, capacity 19154K, committed 19456K, reserved 1067008K
  class space used 2516K, capacity 2690K, committed 2816K, reserved 1048576K
2018-05-09T14:03:54.123+0530: 96.761: Application time: 0.0131957 seconds

这些日志很难快速解释。如果我们有一个可以在可视界面中呈现这些日志的工具,那么就可以轻松快速地了解 GC 发生了什么。我们将在下一节中介绍一个这样的工具来解释 GC 日志。

GCeasy

GCeasy 是最流行的垃圾收集日志分析工具之一。 GCeasy 被开发用于自动识别 GC 日志中的问题。它足够聪明,可以提供解决问题的替代方法。

以下是 GCeasy 提供的重要基本功能:

  • Uses machine learning algorithms to analyze the logs
  • Quickly detects memory leaks, premature object promotions, long JVM pauses, and many other performance issues
  • Powerful and informative visual analyzer
  • Provides the REST API for proactive log analysis
  • Free cloud-based tool for log analysis
  • Provides suggestions on the JVM heap size
  • Equipped to analyze all formats of GC logs

GCeasy.io (http://www.gceasy.io/) 是在线垃圾回收日志分析工具。它需要将日志文件上传到 GCeasy 公有云。

以下是使用在线工具收集详细日志分析的步骤:

  1. Enable GC logs in the application by adding XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:<GC-log-file-path> in the JVM parameters on the server.
  2. Once the GC log file is generated at a specified location, upload the file to the GCeasy cloud by navigating to http://gceasy.io/. It is also possible to upload a compressed ZIP file in case there are multiple log files to be analyzed.
  3. Once the log files are processed, the detailed analysis report will be generated.

该报告组织得当且足够详细,以突出显示导致性能下降的所有可能问题。以下部分解释了 GCeasy 生成的报告中的重要部分。

Tips on JVM tuning

报告的顶部提供了基于垃圾收集日志分析的建议。这些建议是在对日志文件进行彻底分析后由机器学习算法动态生成的。建议中的详细信息还包括问题的可能原因。以下是 GCeasy 在 GC 日志分析后提供的示例建议:

读书笔记《hands-on-high-performance-with-spring-5》JVM内部

JVM Heap Size

报告中的这一部分提供了有关每一代内存的堆分配和峰值内存使用情况的信息。分配的堆大小可能与 JVM 参数中定义的不匹配。这是因为 GCeasy 工具从日志中获取分配的内存信息。我们可能分配了 2 GB 的堆内存,但在运行时,JVM 只能分配 1 GB 的堆内存。在这种情况下,报告会将分配的内存显示为 1 GB。该报告以表格和图形格式显示堆分配。以下是报告中的示例堆大小部分:

读书笔记《hands-on-high-performance-with-spring-5》JVM内部

Key Performance Indicators

关键绩效指标 (KPI) 有助于做出重大决策以提高应用程序的性能。吞吐量、延迟和占用空间是一些重要的 KPI。报告中的 KPI 包括 吞吐量延迟。占用空间基本上描述了 CPU 被占用的时间量。它可以从性能监控工具(例如 JVisualVM)中获得。

Throughput 选项表示应用程序在指定时间段内完成的生产性工作量。 Latency 选项表示 GC 运行的平均时间。

以下是报告中的 KPI 示例:

读书笔记《hands-on-high-performance-with-spring-5》JVM内部

GC Statistics

GC 统计信息部分提供有关垃圾收集器在一段时间内的行为的信息。周期是分析日志的持续时间。 GC 统计数据基于实时分析提供。统计信息包括垃圾收集器运行后回收的字节数、累积 GC 时间(以秒为单位)和平均 GC 时间(以秒为单位)。本节还以表格形式提供有关总 GC 统计信息、次要和完整 GC 统计信息以及 GC 暂停统计信息。

GC Causes

GC Causes 部分提供了有关导致垃圾收集器运行的原因的信息。信息以表格和图形格式提供。除了原因之外,它还提供了有关垃圾收集器执行时间的信息。以下是报告中的一个示例:

读书笔记《hands-on-high-performance-with-spring-5》JVM内部

综上所述,GCeasy是帮助开发者直观解读GC日志的重要工具。

Summary

在本章中,我们了解了 JVM 及其参数。我们了解了与 GC 相关的内存泄漏和常见误解。我们了解了不同的 GC 方法及其重要性。我们了解了导入 JVM 标志,这些标志经过调整以实现更好的性能。

在下一章中,我们将了解 Spring Boot 微服务及其性能调优。微服务是一种应用程序的架构,它具有实现业务能力的松耦合服务。 Spring Boot 使我们能够构建生产就绪的应用程序。