## 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对性能的影响

如果想理解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完成字符串操作的所有方法都已经内联
- 内容介绍
- 第一章 java系统优化
- 第二章 字符串和数字
- 2.1 构造字符串
- 2.2 字符串拼接
- 2.3 字符串格式化
- 2.4 字符串查找
- 2.6 intern方法
- 2.7 StringUtils类
- 2.8 前缀树过滤
- 2.9 数字装箱
- 2.10 BigDecimal
- 第三章 并发和异步编程
- 3.1 不安全的代码
- 3.2 Java并发编程
- 3.2.1 volatile
- 3.2.2 synchronized
- 3.2.3 Lock
- 3.2.4 Condition
- 3.2.5 读写锁
- 3.2.6 semaphore
- 3.2.7 栅栏
- 3.3 Java并发工具
- 3.3.1 原子变量
- 3.3.2 Queue
- 3.3.3 Future
- 3.4 Java线程池
- 3.5 异步编程
- 3.5.1 创建异步任务
- 3.5.2 完成时回调
- 3.5.3 串行执行
- 3.5.4 并行执行
- 3.5.5 接收任务处理结果
- 第四章 代码性能优化
- 4.1 int 转 String
- 4.2 使用Native 方法
- 4.3 日期格式化
- 4.4 switch 优化
- 4.5 优先用局部变量
- 4.6 预处理
- 4.7 预分配
- 4.8 预编译
- 4.9 预先编码
- 4.10 谨慎使用Exception
- 4.11 批处理
- 4.12 展开循环
- 4.13 静态方法调用
- 4.14 高速Map存取
- 4.15 位运算
- 4.16 反射
- 4.17 压缩
- 4.18 可变数组
- 4.19 System.nanoTime()
- 4.20 ThreadLocalRandom
- 4.22 错误优化策略
- 4.22.1 final无法帮助内联
- 4.22.2 subString 内存泄露
- 4.22.3 循环优化
- 4.22.4 循环中捕捉异常
- 第五章 高性能工具
- 第六章 Java注释
- 6.1 JavaDoc
- 6.2 Tag
- 6.2.1 {@link}
- 6.2.2 @deprecated
- 6.2.3 {@literal}
- 6.2.4 {@code}
- 6.2.5 {@value}
- 6.2.6 @author
- 6.2.7 @param 和 @return
- 6.2.8 @throws
- 6.2.9 @see
- 6.2.10 自动拷贝
- 6.3 Package-Info
- 6.4 HTML生成
- 6.5 Markdown-doclet
- 第七章 可读性代码
- 7.1 精简注释
- 7.2 变量
- 7.2.1 变量命名
- 7.2.2 变量的位置
- 7.2.3 中间变量
- 7.3 方法
- 7.3 .1 方法签名
- 7.3.2 小方法
- 7.3.3 单一职责
- 7.3.3 小类
- 7.4 分支
- 7.4.1 if else
- 7.4.2 switch case
- 7.5 发现对象
- 7.3.1 不要用String
- 7.3.2 不要用数组,Map
- 7.6 checked异常
- 7.7 其他
- 7.7.1 避免自动格式化
- 7.7.2 关于Null
- 第八章 JIT优化
- 8.1 解释和编译
- 8.2 C1和C2
- 8.3 代码缓存
- 8.4 JITWatch
- 8.5 内联
- 8.6 虚方法调用
- 第九章 自测
- 9.1 ConcurrentHashMap陷阱
- 9.2 字符串搜索
- 9.3 IO输出
- 9.4 字符串拼接
- 9.5 方法的入参和出参
- 9.6 RPC调用定义的返回值
- 9.7 Integer使用
- 9.8 排序
- 9.9 判断特殊的ID
- 9.10 优化if结构
- 9.11 文件COPY
- 9.12 siwtch优化
- 9.13 Encoder
- 9.14一个JMH例子
- 9.15 注释
- 9.16 完善注释
- 9.17 方法抽取
- 9.18 遍历Map
- 9.19 日期格式化
- 9.20 日志框架设计的问题
- 9.21 持久化到数据库
- 9.22 某个RPC框架
- 9.23 循环调用
- 9.24 Lock使用
- 9.25 字符集
- 9.26 处理枚举值
- 9.27 任务执行
- 9.28 开关判断
- 9.29 JDBC操作
- 9.30 Controller代码
- 9.31 停止任务
- 9.32 log框架
- 第十章 ASM运行时增强
- 10.1 Java字节码
- 10.1.1 基础知识
- 10.1.2 class文件格式
- 10.2 Java方法的执行
- 10.2.1 方法在内存的表示
- 10.2.3 方法在class文件中的表示
- 10.2.3 指令的分类
- 10.2.4 操作数栈的变化分析
- 10.3 Bytecode Outline插件
- 10.4 ASM入门
- 10.4.1 生成类名及构造函数
- 10.4.2 生成main方法
- 10.4.3 调用生成的代码
- 10.5 ASM增强代码
- 10.5.1 使用反射实现
- 10.5.2 使用ASM生成辅助类
- 10.5.3 switch语句的分类
- 10.5.4 获取bean中的property
- 10.5.5 switch语句的实现
- 10.5.6 性能对比
- 第十一章 JSR269编译时增强
- 11.1 Java编译的过程
- 11.2 注解处理器入门
- 11.3 相关概念介绍
- 11.3.1 AbstractProcessor
- 11.3.2 Element与TypeMirror
- 11.4 注解处理器进阶
- 11.4.1 JsonWriter注解
- 11.4.2 处理器与生成辅助类
- 11.4.3 使用生成的Mapper类
- 11.4.4 注解处理器的使用
- 11.5 调试注解处理器
- 11.5.1 Eclipse中调试注解处理器
- 11.5.2 Idea中调试注解处理
- 附录A OQL分析JVM内存