一、前言
在本文中,你将理解 OpenJDK HotSpot Java 虚拟机 (HotSpot JVM) 中的一些零碎常识,以及如何调整它们以获得最佳状态适应你的程序和运行环境。
HotSpot JVM 是一项了不起且灵便的技术。它作为二进制版本实用于每个次要操作系统和 CPU 架构,从微型 Raspberry Pi Zero 始终到蕴含数百个 CPU 内核和 TB 级 RAM 的“大型”服务器。因为 OpenJDK 是一个开源我的项目,HotSpot JVM 简直能够针对任何其余零碎进行编译,并且能够应用选项、开关和标记对其进行微调。
首先,这里有一些背景。HotSpot JVM 的语言是字节码。在撰写本文时,有超过 30 种编程语言能够编译成 HotSpot JVM 兼容的字节码,但迄今为止最受欢迎的、在寰球领有超过 800 万开发人员的当然是 Java。
Java 源代码被编译成字节码(如图 1 所示),以类文件的模式,应用 javac
编译器。在古代开发中,这可能会被 Maven、Gradle 或基于 IDE 的编译器等构建工具形象掉。
图 1. 编译字节码的过程
程序的字节码示意由 HotSpot JVM 在一个虚构堆栈机上执行,该虚构堆栈机晓得多达 256 条不同的指令,每条指令由一个 8 位数字操作码标识;因而,名称是“字节码”。
字节码程序由解释器执行,该解释器获取每条指令,将其操作数压入堆栈,而后执行该指令,移除操作数并将后果留在堆栈中,如图 2 所示。
图 2. 解释器执行字节码后堆栈上的后果
将程序执行从底层环境中形象进去,赋予了 Java“一次编写,随处运行”的可移植性劣势。在一种架构上编译的类文件能够运行在齐全不同架构的 HotSpot JVM 上执行。
如果你认为这种对底层硬件的形象是以就义性能为代价的,那么你是对的。这通常就是 HotSpot JVM 开关、选项和标记的用武之地。
二、JIT 即时编译
用可移植、功能丰富的高级语言(如 Java)编写的程序如何挑战那些从“低级”、“不太敌对”的编程语言(如 C)编译为特定于体系结构的本机代码的程序的性能呢?
答案是 HotSpot JVM 蕴含了性能晋升的即时(JIT)编译技术,它能够分析程序的执行状况,并有选择地优化它认为最有益处的局部。这些被称为程序的热点(因而,将其命名为 HotSpot JVM),它通过应用底层零碎架构的常识动静地将它们编译成本地代码来实现这一点。
HotSpot JVM 蕴含两个 JIT 编译器,称为 C1(客户端编译器)和 C2(服务器编译器),它们提供了不同的优化衡量。
- C1 提供了疾速、简略的优化。
- C2 提供了须要更多剖析的高级优化,而且利用老本更高。
自 JDK 8 公布以来,默认行为始终是在称为分层编译的模式下同时应用这两个编译器,其中 C1 提供了疾速的速度晋升,而 C2 在进行高级优化之前收集了足够的评测信息。生成的本机代码存储在热点 JVM 的内存区域中,称为代码缓存,如图 3 所示。
图 3. Java 编译过程
三、GC 垃圾回收
除了 JIT 技术之外,HotSpot JVM 还包含进步生产力和性能的性能,例如:多线程和主动内存治理以及垃圾收集 (GC) 策略的抉择。
对象被调配在 HotSpot JVM 的一个称为堆的内存区域中,一旦这些对象不再被援用,垃圾收集器就能够将它们清理洁净,并将它们应用的内存回收。
四、符合人体工程学的 HotSpot JVM
HotSpot JVM 具备如此多的灵活性和动静行为,你可能会放心如何配置它以最好地满足你的程序要求。侥幸的是,对于很多用例,你不须要进行任何手动调整。HotSpot JVM 蕴含一个称为 ergonomic(人体工程学)的过程,它在启动时查看执行环境,并依据 CPU 内核数量和可用 RAM 数量为 GC 策略、堆大小和 JIT 编译器抉择一些正当的默认值。以后的默认值是:
- 垃圾收集器:G1 GC
- 初始堆:物理内存的 1/64
- 最大堆:物理内存的 1/4
- JIT 编译器:同时应用 C1 和 C2 的分层编译
通过应用选项 -XX:+PrintFlagsFinal
并应用 grep
命令搜寻 _ergonomic_,你能够看到 HotSpot JVM 将为你的环境抉择的所有 ergonomic 默认值,如下所示:
java -XX:+PrintFlagsFinal | grep ergonomic
intx CICompilerCount = 4 {product} {ergonomic}
uint ConcGCThreads = 2 {product} {ergonomic}
uint G1ConcRefinementThreads = 8 {product} {ergonomic}
size_t G1HeapRegionSize = 2097152 {product} {ergonomic}
uintx GCDrainStackTargetSize = 64 {product} {ergonomic}
size_t InitialHeapSize = 526385152 {product} {ergonomic}
size_t MarkStackSize = 4194304 {product} {ergonomic}
size_t MaxHeapSize = 8403288064 {product} {ergonomic}
size_t MaxNewSize = 5041553408 {product} {ergonomic}
size_t MinHeapDeltaBytes = 2097152 {product} {ergonomic}
uintx NonNMethodCodeHeapSize = 5836300 {pd product} {ergonomic}
uintx NonProfiledCodeHeapSize = 122910970 {pd product} {ergonomic}
uintx ProfiledCodeHeapSize = 122910970 {pd product} {ergonomic}
uintx ReservedCodeCacheSize = 251658240 {pd product} {ergonomic}
bool SegmentedCodeCache = true {product} {ergonomic}
bool UseCompressedClassPointers = true {lp64_product} {ergonomic}
bool UseCompressedOops = true {lp64_product} {ergonomic}
bool UseG1GC = true {product} {ergonomic}
下面的输入来自具备 32 GB RAM 的机器上的 JDK 11,因而初始堆设置为 32 GB 的 1/64(约 512 MB),最大堆设置为 32 GB 的 1/4(8 GB)。
五、自定义
如果你认为默认的设置不适宜你的应用程序,很快乐 HotSpot JVM 在每个畛域都具备高度可配置性。
有三种次要类型的配置选项:
- 规范: 根本启动选项,例如
-classpath
在 HotSpot JVM 实现中很常见。 - -X: 用于配置 HotSpot JVM 的通用属性的非标准选项,例如管制最大堆大小 (
-Xmx
);不能保障所有 HotSpot JVM 实现都反对这些。 - -XX: 用于配置 HotSpot JVM 的高级属性的高级选项。依据文档,这些内容如有更改,恕不另行通知,但 Java 团队有一个治理良好的流程来删除它们。
六、-XX 选项
许多 -XX 选项能够进一步表征如下:
Product. 这些是最罕用的 -XX 选项。
Experimental. 这些是与 HotSpot JVM 中的试验性功能相干的选项,这些性能可能尚未筹备好投入生产。这些选项容许你尝试新的 HotSpot JVM 性能,并且须要通过指定以下内容来解锁它们:
-XX:+UnlockExperimentalVMOptions
例如,在 JDK 11 中应用 ZGC 垃圾收集器能够这样开启:
java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC
一旦一个试验性功能筹备好投入生产,管制它的选项就不再被归类为实验性的,不须要解锁。ZGC 收集器成为 JDK 15 中的 Product 选项。
Manageable. 这些选项也能够在运行时通过 MXBean API 或其余 JDK 工具设置。例如,要在 HotSpot JVM 线程转储中显示 java.util.concurrent 类持有的锁,请应用:
java -XX:+PrintConcurrentLocks
Diagnostic. 这些选项与拜访无关 HotSpot JVM 的高级诊断信息无关。这些选项须要你应用以下内容能力应用:
-XX:+UnlockDiagnosticVMOptions
一个示例诊断选项是:
-XX:+LogCompilation
它批示 HotSpot JVM 输入一个日志文件,其中蕴含 JIT 编译器所做的所有优化的详细信息。你能够查看此输入以理解程序的哪些局部已优化,并确定程序中可能未按预期优化的局部。LogCompilation
输入很具体,但能够在 JITWatch 等工具中可视化,它能够通知你无关办法内联、逃逸剖析、锁省略和 HotSpot JVM 对你运行的代码所做的其余优化。
Developmental. 这些选项容许配置和调试最高级的 HotSpot JVM 设置,并且在你拜访它们之前须要应用非凡的 HotSpot JVM 构建调试。
七、增加和删除的选项
选项开关的增加和删除是在 HotSpot JVM 中次要性能的到来或弃用之后进行的。这里有一些值得注意的中央。
- 在 JDK 9 中,许多
-XX:+Print...
和-XX:+Trace...
日志选项被删除并替换为-Xlog
选项,用于管制由 JEP 158 引入的对立日志记录子系统。 - 在增加了实验性 ZGC、Epsilon 和 Shenandoah 垃圾收集器的选项后,JDK11 中的选项数达到峰值,达到了惊人的 1504 个。
- 随着并发标记扫描(CMS)垃圾收集器的删除,JDK14 中的数据量大幅降落,如 JEP 363 中所述。
图 4. 每个版本的 OpenJDK 中的选项总数(包含产品、试验、诊断和开发)
表 1. 从 OpenJDK 17 中删除的 OpenJDK 16 之前的 HotSpot JVM 选项
表 2. OpenJDK 17 新退出的 HotSpot JVM 选项
八、配置项的生命周期
那么 HotSpot JVM 开发团队如何治理选项的删除呢?自 JDK 9 以来,删除 -XX 选项的过程被扩大为三步过程:弃用、过期和过期,以向用户收回大量正告,提醒他们的 Java 命令行可能很快须要更新。
让咱们看看 HotSpot JVM 如何对 -XX:+AggressiveOpts
选项作出的操作,该选项在 JDK 11 中被弃用,在 JDK 12 中被淘汰,最初在 JDK 13 中过期。
不举荐应用的选项。尽管能够反对这些选项,但会打印一条正告并让你晓得未来可能会删除反对,例如:
./jdk11/bin/java -XX:+AggressiveOpts
OpenJDK 64-Bit Server VM warning: Option AggressiveOpts was deprecated in version 11.0 and will likely be removed in a future release.
过期的选项。这些选项尽管已被删除,但在命令行上仍被承受。(程序)会打印一条正告,让你晓得这些选项未来可能不会被承受,例如:
./jdk12/bin/java -XX:+AggressiveOpts
OpenJDK 64-Bit Server VM warning: Ignoring option AggressiveOpts; support was removed in 12.0
过期的选项。 这些是不举荐应用或过期的选项,其 accept_until
版本小于或等于以后 JDK 版本。当这些选项在其过期的 JDK 版本中应用时,会打印一条正告,例如:
./jdk13/bin/java -XX:+AggressiveOpts
OpenJDK 64-Bit Server VM warning: Ignoring option AggressiveOpts; support was removed in 12.0
齐全失败(不可用)。 当你一旦应用了某个老版本 JDK 中过期的配置时,HotSpot JVM 将在通过该选项并打印正告后启动失败,例如:
./jdk14/bin/java -XX:+AggressiveOpts
Unrecognized VM option 'AggressiveOpts'
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
令人遗憾的是,并不是所有的 option 都以这种有序的形式登场。例如,JDK 9 在引入对立日志记录和弱小的 -Xlog
选项时放弃了对大量选项的反对,这在 Nicolai Palog 的博客中有具体介绍。Java 文档网站上还有一个页面,参考:Convert GC Logging Flags to Xlog。
九、迁徙到高版本的 JDK
那么,你是否筹备将 Java 启动脚本命令迁徙到更高版本的 JDK?兴许你应用了对立启动脚本,其中充斥了你不相熟的选项和配置,并且放心调整会影响应用程序的稳定性。
你能够应用 JaCoLine,Java 命令行查看器来帮忙你。粘贴命令,抉择指标平台,而后剖析你的配置选项将如何工作。见图 5。
图 5. 应用 JaCoLine 剖析命令行选项
十、JVM 参数配置倡议
尽管在 HotSpot JVM 调优方面没有一刀切的倡议,但我置信必定有一些选项能够帮忙你更好地理解程序的执行并做出理智的配置抉择。
以下选项在 JDK 11 及更高版本中可用。我抉择这些开关是因为许多开发人员还没有转向更高版本的 Java。请记住,这些都是可选的;HotSpot JVM 的默认设置十分好。
首先,理解内存应用状况。 在 HotSpot JVM 中分配内存很便宜。垃圾收集老本是指当热点 JVM 清理堆中不再须要的对象时,稍后以执行暂停的模式到期的耗费。
在进步应用程序性能和稳定性方面,理解代码进行的堆调配以及由此产生的 GC 行为可能是最容易解决的问题,因为堆和 GC 配置以及应用程序的调配行为之间的不匹配会导致适度暂停,从而中断应用程序的过程。
应用 JaCoLine Statistics 网页确认配置堆和 GC 日志记录是 JaCoLine 查看的所有命令行中最受欢迎的选项。
要配置堆,请思考以下问题的答案:
- 失常状况下预期最大堆的内存使用量是多少?
-Xmx
设置最大堆大小,例如:-Xmx8g
。-XX:MaxRAMPercentage=n
将最大堆设置为总 RAM 的百分比。- 你冀望堆多快达到其最大值?
-Xms
设置初始堆大小,例如:-Xms256m
.-XX:InitialRAMPercentage=n
将最大堆设置为总 RAM 的百分比。- 如果心愿堆快速增长,能够将初始堆设置为更靠近最大堆。
要解决 OutOfMemory
谬误,须要思考在应用程序内存不足时 HotSpot JVM 应该如何工作。
-XX:+ExitOnOutOfMemoryError
通知 HotSpot JVM 在呈现第一个OutOfMemory
谬误时退出。如果 HotSpot JVM 将主动重新启动,这会很有用。-XX:+HeapDumpOnOutOfMemoryError
通过将堆的内容转储到java_pid.hprof
文件来帮忙诊断内存透露。-XX:HeapDumpPath
定义 heap dump 门路。
其次,抉择垃圾收集器。 大多数硬件上的 JDK 11 人体工程学过程将默认抉择 G1GC 收集器,但它不是 JDK 11 及更高版本中的惟一抉择。
其余可用的垃圾收集器是:
-XX:+UseSerialGC
抉择串行收集器,它在单个线程上执行所有 GC 工作。-XX:+UseParallelGC
抉择并行(吞吐量)收集器,它能够应用多个线程执行压缩。-XX:+UseConcMarkSweepGC
抉择 CMS 收集器。请留神,CMS 收集器在 JDK 9 中已被弃用,并在 JDK 14 中被删除。-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
抉择 ZGC 收集器(在 JDK 11 中是实验性的,在 JDK 14 及更高版本中是规范性能;因而你不须要此开关)。
能够在 HotSpot Virtual Machine Garbage Collection Tuning Guide 中找到无关为你的应用程序抉择收集器的倡议。这是 JDK 11 的文档版本;如果你应用的是更高版本的 Java,请搜寻更新的文档。
为防止过早晋升,请思考你的应用程序是否以高分配率创立短期对象。这可能导致短期对象过早晋升到老年代堆空间,在那里它们将累积,直到须要残缺的垃圾收集。
-XX:NewSize=n
定义新生代的初始大小。-XX:MaxNewSize=n
定义新生代的最大大小。-XX:MaxTenuringThreshold=n
是一个对象在晋升到老年代之前能够存活的最大新生代汇合数。
要记录内存应用状况和 GC 流动,请执行以下操作:
- 应用
-XX:+UnlockDiagnosticVMOptions ‑XX:NativeMemoryTracking=summary ‑XX:+PrintNMTStatistics
获取 HotSpot JVM 退出时内存应用状况的残缺细节。 - 应用以下命令启用 GC 日志记录:
-Xlog:gc
提供根本的 GC 日志记录。-Xlog:gc*
提供具体的 GC 日志记录。
最初,理解 JIT 编译器如何优化你的代码。 一旦你对应用程序的 GC 进展处于可承受的程度感到称心,你就能够查看 HotSpot JVM 的 JIT 编译器是否正在优化你认为对性能很重要的程序局部。
启用简略的编译日志,如下所示:
-XX:+PrintCompilation
将无关每个 JIT 编译的根本信息打印到控制台。-XX:+UnlockDiagnosticVMOptions ‑XX:+PrintCompilation ‑XX:+PrintInlining
增加无关办法内联的信息。
输入示例:
java -XX:+PrintCompilation
77 1 3 java.lang.StringLatin1::hashCode (42 bytes)
78 2 3 java.util.concurrent.ConcurrentHashMap::tabAt (22 bytes)
78 3 3 jdk.internal.misc.Unsafe::getObjectAcquire (7 bytes)
80 4 3 java.lang.Object:: (1 bytes)
80 5 3 java.lang.String::isLatin1 (19 bytes)
80 6 3 java.lang.String::hashCode (49 bytes)
输入中的我的项目(从左到右)如下:
PrintCompilation
在《Java JIT 编译器解释 – 第 1 局部》文章中有阐明。
将 JIT 信息记录到控制台对于查看办法是被 JIT 编译还是内联(或两者)十分有用,但如果你想更深刻地理解 JIT 优化,则须要启用具体的日志记录。
应用 -XX:+UnlockDiagnosticVMOptions ‑XX:+LogCompilation ‑XX:LogFile=jit.log
启用具体的编译日志记录。它反对具体的 XML 格局编译日志记录,能够在 JITWatch 等工具中进行剖析。你能够从 Ben Evans 的“应用 JITWatch 了解 Java JIT 编译,第 1 局部”以及第 2 局部和第 3 局部中理解无关 JITWatch 的更多信息。