0. 背景
JVM经典的内存模型想必大家都很熟悉了,我们都非常熟悉的 栈内存 + 堆内存 的结构中,引用变量和基础数据类型都是直接在栈内存上分配的,而类对象是在堆内存中分配的。
那么,面试题:
Java类对象都是在堆上分配的内存吗?
这个问题的答案是什么呢?
1. JVM虚拟机的内存结构
在回答这个问题之前,我们先回顾下JVM的虚拟机内存模型。根据JVM虚拟机的规范的定义,标准的JVM的内存分为以下几个部分:Java栈内存(一般简称为“栈内存”)
堆内存
PC程序计数器(里面永远存储下一条要被执行的虚拟机指令)
方法区
Native栈内存
其中,每一个Java线程,都会有一个调用栈,Java 的方法调用是在栈内存上通过栈帧的创建和销毁完成的,其中变量的引用和基础类型的内存分配,都是在栈内存的 栈帧 内部完成的,而对象的内存分配,则是在堆内存上面完成的。
这是一个很典型的常识,但是确不一定是绝对的(对于技术来说,哪有什么是绝对的,只要能提升性能、方便开发、方便使用,什么都能做,哈哈哈)
2. 来做一个假想
接下来,我们看一个典型的场景:
// 一个方法public void methodA() {
// 一个对象,它的引用仅限于在改方法内部 Object objectA = new Object();
String str = objectA.toString();
System.out.println("objectA: " + str);
}
在以上的示例代码中,JVM的方法 methodA 调用的过程中,假如有一个 objectA,它的创建是在 methodA 的内部,并且也在它的内部使用,objectA 的引用也没有 流出 方法 methodA 之外的其他地方。那么根据上一章的分析,我们知道, objectA 的生命周期其实和这个方法的调用是一致的了,在 medthodA 调用完成后,objectA 的生命周期也会结束,将会被随后的GC回收调内存。
在上面的这样的场景中,objectA 对象仍然是在堆内存中进行内存的分配,然后在栈内存上通过它的引用来使用它。不过,我们可以做一个假想:
假如对象objectA直接在栈内存上分配内存,那么不就可以避免在堆内存上分配内存了吗?这样做就省去了在堆内存上寻址的过程。同时也不需要依赖堆内存上面的GC来释放不再被使用的objectA的内存了。因为objectA对象的释放完全可以随着栈帧的销毁而销毁。
这个假想看起来是对虚拟机的性能有提升的,那么这个假想是成立的吗?
它是成立的,而且是有应用的。
3. JVM性能增强之 逃逸分析
在上一章节中,一个很关键的过程是,通过一种方法,来确定 objectA 的生命周期没有超过它所在的方法,也即是它没有逃逸 出它的方法。
那么,针对对象没有逃逸出它的方法范围 的使用场景,就可以进行性能优化,可以直接在栈内存上分配内存了。
这样技术在Java 8以后,已经在HotSpot 里面有实现了,详情请参考OpenJDK 的wiki页面(逃逸分析),这里就不再展开了。
除了以上提到的场景之外,关于对象的逃逸,还有以下几种范围,针对每一个逃逸范围都有不同的优化策略:对象没有逃逸出它的方法的范围;对象的栈上分配:直接在栈上分配对象,对象仍然是完整的对象,只是避免了在堆内存的GC过程。
标量替换:把一个对象打散,拆分成许多个不能再分的原始类型(比如int、float等)。
对象没有逃逸出它的线程的范围;可以优化synchronized 锁,如果没有逃逸出线程,那么可以在编译后把锁去掉;
对象会逃逸到全局的作用范围;
4. 总结
通过以上的分析,我们知道目前有些虚拟机(HotSpot)上是实现了栈上分配 的,那么对于最开始的面试题Java类对象都是在堆上分配的内存吗?
答案就是否定的了,因为对象是否在堆上分配,其实是取决于虚拟机的实现的。
所以,如果面试中遇到了这样的问题,请先三思哦~
参考资料:Java ™ HotSpot Virtual Machine Performance Enhancementsdocs.oracle.comOpenJDK wikiwiki.openjdk.java.netEscape analysis for Javaciteseerx.ist.psu.edu