从计算机程序出现的第一天起,对效率的追求就是程序天生的坚定信仰,这个过程犹如一场没有终点,永不停歇的F1方程式竞赛,程序员试车手,技术平台则是在赛道上飞驰的赛车。
概述
java程序是通过解释执行的。当虚拟机发现某个方法或者代码块的运行特别频繁的时候,就会把这些代码设置成为“热点代码”。为了提高热点代码的执行效率,在运行时虚拟机会将这些热点代码编译成与本地平台相关的机械码。并进行各种层次的优化。完成这个任务的就是即时编译器。
即时编译器不是虚拟机的必要的部分。但是即时编译器的好坏,代码优化程度的高低却是衡量虚拟机优秀与否的重要指标之一。
热点代码
上面提到过在运行过程中会被即时编译器编译的“热点代码”主要有两类:
- 被多次调用的方法
- 被多次执行的循环体
前者很好理解,一个方法被调用的多了,方法体内代码的执行次数自然就多,它成为“热点代码”是理所当然的。
而后者则是为了解决一个方法制备调用过一次或者少量的几次。但是方法内部存在循环次数较多的循环体。这样循环体内的代码也被重复执行多次。因此这些代码也应该认为是“热点代码”。
其实判断一段代码是不是热点代码,是不是需要触发即时编译,这样的行为成为热点探测。目前为止(书是第二版。可能到了现在有新的方法出现了。)主要的热点探测方法有两种:
- 基于采样的热点探测。就是虚拟机周期性检查各个线程的栈帧。发现某个方法警察出现在栈顶。那么这个方法就是热点方法。(打个比方:老板周期性看员工工作情况。他看的时候谁表现好就觉得挺好i)。好处是简单,高效。坏处可能不那么准确。(万一某个员工运气好恰好每次认真工作的时候老板来检查呢。)
- 基于计数器的热点探测。这种方式虚拟机要为每个方法方法建立计数器,统计方法的执行次数。好处就是结果精确和严谨。坏处就是实现起来麻烦。
HotSpot采用的就是第二种方法。(具体的实现原理就不说了。感觉人家做好了的,不想自己做没啥必要学。然后我真的一直在声明看书是为了应付面试。拓宽知识面。没有那么多钻研精神。不接受批评)
编译优化技术
之前说了编译的优化,这里简单列举出一些来说明。他们分别是
- 语言无关的经典优化技术之一:公共子表达式消除。
- 语言相关的经典优化技术之一:数组范围检查消除。
- 最重要的优化技术之一:方法内联。
- 最前沿的优化技术之一:逃逸分析。
公共子表达式消除:它的一个普遍应用与各种编译器的经典优化技术。他的含义是如果一个表达式E已经计算过了。并且从先前的计算到现在E中的所有变量的值都没有发生变化。那么E的这次出现就成为了公共子表达式。对于这种表达式,没有必要再花时间对其进行计算。直接使用前面计算过的表达式结果代替E。
如果这种优化仅限于程序的基本块内,便称为局部公共子表达式消除。
如果这种优化的范围涵盖了多个基本块儿,那么就称为全局公共子表达式消除。
数组边界检查消除:它是即时编译器中的一项语言相关的经典优化技术。我们知道java语言是一门动态安全的语言。对数组的读写访问也不是裸指针操作。访问数组元素的时候会进行上下界的范围检查。如果下标越界会抛出异常。这对于程序员来说是很好的,可以避免大部分溢出攻击。但是对于虚拟机的执行子系统来说每次都要判断,也涉及一种性能负担。
而数组边界检查消除就是在编译期根据数据流分析来确定数组的length的值。并判断一个对数组元素的访问有没有下标越界。在执行的时候就不判断了。更加常见的是数组访问发生在循环中的时候,编译器只要通过数据流分析就可以判定循环变量的取值范围是不是在合法区间内。整个循环中都可以把数组的上下界检查消除。这就可以节省很多次的条件判断操作。
方法内联:前面我们说了方法内联是编译器最重要的优化手段之一。除了消除方法的调用成本之外,它更重要的意义是为了其他优化手段建立良好的基础。
方法内联的优化行为看起来很简单,不过是把目标方法的代码“复制”到发起调用的方法之中。避免发生真实的方法调用而已。但是实际上java虚拟机中的内联过程并不是这么简单。因为java中反射指令调用私有方法,父类方法等,还有运行时接收者的多态选择等等。编译期做内联的时候根本无法确定应该使用哪个方法版本。
由于java语言提倡使用面向对象的编程方法进行编程。而java对象的方法默认就是虚方法,因此内联其实和虚方法是有一定的矛盾的。(java语言中默认的实例方法就是虚方法)。
编译器在内联的时候非虚方法可以直接进行内联,这时候的内联是有稳定前提保障的。
如果遇到虚方法,则会查询此方法在当前程序下是否有多个目标版本进行选择: - 如果只有一个版本则可以内联,不过这种内联属于激进优化(看名知意,有一定风险的呦!),需要预留一个“逃生门”。成为守护内联。如果运行没问题就可以一直运行下去,但是如果运行有问题就退回到解释状态执行,或者重新进行编译。
- 如果有地哦个版本可选择,编译器还有进行最后一次努力:使用内联缓存就是建立在目标方法正常入口之前的缓存。在未发生调用时缓存是空。发生调用后纯起来上次接受的版本。如果下次调用还是这个版本直接使用缓存。如果下次调用不是这个版本了,就取消内联。查找虚方法进行方法分派。
逃逸分析:它是目前java虚拟机中比较前沿的优化技术。它与类型继承关系分析一样。并不是直接优化代码的手段。而是为其他优化手段提供依据的分析技术。
逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,他可能被外部方法所引用。例如作为调用参数传递到其他方法中。称为方法逃逸。甚至还有可能被外部线程所访问,称为线程逃逸。
其实这个逃逸分析挺有用的,起码让我觉得很实用。首先我们确定一个对象不会逃逸到方法外或者线程外(只会在本方法中使用)。我们可以对这个变量做一些高效的优化。 - 栈上分配:java虚拟机中,我们都知道java堆上分配创建对象的内存空间几乎是常识。(不了解的可以看我上面写的java垃圾收集和内存分配
)但是如果一个方法中的对象确定不会发生逃逸,我们可以就在栈上分配内存。对象所占用的空间完全随着栈帧出栈而销毁。一般来讲,不会逃逸的对象比较多。所以我们使用栈上分配,大量的对象会随着方法的结束而自动销毁,垃圾收集的压力会很小。 - 同步消除:线程同步本身就是一个比较耗时的过程,如果一个逃逸分析可以确定一个变量不会逃逸出线程,无法被其他线程访问。那么这个对象的读写都不会有竞争,对这个变量实施的同步措施也就可以消除了。
- 标量替换:java中一个数据如果无法再分解则被称为标量。同样如果一个数据还可以被分解称为聚合量。java中对象就是典型的聚合量。
如果把一个java对象拆散,根据程序的访问情况将其使用到的成员变量恢复原始类习惯来访问就叫标量替换。如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆分的话。我们真正执行的时候可能就不创建这个对象,而是直接创建他的若干个被这个方法使用到的成员变量来替代。
本章大概就讲了这么多内容,我们也根据热点探测和几种常见的编译器优化技术讲解来让我们对java编译器的了解更加深入。
然后全文手打不易,如果你觉得有帮到你或者有点用,别吝啬的点个喜欢和点个关注哦~~有不同的意见或者建议也欢迎留言或者私信交流。
网友评论