Java虚拟机创建了C1和C2编译器线程,用以优化应用程序的性能。但是有时这些线程会消耗大量CPU资源。在这篇文章中,我们将深入探讨C1和C2编译器线程,以及如何解决它们可能导致的高CPU消耗问题。
Hotspot JIT
HotSpot JIT(Just-In-Time)编译器是Java虚拟机(JVM)中的即时编译器,它负责将Java字节码转换为本地机器代码。HotSpot是Oracle JDK和OpenJDK中默认的JVM实现,它包含两个主要的即时编译器:C1(Client Compiler)和C2(Server Compiler)。
HotSpot JIT编译器的工作方式是在运行时动态地将经常执行的字节码编译为本地机器代码,以提高程序的性能。它通过以下步骤实现:
- 解释执行(Interpretation):初始阶段,JVM会对Java字节码进行解释执行,不生成本地机器代码,这使得程序可以立即运行。
- 即时编译(Just-In-Time Compilation):当某段代码(通常是被频繁执行的热点代码)被识别为性能瓶颈时,HotSpot JIT编译器将这些热点代码编译成本地机器代码。这个过程包括对代码进行优化,以提高执行速度。
- 本地机器代码执行(Execution of Native Code):一旦代码被编译成本地机器代码,JVM会直接执行这些代码,而不是再次解释执行对应的字节码。
C1编译器通常用于快速启动和简单的应用程序,因为它生成的代码速度较快,但优化程度较低。而C2编译器更加激进,会花费更多时间进行更深层次的优化,生成更高效的本地机器代码,适用于需要更高性能的场景。
HotSpot JIT编译器的工作方式有助于提高Java程序的性能,因为它能够在运行时优化热点代码,将其转换为更高效的本地机器代码,从而减少解释执行的开销,提高程序运行速度。
代码缓存
代码缓存(Code Cache)是Java虚拟机(JVM)中用于存储已编译代码的特定区域。在JIT(Just-In-Time)编译器将Java字节码编译成本地机器代码时,这些生成的本地机器代码被存储在代码缓存中。
代码缓存的作用是保存已经编译过的代码,以便在程序的后续执行中直接使用这些本地机器代码,而无需重复地进行编译。这样可以提高程序的性能,因为避免了重复的编译过程,减少了解释执行的开销。
C1和C2编译器区别
在Java早期阶段,存在两种类型的JIT(即时编译)编译器,分别是Client(客户端)和Server(服务器)。根据所需的JIT编译器类型,需要下载并安装相应的JDK。例如,如果您正在构建桌面应用程序,则需要下载具有“客户端”JIT编译器的JDK;如果是构建服务器应用程序,则需要下载具有“服务器”JIT编译器的JDK。
一旦应用程序启动,客户端JIT编译器就会开始对代码进行编译。而服务器JIT编译器则会观察代码执行相当长的一段时间。根据其获取的执行知识,服务器JIT编译器将开始进行JIT编译。尽管服务器JIT编译速度较慢,但生成的代码将比客户端JIT编译器生成的代码更优化,性能更出色。
然而,现代的JDK现在内置了客户端和服务器JIT编译器。这两个编译器都尝试对应用程序代码进行优化。在应用程序启动阶段,会使用客户端JIT编译器对代码进行编译。随着程序执行知识的积累,随后会采用服务器JIT编译器对代码进行编译。这种方法在JVM中被称为分层编译。
JDK开发人员通常将这两种编译器称为客户端和服务器JIT编译器,而内部则分别称为c1和c2编译器。因此,客户端JIT编译器所使用的线程被称为C1编译器线程,而服务器JIT编译器所使用的线程被称为C2编译器线程。
C1、C2编译器线程
C1、C2 编译器线程的默认数量根据运行应用程序的容器/设备上可用的 CPU 数量确定。下表总结了 C1、C2 编译器线程的默认数量:
中央处理器 | c1 线程 | c2 线程 |
1 | 1 | 1 |
2 | 1 | 1 |
4 | 1 | 2 |
8 | 1 | 2 |
16 | 2 | 6 |
32 | 3 | 7 |
64 | 4 | 8 |
128 | 4 | 10 |
C1、C2 编译器优化
当c1和c2编译器线程消耗大量CPU时,以下是解决该问题的潜在解决方案:
什么都不做
如果C2编译器线程的CPU消耗只是间歇性地偏高而不是持续性的,并且这种情况并未对您的应用程序性能造成明显影响,可以考虑暂时忽略该问题。在某些情况下,临时的CPU高消耗可能是正常的,可能是因为JIT编译器正在进行优化或在应用程序启动后初始编译所致。
然而,如果这种间歇性高CPU消耗开始对应用程序的性能产生负面影响,或者频繁发生,并且持续时间较长,那么可能需要进一步调查和解决。此时,可以考虑采取一些步骤,例如监视JIT编译器的行为、分析编译日志、调整JVM参数或升级到更新的JVM版本,以寻找潜在的解决方案。
总体来说,仅当间歇性的C2编译器线程高CPU消耗并未对应用程序的整体性能产生重大影响时,暂时忽略该问题可能是一个可行的做法。但如果情况变得更加频繁或持续,可能需要更深入地调查和处理。
分层编译
将 -XX:-TieredCompilation JVM参数传递给应用程序将禁用JIT(Just-In-Time)热点编译。这意味着代码将不会根据执行频率进行动态优化,从而可能降低CPU消耗。然而,需要注意的是,作为副作用,您的应用程序性能可能会受到影响,因为禁用了JIT编译会导致代码执行时不再进行实时优化。
此参数的使用是一种权衡:通过降低CPU消耗来解决高CPU消耗问题,但可能以牺牲应用程序性能为代价。因此,在使用这个参数之前,需要仔细权衡,并在实际应用程序环境中进行测试,以确保最终结果不会对应用程序的整体性能产生不可接受的影响。
设置分层等级
当CPU峰值是由C2编译器线程单独引起时,你可以选择单独关闭C2编译。通过传递 -XX:TieredStopAtLevel=3 参数,可以实现这一目的。此参数的作用是仅启用C1编译器,同时禁用C2编译器。
这种方法可以降低CPU消耗,因为禁用C2编译器会使系统只使用较轻量级的C1编译器,但需要注意的是,这可能会影响到应用程序的性能。
在使用此参数之前,建议进行详尽测试,以确保对应用程序性能的影响在可接受范围内。选择禁用C2编译器应慎重考虑,因为可能会牺牲应用程序的性能优化能力。
编译分为四层:
编译级别 | 描述 |
0 | 解释代码 |
1 | 简单的c1编译代码 |
2 | 有限的c1编译代码 |
3 | 完整的c1编译代码 |
4 | C2编译代码 |
打印编译信息
-XX: PrintCompilation 是一个非常有用的JVM参数。通过传递此参数给您的应用程序,JVM将会打印有关应用程序编译过程的详细信息,这可以帮助你更好地了解代码的实际编译情况。
打印编译信息可以提供有关哪些方法被编译、何时被编译以及使用了哪种类型的编译器(比如C1或C2)等方面的详细信息。这对于调整和优化应用程序的性能非常有帮助,因为您可以通过查看输出信息来了解编译器在何处花费时间,从而有针对性地进行优化。
但需要注意的是,输出的信息可能会非常详细和庞大,可能会对系统性能产生一定的影响。因此,在生产环境中使用此参数时要小心谨慎,并且最好在测试环境中进行尝试和调整,以避免对实际应用程序的性能产生不必要的负面影响。
设置缓存区大小
Hotspot JIT编译器在JVM内存中有一个代码缓存区域,用于存储它编译和优化的代码。默认情况下,代码缓存区域的大小为240MB。
可以通过将 -XX:ReservedCodeCacheSize=N 传递给程序来增加代码缓存的大小。例如 -XX:ReservedCodeCacheSize=512m 进行指定。
增加代码缓存的大小有助于提高JIT编译器在其中存储优化代码的容量,从而有可能减少编译器线程的CPU消耗。这种调整可以为编译器提供更多的空间,以存储更多的编译代码,减少由于不断重编译代码而导致的性能损失。
增加代码缓存的大小也会占用更多的内存资源。在进行此类调整时,请确保考虑到系统的内存限制以及其他应用程序或组件对内存的需求,以避免因为过度分配内存而导致系统性能问题。最佳做法是进行适度的调整并在测试环境中进行验证,以确保对应用程序性能的提升并没有不必要地牺牲其他方面的系统资源。
设置编译线程数
可以使用参数 -XX:CICompilerCount 来控制C2编译器线程的数量。默认情况下,C2编译器线程的数量由JVM根据CPU核心数量和其他因素自动确定。但有时可能会发现C2编译器线程数量较少,尤其是在具有多个CPU处理器或内核的系统上。
通过捕获线程转储并上传到适当的工具(如诊断工具或性能监控工具),我们可以查看C2编译器线程的实际数量。如果C2编译器线程数过少,您可以尝试使用 -XX:CICompilerCount=8 这样的参数来手动增加C2编译器线程的数量。
增加C2编译器线程的数量可能有助于提高JIT编译的并发性能,特别是在具有更多CPU核心的系统上。但请注意,过多的编译器线程可能会导致资源竞争和性能下降。因此,在调整此参数之前,请务必进行仔细的测试和评估,以确保其对应用程序性能的实际影响是积极的,并且不会造成其他系统方面的负面影响。