当初在面试时,当面试官问道jvm的对象分配在哪里时,我十分自信的回答道:“分配在堆中”。盲目自信。
当然也不是说这种回答是错误的。其实对象还可以分配在栈内存当中,接下来就说一下对象是如何分配在栈内存以及为什么分配到栈中。
如何判断一个对象是不是应该分配到栈内存当中呢。那就不得不说一下逃逸分析了。
逃逸分析不是一种优化机制,而是一种判断某一段代码是不是可以被优化的一种判断方法。逃逸分析大体上分为两种。一是方法逃逸。二是线程逃逸。
方法逃逸又分为三种:
1.全局赋值逃逸,这种逃逸就是一个方法中的对象赋值给了一个全局变量。
public class Reg {
public static Object obj;
/**
* 对象逃逸出方法
*/
public void teset(){
obj = new Object();
System.out.println(obj);
}
}
2.方法返回逃逸,一个方法将方法体中创建的对象做了返回。
public Object teset(){
Object obj = new Object();
System.out.println(obj);
return obj;
}
3.实例引用逃逸,一个方法体中的对象作为形参被别的方法引用。
/**
* 对象没有逃逸出方法
*/
public void teset(){
Object obj = new Object();
System.out.println(obj);
test1(obj);
}
public void test1(Object obj){
System.out.println(obj.toString());
}
线程逃逸:
1.当一个对象赋值给了静态变量,就是逃逸出了线程。
public class Reg {
public static Object obj;
/**
* 对象逃逸出线程
*/
public void teset(){
obj = new Object();
System.out.println(obj);
}
}
上面说了逃逸分析的几种类型,那么下面说一下逃逸分析完了之后做什么。
当逃逸分析发现某一个对象没有逃逸出方法时,JVM就会对代码进行优化。如何优化?
1.栈上分配:
JIT(即时编译器)将某个对象分配到栈内存当中。当对象分配到栈内存当中时就不需要进行垃圾回收。因为垃圾回收主要是针对堆内存的,栈内存中的对象会随着栈帧的弹出进行销毁。
2.标量替换
标量:一个不可再分的变量,如八大基本数据类型以及reference。
当一个对象是不可被外部方法访问时,并且这个对象是可以被分解的,那么程序运行时不一定会创建这个对象,而改为创建它的若干个被这个方法使用到的成员变量来代替,JIT将一个聚合量替换成多个标量,而标量一般也是存储在栈内存当中的,这也是现在代码优化的主要方法。
public class Reg {
public void getStudent(){
Student student = new Student(18,"窝嫩叠");
System.out.println("age:"+student.age+",name:"+student.name);
}
}
class Student{
public int age;
public String name;
public Student(int age,String name){
this.age = age;
this.name = name;
}
}
/**
标量替换之后
*/
public void getStudent(){
int age = 18;
String name = "窝嫩叠";
System.out.println("age:"+age+",name:"+name);
}
3.锁消除
众所周知,锁是一个十分消耗内存的机制。当逃逸分析时,分析出这个对象无法被其他线程访问,就会将这个对象的锁消除掉。
public void f() {
Object hollis = new Object();
synchronized(hollis) {
System.out.println(hollis);
}
}
/**
消除锁之后
*/
public void f() {
Object hollis = new Object();
System.out.println(hollis);
}
逃逸分析是默认开启的,当然并不是说逃逸分析没有问题,逃逸分析肯定也是有时间和空间消耗的,当逃逸分析完之后发现没有没逃逸的对象,就会出现得不偿失的问题。
更新补充:JIT即时编译器在java17中因为没有被广泛使用被删除了。