1、想一想
在小编刚开始学习Java语言的时候,然后知道了JVM,之前老师一直说Java中的实例对象就存储在JVM中的堆区。
先给大家上一张JVM的内存模型图,这图应该很熟悉吧,应该对每一块干什么用的多多少少也有了解个大概。
过了几年,Java中的实例对象全部都是存储在堆区的概念,已经被小编我深深的烙印在心中。
还记得小编在面试的时候,遇到过一个类似的问题:
面试官:你知道Java的实例对象是存储在JVM中哪个区域么?
小编:堆区。
面试官:那假设在方法中new了一个百万个对象,也还是全部存储在堆区吗?
那么关键的地方来了,当初对于年少无知的小编来说,哪会想那么多,就死脑筋认为只要是实例对象就只会存储在堆区中。
如果小伙伴你们也不知道答案,那么带着这个问题,来看实际操作一波吧。
typescript
public class Test_1 {
public static void main(String[] args) {
for (int i=0;i < 10000;i++){
create();
}
while (true);
}
public static void create(){
Test_1 test = new Test_1();
}
}
在代码中,循环了1w次,通过HSDB工具(HSDB可以查看JVM在运行时数据区的内容),很明显能看出,在堆区中,确实存在1w个Test_1对象,那么现在改成把循环改成100w,再看看。
这下好了,count并没有达到100w个,最初小编想的是是不是被GC回收了,然后打印GC日志也没有发现。
那么问题来了,剩下的对象跑哪儿去了???
2、看一看
其实这里就涉及到一个知识点,叫做:逃逸分析,默认逃逸分析是开启的,我们先把逃逸分析关闭掉,再试试。
把逃逸分析关闭之后,通过查看对象,这下总算有100w个了。
3、什么是逃逸分析?
用官方的话来说,逃逸分析是一种确定指针动态范围的方法,可以分析在程序的哪些地方可以访问到指针。这里的指针可以理解成java的实例对象的引用地址,而指针动态范围可以理解为对象的访问修饰符(public、private等)。
上面那样的解释估计很多小伙伴都不懂,用代码来举个例子。
在start方法中,new了一个Test_1对象,很明显test_1这个对象是不是只能在start()方法中使用,其他地方都不能够使用。
这种对象就可以理解为不逃逸对象,因为它不能被其他地方访问到。
typescript
public class Test_1 {
public static void main(String[] args) {
start();
}
public static void start() {
Test_1 test_1 = new Test_1();
}
}
那现在将start方法改一下,把test_1这个对象返回出去,那么这个对象就是可以理解为一个逃逸对象,因为它被返回出去了,只要是调用了start这个方法,就可以拿到这个对象,被其他地方访问到。
或者说当一个实例对象的引用指针被多个方法或线程引用时,我们称这个指针发生了逃逸。
csharp
public static Test_1 start() {
Test_1 test_1 = new Test_1();
return test_1;
}
最后通过这种逃逸和不逃逸的现象,来进行分析,就称之为:逃逸分析。
4、栈上分配
栈上分配就是基于逃逸分析这个分析,最后分析出来的一种优化方式,最大的好处应该是减少gc的压力,把那些不逃逸的对象分配在栈上。这样逃逸分析完后可以确定哪些变量可以分配在栈上,栈的分配比堆快,性能好。
这样就像本文一开始演示的一样,100w对象并没有全部在堆中,而把逃逸分析关闭了,就都放在堆中了。
ruby
逃逸分析可以通过这个参数控制,-XX:+/-DoEscapeAnalysis,+就是表示开启,-就是表示关闭。
下次如果再遇到问对象是不是全部放在堆上堆这种问题,可不要直接说都放在堆上。
当然栈上分配只是其中一种优化,还有包括标量替换 、锁消除等等,在逃逸分析的时候,如果你定义的对象的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。