【Java面试题】Java类对象都是在堆上分配的内存吗?

0. 背景

JVM经典的内存模型想必大家都很熟悉了,我们都非常熟悉的栈内存 + 堆内存的结构中,引用变量和基础数据类型都是直接在栈内存上分配的,而类对象是在堆内存中分配的。

那么,面试题:

1
Java类对象都是在堆上分配的内存吗?

这个问题的答案是什么呢?

1. JVM虚拟机的内存结构

在回答这个问题之前,我们先回顾下JVM的虚拟机内存模型。根据JVM虚拟机的规范的定义,标准的JVM的内存分为以下几个部分:

  • Java栈内存(一般简称为“栈内存”)
  • 堆内存
  • PC程序计数器(里面永远存储下一条要被执行的虚拟机指令)
  • 方法区
  • Native栈内存

其中,每一个Java线程,都会有一个调用栈,Java 的方法调用是在栈内存上通过栈帧的创建和销毁完成的,其中变量的引用和基础类型的内存分配,都是在栈内存的 栈帧 内部完成的,而对象的内存分配,则是在堆内存上面完成的。

这是一个很典型的常识,但是确不一定是绝对的(对于技术来说,哪有什么是绝对的,只要能提升性能、方便开发、方便使用,什么都能做,哈哈哈)

2. 来做一个假想

接下来,我们看一个典型的场景:

1
2
3
4
5
6
7
8
// 一个方法
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 对象仍然是在堆内存中进行内存的分配,然后在栈内存上通过它的引用来使用它。不过,我们可以做一个假想:

1
2
假如对象 objectA 直接在栈内存上分配内存,那么不就可以避免在堆内存上分配内存了吗?
这样做就省去了在堆内存上寻址的过程。同时也不需要依赖堆内存上面的GC来释放不再被使用的 objectA 的内存了。因为 objectA 对象的释放完全可以随着栈帧的销毁而销毁。

这个假想看起来是对虚拟机的性能有提升的,那么这个假想是成立的吗?

它是成立的,而且是有应用的。

3. JVM性能增强之 逃逸分析

在上一章节中,一个很关键的过程是,通过一种方法,来确定 objectA 的生命周期没有超过它所在的方法,也即是它没有逃逸 出它的方法。

那么,针对对象没有逃逸出它的方法范围 的使用场景,就可以进行性能优化,可以直接在栈内存上分配内存了。

这样技术在Java 8以后,已经在HotSpot 里面有实现了,详情请参考OpenJDK 的wiki页面(逃逸分析),这里就不再展开了。

除了以上提到的场景之外,关于对象的逃逸,还有以下几种范围,针对每一个逃逸范围都有不同的优化策略:

  1. 对象没有逃逸出它的方法的范围;
    1. 对象的栈上分配:直接在栈上分配对象,对象仍然是完整的对象,只是避免了在堆内存的GC过程。
    2. 标量替换:把一个对象打散,拆分成许多个不能再分的原始类型(比如int、float等)。
  2. 对象没有逃逸出它的线程的范围;
    1. 可以优化synchronized 锁,如果没有逃逸出线程,那么可以在编译后把锁去掉;
  3. 对象会逃逸到全局的作用范围;

4. 总结

通过以上的分析,我们知道目前有些虚拟机(HotSpot)上是实现了栈上分配 的,那么对于最开始的面试题

  • Java类对象都是在堆上分配的内存吗?

答案就是否定的了,因为对象是否在堆上分配,其实是取决于虚拟机的实现的。

所以,如果面试中遇到了这样的问题,请先三思哦~

参考资料:
JVM 逃逸分析

OpenJDK wiki

Escape analysis for Java (1999)

坚持原创技术分享,您的支持将鼓励我继续创作!