Java虚拟机5 编译与优化

Java虚拟机 系列文章

Java虚拟机1 内存管理、GC,包括 Shenandoah ZGC
Java虚拟机2 G1垃圾回收详解, 参数, 日志
Java虚拟机3 Class文件及类加载
Java虚拟机4 方法调用原理、动态类型支持
Java虚拟机5 编译与优化 (本文)
Java虚拟机6 内存模型、线程、锁

总结 Java 不支持的语法特性
Java 协程:Loom Project 实战
其他JVM语言

Java编译

  • 前端编译器:JDK的Javac,把java文件编译成class文件
  • JIT(Just In Time) 即时编译器:HotSpot的C1、C2、Graal编译器,运行期把字节码转变成本地机器码
  • AOT(Ahead Of Time) 提前编译器:Jaotc、GCJ等,直接把程序编译成目标机器二进制代码,不常用

前端编译

编译过程:

  1. 初始化插入式注解处理器 (通过 javac -processor 指定)。
  2. 解析与填充符号表,包括:
    词法、语法分析。将源代码的字符流转变为标记集合,构造出抽象语法树。
    填充符号表。产生符号地址和符号信息。
  3. 插入式注解处理器的注解处理过程。
  4. 分析与字节码生成,包括:
    标注检查。对语法的静态信息进行检查。
    数据流及控制流分析。对程序动态运行过程进行检查。
    解语法糖。将简化代码编写的语法糖还原为原有的形式。
    字节码生成。将前面各个步骤所生成的信息转化成字节码。

前端编译几乎不做性能优化,原因是Java虚拟机上有很多语言,而前端编译的优化只对单个语言生效。

即时编译(JIT)

Java程序首先解释执行,当某些代码块运行特别频繁时,就会把这些代码认定为“热点代码”,并将其编译成本地机器码,并进行优化。

HotSpot虚拟机有三个即时编译器:

  • C1:客户端编译器,可通过 -client 指定
  • C2:服务断编译器,可通过 -server 指定
  • Graal:JDK10新加入,目标是替代C2

这种解释器和编译器搭配使用的模式称为混合模式(Mixed Mode)
参数 -Xint:强制虚拟机运行于解释模式(Interpreted Mode),这时全部代码都使用解释方式执行。
参数 -Xcomp:强制虚拟机运行于编译模式(Compiled Mode),这时候将优先采用编译方式执行程序。
-version 参数可以显示运行模式,如下所示:

java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)

1.7之后server模式默认开启分层编译。

分层编译:

  • 第0层。程序纯解释执行,并且解释器不开启性能监控功能(Profiling)。
  • 第1层。使用客户端编译器将字节码编译为本地代码来运行,进行简单可靠的稳定优化,不开启性能监控功能。
  • 第2层。仍然使用客户端编译器执行,仅开启方法及回边次数统计等有限的性能监控功能。
  • 第3层。仍然使用客户端编译器执行,开启全部性能监控,除了第2层的统计信息外,还会收集如分支跳转、虚方法调用版本等全部的统计信息。
  • 第4层。使用服务端编译器将字节码编译为本地代码,相比起客户端编译器,服务端编译器会启用更多编译耗时更长的优化,还会根据性能监控信息进行一些不可靠的激进优化。

提前编译

  • 1998年,GCJ (GNU Compiler for Java)
  • 2013年,ART(Android Runtime)
  • 2017年,Java9 jaotc

提前编译可以做耗时很高的全程序分析。

即时编译的优势:

  • 性能分析制导优化
    比如某个程序点抽象类通常是什么实际类型、条件判断通常会走哪条分支。
  • 激进预测性优化
    做一些有可能出错的优化,出错后退回到低级编译器甚至解释器。
  • 链接时优化
    可以实现跨链接库的优化

编译优化

在这里插入图片描述

优化过程举例
原始代码:

class B {
    int value;
    int getValue() {
        return value;
    }
}
void foo() {
    int y = b.get();
    // do something
    int z = b.get();
    int sum = y + z;
}

第一步:方法内联

void foo() {
    int y = b.value;
    // do something
    int z = b.value;
    int sum = y + z;
}

第二部:冗余访问消除

void foo() {
    int y = b.value;
    // do something
    int z = y;
    int sum = y + z;
}

第三部:复写传播

void foo() {
    int y = b.value;
    // do something
    y = y;
    int sum = y + y;
}

第四部:无用代码消除

void foo() {
    int y = b.value;
    // do something
    int sum = y + y;
}

方法内联
消除方法调用成本,并且是其他很多优化的前提。
非虚方法可以直接内联。
通过类型继承关系分析,对只有一个版本的虚方法进行内联。称为守护内联。
加载新类后,守护内联可能失效,需要回退编译结果。
有多个版本的虚方法,使用内联缓存。

逃逸分析
基本原理:在一个方法里创建对象后,该对象可能被其他方法访问,称为方法逃逸;也可能被其他线程访问,称为线程逃逸。对不逃逸的对象可进行一些优化:

  • 栈上分配
    对象分配到栈上,支持不逃逸或方法逃逸。
  • 标量替换
    不去创建对象,而是直接创建它的被这个方法使用的成员变量来代替。仅支持不逃逸。
  • 同步消除
    如果一个变量不会逃逸出线程,这个变量的同步措施就可以消除。

公共子表达式消除
如果一个表达式之前已经被计算过了,并且表达式中所有变量都没有发生变化,则不需要重新进行计算。

数组边界检查消除
比如在循环中进行数组访问,如果分析出下标取值范围不越界,则可以消除上下界检查。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值