zxdsb666. 发表于 2021-8-26 15:40

深入理解jvm - 编译优化(下)

# 前言

本文接上文的内容继续讲述:[深入理解jvm - 编译优化(上)](https://juejin.cn/post/6999929847271292959)

# 概述

1. 补充后端优化的另一项内容提前编译器的处理

2. 介绍jvm的几项重点优化措施

   1. **方法内联(重要)**
   2. **逃逸分析(先进)**

   3. **公共子表达式消除(经典)**

   4. **数组边界检查消除(语言经典)**

# 后端优化

## 提前编译器

提前编译器的历史其实已经很久了,但是在java领域知道andirod的崛起才被java关注,在讲解关于提前编译器的关注之前,我们来看下提前编译器的优劣

### 优点

+ 解决即时编译器在程序中占用运算资源。

+ 即时编译器进行缓存加速

+ 提前编译的代码质量。

> 书中提到了过程间分析指的是什么?
>
> 目前的java在过程间分析优化力度不够,同时由于静态编译的方式可以在全程序进行优化。

### java的实践

​        针对上面的优点,jdk9实现了一个Jactc 提前编译器。说完提前编译器的优势,下面看下即时编译器的优势:

1. 性能分析指导优化:对于一些动态代码和抽象方法,可以通过动态分析得到

2. 激进型的优化:可以做一些并不保证完全正确的深度优化,并且可以回退到保护程序。

3. 链接时优化:java天生支持即时编译产生本地代码。

​        关于提前编译的内容只需要基本了解即可。下面我们来看下关于jvm更多的底层优化。



## 底层优化

下面是关于jvm的底层优化内容,jvm的底层优化内容非常多,比如:方法内联、冗余重复消除、复写传播、无用代码消除等等。这里挑选了书中的几项内容进行介绍:

- **方法内联(重要)**

- **逃逸分析(先进)**

- **公共子表达式消除(经典)**

- **数组边界检查消除(语言经典)**



### 方法内联

含义:即把被内联的方法搬到内联块的内部。简单来说就是把多层方法嵌套调用合并到一个方法,减少栈桢堆积。

如何实现?

​        实现的前提条件:首先必须是 **非虚方法**,即可以不通过虚方法调用的静态方法。

> C和C++使用明确虚和非虚方法规划界限。

Java的具体实现:引入类型继承关系分析和实现,确定在目前已加载的类中,某个接口是否有多于一种的实现、某个类是否存在子类、某个子类是否覆盖了父类的某个虚方法等信息”。

上方简单来说可以概括为下面这几点:

1. 确定接口的实现者以及是否可以实现

2. 是否有继承关系

3. 是否存在重写方法



### 方法逃逸

逃逸分析的基本原理是:分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中。

一个对象在方法定义中有可能被外部引用,这又引申出一个重要的优化手段:**栈上分配**,栈上分配简单来说就是可以减少堆空间的开辟并且提高内存的回收效率。

> **java只支持方法的逃逸,并不支持线程的逃逸。**

书中介绍目前逃逸分析的情况如下:

1. 改进空间非常大

2. **jdk6才初步支持**

3. 涉及复杂的算法

另一项优化方式是标量替换:

​        什么是标量:如果变量无法更少的单位表示(int, byte, boolean等),那么这些变量成为标量。

​        什么是聚合量:可以继续分解的叫做聚合量。

​        标量替换:**把一个java对象拆散,根据程序的访问情况将其用于成员变量恢复和访问。**

> 结合逃逸分析和标量替换,**逃逸分析会把不能被外部访问并且可以被标量替换表示的对象进行不创建对象。**同样再次强调只支持方法内处理,不支持方法逃逸。

然后是同步消除的优化:

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

最后如果一个变量不会出现逃逸,那么动解除同步措施。

### 公共子表达式

什么是公共子表达式?如果一个表达式E之前已经被计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就称为公共子表达式。对于这种表达式,没有必要花时间再对它重新进行计算,只需要直接用前面计算过的表达式结果代替E。

案例:

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

如果此时表达式被计算过一遍,他会被替换为下面的方式:

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

### 数组边界检查消除

java的数组和c以及c++的数组不同他并不是裸指针的方式操作数组,为了保证数组的访问安全,jvm的底层在每次的操作的时候都需要对于数组的边界进行检查操作,即一个含头不含尾的判断:[start, end).

针对这个问题,java是通过如下的方式考虑优化的:

1. 如果可以界定数组访问范围,理论上可以抵消数组访问的消耗

2. 提前到编译期间完成

3. 隐式异常处理:比如空指针和除数为0的异常。



####   最终处理方式:

​        使用一个segment fault 信号进行替注册,保证多数访问不为null时候不进行判断为空的操作。一旦异常则转到异常处理器处理并且抛出异常。

但是这种处理方式有问题:**用户态和内核态的频繁转换。**但是hotspot会根据实际的方式进行动态判断选择使用边界检查消除还是使用原始的策略模式运行。



# 总结

本节内容较为简单,主要讲述了jvm的优化方式,包括后续的底层优化方式,关于底层优化的内容实际上非常多,但是本文只记录了书中提到的四种重要的优化方式。



# 写在最后

关于jvm的基本内容已经介绍完毕,下一节为总结内容。

myloveperonjj 发表于 2021-8-26 15:46

学习了。嗯,学会了
页: [1]
查看完整版本: 深入理解jvm - 编译优化(下)