• 深入理解Java虚拟机(九)——后端编译与优化


    即时编译器

    Java程序最初都是通过解释器进行执行,当发现某个方法或者代码块被运行得非常频繁,这些代码就被认为是热点代码,为了提高这些代码得运行效率,虚拟机会把热点代码编译成本地机器码,并进行优化,运行时完成这个任务的后端编译器被称为即时编译器

    解释器与编译器

    主流Java虚拟机内部同时包含解释器与编译器。

    • 解释器优点:当程序需要迅速启动和运行的时候,解释器可以省去编译的时间,立即运行代码。

    • 编译器优点:当程序启动后,编译器将执行频繁的代码编译成本地代码,减少解释器的中间损耗,提高执行效率。

    • 内存问题:编译器编译好的代码会占用本地内存,所以,如果内存不够,可以尽可能地利用解释编译,进行逆优化

    编译对象和触发条件

    热点代码

    • 被多次调用的方法。
    • 被多次执行的循环体。

    热点代码的编译目标都是整个方法,而不是单独的一段代码。

    编译时会传入方法的入口字节码序号,然后在编译后,直接替换字节码初的方法,这种编译过程被称为栈上替换

    热点探测

    检测某段代码是不是热点代码的行为被称为热点探测

    主流的两种热点探测方法:

    • 基于采样的热点探测:周期性地去检查各个线程地调用栈顶,如果发现某个方法经常出现在栈顶,那么这个方法就是热点方法。
      优点:简单高效,容易去获取方法调用关系。
      缺点:很难精确计算一个方法的热度,容易受到干扰,如线程阻塞。
    • 基于计数器地热点探测:虚拟机为每个方法建立一个计数器,统计方法地调用次数,经常被调用就是热点代码。
      优点:精确计算每个方法的热度。
      缺点:额外的开销维护计数器,花费空间,不能获取方法的调用关系。

    HotSpot使用计数器的方法,并设计了两种计数器:

    • 方法调用计数器:计算方法的调用次数。
    • 回边计数器:计算循环代码的次数。

    编译过程

    客户端编译器

    三段式编译:

    1. 第一个阶段,将子节码转化成一种高级的中间代码表示。目的时为了更用以实现代码的优化。也完成了一部分基础的优化,如方法内联和传播优化。
    2. 第二个阶段,再从上一个阶段的代码转化为低级中间代码表示,完成空值检查消除、范围检查消除等。
    3. 最后阶段,使用线性扫描算法,为上一步产生的代码分配寄存器,并做窥孔优化,然后产生机器码。
      在这里插入图片描述

    即时编译优点

    1. 性能分析制导优化:分析代码的运行的情况,进行集中优化。
    2. 激进预测性优化:虚拟机会根据继承类关系分析等一系列激进的猜测去做虚拟化,预测程序之后的运行情况,再优化。
    3. 链接优化:主程序和各个动态链接库可以各自进行优化。

    提前编译器

    破坏了Java语言的平台无关性。但是在Android平台上很有优势。

    提前编译优点

    1. 不需要在运行的时候占用资源。
    2. 本质是给即时编译器做了缓存加速,改善程序的启动时间。
    3. 提前编译的时候没有执行时间和资源限制的压力,可以没有顾忌地进行优化。

    编译器优化技术

    方法内联

    • 基本原理:将目标方法的代码转移到发起调用的地方,减少真实的方法调用。

    需要通过类型继承关系分析,确认到底调用的是哪个方法。

    逃逸分析

    不是直接优化代码,而是一种为优化提供的分析技术。

    • 基本原理:一个对象再方法中被定义后,如果被外部方法调用,就是方法逃逸;如果被外部线程访问,就是线程逃逸。不逃逸、方法逃逸、线程逃逸,就是对象从低到高的逃逸程度。根据对象的逃逸程度再进行不同的优化手段。

    优化方法:

    • 栈上分配:当确定一个对象不会被其他线程访问,就可以将对象分配到栈上,而不是堆上,这样可以减轻堆上垃圾回收的压力,让对象随着方法调用结束被销毁。支持方法逃逸,不支持线程逃逸。
    • 标量替换:如果确定一个对象不会在方法外被访问,就可以在实例化对象的时候,不创建对象,而是拆散成多个被这个方法调用成员变量来代替。不支持方法逃逸和线程逃逸。
    • 同步消除:如果确定一个变量不会回被其他线程访问,就可以消除对这个变量的同步措施,提高运行效率。

    公共子表达式消除

    • 基本原理:如果一个计算表达式之前被计算过了,而且其中的变量没有被修改,那就称为公共子表达式。对于这种表达式就不在计算,直接用之前计算过的结果进行代替。

    数组边界检查消除

    在编译的时候就判断数组是否会越界,这样在运行的时候就不需要每次在读取数组值的时候进行越界判断了。

    示例

    1. 初始代码
    static class B {
    	int value;
    	final int get() {
    		return value;
    	}
    }
    public void foo() {
    	y = b.get();
    	// ...do stuff...
    	z = b.get();
    	sum = y + z;
    }
    
    1. 方法内联
    
    public void foo() {
    	y = b.value;
    	// ...do stuff...
    	z = b.value;
    	sum = y + z;
    }
    
    1. 冗余存储消除
    
    public void foo() {
    	y = b.value;y
    	// ...do stuff...
    	z = y;
    	sum = y + z;
    }
    
    1. 复写传播
    
    public void foo() {
    	y = b.value;y
    	// ...do stuff...
    	y = y;
    	sum = y + y;
    }
    

    4.无用代码消除

    
    public void foo() {
    	y = b.value;
    	// ...do stuff...
    	sum = y + y;
    }
    
  • 相关阅读:
    博弈论进阶之树的删边游戏与无向图的删边游戏
    博弈论进阶之Every-SG
    HDU 3595 GG and MM(Every-SG)
    博弈论入门之斐波那契博弈
    博弈论入门之威佐夫博弈
    博弈论进阶之Multi-SG
    博弈论进阶之Anti-SG游戏与SJ定理
    博弈论进阶之SG函数
    【每天一个Linux命令】12. Linux中which命令的用法
    《数学之美》之悟——学懂数学为何如此重要?
  • 原文地址:https://www.cnblogs.com/lippon/p/14117649.html
Copyright © 2020-2023  润新知