JVM之编译、优化

发布时间 2023-04-11 21:09:33作者: 哦、菜狗啊

一、 解释器、编译器

​ 主流虚拟机内部都采用解释器与编译器并行的方式。

​ 解释器与编译器两者各有优势:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即运行。当程序启动后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码,这样可以减少解释器的中间损耗,获得更高的执行效率。

​ 编译也分即时编译(Just In Time,JIT)和提前编译(Ahead Of Time,AOT)。

​ JIT有“客户端编译器”(Client Compiler)和“服务端编译器”(Server Compiler),或者简称为C1编译器和C2编译器(部分资料和JDK源码中C2也叫Opto编译器),第三个是在JDK 10时才出现的、长期目标是代替C2的Graal编译器。

​ 提前编译有两种实现:
​ 1. 与传统C、C++编译器类似,在程序运行之前把程序代码编译成机器码的静态翻译工作
​ 2. 把原本即时编译器在运行时要做的编译工作提前做好并保存下来,下次运行到这些代码(譬如公共库代码在被同一台机器其他Java进程使用)时直接把它加载进来使用

​ 为了在解释器和编译器之间平衡,采用分层编译,根据编译器编译、优化的规模与耗时,划分出不同的编译层次,其中包括:

  • 第0层。程序纯解释执行,并且解释器不开启性能监控功能(Profiling)。
  • 第1层。使用客户端编译器将字节码编译为本地代码来运行,进行简单可靠的稳定优化,不开启性能监控功能。
  • 第2层。仍然使用客户端编译器执行,仅开启方法及回边次数统计等有限的性能监控功能。
  • 第3层。仍然使用客户端编译器执行,开启全部性能监控,除了第2层的统计信息外,还会收集如分支跳转、虚方法调用版本等全部的统计信息。
  • 第4层。使用服务端编译器将字节码编译为本地代码,相比起客户端编译器,服务端编译器会启用更多编译耗时更长的优化,还会根据性能监控信息进行一些不可靠的激进优化。

以上层次并不是固定不变的,根据不同的运行参数和版本,虚拟机可以调整分层的数量。

二、编译优化

  1. 方法内联(消除方法调用成本、为其他优化建立基础)
static class B{
	int value;
	final int get(){
		return value;
	}
}

public void foo(){
	z = b.get();
}

内联为----->

public void foo(){
	z = b.value;
}

  1. 逃逸分析

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

  • 栈上分配:直接将对象创建在栈上,减少垃圾回收的消耗资源。支持方法逃逸,不支持线程逃逸
  • 标量替代:把聚合量分解,用成员变量代替对象,即不创建完整对象。不允许逃逸出方法
  • 同步消除:线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以安全地消除掉。

例子:

public int test(int x){
	int xx = x + 2;
    Point p = new (xx, 42);
    return p.getX();
}

第一步,将Point的构造函数和getX方法进行内联优化:

public int test(int x){
	int xx = x + 2;
    Point p = point_memory_alloc(); //在堆内存中分配p对象的示意方法
    p.x = xx;	//Point构造函数被内联后的样子
    p.y = 42;
    return p.x;	//Point::getX()方法被内联后的样子
}

第二步,经过逃逸分析发现Point对象不会发生任何程度的逃逸,所以可以对其进行标量替换

public int test(int x){
	int xx = x + 2;
    int px = xx;	
    int py = 42;
    return px;
}

第三步,无用代码消除

public int test(int x){
	return x + 2;
}
  1. 公共子表达式消除

比如:int a = (c * b) + d + (c * b)那么就会优化为int a = E + d + E,即不会反复计算同样表达式

  1. 数组边界消除

​ 就是减少边界的检查,减少资源消耗。例如数组下标是一个常量,如foo[3],只要在编译期根据数据流分析来确定foo.length的值,并判断下标“3”没有越界,执行时的时候就无需判断了。更加典型情况是,对于数组访问发生在循环中,并且使用循环变量对数组进行访问。如果编译器只要通过数据流分析就可以判定循环遍历取值范围永远在[0, foo.length)之内,就可以节省很多次条件判断操作。