JVM 内存结构:一栋宿舍楼的分房故事,面试再问也不慌

2026/11/09 Java

故事场景:JVM 是一栋宿舍楼

JVM 启动的时候,会在内存里盖起一栋宿舍楼。Java 程序跑起来,所有的变量、对象、方法调用,都得在这栋楼里找个房间住下。

这栋楼有五间房,面试官最爱问的就是:每间房住着谁,谁是大家共用的、谁是每人一间的。

  • 程序计数器:每人手里的一张行程单,线程私有;
  • 虚拟机栈:方法住的单间,一方法一间房,线程私有;
  • 本地方法栈:给 native 方法留的后门,线程私有;
  • 堆:所有对象住的大院子,线程共享;
  • 方法区:楼管处的档案室,线程共享。

一句话先记住:线程私有的三间房(程序计数器、虚拟机栈、本地方法栈)跟着线程生、跟着线程死;线程共享的两间房(堆、方法区)是整栋楼的公共区域。

第一问:程序计数器——每人手里的行程单

程序计数器是最小的一块内存,小到面试官都不好意思单独考它,但它偏偏是唯一一个不会抛出 OutOfMemoryError 的区域。

它的作用就一句话:记录当前线程正在执行的那行字节码跑到哪了。

为什么每个线程要各拿一张行程单?因为 CPU 是轮流给线程干活的:线程 A 跑到一半被切出去,线程 B 上来跑一会儿,等 A 再回来,它得知道自己上次跑到哪一行,才能接着跑——行程单就是干这个的。

public int add(int a, int b) {
    int sum = a + b;   // 行程单记下:跑到这一行了
    return sum;        // 下一行,继续
}

记住两点就够:线程私有、不会 OOM。面试官追问“为什么不会 OOM”,答案是它存的只是一个地址编号,空间固定,不存在“越住越大”的可能。

第二问:虚拟机栈——方法住的单间

虚拟机栈是方法调用的家。每个方法被调用时,JVM 就在栈里给它开一间房,这间房叫栈帧。方法里用到的局部变量、操作数、返回地址,全塞在这间房里;方法跑完,房就退了。

public void buyTicket() {
    int price = 120;          // 局部变量,住在 buyTicket 的栈帧里
    String seat = chooseSeat();
    pay(price, seat);
}

public String chooseSeat() {
    return "A12";             // chooseSeat 另开一间栈帧,跑完就退房
}

buyTicket 调用 chooseSeat 的时候,两间房是摞在一起的——栈就是这么一层层摞起来的,先进后出。

这间房最出名的事故叫 StackOverflowError:方法一层层往下调,房摞得太高,楼顶被捅穿了。

public void endless() {
    endless();   // 自己调自己,栈帧无限摞下去
}

跑起来就是经典的 StackOverflowError。面试官常接着问:“栈溢出和 OOM 是一回事吗?”不是:栈溢出是房摞太高,OOM 是房不够住——一个是深度超了,一个是总量超了。

第三问:堆——所有对象住的大院子

堆是整栋楼里最大的一块地,也是 GC(垃圾回收)的主战场。所有 new 出来的对象,都住在这里。

User user = new User("阿飞");  // new 出来的 User 对象住堆里

注意这行代码里住的地方是分开的:对象本身住堆里,user 这个引用(地址牌)住在虚拟机栈的栈帧里。引用是门牌号,对象是房间,别搞混——面试官就爱从这儿下手。

堆按住户的寿命又分成年轻代和老年代:新对象先进年轻代的 Eden,活过几次 GC 晋升到老年代。分代的道理上一篇已经讲过:年轻代朝生暮死、老年代长寿稳定,区别对待才划算。

堆太小会出什么事?对象越住越多、住不下了,就是最常见的 OutOfMemoryError: Java heap space:

List<byte[]> list = new ArrayList<>();
while (true) {
    list.add(new byte[1024 * 1024]);  // 每循环往堆里塞 1MB,迟早塞爆
}

调大堆的办法面试也常问:-Xms 是起步大小,-Xmx 是上限。顺带记住:堆是线程共享的,所以对象才需要考虑线程安全;栈是线程私有的,所以局部变量天生线程安全。

第四问:方法区——楼管处的档案室

方法区住的是类的信息:类名、字段、方法、静态变量、常量,还有运行时常量池。类加载器把 class 文件读进来,拆出来的“档案”就存在这里。

public class Ticket {
    public static int totalSold = 0;   // 静态变量,住方法区
    public static final int PRICE = 120; // 常量,住方法区里的常量池
}

这里有个版本变迁,是面试高频考点:

  • JDK 7 及以前:方法区叫“永久代”(PermGen),是堆里划出来的一块地,大小固定,容易爆 OutOfMemoryError: PermGen space;
  • JDK 8 及以后:永久代被废掉,方法区的实现改叫元空间(Metaspace),搬到了本地内存里,不再占用堆,大小默认只受本机内存限制。

一句话:JDK 8 以后,方法区还在,但实现从永久代换成了元空间,从堆里搬到了堆外。

顺带提一句:字符串常量池在 JDK 7 就从方法区搬进了堆——String s = "hello" 里的 "hello" 住堆里,别再说它住方法区了。

第五问:本地方法栈——给 native 方法留的后门

本地方法栈和虚拟机栈是双胞胎:虚拟机栈服务 Java 方法,本地方法栈服务 native 方法——就是那些用 C/C++ 写的方法,比如 System.currentTimeMillis() 底层调的那个。

public native long currentTimeMillis();  // native 方法,走本地方法栈

平时你基本碰不到它,面试也很少深挖,记住一句就行:本地方法栈就是虚拟机栈的 native 版,同样线程私有,同样可能栈溢出。

HotSpot 虚拟机甚至把这两间房合并成了一间——知道有这回事,面试就够用了。

一张表记住今天的内容

房间 住着谁 线程私有还是共享 出事会怎样
程序计数器 当前执行的字节码地址 私有 不会 OOM
虚拟机栈 栈帧:局部变量、方法调用 私有 栈太深 → StackOverflowError
本地方法栈 native 方法的调用 私有 同样可能栈溢出
堆 所有 new 出来的对象 共享 住满了 → OutOfMemoryError: Java heap space
方法区(元空间) 类信息、静态变量、常量 共享 类太多 → OutOfMemoryError: Metaspace

文末留一句话

下次面试官问“JVM 内存结构讲讲”,别从名词开始背,先讲这栋宿舍楼:行程单、单间、后门、大院子、档案室——五间房的住户和归属讲清了,StackOverflowError 和 OutOfMemoryError 各从哪间房里爆出来的,自然也就顺下来了。


和这篇搭着看:堆里住的对象最后是怎么被清走的,见上周那篇《JVM 垃圾回收:小区垃圾清运队,面试再问也不慌》,这篇讲分房、那篇讲清运,正好一对;想先弄清 JVM 到底是什么、和 JDK、JRE 是什么关系,见我早年写的《Java JDK,JRE和JVM分别是什么》。

动手验证:给 JVM 加上 -Xms64m -Xmx64m 跑一跑上面那段往 List 里塞数据的代码,亲眼看看 OutOfMemoryError: Java heap space 长什么样,再把递归那段跑一遍,对比 StackOverflowError 的报错——两个错误各住哪间房,跑一次就记住了。想在云上练手可以看我整理的云服务器选购指南,新客首单很便宜,别买贵了;算内存、转时间戳这类小活儿,我的在线工具箱 itupload.cn 打开就能用。

Show Disqus Comments

Search

    Post Directory