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