【012】【Java晚期(运行期)优化】
解释器与编译器
解释器与编译器两者各有优势: 当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。当程序运行环境中内存资源限制较大(如部分嵌入式系统中),可以使用解释执行节约内存,反之可以使用编译执行来提升效率。同时,解释器还可以作为编译器激进优化时的“逃生门”,让编译器根据概率选择一些大多数时候都能提升运行速度的优化手段。
分层编译
第0层,程序解释执行,解释器不开启性能监控功能(Pofiling),可触发第1层编译。
第1层,也称为C1编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必要将加入性能监控的逻辑。
第2层(或2 层以上),也称为C2 编译,也是将字节码编译为本地代码,但是会启用一些编译时较长的优化,其至会根据性能监控信息、进行一些不可靠的激进优化。
实施分层编译后, Client Compiler 和Server Compiler 将会同时工作,许多代码都可能会被多次编译,用Client Compiler 获取更高的编译速度,用Server Compiler 来获取更好的编译质量,在解释执行的时候也无须再承担收集性能监控信息的任务。
热点代码
(1)被多次调用的方法。(2)被多次执行的循环体。
热点探测判定方式
(1)基于采样的热点探测:采用这种方法的虚拟机会周期性地检查各个线程的栈顶,如果发现某个(或某些)方法经常出现在栈顶,那这个方法就是“热点方法”。基于栈顶的热点探测的好处是实现简单、高效,还可以很容易地获取方法调用关系(将调用堆栈展开即可),缺点是很难精确地确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测。
(2)基于计数器的热点探测:采用这种方法的虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法”。这种统计方法实现起来麻烦一些,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系,但是它的统计结果相对来说更加精确和严谨。
Server Compiler
执行所有经典的优化编译,无用代码消除、循环展开(通过将循环体代码复制多次实现。循环展开能够增大指令调度的空间,减少循环分支指令的开销。循环展开可以更好地实现数据预取技术)、循环表达外提(Loop-Expression Hoisting) 、消除公共子表达式、常量传播、基本块重排序,还会实施一些Java语言特性密切相关的优化技术,如范围检查消除、空值检查消除,此外,还可能根据解释器或Client Compiler提供的性能监控信息,进行一些不稳定的激进优化,如守护内联?、分支频率预测等。
公共子表达式消除
如果一个表达式E已经计算过了, 并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E 的这次出现就成为了公共子表达式。对于这种表达式,没有必要花时间再对它进行计算.只需要直接用前面计算过的表达式结果代替E就可以了。如果这种优化仅限于程序的基本块内,便称为局部公共于表达式消除,如果这种优化的范围涵盖了多个基本块,那就称为全局公共子表达式消除。
数组边界检查消除
消除不必要的数组边界检查
方法内联
把目标方法的代码“复制”到发起调用的方法中,避免发生真实的方法调用。
逃逸分析
分析对象动态作用域: 当一个对象在方法中被定义后, 有可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸。甚至还有可能被外部线程访问到,譬如赋值给类变量,或可以在其他线程中访问的实例变量, 称为线程逃逸。如果能证明一个对象不会逃逸到方法或线程之外,也就是别的方法或线程无法通过任何途径访问到这个对象,则可能为这个变量进行一些高效的优化.
(1)栈上分配,将变量直接分配在栈桢中,大量的对象就会随着方法的结束而自动销毁了, 垃圾收集系统的压力将会小很多。
(2)同步消除:线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他钱程访问,那这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以消除掉。
(3)标量替换,标量是指一个数据已经无法再分解成更小的数据来表示,逃逸分析证明一个对象不会被外部访问, 并且这个对象可以被拆散的话, 那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。将对象拆分后,除了可以让对象的成员变量在栈上分配和读写之外,还可以为后续进一步优化手段创建条件。