java早期和晚期编译优化——知识点速览

早期编译优化

编译过程大致分为3类:

  1. 解析与填充符号表
  2. 注解处理
  3. 分析与字节码生成

源码JavaCompiler里的关键过程:
第一步:
在这里插入图片描述

第二步语法分析、词法分析
在这里插入图片描述

第三步:

在这里插入图片描述

第四步:
执行注解处理
在这里插入图片描述

接着就是语义分析及字节码生成
在这里插入图片描述

以上的关键点:

  • 词法语法解析是第一步,生成符号
  • 注解处理是第二步
  • 然后语法糖、字节码都是第三步的事情。

上述步骤的详细解释:

第一步:

-------词法分析:

就是代码转成token标记。
例如int a=b+2 转成 Int \a=\b+\2 这6个token。

-------语法分析(注意实际上只是生成一个语法树,还没做语法的校验):

根据生成的token,构造一个抽象语法树。

-------填充符号表:

生成一个符号地址和符号信息构成的表格。
(后面第三步的阶段会用于语义分析中的标注检查, 比如名字的使用是否和说明一致,也会用于产生中间代码)
符号表是目标代码生成时的地址分配的依据

第二步:

-------注解处理器:

注解处理器会扫描抽象语法树中带注解的元素, 并进行语法树的更新。
更新之后我们会重新走回解析与填充的过程,重新处理。
这个处理器是一种插件,我们可以自己不断往其中去添加。

注意,上面这2步只是简单去对源文件做转换, 还不涉及任何语法相关的规则。

第三步:

-------语义分析:

判断语法树是否正确。分为2种检查:

  1. 标注检查: 检查变量是否已被声明、 赋值、等式的数据类型是否匹配
    标注检查中会进行常量折叠, 把a=1+2折叠成3
    标注检查的范围比较小,不会有太多上下文依赖。

  2. 数据及控制流分析
    对程序上下文逻辑更进一步验证
    这里会涉及很多交互的上下文交互依赖
    比如 带返回值的方法是否全路径都包含了返回、 受检异常是否被外部处理、局部变量使用前是否被赋值。

final 局部变量(或者final参数)和非final局部变量,生成的class文件没有区别。
因为局部变量不会在常量池中持有符号引用, 所以不会有acesses_flasg信息。
所以class文件不知道局部变量是不是final, 因此final局部对运行期没有任何影响, 只会在编译期去校验。

-------解语法糖:

虚拟机本身不支持这种语法, 但是会在编译阶段 把这些语法糖转为 普通的语法结构(换句话说做了把语法糖代码变成了普通代码, 例如自动装拆箱,可能就是转成了包装方法的特定调用)

-------字节码生成:

对象的初始化顺序, 实际上会在字节码生成阶段, 收敛到一个方法中。 即init中控制了那些成员、以及构造方法的调用顺序
类初始化同理,也是收敛到一个 中
PS: 注意,默认构造器是在填充符号表阶段完成的。
字符串的替换(+操作转成sb) 是在字节码阶段生成的。
完成了对语法树的遍历之后,会把最终的符号表交给ClassWRITE类,设计概念从一个字节码和文件


晚期编译优化

HotSpot中, 解释器与编译器共存
当程序刚启动时,会先马上使用解释器发挥作用。
在程序运行后, 编译器逐步发挥作用,把还没用到的代码逐步编译。
内存资源比较少的情况下,可以用解释器来跑程序,减少编译生成的文件。
如果编译器的优化出现bug,可以通过“逆优化”回退到最初的解释器模式来运行

解释器Interperter

编译器

有两种编译器

  • Client Compiler ——C1编译器, 更高的编译速度

  • Server Compiler——C2编译器, 更好的编译质量
    即选择了-client或者-server时会用到。

  • 默认混合模式: 解释器和编译器共存, 即MixedMode。

关于这2种编译器的参数:

  • -Xint参数: 强制使用解释模式
  • -Xcomp参数: 强制使用编译模式( 但是如果编译无法进行时, 解释会介入)

混合模式中, 解释器需要收集性能信息,提供给编译阶段判断和优化, 这个性能信息有点浪费
因此JDK7引入了分层编译策略:

  • 第0层: 解释执行。 不开启性能监控。
  • 第1层: C1编译, 把字节码编译为本地代码, 进行一些简单优化, 加入性能监控
  • 第2层: C2编译, 启动耗时较长的优化, 根据性能监控信息进行激进优化

CC和SC编译过程的区别:

  • client Compiler 编译过程:
    前端字节码-》 方法内联/常量传播(基础优化)-》 HIR(高级中间代码)-》 空值检查消除/范围检查消除
    -》 后端把HIR转成LLR(低级中间代码)-》 线性扫描算法分配寄存器-》窥孔优化-》机器码生成-》本地代码生成
    都是一些不需要运行期信息就能做优化的操作

  • serverCompiler编译过程:
    会执行所有的经典优化动作
    会根据cc或者解释器提供的监控信息,进行激进的优化
    寄存器分配器是一个全局图着色分配器


晚期优化的一些常见措施(即运行中才会做优化的步骤)

----热点代码

  1. 被多次调用的方法。 会触发JIT编译
  2. 被多次执行的循环体, 会触发OSR编译(栈上替换), 发生在方法执行过程中, 所以是在栈上编译并切换方法。

HotSpot 使用 计数器的热点探测法确定热点代码。
* 给每个方法建立方法计数器, 在一个周期中如果超过阈值, 就触发JIT编译,编译后替换方法入口。
* 如果一个周期内没超过,则计数器/2(半衰)
* 如果没有触发时, 都是用解释方式 按照字节码内容死板地运行。

该计数器的相关参数
-XX:-UserCounterDecay 关闭热度衰减
-XX: CounterHalfLifeTime 设置半衰期-XX:CompileThreshold 设置方法编译阈值

回边计数器就是计算循环次数的计数器
* 没有半衰
* 但是当触发OSR编译时,会把计数器降低,避免还在运行时重复触发。
* 会溢出, 并且会把方法计数器也调整到溢出。
* clint模式和server模式中, OSR的阈值计算公式不同, clint= CompileThredshold * osr比率, server= CompileThredshold * (osr比率 - 解释器监控比率)

—冗余访问消除:

如果已经拿到了 a.value, 该方法内a.value一定不会变的话, 那么后续用到时就不再从a中取value了
复写传播:

y=b.value
z=y
c = z + y

变成

y = b.value
y = y
c = y + y

无用代码消除:
去掉上面的Y=y

----公共子表达式消除

就是对一些比较长的计算公式做化简
a+(a+b)2
会优化成
a
3+b*2
尽可能减少计算次数

—数组边界检查:

如果能确定某个for循环里的数组取值操作一定不会超出数组范围,那么在做[]取值操作时,不会做数组边界检查。

—隐式异常处理:

if(a == null) {
xxx
}
else{
throw Exception
}
优化成
try {
xxx
} catch(Exception e) {
throw e
}

为什么隐式异常处理不能放在早期优化?
在这里插入图片描述

----方法内联:

不能被继承重写的方法,比如私有、构造器、静态之类的方法,可以直接在早期优化中做内联优化。
而其他会被抽象继承实现的方法在编译器无法做内联,因为他不知道实际是用哪一段代码。

  • final方法并不是非虚方法(为什么呢)
  • 类型继承关系分析CHA: 如果发现虚方法,CHA会查一下当前虚拟机内该方法是否有多个实现, 如果发现只有这一种实现,那么就可以直接内联。
  • 如果后续有其他的class动态加载进来后,该方法有多个实现了,并且被使用到了,那么就会抛弃已编译的内联代码,回退到解释状态执行。
  • 内联缓存: 即使程序中发现该方法有多个实现, 依然对第一个使用的那个方法做内联,除非有其他重写方法被调用(即虽然你定义了,但是你很可能不用,所以我一直使用你的第一个方法,除非你真的用了多种重写方法去跑。

----逃逸分析:

分析new 出来的对象是否不会逃逸到方法外, 如果确认只在方法内使用,外部不会有人引用他, 那么就会做优化,比如:
* 不把new出来的对象放到堆,而是放到方法栈上,方法结束了对象直接消失。
* 不需要对这种对象做加锁、同步操作了
* 标量替换: 把这个对象里的最小基本类型成员拆出来作为局部变量使用。

----java和C++, 即时编译和静态编译的区别:

  1. 即时编译可能会影响用户体验,如果在运行中出现较大影响的延迟的话

java中虚方法比C++要多, 因为做各种内联分析消耗的检查和优化的就越多越大
3.
java中总是要做安全检查, C++中不做,出错了我就直接崩溃了越界了
4.
C++中内存释放让用户控制, 无需后台弄一个垃圾回收器总是去检查和操作
5.
java好处: 即时编译能够以运行期的性能监控进行优化,这个是C++无法做到的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值