## 8.2 C1和C2 虚拟机运行有俩种模式,一种是client模式,一种是server模式,前者启动虚拟机使用-client,后者使用-server,这俩种模式的命名也是因为这俩个参数名字得来。client模式使用C1编译器,有较快的启动速度,简单的将字节码编译为机器码,一些Java UI,比如NetBean使用这种模式。Web应用通常使用server模式,server模式采用了重量C2编译器,C2 比 C1 编译器编译的更加激进,性能更高。C2提供了内联优化,循环展开,Dead-Code删除,分支预测等优化 启动client模式 ~~~java java -clinet ~~~ 启动server模式 ~~~java java -server ~~~ 通常来说,在服务器或者64位机器上,默认都是以server模式启动,32位机器默认采用client模式。有些服务器上,甚至没有client模式,究竟虚拟机运行在何种模式,可以通过java -version 进一步确定。 ~~~java $ java -version java version "1.8.0_45" Java(TM) SE Runtime Environment (build 1.8.0_45-b14) Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, mixed mode) ~~~ 在JDK7版本,-server模式还存在一种选项,开启TieredCompilation。这这种模式默认开启client以获得较快性能,一旦程序运行起来,则采用C2编译器。jdk8以上默认开启了这种模式 以Spring框架为例子,通常启动Spring框架的的时候,会启用容器加载被@Controller,@Service,@Configuration等注解的类,这种类的构造函数或者初始化方法在加载执行一次后,在Spring应用里就很少再被调用,JVM不再优化这部分代码。因此使用TieredCompilation模式对于Java Web应用来说,是非常合适的选择。 基本上来讲,虚拟机会监控执行的方法,设定方法调用计数器,执行很频繁的方法(成为Hot Method,也是Hotpot VM名称的由来),在server模式下,默认执行10,000次那么这个方法的优化会被JIT作为一个任务放到一个优化的队列进行异步优化。C1队列或者C2队列,这个队列并非先进先出队列,是按照调用次数高,优先级高编译,编译线程个数取决于CPU的数量,如下是一个摘自默认的线程个数 | P | 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 | 被优化的代码会被放到代码缓存CodeCache,这样再次执行代码得时候,JVM将会使用新的优化代码。 > 对于for循环执行频繁的代码块,JIT也会为维护一个回边计数器,当超过设定阀值,会进行优化编译,这种编译叫做栈上替换(OSR),因为即使循环被编译了,这也是不够的:JVM 必须有能力当循环正在运行时,开始执行此循环已被编译的版本。换句话说,当循环的代码被编译完成,那么循环的下个迭代执行最新的被编译版本则会更加快 可以通过下图,理解解释执行,解释执行以及C1和C2对性能的影响 ![WX20190716-124349@2x](https://img.kancloud.cn/49/be/49be0307c32d5c7a1bb6ef6dfe7ae285_2012x916.png) 如果想理解JIT过程,最好的办法是通过JIT日志观察,可以使用 -XX:+PrintCompilation 开启JIT,虚拟机运行的时候,会输出到标准控制台,如下程序,启用 -XX:+PrintCompilation ~~~java 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的编译器代号,有如下 * 0,解释执行 * 1,Simple C1 Compiled Code * 2,Limited C1 Compiled Code * 3 Full C1 Compiled Code * 4 C2 Compiled Code C1编译器有三个执行阶段,而C2只有一个。通常来说编译优化,会从0到3然后到4阶段。1和2阶段指的是有限优化,限于篇幅,不再详细介绍C1的具体编译过程。 在第二列和第三列中间,有符号对方法进行补充说明,如n表示这是一个native调用,s表示这是一个同步方法 最后一列是编译的方法,数字部分是表示字节码大小,比如ID为24的方法是getMessage,是在263毫秒使用C1编译,getMessage的字节码大小是3个字节 ~~~java 263 24 s 1 com.ibeetl.code.jit.HelloWorld::getMessage (3 bytes) ~~~ getMessage方法的字节码如下 ~~~ LDC "world" ARETURN ~~~ LDC 指令占用一个字节,表示把常量池的数据放到操作栈里,LDC的参数是占一个字节,指向当前的类的常量池,这里就是常量"world",ARETURN 取出栈里的数据并返回,因此总共3个字节 尽管有些方法没有被调用,但虚拟机初始化过程中早已经被调用,比如String.indexOf,String.hashCode等等,这些方法会最早被编译成机器码 如果查看sayHello方法,JIT的日志如下 ~~~java 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编译的机器码替换了。 如果想进一步观察内联信息,可以为虚拟机增加如下参数 ~~~java -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInlining ~~~ 再次运行HelloWorld程序,会有更丰富的信息输出,如下是一个关于getMeesage的片段 ~~~java 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完成字符串操作的所有方法都已经内联