#Java #JVM #面试 #底层原理
吐槽 :JVM 内存模型那堆破概念,背了忘,忘了背,面试一紧张就把"堆"和"栈"说反?
真相:其实根本不用死记硬背。今天不讲人话,我们直接开一家火锅店,边吃边聊。
💡 提示:文中包含字符画图解,手机端如果显示错位,建议横屏或在 PC 端观看效果最佳。
哈喽,我是 Re-Zero。
面试官问你 JVM 内存结构,你要是只会说"堆是共享的,栈是私有的",大概率也就是个及格分。
咱换个思路:JVM 根本就是一家生意火爆的火锅店! 菜品是对象,服务员是线程,后厨是垃圾回收。
来,拿好筷子,进店了!🥢
🗺️ 先看一眼"后厨地图"
别被什么 Runtime Data Area 这种洋文吓到,直接看这张图,这就是 JVM 的全景落地窗:
Plaintext
+---------------------------- JVM 内存区域 -----------------------------+
| |
| +========================+ +============================+ |
| | 线程私有 (Private) | | 线程共享 (Shared) | |
| | (每个服务员手里的托盘/本子)| | (餐厅的大堂和后厨) | |
| +========================+ +============================+ |
| | | | | |
| | 1. 程序计数器 (PC) | | 4. 方法区 (Method Area) | |
| | [ 记录执行行号 ] | | [ 菜单、秘方、店名 ] | |
| | | | | |
| | 2. 虚拟机栈 (Stack) | | 5. 堆 (Heap) | |
| | [ 局部变量、引用 ] | | [ 摆满菜的大圆桌 ] | |
| | | | | |
| | 3. 本地方法栈 (Native) | | | |
| | [ C++ 外聘厨房 ] | | | |
| +------------------------+ +----------------------------+ |
| |
+---------------------------------------------------------------------+
看不懂?没事,咱们的火锅店马上开张,你马上就懂了!
🏪 核心区实战:这地儿到底是干嘛的?
1. 方法区 (Method Area) ------ 墙上的菜单与祖传秘方 🥘
- 谁能进:全店共享,就一份。(线程共享,只有一份)
- 咋回事 :
这就是贴在墙上的菜单 和配方 。
店里卖啥菜(类信息)、毛肚烫几秒(方法代码)、店名叫啥(静态变量static),全在这儿挂着。
只要店开着,这玩意儿就不能动,谁来都看这一份。 - 小知识:JDK8 之后,这里改叫"元空间",不再占用堆内存,而是用本地内存。
2. 堆 (Heap) ------ 永远在加菜的大圆桌 🍖
- 谁能进:全店共享,最占地儿。(线程共享,内存占用最大)
- 咋回事 :
这就是店中央那张超级大圆桌 !
凡是你new出来的肉啊、菜啊(对象实例),全往这儿堆。
服务员(线程)随便上菜、客人随便夹菜。
⚠️ 重点 :这里有位无情的保洁阿姨(GC),她天天在桌边巡逻。一旦发现哪盘菜(对象)虽然占着地儿,但已经没有服务员(引用)在乎它了,她反手就是一个回收!这也是最容易爆满(OOM)的地方。 - 有趣的事实:堆还分"年轻代"和"老年代",就像火锅店的"新鲜食材区"和"腌制食材区"。
3. 虚拟机栈 (VM Stack) ------ 服务员手里的托盘 🥏
- 谁能进:一人一个,别抢。(线程私有,生命周期极短)
- 咋回事 :
每个服务员(线程)手里都端着个托盘 。
接待一桌客人(调用一个方法),就往托盘上放一个格子(栈帧 )。
格子里写着:客人点的单(局部变量 )、菜在大圆桌哪个位置(引用 )。
这桌走了(方法执行完),格子直接撤,盘子一收,干干净净。 - 注意:托盘大小有限,如果盘子堆太高(递归太深),就会"托盘溢出" (StackOverflowError) !
4. 程序计数器 (PC Register) ------ 服务员的小本本 📝
- 谁能进:一人一个,不仅小还不起眼。(线程私有,内存极小,唯一不会内存溢出的区域)
- 咋回事 :
这是服务员的随身记事本 。
正干着活呢,经理喊去搬酒(线程切换),回来得知道刚才活干到哪了吧?
"哦,刚才正要给 3 号桌加汤(执行到第 15 行)",全靠它记着。全店唯一不会炸(OOM)的地方。
5. 本地方法栈 (Native Method Stack) ------ 外聘的异国料理师傅 👨🍳
- 谁能进:一人一个。(线程私有)
- 咋回事 :
Java 厨师搞不定的硬菜(底层硬件操作),需要调用 C/C++ 写的native方法,这块区域就是给这些"外聘大厨"用的专用厨房。 - 冷知识:虽然逻辑上它俩是分开的,但最常用的 HotSpot 虚拟机为了省事,直接把"外聘厨房"和"服务员托盘"合二为一了(本地方法栈和虚拟机栈合体)。
💻 现场演示:一行代码的"火锅之旅"
我们通过一段代码,把这些概念串起来:
java
public class HotPotRestaurant {
// 【方法区】:static 变量,属于类信息
public static String shopName = "Re-Zero Hotpot";
// 【常量池】:标准配料(如"Spicy"),谁用谁拿,逻辑上属于方法区的一部分
public static final String FLAVOR = "Spicy";
public static void main(String[] args) {
// 【栈】:main方法栈帧入栈
// 【堆】:new出来的对象放在堆里
// 【栈】:waiter变量只是引用,放在栈帧的局部变量表里
HotPotRestaurant waiter = new HotPotRestaurant();
waiter.serve();
}
public void serve() {
// 【栈】:serve方法栈帧入栈
// 【栈】:局部变量count存在栈帧里
int count = 2;
// 【堆】:Beef对象在堆里分配内存
// 【栈】:beef引用指向堆里的对象
Beef beef = new Beef();
// 【方法区】:访问静态变量
System.out.println("Welcome to " + shopName);
}
// serve方法结束 -> 栈帧弹出 -> count和beef引用销毁
// 堆里的Beef对象变成"孤儿",等待GC回收
}
class Beef {
// 简单的食材类
}
🧠 内存动态图解(Text Version)
当代码跑到 serve() 里面时,你的脑子里应该有这张图:
Plaintext
【栈 (Stack)】 【堆 (Heap)】
+------------------------+ +-------------------------+
| [ 栈帧: serve() ] | | |
| ---------------------- | | +-----------------+ |
| 局部变量 count = 2 | | | Beef Object | |
| 引用 beef ----------> | ---> | | (地址: 0x999) | |
| | | +-----------------+ |
+------------------------+ | |
| [ 栈帧: main() ] | | +-----------------+ |
| ---------------------- | | | HotPotRestaurant| |
| 引用 waiter ---------> | ---> | | (地址: 0x666) | |
| | | +-----------------+ |
+------------------------+ +-------------------------+
【方法区 (Method Area)】
+---------------------------------------+
| 类信息: HotPotRestaurant.class |
| 静态变量: shopName = "Re-Zero Hotpot" |
| 常量池: "Spicy" |
+---------------------------------------+
🧱 编外人员:直接内存 (Direct Memory)
除了上面 5 个,还有一个狠角色:直接内存 (Direct Memory)。
- 比喻 :后厨的"外卖直通窗口"。
- 为啥用它 :
普通传菜(IO)得从仓库(内核态)搬到厨房(用户态)再端给客人,累死个人。
直接内存(NIO)就是开了个直通窗口,直接从仓库拿了菜就给客人,零拷贝,快得飞起! - 代价:这是店外面的地(本地内存),不归 JVM 管,用多了容易把店外面搞炸。
- 应用:NIO 的DirectBuffer就用这个,传输大文件时特别快。
📝 总结:面试救命表
别背长篇大论了,把这张表存好,面试前看一眼就行:
| 区域名称 | 英文 | 存什么?(人话版) | 线程关系 | 会 OOM 吗? |
|---|---|---|---|---|
| 程序计数器 | PC Register | 记事本:下一行运行哪句 | 🔒 私有 | ❌ 不会 (唯一) |
| 虚拟机栈 | VM Stack | 托盘:局部变量、引用 | 🔒 私有 | ✅ (StackOverflow) |
| 本地方法栈 | Native Stack | 外聘厨房:C++ 方法 | 🔒 私有 | ✅ (StackOverflow) |
| 堆 | Heap | 大圆桌:所有对象实例 | 🌐 共享 | ✅ (OOM 高发区) |
| 方法区 | Method Area | 菜单:类结构、静态变量 | 🌐 共享 | ✅ (元空间 OOM) |
🤔 思考题:String 的三重分身
我们在代码里写:(不考虑字符串常量池去重等复杂情况,仅从对象创建角度回答)
java
String s = new String("abc");
结合今天的知识,请问:
- s 放在哪里?
- new String() 放在哪里?
- "abc" 放在哪里?
(提示:想想栈、堆和方法区/常量池的关系)