Java面试题之关于JIT编译器的那些个优化项

一、关于解释器和编译器

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;
 }
}

3、方法内联的优点

我们都知道方法在调用时会创建一个栈帧(Stack Frame),栈帧中会保存局部变量表操作数栈方法出口等信息,每一个方法从调用到执行完成的过程,就代表着一个栈帧在虚拟机栈中入栈到出栈的过程。

通过方法内联,可以减少栈帧的创建,方法调用入栈和出栈的流程,提升程序运行效率。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/lingbomanbu_lyl/article/details/125574967
今日推荐