标量替换和栈上分配都是基于逃逸分析的
逃逸分析
分析变量能否逃出它的作用域
- 全局变量赋值逃逸
- 方法返回值逃逸
- 实例引用逃逸
代码示例:
package com.xiaoyao.jvm;
class EscapeTest {
public static SomeClass someClass;
// 全局变量赋值逃逸
public void globalVariablePointerEscape(){
someClass = new SomeClass();
}
// 方法返回值逃逸
// someMethod(){
// SomeClass someClass = methodPointerEscape();
// }
public SomeClass methodPointerEscape(){
return new SomeClass();
}
// 实例引用传递逃逸
public void instancePassPointerEscape(){
this.methodPointerEscape().printClassName(this);
}
}
class SomeClass{
public void printClassName(EscapeTest escapeTest){
System.out.println(escapeTest.getClass().getName());
}
}
这段代码演示了三种逃逸的场景,
globalVariablePointerEscape方法把一个局部变量赋值给了一个静态变量,局部变量的作用域是在方法内部,类变量的作用域是在类里面,所以作用域被放大了,发生了逃逸;
methodPointerEscape方法里面返回了一个对象,这个对象的作用域一开始也是在方法内部,但是作为返回值返回了,假设有另外一个方法someMethod方法调用了methodPointerEscape方法,那methodPointerEscape方法里的new SomeClass()的作用域就会扩张到了someMethod方法里的someClass,所以也发生了逃逸;
instancePassPointerEscape方法,这里的this传给了下面SomeClass类的printClassName方法,this的作用域原先是在当前实例下的,但是现在扩张到了SomeClass类下面去了,所以也发生了逃逸;
另外还有线程逃逸,这里没做代码演示,线程逃逸其实比较好总结,当赋值给类变量,或者赋值给其他线程中可以访问的实例变量,就会发生线程逃逸;
- 线程逃逸
- 赋值给类变量或可以在其他线程中访问的实例变量
以上是对象逃逸的四种场景,JVM在做逃逸分析的时候,会针对这些场景进行分析,分析完成之后,会为这些对象做一个逃逸状态标记,一个对象主要有三种逃逸状态标记;
- 全局级别逃逸:一个对象可能从方法或者当前线程中逃逸
也就是说,其他的方法或者其他的线程也可以访问到这个对象- 对象呗作为方法的返回值
命中到上述演示代码中的 方法返回值逃逸 - 对象作为静态字段(static field)或者成员变量(field)
命中到上述演示代码中的 全局变量赋值逃逸 - 如果重写了某个类的finalize()方法,那么这个类的对象都会被标记为全局逃逸状态并且一定会放在堆内存中
- 对象呗作为方法的返回值
- 参数级别逃逸
- 对象被作为参数传递给一个方法,但是除这个方法之外无法访问、对其他线程不可见
命中到上述演示代码中的 实例引用传递逃逸
- 对象被作为参数传递给一个方法,但是除这个方法之外无法访问、对其他线程不可见
- 无逃逸:一个对象不会逃逸
标量替换
什么是标量?
- 标量:不能被进一步分解的量
- 基础数据类型
- 对象引用
因为这些是没有办法继续分解的
和标量对应的是聚合量
- 聚合量:可以进一步分解的量
比方说字符串就是一个聚合量,因为字符串是用字节数组实现的,可以分解,又比如说我们自己定义的变量也是聚合量;
什么是标量替换?
- 通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是创建它的成员变量来代替;
代码示例:
class SomeTest1{
public void someTest(){
SomeTest someTest = new SomeTest();
someTest.age = 1;
someTest.id = 1;
}
}
class SomeTest{
int id;
int age;
}
上述代码的SomeTest类里面有两个成员变量,这些成员变量都是基础类型,也就是标量;然后在SomeTest1中有个someTest方法,方法里面new SomeTest(),并将age和id都赋值为1;
如果开启了标量替换,并不会直接创建SomeTest这个实例,而是创建SomeTest的成员变量去代替;
开启标量替换之后,上述代码就会变成下面这样:
class SomeTest1{
public void someTest(){
// SomeTest someTest = new SomeTest();
// someTest.age = 1;
// someTest.id = 1;
// 开启标量替换之后
int age = 1;
int id = 1;
}
}
class SomeTest{
int id;
int age;
}
把对象进行标量替换之后,原本的SomeTest对象就不需要分配内存空间了;
- -XX:+EliminateAllocations开启标量替换(JDK8默认开启)
栈上分配
我们知道JAVA里面绝大部分对象都是存在堆里面的,当对象没用的时候,靠垃圾回收器去回收对象;
什么是栈上分配?
- 通过逃逸分析,能够确认对象不会被外部访问,就直接在栈上分配对象
那么在栈上分配对象的话,这个对象占用的空间,就会在栈帧出栈的时候被销毁;
所以通过栈上分配可以降低垃圾回收的压力;
小结
这一章节首先探讨了什么是逃逸分析,如果经过逃逸分析,发现变量不会被外部访问到,那么会有两种优化:
一是 标量替换:标量替换可以把聚合量用若干个标量代替,从而节省内存;
二是 栈上分配:直接在栈上分配这个对象,可以降低垃圾回收的压力;
相关JVM参数
参数 | 默认值(JDK8) | 作用 |
---|---|---|
-XX:+DoEscapeAnalysis | 开启 | 是否开启逃逸分析 |
-XX:+EliminateAllocations | 开启 | 是否开启标量替换 |
-XX:+EliminateLocks | 开启 | 是否开启锁消除 |