故事场景: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 打开就能用。