Understand JVM Part III
编译与代码优化
1 编译过程与优化
1.1 解析填充符号表
- 词法分析
- 语法分析
- 填充符号表
1.2 注解处理器
- 处理注解信息
1.3 语义分析与字节码生成
- 标注检查(类型匹配、常量折叠等)
- 数据与控制流分析(分支返回值检查、异常处理、局部变量的赋值等)
- 局部变量的final含义在编译期保证,运行期常量没有访问标志
- 解语法糖
- 泛型、变长参数、自动拆装箱
- 字节码生成
- 除写入磁盘字节码信息外,额外修改了少量代码(父类的实例构造器、变量初始化、语句块等按顺序收缩到
<init>
或<clinit>
方法中,或者如StringBuilder的自动生成等)
- 除写入磁盘字节码信息外,额外修改了少量代码(父类的实例构造器、变量初始化、语句块等按顺序收缩到
2 运行过程与优化
2.1 即时编译器
2.1.1 热点代码识别
- 可识别热点方法与热点代码块(执行期间进行,栈上替换)
- HotSpot使用计数器热点探测方式(还有采样的方式,即周期性的检查栈顶的方法,找到经常出现的)
- 计数器分为方法调用计数器和回边计数器
- 计数器阀值默认Client下1500,Server下10000
- 执行方法时,先检查有没有JIT版本,存在使用,否则该方法的调用计数器+1,然后判断调用计数器与回边计数器值之和是否超过方法调用计数器阀值,是则向JIT编译器发出编译请求
- 默认设置下方法调用计数器超过一定时间减半,该过程为热度衰减,时间段称为方法统计的半衰期
- 回边计数器统计循环体中代码执行的次数(字节码中遇到控制流向后跳转称为回边)
- 回边计数器阀值通过计算公式:
- Client模式:方法调用计数器阀值 * OSR比率(OnStackReplacePercentage) / 100 = 13995(默认)
- Server模式:方法调用计数器阀值 * OSR比率(OnStackReplacePercentage) - 解释器监控比率(InterpreterProfilePercentage) / 100 = 10700(默认)
- 遇到回边指令时,检查是否有编译版本,没有则回边计数器+1并判断二计数器之和是否超过阀值,超过则发出OSR编译请求,并降低回边计数器值以继续使用解释模式运行来等待编译。
2.1.2 编译过程
2.2 编译优化
2.2.1 公共字表达式消除(类似合并同类项)
a * c
与c * a
合并a + a
=>a * 2
2.2.2 数组边界检查消除
- 上下文判断索引不会越界,去掉检查数组越界的逻辑
- 隐式异常优化
2.2.3 方法内联
- 非虚方法直接内联
- 虚方法查询CHA,如果只有一个版本使用,可以内联,但需要设置逃生门以切换会解释模式
- 内联缓存,虚方法入口前,发生一次调用后,缓存方法接收者版本,每次调用都比较方法接收者版本,如果每次调用方法接收者版本一致,则内联继续使用
- 逃逸分析,目前不成熟
- 对象在方法中定义,可能会传入其他外部方法或赋值给其他线程可访问的变量,称之为方法逃逸或线程逃逸
- 如果证明一个对象不会逃逸,则可进行特殊优化:
- 栈上分配(目前HotSpot不支持),直接在方法帧上分配对象内存,出栈直接销毁,避免GC
- 同步消除,对该变量的同步措施去除(没人来竞争)
- 标量替换,将变量拆散(如对象),需要时不创建对象,而创建可被该方法使用到的成员变量
2.3 Java编译器的劣势与优势
2.3.1 劣势
- 受制于编译成本
- 类型安全检查成本
- 多态选择频率极大
- 动态扩展,加载新的类改变整体继承关系,全局优化难以施展
- 对象堆内存分配,局部变量才栈上分配,C++多种方式
- GC
2.3.2 优势
- 动态安全
- 动态扩展
- 垃圾回收
- 别名分析
- 运行期间监控为基础的优化(调用频率预测,分支频率预测,裁剪未选中分支)