目录
一、关于解释器和编译器
1、概述
2、JIT(Just In Time)编译器
二、JIT优化之锁粗化 & 锁消除
三、JIT优化之逃逸分析 & 栈上分配 & 同步消除 & 标量替换
1、概念描述
2、逃逸分析代码示例
3、逃逸分析失效示例
四、JIT优化之方法内联(Inline Method)
1、概述
2、方法内联示例
3、方法内联的优点
一、关于解释器和编译器
1、概述
Java到底是一门解释型语言还是编译型语言呢?这取决于Java虚拟机,现在主流的商用虚拟机如HotSpot,都同时包含解释器与编译器,我们可以认为Java即是一门解释型语言,也是一门编译型语言。
关于解释器与编译器的优势:
解释器:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。
编译器:程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地机器码,获取更高的执行效率。
2、JIT(Just In Time)编译器
当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码“。为了提高热点代码的执行效率,虚拟机会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler)。
备注:为什么叫HotSpot虚拟机呢?字面意思就是热点虚拟机,其中的一部分功能就是会对热点代码进行优化。
二、JIT优化之锁粗化 & 锁消除
锁粗化和锁消除都是针对synchronized平台级锁所做的优化,具体可参考:Java面试题之synchronized关键字原理以及锁相关。
三、JIT优化之逃逸分析 & 栈上分配 & 同步消除 & 标量替换
1、概念描述
逃逸分析:所谓逃逸分析简单来说就是,分析创建的对象除了在线程栈中能访问,还能不能被其它线程线程所引用,用于确定创建的对象到底在堆中分配还是栈上分配。
栈上分配:我们都知道对象一般都是在堆上分配,如果逃逸分析发现对象只能在线程栈中访问,则会直接在栈上分配。
同步消除:如果逃逸分析发现一个变量无法被其它线程访问,那这个变量的读写就不会有竞争,对这个变量实施的同步措施也就可以消除掉。比如不保存对象头的Mark Word信息。
标量替换:把对象拆散,将其使用到的成员变量恢复原始类型来访问。如果逃逸分析发现对象只能在当前线程栈中可访问,且对象可以被拆散,那么程序可能不创建这个对象,而是直接在栈上分配成员变量。
备注:所谓标量是指一个数据已经无法再分解成更小的数据来表示了,比如原始数据类型等,都不能再分解,可以称之为标量。如果一个数据可以再分解,我们称作为聚合量,Java中的对象就是最典型的聚合量。
2、逃逸分析代码示例
@Data class User { private Integer id; private String name; } /** * 关闭逃逸分析JVM参数:-XX:-DoEscapeAnalysis */ public class EscapeAnalysisExample { public static void createUser() { User user = new User(); user.setId(1); user.setName("Nick"); } public static void main(String[] args) { long start = System.currentTimeMillis(); for (int count = 0; count < 1024 * 1024 * 50; count++) { createUser(); } long end = System.currentTimeMillis(); System.out.println("耗时为: " + (end - start) + " ms"); } }
- 默认逃逸分析是开启的,运行程序的执行时间为:
5ms
。 - 加上虚拟机参数-XX:-DoEscapeAnalysis,关闭逃逸分析,运行程序的执行时间为:
267ms
。
3、逃逸分析失效示例
@Data class User { private Integer id; private String name; private List<String> hobbies = new ArrayList<>(); }
开启逃逸分析,当我们在User类中加上一行代码后,再次执行程序,发现程序执行时间又变成了277ms。
为什么程序的执行时间又会变慢呢?
我们都知道,线程栈的大小是有限的,由虚拟机参数-Xss决定。由于动态集合里无法确定有多少个对象,会占用多少内存,所以此时创建的对象是在堆上分配,而不是在栈上分配。
四、JIT优化之方法内联(Inline Method)
1、概述
关于方法内联的介绍我们在Java官方提供的文档里可以看到,运行时机器码生成器或者优化器会内联final修饰的方法,把方法调用替换成方法体里的代码。
备注:官方文档地址可参考Java语言规范。
疑问:是不是只有final方法才有可能在运行时优化被内联呢?
其实并不是的,final方法可以被内联是因为final方法不能被重写,在编译时即可以确定。
除了final方法,非虚方法可以直接被内联,这些方法可以在解析阶段确定唯一的版本。
遇到虚方法,如果该方法只有一个版本(Ps:由于多态可能会有多个版本),也可以进行内联。如果该方法有多个版本,使用内联缓存完成方法内联。
非虚方法定义:静态方法、私有方法、实例构造器、父类方法4类。
虚方法定义:实例方法都是虚方法。
备注:更多关于内联缓存的概念请参考周志明的《深入了解Java虚拟机第2版》。
2、方法内联示例
class Point { int x, y; void move(int dx, int dy) { x += dx; y += dy; } } /** * 方法内联示例 * @author 刘亚楼 * @date 2022/6/29 */ public class MethodInlineExample { public static void main(String[] args) { Point[] points = new Point[1000]; for (int count = 0; count < points.length; count++) { points[count] = new Point(); points[count].move(count, points.length - 1 - count); } } }
上面main方法里的代码会被替换成如下:
public static void main(String[] args) { Point[] points = new Point[1000]; for (int count = 0; count < points.length; count++) { points[count] = new Point(); points[count].x += count; points[count].y += points.length - 1 - count; } }
我们都知道方法在调用时会创建一个栈帧(Stack Frame),栈帧中会保存局部变量表,操作数栈,方法出口等信息,每一个方法从调用到执行完成的过程,就代表着一个栈帧在虚拟机栈中入栈到出栈的过程。
通过方法内联,可以减少栈帧的创建,方法调用入栈和出栈的流程,提升程序运行效率。