📕我是廖志伟,一名Java开发工程师、《Java项目实战——深入理解大型互联网企业通用技术》(基础篇)、(进阶篇)、(架构篇)清华大学出版社签约作家、Java领域优质创作者、优快云博客专家、阿里云专家博主、51CTO专家博主、产品软文专业写手、技术文章评审老师、技术类问卷调查设计师、幕后大佬社区创始人、开源项目贡献者。
📘拥有多年一线研发和团队管理经验,研究过主流框架的底层源码(Spring、SpringBoot、SpringMVC、SpringCloud、Mybatis、Dubbo、Zookeeper),消息中间件底层架构原理(RabbitMQ、RocketMQ、Kafka)、Redis缓存、MySQL关系型数据库、 ElasticSearch全文搜索、MongoDB非关系型数据库、Apache ShardingSphere分库分表读写分离、设计模式、领域驱动DDD、Kubernetes容器编排等。不定期分享高并发、高可用、高性能、微服务、分布式、海量数据、性能调优、云原生、项目管理、产品思维、技术选型、架构设计、求职面试、副业思维、个人成长等内容。
🌾阅读前,快速浏览目录和章节概览可帮助了解文章结构、内容和作者的重点。了解自己希望从中获得什么样的知识或经验是非常重要的。建议在阅读时做笔记、思考问题、自我提问,以加深理解和吸收知识。阅读结束后,反思和总结所学内容,并尝试应用到现实中,有助于深化理解和应用知识。与朋友或同事分享所读内容,讨论细节并获得反馈,也有助于加深对知识的理解和吸收。💡在这个美好的时刻,笔者不再啰嗦废话,现在毫不拖延地进入文章所要讨论的主题。接下来,我将为大家呈现正文内容。
文章目录
- 面试该怎么说JVM内存模型
- 线程自己独享的
- 大家共享的
- 直接内存
- 自我提问
- 视觉化描述
- 不同版本的JVM内存模型
面试该怎么说JVM内存模型
JVM内存模型说白了就是Java程序运行时内存怎么分的,分两块儿:
线程自己独享的
程序计数器:相当于线程的“进度条”,记录代码执行到哪一行了。
虚拟机栈:就是平时说的“栈”,存方法调用时的局部变量(比如int a=1),递归太深就会爆栈溢出。
本地方法栈:给JVM底层用的Native方法(比如C写的代码)服务的,和虚拟机栈类似。
大家共享的
堆:所有new出来的对象都在这儿,分成“新生代”(刚创建的对象)和“老年代”(熬过GC存活下来的),内存不够就OOM(内存溢出)。
方法区(元空间):存类的信息、常量池这些,以前叫永久代,后来改到本地内存了,不容易爆内存。
直接内存
不走JVM管的堆,比如用NIO的时候的堆外内存,用多了也会OOM,但不容易被监控到。
关键记住:
- 堆和方法区是线程共享的,容易有并发问题,得用锁或者CAS。
- 实际开发里,调优参数比如-Xmx(堆最大)、-Xss(栈大小)得根据业务来设,别拍脑袋。
- 内存溢出三大坑:堆里对象太多(比如缓存没清理)、方法区类加载爆炸(比如动态生成类)、栈里递归太深。
自我提问
-
递归太深为什么会爆栈溢出?
虚拟机栈里存的是方法调用的栈帧(你可以想象成每个方法调用就往栈里压一个“档案袋”,里面装着局部变量、操作数)。
如果递归调用层数太多(比如没有终止条件),栈帧就会一直叠加,超过虚拟机栈的最大容量(比如默认1MB),这时候就会抛出StackOverflowError,就像往杯子里倒水直到溢出来。 -
本地方法栈到底是干嘛的?
虚拟机栈:负责Java方法的调用(比如你写的public void foo())。 本地方法栈:专门给JVM内部调用的Native方法用的(比如用C/C++写的native方法,如Object.hashCode()底层实现、文件读写等系统级操作)。 关键区别:一个管Java方法,一个管底层非Java代码,结构类似但服务对象不同。
-
直接内存为什么“不容易被监控到”?
直接内存(堆外内存)是通过Native方法(如Unsafe.allocateMemory)直接向操作系统申请的内存,比如NIO的ByteBuffer.allocateDirect()。
监控难点:
不归JVM堆管理,所以常规工具(如jstat)看不到这部分内存占用。 但OOM时会提示OutOfMemoryError: Direct buffer memory(JDK8+)。
解决方案:可以通过-XX:MaxDirectMemorySize参数限制大小,或者用BufferPoolMXBean监控。
视觉化描述
- 虚拟机栈溢出(StackOverflowError)示意图
[虚拟机栈结构]
|-------------------|
| 方法B的栈帧 | ← 栈顶(当前执行位置)
| 局部变量表 |
| 操作数栈 |
| 动态链接 |
| 返回地址 |
|-------------------|
| 方法A的栈帧 | ← 递归调用前
|-------------------|
问题点:递归调用(方法A → 方法B → 方法A → ...)导致栈帧不断叠加,超过栈容量(比如默认1MB),触发溢出。
- 本地方法栈 vs 虚拟机栈
[JVM线程内存结构]
|-----------------------|
| 线程私有区域 |
|-----------------------|
| 程序计数器 | → 记录执行位置
|-----------------------|
| 虚拟机栈 | → Java方法调用栈帧(如`public void foo()`)
|-----------------------|
| 本地方法栈 | → Native方法调用栈帧(如`native hashCode()`)
|-----------------------|
关键区别:两者结构类似,但服务对象不同(Java方法 vs JVM底层C/C++方法)。
- 堆外内存(直接内存)与JVM堆的关系
[内存分配示意图]
|-----------------------|
| JVM进程内存 |
|-----------------------|
| JVM堆 | → 由JVM管理(GC回收)
|-----------------------|
| 直接内存(堆外) | → 通过`Unsafe.allocateMemory`申请
|-----------------------|
| 操作系统内存 | → 其他进程或系统使用
|-----------------------|
监控难点:直接内存不通过JVM分配,常规工具(如jmap)无法直接观测,但OOM时会明确提示Direct buffer memory。
- JVM内存全景图(核心分区)
[JVM内存模型全景]
|-----------------------|
| 线程共享区域 |
|-----------------------|
| 堆(Heap) | → 对象实例(新生代/老年代)
|-----------------------|
| 方法区(元空间) | → 类信息、常量池
|-----------------------|
|-----------------------|
| 线程私有区域 |
|-----------------------|
| 程序计数器 | → 每个线程独立
|-----------------------|
| 虚拟机栈 | → Java方法栈帧
|-----------------------|
| 本地方法栈 | → Native方法栈帧
|-----------------------|
| 直接内存 | → 堆外内存(非JVM管理)
|-----------------------|
面试回答技巧:
结合示意图描述:比如“堆是对象存储区,类似一个仓库分成新货区(新生代)和旧货区(老年代),GC像清洁工定期清理”。
举例实际场景:
栈溢出:递归调用无终止条件 → StackOverflowError。
堆OOM:缓存对象未释放 → OutOfMemoryError: Java heap space。
调优参数关联:
-Xss256k:设置虚拟机栈大小为256KB,限制递归深度。
-XX:MaxDirectMemorySize=1g:限制直接内存大小。
不同版本的JVM内存模型
-
Java 7及之前
方法区实现:
通过永久代(PermGen)实现,存储类元信息、常量池等。 问题:容易触发OutOfMemoryError: PermGen space(类加载过多或动态代理滥用)。
堆外内存:
直接内存(如NIO的DirectBuffer)由开发者手动管理,监控工具支持有限。
-
Java 8(核心变化)
方法区重构:
永久代移除,改为元空间(Metaspace),由本地内存(Native Memory)直接管理。 优势: 避免永久代固定大小导致的OOM(元空间默认无上限,但受物理内存限制)。 类元数据自动释放(如类加载器被回收时)。 参数调整: -XX:MetaspaceSize(初始大小)和-XX:MaxMetaspaceSize(上限)。
字符串常量池迁移:
从永久代移至堆内存,减少永久代压力。
-
Java 9及以后
G1作为默认GC(Java 9+):
堆内存划分为多个等大小Region,不再严格分新生代/老年代,降低Full GC停顿时间。
ZGC/Shenandoah GC(Java 11+):
支持TB级堆内存,暂停时间极短(<10ms),但需显式启用(如-XX:+UseZGC)。
堆外内存增强:
Native Memory Tracking(NMT)工具完善,可监控直接内存(jcmd <pid> VM.native_memory)。
-
Java 17(LTS版本)
元空间优化:
支持压缩类指针(Compressed Class Pointers)默认开启,减少内存占用。
分代式ZGC(Java 15+实验性):
引入分代概念,针对新生代和老年代优化回收效率。
移除过时GC:
如CMS(Concurrent Mark-Sweep)被废弃(Java 14弃用,Java 17移除)。
关键总结(面试回答)
方法区演变:
永久代(Java 7)→ 元空间(Java 8+),避免OOM,动态扩展。
堆外内存:
从手动管理到工具支持(NMT),参数可约束(MaxDirectMemorySize)。
GC机制:
G1/ZGC成为主流,减少停顿时间,支持超大堆。
版本升级影响:
Java 8+项目需注意元空间监控,Java 11+可探索低延迟GC(如ZGC)。
示例:
面试官:Java 8为什么要移除永久代?
回答:永久代大小固定,容易因类加载过多导致OOM,且调优困难(需预估-XX:PermSize)。Java 8改用元空间(本地内存管理),支持动态扩展,且类元数据随类加载器回收自动释放,降低了内存泄漏风险。
📥博主的人生感悟和目标
希望各位读者大大多多支持用心写文章的博主,现在时代变了,信息爆炸,酒香也怕巷子深,博主真的需要大家的帮助才能在这片海洋中继续发光发热,所以,赶紧动动你的小手,点波关注❤️,点波赞👍,点波收藏⭐,甚至点波评论✍️,都是对博主最好的支持和鼓励!
- 💂 博客主页: Java程序员廖志伟
- 👉 开源项目:Java程序员廖志伟
- 🌥 哔哩哔哩:Java程序员廖志伟
- 🎏 个人社区:Java程序员廖志伟
- 🔖 个人微信号:
SeniorRD
📙经过多年在优快云创作上千篇文章的经验积累,我已经拥有了不错的写作技巧。同时,我还与清华大学出版社签下了四本书籍的合约,并将陆续出版。这些书籍包括了基础篇、进阶篇、架构篇的📌《Java项目实战—深入理解大型互联网企业通用技术》📌,以及📚《解密程序员的思维密码–沟通、演讲、思考的实践》📚。具体出版计划会根据实际情况进行调整,希望各位读者朋友能够多多支持!
🔔如果您需要转载或者搬运这篇文章的话,非常欢迎您私信我哦~