Skip to content

性能微优化-JIT

About 6210 wordsAbout 21 min

性能新书

2026-08-19

第5章的性能优化策略适合各种开发语言,应用这些策略有较大的系统性能提升。有些场景下,核心系统或者工具类性能有极致的要求,这时候可以应用Java 自身的性能最佳实践,这些实践需要以Java编译和运行作为基础知识,掌握字节码和JIT等知识。

学完本章,将掌握如下知识

  • Java编译,把Java源码编译成字节码,并在编译过程中做的性能优化。
  • 字节码的基本知识,包括如何查看字节码,以及字节码的常用指令。本章的部分性能优化策略需要结合字节码解释。同时Java系統很多高级功能均是通过动态字节码生成实现
  • JIT基本知识,包括了解JIT的运行过程,以及观察JIT。通过对核心系统或者工具的运行的JIT日志进行分析,确保实现预期的Java优化。
  • Java语言中的一些高性能实践,可以应用核心系统或者工具库提高性能,比如String的最佳使用办法,位运算等

JIT

前三节涉及到JVM的知识,读者可以先阅读6.4节以后的内容,如果有不理解,在查看前3节的相应内容

读懂字节码有助于了解JVM执行代码过程,找到可能得性能优化点,或者使用字节码实现性能优化。阅读完本节,你将掌握如下知识

  • JVM规范中核心概念栈帧以及变量表,操作数栈。
  • 如何查看字节码,有助于我们在后续章节掌握Java的性能微优化知识
  • 如何直接编写字节码,有助于完成或者掌握Java性能优化中的反射,这是一个很多Java工具需要使用的性能优化点。同时直接在运行中生成字节码能完成Java中许多高级功能,如AOP,RPC等。

通过 javac 将程序源代码编译,转换成 java 字节码,JVM 通过模板(TemplateTable)方式把字节码将其翻译成对应的机器指令,逐条读入,逐条解释翻译。执行速度必然会比的二进制字节码程序慢很多。为了提高执行速度,从JDK1.2开始引入了 JIT 技术。

JIT是JVM的重要组成部分,JIT通过分析程序代码,找到热点的执行代码,会部分的把字节码编译成机器码保存起来用于下次调用,对于较小得方法,会尝试进行内联展开。

应用程序大部分情况下很少考虑到JIT的优化,这是一个自动过程。不过对于性能要求极高的工具或者关键服务类,还是可以考虑JIT对代码得优化影响,有时候性能能提高数百倍。

本节内容较多,学习完此节,掌握如下知识

  • JVM解释和编译执行
  • JIT的C1和C2编译方式
  • 代码缓存结构,用于存放编译的代码
  • 使用JITWatch可视化工具分析JIT的日志
  • JIT内联优化例子
  • JIT对虚方法调用例子

解释和编译

Java VM 使用了JIT(Just-in-time简称,程序即时编译) 编译器,在运行时刻进将字节码通过模板方式转为高效的机器码,也可以在运行时候通过分析,进一步编译成更加高效的机器码,因此现在Java程序,已经能跟C和C++具有一样的性能

对于操作系统来说,识别的是机器码指令,因此像C,C++高级语言都是将源程序编译成二进制码目标文件,然后链接成库文件或者可执行文件,这些二进制针对特定的平台,比如WINDOW,Linux。 这种方式称为AOT(Ahead-of-time,在执行执行前编译),对于PHP,则采用解释执行,可以运行在任何平台上,只要在平台上安装正确的解释器。解释器会翻译PHP代码成二进制代码执行,如果PHP被反复执行,则需要一遍又一遍翻译成机器码。解释执行的效率比编译后执行的效率低很多

注:PHP8也开始支持JIT

解释执行的优点是可以直接在任何平台上运行,不需要再编译,另外,如果代码更改,也可以直接解释执行而不需要再次编译成目标平台的机器码。

JVM的解释执行使用基于模板的解释器,比如,对于iload_1指令,从本地变量表读取索引为1的值放入操作数栈

iload_1

字节码iload_1映射成机器码

MOV -0x8(%r14), %eax  # ILOAD_1
movzbl    0x1(%r13),  %ebx # 下一个指令
inc       %r13  
mov       $0xff40,%r10  
jmpq      *(%r10,%rbx,8)

虚拟机对于最常用的代码,会尝试编译成机器码放到"Code Cache 代码缓存“里,如下图所示

JIT也会做其他优化,比如,对于多态调用,采用的是虚方法表中查找,在JIT在执行多次后,如果收集到足够的信息,会取消虚方法表查找过程,改为直接调用,具备与静态调用方法一样的性能。如果随后发现存在多态调用,则可能取消这部分优化,称之为逆优化(Deoptimization)

对于小的方法,JIT也可以优化为内联调用,比如属性字段的getter和 setter方法

public int getAge(){
  return age;
}

当调用user.getAge()的时候,JIT会直接优化成user.age对应的机器码。

通过虚拟机选项 -Xint 可以强制使用解释执行,如下JMH测试用于对比解释执行和编译机器码执行的性能

//JITTest.java
int x=0,y=0;
@Benchmark
@Fork(value=1,jvmArgsAppend="-Xint")
public  int  byteCode(){
    return x+y;
}

@Benchmark
@Fork(value=1)
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public  int  machineCode(){
    return x+y;
}

@Benchmark
@Fork(value=1)

public  int  machineCodeWithInline(){
    return x+y;
}

bytecode使用了@Fork(value=1,jvmArgsAppend="-Xint") ,虚拟机参数-Xint解释执行。其性能远远低于machinecode方法,machineCode指示JIT不做内联优化,因此性能又远低于machineCodeWithInline

Benchmark                       Mode  Cnt        Score   Error   Units
JITTest.byteCode               thrpt    2    20206.601          ops/ms
JITTest.machineCode            thrpt    2   527735.191          ops/ms
JITTest.machineCodeWithInline  thrpt    2  1546369.885          ops/ms

C1和C2

虚拟机运行有俩种模式,一种是client模式,一种是server模式,前者启动虚拟机使用“-client”,后者使用“-server”,这俩种模式的命名也是因为这俩个参数名字得来。client模式使用C1编译器,有较快的启动速度,简单的将字节码编译为机器码,一些Java UI,比如NetBean使用这种模式。Web应用通常使用server模式,server模式采用了重量级的C2编译器,C2 比 C1 编译器编译的更加激进,性能更高。C2提供了内联优化,循环展开,Dead-Code删除,分支预测等优化,现在版本的JDK在64位机器上默认情况下,均使用C2模式

启动client模式

java -clinet

启动server模式

java -server

基本上来讲,虚拟机会监控执行的方法,设定方法调用计数器,执行很频繁的方法(成为Hot Method,也是Hotpot VM名称的由来),在server模式下,默认执行10,000次那么这个方法的优化会被JIT作为一个任务放到一个优化的队列进行异步优化。C1队列或者C2队列,这个队列并非先进先出队列,是按照调用次数高,优先级高编译,编译线程个数取决于CPU的数量,如下是一个摘自默认的线程个数

CPUC1 编译线程个数C2 编译线程个数
111
211
412
812
1626
3237
6448
128410

被优化的代码会被放到代码缓存CodeCache,这样再次执行代码得时候,JVM将会使用新的优化代码。

对于for循环执行频繁的代码块,JIT也会为维护一个回边计数器,当超过设定阀值,会进行优化编译,这种编译叫做栈上替换(OSR),因为即使循环被编译了,这也是不够的:JVM 必须有能力当循环正在运行时,开始执行此循环已被编译的版本。换句话说,当循环的代码被编译完成,那么循环的下个迭代执行最新的被编译版本则会更加快

可以通过下图,理解解释执行,解释执行以及C1和C2对性能的影响

如果想理解JIT过程,最好的办法是通过JIT日志观察,可以使用 -XX:+PrintCompilation 开启JIT,虚拟机运行的时候,会输出到标准控制台,如下程序,启用 -XX:+PrintCompilation

public class HelloWorld {

  public static void main(String[] args){
    HelloWorld say = new HelloWorld();
    for(int i=0;i<20000;i++){
      say.sayHello();
    }

  }
  public void sayHello(){
    String msg = getMessage();
    String output = "hello"+msg;
  }
  public synchronized  String getMessage() {
    return "world";
  }
}

会在控制台有如下输出

    209    1     n 0       java.lang.System::arraycopy (native)   (static)
    239    2       4       java.lang.Object::<init> (1 bytes)
    240    3       3       java.lang.String::equals (81 bytes)
    241    5       4       java.lang.String::charAt (29 bytes)
    241    6       3       sun.nio.cs.UTF_8$Encoder::encode (359 bytes)
    242    4       3       java.lang.System::getSecurityManager (4 bytes)
    249    7       4       java.lang.String::indexOf (70 bytes)
    250    8       4       java.lang.String::hashCode (55 bytes)
    253    9       3       java.lang.Math::min (11 bytes)
    253   10       3       java.lang.String::startsWith (7 bytes)
    253   11       3       java.lang.String::toCharArray (25 bytes)
    256   12       1       java.net.URL::getQuery (5 bytes)
    256   13       3       java.lang.AbstractStringBuilder::append (50 bytes)
    256   14       3       java.lang.String::getChars (62 bytes)
    257   15       3       java.lang.String::indexOf (166 bytes)
    258   16       3       java.lang.String::startsWith (72 bytes)
    260   17       1       java.io.File::getPath (5 bytes)
    261   19       3       java.lang.String::<init> (62 bytes)
    261   20       3       java.lang.StringBuilder::append (8 bytes)
    262   23       3       java.util.Arrays::copyOfRange (63 bytes)
    262   18       3       java.lang.AbstractStringBuilder::<init> (12 bytes)
    263   21       3       java.lang.StringBuilder::toString (17 bytes)
    263   22       3       java.lang.StringBuilder::<init> (7 bytes)
    263   24  s    1       com.ibeetl.code.jit.HelloWorld::getMessage (3 bytes)
    263   25       3       com.ibeetl.code.jit.HelloWorld::sayHello (26 bytes)
    264   26       4       java.lang.AbstractStringBuilder::append (50 bytes)
    268   27       4       java.lang.String::getChars (62 bytes)
    269   14       3       java.lang.String::getChars (62 bytes)   made not entrant
    270   13       3       java.lang.AbstractStringBuilder::append (50 bytes)   made not entrant
    270   28       4       java.util.Arrays::copyOfRange (63 bytes)
    271   29       4       java.lang.String::<init> (62 bytes)
    273   23       3       java.util.Arrays::copyOfRange (63 bytes)   made not entrant
    274   19       3       java.lang.String::<init> (62 bytes)   made not entrant
    274   30       4       com.ibeetl.code.jit.HelloWorld::sayHello (26 bytes)
    276   25       3       com.ibeetl.code.jit.HelloWorld::sayHello (26 bytes)   made not entrant

第一列数字表示时间,从虚拟机启动开始计算,这一列说明了JIT的优化时间点,第二列是JIT的优化代码块分配的一个ID,第三列是JIT的编译器代号,有如下

编译器代号(Level)解释
0表示解释执行
1Simple C1 Compiled Code
2Limited C1 Compiled Code
3Full C1 Compiled Code
4C2 Compiled Code

通常来说编译优化,会从0到3然后到4阶段。1和2阶段指的是有限优化,当JIT日志显示方法在编译器4的时候,意味着性能最优

在第二列和第三列中间,有符号对方法进行补充说明,如n表示这是一个native调用,s表示这是一个同步方法

最后一列是编译的方法,数字部分是表示字节码大小,比如ID为24的方法是getMessage,是在263毫秒使用C1编译,getMessage的字节码大小是3个字节

  263   24  s    1       com.ibeetl.code.jit.HelloWorld::getMessage (3 bytes)

getMessage方法的字节码如下

LDC "world"
ARETURN

LDC 指令占用一个字节,表示把常量池的数据放到操作栈里,LDC后面跟一个字节的索引,表示其值在常量池的位置索引,也占一个字节,这里就是常量"world"。"ARETURN"指令占用1个字节,是指携带操作数栈顶的对象引用(Object Reference)返回。因此getMessage总共3个字节

日志中显示的有些方法,在虚拟机初始化过程中早已经被调用,比如String.indexOf,String.hashCode等等,这些方法会最早被编译成机器码

如果查看sayHello方法,JIT的日志如下

 263   25       3       com.ibeetl.code.jit.HelloWorld::sayHello (26 bytes)
 274   30       4       com.ibeetl.code.jit.HelloWorld::sayHello (26 bytes)
 276   25       3       com.ibeetl.code.jit.HelloWorld::sayHello (26 bytes)   made not entrant

可以看到263毫秒,使用了C3进行编译;然后274毫秒使用了C4编译;276毫秒,sayHello方法后面指示有made not entrant,表示C3编译的结果无效,这里表示被C4编译的机器码替换了。

如果想观察内联信息,可以为虚拟机增加如下参数

-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation  -XX:+PrintInlining

再次运行HelloWorld程序,会有更丰富的信息输出,如下是一个关于getMeesage的片段

4       com.ibeetl.code.jit.HelloWorld::sayHello (26 bytes)                      
              @ 1   com.ibeetl.code.jit.HelloWorld::getMessage (3 bytes)   inline
              @ 9   java.lang.StringBuilder::<init> (7 bytes)   inline (hot)     
              @ 14   java.lang.StringBuilder::append (8 bytes)   inline (hot)    
              @ 18   java.lang.StringBuilder::append (8 bytes)   inline (hot)    
              @ 21   java.lang.StringBuilder::toString (17 bytes)   inline (hot) 
3       com.ibeetl.code.jit.HelloWorld::sayHello (26 bytes)   made not entrant

可以清楚看到在C4阶段,getMessage方法内联了,同时sayHello方法再调用StringBuilder完成字符串操作的所有方法都已经内联

代码缓存

当JIT编译代码后,会放入一个叫代码缓存(CODE CACHE)的地方,这在JDK7 update 40 以后大小为240M。在JDK9以后,根据JEP1 97 CodeCache 进一步划分3个区域来存放代码缓存。注意,通常不需要用户自己设置大小。

区域设置描述
Non-method code-XX:NonMethodcodeHeapSize存放非Code Cache数据,比如TemplateTable
Non-profiled nmethod code-XX:NonProfiledCodeHeapSize存放被JIT编译并优化过的代码
Profiled nmethod code-XX:ProfiledCodeHeapSize存放被JIT编译,并存放有性能分析信息(比如计数),未来会进一步优化的代码。

根据在代码缓存对性能影响非常重要,如果缓存不够,一些优化后的代码不得不被清空以让其他优化进入代码缓存

可以通过XX:+PrintFlagsFinal来打印平台所有参数默认值,比如,我的Mac机器行,有如下输出

 InitialCodeCacheSize                      = 2555904
 ReservedCodeCacheSize                     = 251658240  
 CodeCacheExpansionSize                    = 65536
 UseCodeCacheFlushing = true

所示所示,代码缓存默认初始化大小为2555904字节,每次增长6536字节,代码缓存大小为251658240字节

-XX:+PrintCodeCache 用于打印代码缓存使用情况,这是在程序退出时候打印到控制台

CodeCache: size=245760Kb used=1128Kb max_used=1147Kb free=244632Kb
bounds [0x0000000106e00000, 0x0000000107070000, 0x0000000115e00000]
total_blobs=291 nmethods=35 adapters=170
compilation: enabled

size 表示代码缓存大小,这并不是实际使用,这是一个最大值,used表示实际占用的大小,max_used比used大,表示实际占用的内存大小,需要参考这个指标作为设定Code Cache大小,free是计算size-used的值

当代码缓存满的时候,JIT通常会清理掉一部分Code Cache,这使用UseCodeCacheFlushing来控制

UseCodeCacheFlushing = true

可以使用XX:-UseCodeCacheFlushing 关闭自动清理,这样JIT将停止编译新的代码。另外一种清理Code Cache原因JIT认为优化认为是无效的,将会退出这部分优化代码,比如虚方法调用出现的逆优化

可以通过-XX:ReservedCodeCacheSize=*N* 来设置代码缓存,通常不需要那么做,除非你通过打印代码缓存,认为CodeCache使用过大或者过小

java -XX:ReservedCodeCacheSize=24960K

注意,内联策略也会影响代码缓存大小,比如设置内联嵌套层次,最大的内联代码大小等,我们将在8.5一节详细说明

JITWatch

人工解读JIT日志难度比较大,可以用JITWatch可视化分析日志,它是开源工具,放在github上,如下安装JITWatch并启动JITWatch

git clone git@github.com:AdoptOpenJDK/jitwatch.git
cd jitwatch
mvn clean install -DskipTests=true
java -Duser.country=US -Duser.language=en  -jar  ui/target/jitwatch-ui-shaded.jar

启动界面如下

点击菜单Open Log可以加载JIT日志,为了获得JIT日志,需要在启动应用程序的虚拟机增加一个参数

-XX:+LogCompilation

虚拟机会在程序运行的目录生成一个hotspot_pidXXX.log的XML文件,XXX是进程ID,日志文件是一个xml格式,记录了JIT优化过程,其内容很难看懂。但通过JITWatch,可以方便可视化的分析日志文件

Open Log 按钮用于加载日志,点击按钮,选择日志文件即可以加载成功。JITLog窗口的下方面板是控制台,会有输出,表示加载成功

Selected log file: /Users/xiandafu/git/code/hotspot_pid51119.log

Using Config: Default

Click Start button to process the JIT log

在点击Start按钮前,还需要配置JITWatch,告诉程序的源代码位置和编译后的class位置,点击Config按钮进行配置即可,我们可以本章的附带例子HelloWorld为例子

配置好项目源代码和Class,点击Start按钮,JITWatch会消耗数秒分析日志。显示结果如下

左边是一个按照Java包组织的类视图,所有参与编译的的类都在这里,我们可以找到HelloWorld,则右边面板显示了编译的方法,绿色打勾的类或者包意味着JIT进行了编译优化。如上,getMessage方法被编译了,main方法和sayHello都被编译了,然后HelloWold()因为是构造函数,只执行了一次,没有被JIT优化编译。

单击右边面板的sayHello方法。会弹出新的窗口,从左到右包含三个面板,分别是方法的源码,字节码,和编译后的机器码,要获取机器码,需要一个Debug JVM并开启-XX:PrintAssembly,查看机器码超出了本书的范畴,就不再本书介绍。

点击工具栏按钮chain,JITWatch会打开一个新的面板,列出sayHello方法的优化明细,并在顶部表明此方法被C2/Level 4 编译

如上图右侧面板Key解释

Key解释
inlined绿色表示inline,比如getMessage方法已经内联到sayHello方法里了,我们再前面也分析过,getMessage得字节码只占用三个字节,很容易被内联
Compiled红色表示编译,sayHello被JIT编译,但是否被内联,我们需要查看其调用者main方法,可以查看main方法的chain,也可以看到sayHello同时也被内联了
Virtual Call表示虚方法调用,简单说明虚方法调用,就是在Java支持多态,虚拟机需要查询一个虚表才能知道方法调用的是具体哪个类的哪个方法,这样性能就比较慢了。虚拟机通过统计如果此调用总是指向1个方法或者2个方法,可以进行优化达成类似静态方法调用新能

限于篇幅,更多的JITWatch,参考官网文档https://github.com/AdoptOpenJDK/jitwatch。我们通常只需要通过如上面板方法确认C2/Level4 编译,并且是否有内联,Virtual Call优化即可。

内联

我们在6.3.1 中已经看到内联有利于性能优化,通过-XX:+PrintFlagsFinal可以看到JIT对内联的默认设定,在我的64位Mac的 JDK8上,如下值

参数名称默认值含义
MaxInlineSize35字节一个方法能被内联得最大字节,默认为35字节,可以设定较小值,比如6,保证只有一些简单方法被内联.设定较大字节,能使得一些较大方法被内联,带来性能的提升。
MinInliningThreshold250次一个计数,默认250次,超过这个次数,JIT决定内联,较小得方法不受这个参数影响
MaxInlineLevel9默认配置为最多允许9层嵌套得方法被内联,比如a(),调用b(),b()调用c(),因此,a()在内联b的时候可以内联c
InlineSmallCode2000字节,默认为2000字节,一个阀值,当要内联一个已经被编译的方法时候,其机器码的最大字节数不超过此值大小 。否则,直接调用此方法
FreqInlineSize325字节325字节,调用频繁的方法能被内联的最大字节码大小。

查看JIT的内联日志需要启用如下虚拟机参数

-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining

比如本章的Hellowrld的输出

 java.lang.StringBuilder::append (8 bytes)                                  
       @ 2   java.lang.AbstractStringBuilder::append (50 bytes)   callee is 
     @ 18   java.lang.StringBuilder::append (8 bytes)                       
       @ 2   java.lang.AbstractStringBuilder::append (50 bytes)   callee is 
     @ 21   java.lang.StringBuilder::toString (17 bytes)                    
       @ 13   java.lang.String::<init> (62 bytes)   callee is too large     
com.ibeetl.code.jit.HelloWorld::main @ 10 (27 bytes)                        
   @ 17   com.ibeetl.code.jit.HelloWorld::sayHello (26 bytes)   inline (hot)
     @ 1   com.ibeetl.code.jit.HelloWorld::getMessage (3 bytes)   inline (ho
     @ 9   java.lang.StringBuilder::<init> (7 bytes)   inline (hot)         
     @ 14   java.lang.StringBuilder::append (8 bytes)   inline (hot)        
     @ 18   java.lang.StringBuilder::append (8 bytes)   inline (hot)

内联通常有如下信息显示,指示内联是否成功

参数名称含义
inline (hot)表示方法被标记为内联
callee is too largeC1打印出来信息,指示方法超过MaxInlineSize 不能内联
hot method too bigC2打印出来的信息,指示方法大小超过FreqInlineSize
already compiled into a big method内联一个已经编译的方法,大小超过了InlineSmallCode值

我们也可以通过上一节介绍的JITWatch来观察方法是否被内联,以及没有被内联的原因

虚方法调用

调用方法的另外一个成本来自于虚方法调用,Java支持多态,在运行时候具体调用哪个方法,需要查询一张vtable,这是一个耗时的操作,vtable是虚拟函数表,每个类都维护一个vtable,包含了自有函数(非final,非static,非private)和父类的函数虚拟表:如下俩个类

class Foo {
  int  say(int x) {
    return x+2;
  }
  int  say() {
    return0;
  }
}
class Bar extends Foo{
  int  say(int x) {
    return x+1;
  }

}

那么对于Foo类,则vtable如下

  • say(int)->Foo.say(int)
  • say() ->Foo.say()
  • hasCode()->Object.hasCode
  • ...忽略其它Object方法

对于Bar类,vtable如下

  • say(int)-> Bar.say(int)
  • say() ->Foo.say()
  • hasCode()->Object.hasCode
  • ..忽略其它Object方法

当在java中调用方法虚方法的时候,并不知道具体调用的是哪一个方法,比如对应如下代码片段的foo.say()调用,字节码是使用INVOKEVIRTUAL

public void test() {
  Foo foo =  getFoo();
  foo.say();
}

public  Foo getFoo(){
  return new Bar();
}

虚拟机指令为

INVOKEVIRTUAL com/ibeetl/code/jit/Foo.say ()V

这意味这运行时,JVM不能直接使用INVOKEVIRTUAL后面的参数当着方法的入口地址直接调用,需要查询foo变量对应的类的虚方法,找到也就是Bar类虚方法表中say()的调用地址。

JIT 通过优化,如果发现虚方法总是调用同一个目标对象的方法或者2个目标对象的方法,会尝试优化成直接调用。如果后期发现实例改变,则会退出优化,如下是JMH验证,type表示测试的目标对象个数

//JITVirtualTest
@Param({"1","2","3"})
int type;
static Foo[] classes = new Foo[]{new Foo(), new Bar(),new Boos()};
int x = 1;

@Benchmark
public  int  call(){
    Foo foo = getFoo();
    return foo.say(x);
}

private  Foo getFoo() {
    int classType = ThreadLocalRandom.current().nextInt(type);
    return classes[classType];
}

此JMH代码会把@Param带入多次运行,每次运行中都根据随机结果从classes中取出对象,并调用say方法

性能测试结果,当type=1的时候,getFoo()方法总是返回一个对象实例时候,性能是最好的。其次是当getFoo() 返回Foo或者Bar的时候,其性能也不错。

Benchmark            (type)   Mode  Cnt       Score   Error   Units
JITVirtualTest.call       1  thrpt    2  278486.118          ops/ms
JITVirtualTest.call       2  thrpt    2  101794.605          ops/ms
JITVirtualTest.call       3  thrpt    2   50376.468          ops/ms

JIT的负载影响

使用JIT一个需要关注点是JIT带来对系统的负载影响,C1和C2默认情况下,根据服务器的可用CPU,会各自创建多个C1和C2线程,并在服务刚启动时候使用CPU用来优化代码。如果你使用JDK自带的jstack命令,可能看到有如下输出,JVM使用了一个线程用于C1编译,一个线程用于C2编译

> jstatck pid
........
"C2 CompilerThread0" #7 daemon prio=9 os_prio=31 cpu=439.25ms elapsed=1680.97s tid=0x00007faaa3009400 nid=0xa203 waiting on condition  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
   No compile task

"C1 CompilerThread0" #10 daemon prio=9 os_prio=31 cpu=381.72ms elapsed=1680.97s tid=0x00007faa9f822600 nid=0x5c03 waiting on condition  [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
   No compile task

任意一个系统在启动后,JIT都消耗大量CPU完成优化编译。 这个时候有可能造成服务超时不可用。如果不期望在JIT过的的消耗CPU资源,比如过的消耗CPU资源导致服务不可用或者服务超时,可以禁止C2优化。如下是AWS的Lambda 函数设置的虚拟机参数,表示按照Level 1优化即可。

-XX:TieredStopAtLevel=1

如果服务器资源足够,希望有更多的C2线程参数优化,下图指定4个线程,则使用

-XX:CICompilerCount=4

最佳方式还是把系统部署在多核CPU上以使得JIT优化不会完全占用所有CPU,或者在系统启动后进行预热后再提供对外服务。关于这一部分,需要参考高可用篇的《预热》

知行合一