连载中 2/20

运行时数据区:堆、栈、方法区到底怎么分

2026-09-11 · 175 阅读 · 0 评论 · 0 赞

先分清两类地皮

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() 看看对象的完整一生:怎么分配、长什么样、怎么被访问。

503

10 年全栈工程师 · 503咖啡馆主理人

#运行时数据区#堆与栈#方法区#元空间#StackOverflowError

评论 (0)

相关推荐

连载中 12/20

排查四件套:jstack、jmap、jstat、jcmd 的实战分工

jstack 看线程在干什么,jmap 看堆里装了什么,jstat 看运行时在变什么,jcmd 是统一入口。四把刀各管一段,配合着用没有查不动的现场。

#jstack#jmap#jstat#jcmd#排查工具
2026-09-16 · 5 阅读 · 0 评论 · 0 赞
连载中 11/20

GC 日志:把回收过程翻译成人话

一行 GC 日志里塞着七种信息:谁触发的、收了哪、停了多久、活了哪些。加上 -Xlog 配置,再加上日志分析工具,GC 不再是只能盯监控曲线的黑盒。

#GC日志#Xlog#日志分析#GC监控#Full GC排查
2026-09-16 · 13 阅读 · 0 评论 · 0 赞
连载中 10/20

ZGC:亚毫秒停顿是怎么炼成的

百 G 大堆停顿不到一毫秒,靠的是把搬家全部挪到并发阶段——着色指针让引用自带状态,读屏障让搬运中的对象依然可访问。代价是吞吐与内存,收益是停顿与堆大小解耦。

#ZGC#着色指针#读屏障#亚毫秒停顿#分代ZGC
2026-09-15 · 8 阅读 · 0 评论 · 0 赞