1. 引入
首先回答标题中的问题:Java对象一定会被分配到堆上吗?答案是:不一定。
Java
中创建的对象一般会分配到堆上,当堆空间不足时,就会触发GC
进行垃圾回收,但是GC
次数太多会影响程序的性能。
在编译期间,编译器会对代码做很多优化,为了减少内存堆分配压力,JVM
提供了一项重要优化技术:逃逸分析。逃逸分析得出的结论为后续优化措施提供依据。
2. 什么是逃逸分析
逃逸分析(Escape Analysis
):JVM
提供的一种优化技术,用于分析对象会不会发生逃逸。
Q:如何理解逃逸?
逃逸可以理解为会不会在作用域范围外被调用。如:一个方法内定义的变量,会不会在这个方法外被使用,如果否,则认为未逃逸;如果是:则认为会发生逃逸,这就是方法逃逸。
根据上述的理解,可以分为不同的逃逸方式。对象的逃逸程度从高到低:
- 线程逃逸:一个对象在方法内被定义后,可能被外部线程访问,如:赋值给可以在其他线程中访问的实例变量;
- 方法逃逸:一个对象在方法内被定义后,可能会被外部方法引用;
- 不逃逸:仅在作用域范围内使用。
根据逃逸分析的结果来决定优化策略
3. 优化策略
3.1 栈上分配(Stack Allocations
)
- 将对象分配到栈上,对象占用的内存空间可以随着栈帧出栈(即方法的结束)而销毁,这样垃圾收集的压力会下降很多。
- 整个过程通过判断对象,来决定其是否必须要存在堆上,如果不需要的话,则可以被分配到栈上,栈随着线程的消逝而消逝,这样能够减少了
GC
的频率,从而提高性能。 - 栈上分配支持方法逃逸,不支持线程逃逸。
【例如】
Java
代码解读
复制代码
public void test(){
Student s = new Student();
s.setName("张三");
s.setAge(22);
System.out.println(s.getAge());
}
逃逸分析后得出的结论为:不逃逸,对象
s
只作用于该方法内,不会被其他方法/线程引用,所以该对象可以分配到栈上。
Java
代码解读
复制代码
public Student test(){
Student s = new Student();
s.setName("张三");
s.setAge(22);
return s;
}
该方法的返回值为Student对象,逃逸分析后,得出的结论是:对象s可能会被其他方法/线程引用,所以该对象只能分配到堆上。
3.2 标量替换(Scalar Replacement
)
Q:什么是标量?
标量可以理解为:不可拆解的数据,如:
int
,long
等数值类型。与之相对的概念为聚合量(
Aggregate
):即可以拆解的数据,如:Java中的对象。
标量替换就是将Java
对象拆散,根据程序访问的情况,将其用到的成员变量恢复到原始类型来访问。
这样做的好处:对象的成员变量在栈上分配和读写;为后续进一步优化创造条件。可以将标量替换看作栈上分配的一种特例,实现更加简单,但对逃逸的要求更高,不允许对象逃逸出方法范围内。
【例如】
Java
代码解读
复制代码
//标量替换前的代码
public static void main(String[] args) {
User user = new User("张三",33);
System.out.println("姓名:" + s.getName() + " 年龄:" + s.getAge());
}
// 标量替换后的代码
public static void main(String[] args) {
String name = "张三";
int age = 33;
System.out.println("姓名:" + name + " 年龄:" + age);
}
3.3 同步消除(Synchronization Elimination
)
线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以安全消除掉。
如果JVM
通过逃逸分析,发现一个对象只能从一个线程访问到,访问这个对象时可以不加同步锁,如:如果程序中使用了synchronized
锁,JVM
会将synchronized
锁消除。
4. Java对象内存分配流程