钩子:一行代码,两个世界
你已经写过几十遍这行代码了:
Person p = new Person("小明", 18);
问一个从没人问过你的问题:这行代码产生的东西,存在内存的哪个位置?
答案是:p 和 new Person(...) 根本不住在同一个地方。而且这事如果你不搞清楚,后面两个最吓人的 Error(StackOverflowError、OutOfMemoryError)你就永远分不清什么时候遇到哪个。
核心:内存分三个"学区"
JVM 拿到内存后不是一整块用,而是分了区。新手阶段记住三个就够:
| 区 | 住谁 | 特点 |
|---|---|---|
| 栈(Stack) | 方法调用 + 局部变量 | 跑得飞快,空间小,线程私有 |
| 堆(Heap) | 所有 new 出来的对象 | 空间大,所有线程共享 |
| 方法区 | 类信息、静态变量、常量池 | 类的"户口档案",一份就够 |
(还有个程序计数器,负责记录每个线程执行到哪一行,先混个脸熟。)
现在把开头那行代码拆两半:
Person p = new Person("小明", 18);
↑ ↑
住在【栈】 住在【堆】
(引用变量) (对象本体)
p 是 main 方法里的局部变量,住在栈里。但 p 这个格子里存的不是对象,是对象在堆里的地址——像一张快递单号。真正的 Person 对象(里面的 name、age)本体在堆里。
这就是为什么第 22 篇 == 比较的是地址:因为引用变量里装的就是地址。

实验:亲眼把栈撑爆
栈不大,方法调用却是"套娃"——每调一个方法就压一层"栈帧"。用没有出口的递归试试它能叠多高:
static void recurse() {
depth++;
recurse(); // 没有出口的递归
}
我在本机实测(demo 里有完整代码):
递归到第 21481 层时栈满了,抛出 StackOverflowError
有意思的是:同一份代码换个跑法,数字就变了。我在 IDEA 里跑到 21481 层,改用 Maven 命令行跑只有 11360 层——差了将近一倍。因为这个层数由栈有多大决定,而栈大小可以用 -Xss 参数配(IDE 默认给得宽一些)。
层数会变,结论不变:StackOverflowError = 栈满了。平时你写"递归忘记写出口"撞见的就是它。
堆也一样会满。本机实测 JVM 的堆上限约 4GB:对象只 new 不放,new 到天荒地老,就会见到 OutOfMemoryError = 堆满了。
两个 Error 对号入座:
| Error | 哪个区满了 | 典型场景 |
|---|---|---|
| StackOverflowError | 栈 | 递归没出口 |
| OutOfMemoryError | 堆 | 对象只生不灭 |
至于"对象只生不灭"怎么办——下一篇的主角 GC(垃圾回收)就是来管这事的。
速查表
| 要点 | 内容 |
|---|---|
| 栈 | 方法调用 + 局部变量,快、小、线程私有 |
| 堆 | 所有 new 的对象,大、共享 |
| 方法区 | 类信息 + 静态变量 + 常量池 |
| 引用变量 | 存的是对象地址,不是对象本身 |
| StackOverflowError | 栈满(递归没出口) |
| OutOfMemoryError | 堆满(对象只生不灭 → 下篇 GC) |
本文示例代码均已本地实测通过,可直接运行。
对应文件:JvmMemoryDemo.java
橙码酱,分享更有用的技术知识,助你成为更优秀的开发者。