先分清两类地皮
JVM 的内存分五块,按"归谁所有"可以一刀切成两类:线程私有的程序计数器、虚拟机栈、本地方法栈,和全局共享的堆、方法区。为什么这么分?道理很朴素——线程私有的数据(比如当前执行到哪一行)不需要同步,各用各的互不干扰;需要共享的(对象实例、类信息)才集中放在公共区域,由 GC 统一照看。
线程私有三兄弟
程序计数器是五块里最小也最安静的:记录当前线程执行到的字节码行号,是唯一不会发生 OOM 的区域。虚拟机栈才是重头戏:每个方法调用都会压入一个栈帧,里面装着局部变量表、操作数栈、动态链接和方法返回地址——方法的调用与返回,就是栈帧的入栈与出栈。
// StackOverflowError 的经典制造方式:递归没有出口
public void call() {
call(); // 每次调用压一个栈帧,栈深 -Xss 默认 1M,很快压穿
}
// Exception in thread "main" java.lang.StackOverflowError本地方法栈与虚拟机栈同理,区别只是服务对象:它为 native 方法(C/C++ 实现)服务。HotSpot 把两者合在了一起,日常排查时可以当作一回事。
共享的两块
堆是对象实例的主战场,也是 GC 的主战场——new 出来的东西几乎都在这里,后面所有关于垃圾回收的篇幅,追的都是这块内存。堆内部还会分新生代、老年代,那是 GC 篇的主角,这里先立个牌子。方法区存储类元信息、运行时常量池、静态变量。它的历史包袱值得一提:JDK 8 之前靠"永久代"实现,容易和堆挤在一起出各种诡异问题;JDK 8 之后改为元空间(Metaspace),搬进了本地内存,不再受堆大小约束,只受物理内存限制。
一次方法调用的完整旅程
public int calc() {
int a = 1; // a 进栈帧的局部变量表
User user = new User(); // user 引用在栈,对象本体在堆
return a + user.getId();
}
// 调用 calc:栈帧入栈 → 执行字节码 → 结果压入操作数栈 → 栈帧出栈
// calc 结束后:栈帧整体弹出,user 引用消失
// 但 new User() 那个对象还在堆里——它生死不由栈决定,由 GC 决定这段代码藏着一个关键认知:引用在栈上,对象在堆里。栈帧弹出只是扔掉了"地址条",真正的对象还躺在堆里等着 GC 审判——这就是后续所有内存问题排查的认知起点。
常见误区盘点
| 误区 | 真相 |
|---|---|
| 方法区 = 永久代 | 永久代只是 JDK 7 及以前的实现,8 之后是元空间(本地内存) |
| -Xmx 就是进程的全部内存 | 堆只是大头,元空间、线程栈、直接内存都在外面,RSS 可能远超 -Xmx |
| 栈里只放局部变量 | 还有操作数栈、动态链接、返回地址,栈深是可调的(-Xss) |
| OOM 都在堆上 | 栈、元空间、直接内存各有各的溢出方式,报错信息各不相同 |
地皮划分清楚了,下一篇走进堆里,跟着一行 new User() 看看对象的完整一生:怎么分配、长什么样、怎么被访问。
评论 (0)