社交媒体

JVM 内存模型

JVM 内存模型

钩子:一行代码,两个世界

你已经写过几十遍这行代码了:

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 篇 == 比较的是地址:因为引用变量里装的就是地址。

34-内存三学区分布图.png

实验:亲眼把栈撑爆

栈不大,方法调用却是"套娃"——每调一个方法就压一层"栈帧"。用没有出口的递归试试它能叠多高:

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


橙码酱,分享更有用的技术知识,助你成为更优秀的开发者。