基本原理

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

栈上分配

如果确定一个对象不会逃逸出线程之外,可以对这个对象进行栈上分配。
当然,这里给您提供另一个使用Java栈上分配的示例:

假设我们有一个方法,它需要创建大量的String对象并对它们进行操作。在常规情况下,每次创建String对象时都会在堆上分配内存,这可能会导致性能问题。

为了避免这种情况,我们可以使用Java栈上分配技术来优化代码。具体做法是将StringBuilder对象声明为局部变量,并在该对象上执行操作,最终将其转换为String对象并返回。以下是示例代码:

1
2
3
4
5
6
7
public String buildString(int count) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < count; i++) {
builder.append("Hello, world!");
}
return builder.toString();
}

在上面的代码中,我们将StringBuilder对象声明为局部变量,并在循环中执行操作。最后,我们调用toString()方法将StringBuilder对象转换为String对象并返回。

由于StringBuilder对象是局部变量且未逃逸到方法外部,所以它将在栈上分配,而不是在堆上分配,从而提高代码的性能和效率。

标量替换

若一个数据已经无法再分解成更小的数据来表示了,Java虚拟 机中的原始数据类型(int、long等数值类型及reference类型等)都不能再进一步分解了,那么这些数据 就可以被称为标量。
初始代码:

1
2
3
4
5
6
// 完全未优化的代码 
public int test(int x) {
int xx = x + 2;
Point p = new Point(xx, 42);
return p.getX();
}

第一步实施内联优化:

1
2
3
4
5
6
7
// 步骤1:构造函数内联后的样子 
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()被内联后的样子
}

第二步,经过逃逸分析,发现在整个test()方法的范围内,Point对象不会发生任何程度的逃逸,这样可以对它进行标量替换优化,把其内部的x和y直接置换出来,分解为test()方法内的局部变量,从而避免Point对象实例被实际创建,优化后的结果如下所示:

1
2
3
4
5
6
7
// 步骤2:标量替换后的样子 
public int test(int x) {
int xx = x + 2;
int px = xx;
int py = 42
return px;
}

第三步,通过数据流分析,发现py的值其实对方法不会造成任何影响,那就可以放心地去做无效代码消除得到最终优化结果,如下所示:

1
2
3
4
// 步骤3:做无效代码消除后的样子 
public int test(int x) {
return x + 2;
}

同步消除

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