0
点赞
收藏
分享

微信扫一扫

JVM 虚拟机之编译优化


本文主要是概念篇,总结和摘录了 JVM 对于编译优化中存在的几个核心概念: 热点探测、方法内联、逃逸分析、公共子表达式消除、数组边界消除等。

热点探测 (Hot Spot Detection)

当虚拟机发现某个方法或者代码块运行特别频繁,就会把这些代码认定为 “热点代码”(Hot Spot Code)。

判断代码是不是热点代码的这种行为被称为 “热点探测”(Hot Spot Detection) ,有两种热点代码判断的方法:

1. 基于采样的热点探测(Sample Based Hot Spot Code Detection)

采用虚拟机周期性检查哥哥线程的调用栈顶,如果发现某个方法经常出现在栈顶,那么这个方法就是“热点方法”。基于采样的热点探测的好处就是简单、高效,还很容易获取方法的调用关系(将调用堆栈展开即可),缺点就是很难准确的描述一个方法的热度,容易受到线程阻塞或者别的外籍因素影而骚乱热点探测。

2. 基于计数器的热点探测(Counter Based Hot Spot Code Detection)

采用这种方法的虚拟机会为 每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为 它是“热点方法”。这种统计方法实现起来要麻烦一些,需要为每个方法建立并维护计数器,而且不能 直接获取到方法的调用关系。但是它的统计结果相对来说更加精确严谨。

HotSpot 虚拟机中采用的是基于计数器的热点探测方法,有一下两类计数器:

  • 方法调用计数器(Invocation Counter),服务器模式默认 10000 次
  • 回边计数器(Back Edge Counter),服务器模式默认 10700 次

方法内联

方法内敛的优化是把目标方法的代码复制到调用的方法之内,避免发生真实的方法调用。

  • 热点探测技术触发
  • 方法体大小限制
  • 使用方法内联提升性能

测试代码:

/**
* VM Args:
* -XX:+PrintCompilation 控制台打印编译过程信息
* -XX:+UnlockDiagnosticVMOptions 解锁对 JVM 进行诊断的选项参数,默认关闭
* -XX:+PrintInlining 打印方法内联
* @author Administrator
*/
public class MethodInliningTest {

public static void main(String[] args) {
testInline(args);
}

public static int add(int a, int b) {
return a + b;
}

public static void testInline(String[] args) {
for (int i = 0; i < 1000000; i++) {
add(i,i+1);
}
}
}

查看打印信息,当前代码触发方法内联 JVM 虚拟机之编译优化_java

逃逸分析(Escape Analysis)

**逃逸分析(Escape Analysis)**是目前Java虚拟机中比较前沿的优化技术,它与类型继承关系分析一样,并不是直接优化代码的手段,而是为其他优化措施提供依据的分析技术。

逃逸分析的基本原理是: 分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度

栈上分配(Stack Allocations)

在Java虚拟机中,Java堆上分配创建对象的内存空间几乎是 Java程序员都知道的常识,Java堆中的对象对于各个线程都是共享和可见的,只要持有这个对象的引用,就可以访问到堆中存储的对象数据。虚拟机的垃圾收集子系统会回收堆中不再使用的对象,但回收动作无论是标记筛选出可回收对象,还是回收和整理内存,都需要耗费大量资源。如果确定一个对象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。在一般应用中,完全不会逃逸的局部对象和不会逃逸出线程的对象所占的比例是很大的,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,垃圾收集子系统的压力将会下降很多。栈上分配可以支持方法逃逸,但不能支持线程逃逸。

标量替换(Scalar Replacement)

若一个数据已经无法再分解成更小的数据来表示了,Java虚拟机中的原始数据类型(int、long等数值类型及reference类型等)都不能再进一步分解了,那么这些数据就可以被称为标量。相对的,如果一个数据可以继续分解,那它就被称为聚合量(Aggregate),Java 中的对象就是典型的聚合量。如果把一个Java对象拆散,根据程序访问的情况,将其用到的成员变量恢复为原始类型来访问,这个过程就称为标量替换。假如逃逸分析能够证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么程序真正执行的时候将可能不去创建这个对象,而改为直接创建它的若干个被这个方法使用的成员变量来代替。将对象拆分后,除了可以让对象的成员变量在栈上 (栈上存储的数据,很大机会被虚拟机分配至物理机器的高速寄存器中存储)分配和读写之外,还可以为后续进一步的优化手段创建条件。标量替换可以视作栈上分配的一种特例,实现更简单(不用考虑整个对象完整结构的分配),但对逃逸程度的要求更高,它不允许对象逃逸出方法范围内。

同步消除(Synchronization Elimination)

线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争, 对这个变量实施的同步措施也就可以安全地消除掉。

VM 参数:

-XX:+EliminateLocks       开启锁消除(JDK1.8 默认开启)

公共子表达式消除(Common Subexpression Elimination)

公共子表达式消除是一个非常经典的、普片应用各种编译器的优化技术。


在同一个作用域下,如果程序中有公共的计算表达式,多次出现参与运算的结果相同,计算结果为 E,那么后面的表达式直接用 E 替代结果。


一个表达式已经被计算过了,并且从先前计算到现在的计算所有的变量值都没有发生变化,称为 E 为公共表达式。对于这种表达式,我们就不需要重新计算了,只需要使用之前计算过的表达式结果替代 E 。如果这种代码优化局限与程序代码块内,可称为局部公共表达式消除(Local Common Subexpression Elimination)。如果这种优化的范围覆盖了多个代码块,那就称为全局公共子表达式消除(Global Common Subexpression Elimination)。

举个例子:

int d = (c * b) * 12 + a + (a + c * b);

虚拟机的即时编译器会进行代码优化(因为在代码块内 c、b 的之没有改变,编译器认为没必要多次计算),上面的表达式会被优化为:

int d = E * 12 + a + (a + E);

在这个时候编译器也可能会进行代数简化(Algebraic Simplification)。

int d = E * 13 + a + a

经过简化后再计算起来就可以节省一些 CPU 的时间。

数组边界检查消除(Array Boounds Checking Elimination)

数组边界检查消除(Array Boounds Checking Elimination)是及时编译器优化的经典技术。对于 Java 而言本来就是一门动态安全的语言,比如我们访问一个数组 foo[i] 的时候:

  1. 先去检查数组是否为空,如果 foo 为空返回​​NullPointException​​异常。
  2. 如果数组越界会得到​​ArrayIndexOutOfBoundsExecption​​异常。

其实 JVM 为我们做了非常多的隐含条件判断。

但是对于这些操作,为了安全肯定是要做的,但是如果对 foo[i] 循环取值,如果编译器通过分析发现可以判定循环变量的取值永远在区间 [0, foo.length] 之内,那么在这个循环中我们就可以吧这个那个数组的上下界限检查消除掉。

举个例子:

if (i >= 0 && i < foo.length) {
return foo[i];
}

优化过后:

return foo[i];

除此之外还有一种隐式异常处理,Java 空指针检查和算数运算除数为 0 的检查都采用了这种方案。 举个例子:

if (foo != null) {
return foo.value;
} else {
throw new NullPointException();
}

在隐式异常优化之后:

try {
return foo.value
} catch (segment_fault) {
uncommon_trap();
}

虚拟机会注册一个 Segment Fault 的信号异常处理器,当 foo 不为空的时候,访问 value 是没有任何开销的。但是如果 foo 为空,会进入异常处理涉及到进程从用户态转到内核态的处理,处理完成后又回到用户态,同样一次操作比判空检查慢的多。HptSpot 会更具运行期收集到的监控信息自动选择最优方案。

其他优化:自动装箱消除(Autobox Elimination)、安全点消除 (Safepoint Elimination)、消除反射(Dereflection)等。

参考文档

  • 《深入理解 JVM 虚拟机》周志明
举报

相关推荐

0 条评论