代码跑着跑着就 OOM 了?带你手撕 JVM 内存模型,这回彻底搞懂对象到底住哪儿!

举报
喵手 发表于 2025/12/08 20:29:30 2025/12/08
【摘要】 开篇语哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,...

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛

  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。

  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!

0. 前言:那些年被 JVM 毒打的岁月 🤕

回想当年我刚入行那会儿,觉得自己写的 Java 代码那是天衣无缝。直到有一天,生产环境的服务器突然不做声了,日志里只有一行触目惊心的红字:java.lang.OutOfMemoryError: Java heap space。当时我的冷汗唰地一下就下来了,第一反应就是重启(万能的重启大法),结果没过半小时,它又挂了!😭

那时候我不懂啊,心想内存不是自动回收的吗?咋还能爆呢?后来被大佬按头安利了 JVM 内存模型,我才恍然大悟:这就好比你开个饭馆,不知道哪里是厨房、哪里是库房、哪里是垃圾桶,这饭馆能不倒闭吗?

今天,咱们就抛开那些晦涩的官方文档,用咱们全栈都能听懂的“人话”,给 JVM 内存区域做个全方位的 CT 扫描!

1. 宏观鸟瞰:JVM 的“户型图” 🏠

首先,咱们得有个全局观。JVM 跑起来的时候,它把内存划分成了好几个区域。你可以把它想象成一个精细化管理的大豪宅

这个豪宅里主要分两类地盘:
  1. 私人领地(线程私有):只有你自己能进,别人进不来。比如:虚拟机栈、本地方法栈、程序计数器。这就像你的卧室,私密性极强,不需要考虑线程安全问题,你人走了(线程结束),这地儿也就清空了。
  2. 公共大厅(线程共享):谁都能来踩一脚。比如:堆(Heap)、方法区(Method Area)。这就像客厅和厨房,张三能来,李四也能来,所以这里最容易出并发问题,垃圾回收(GC)主要也是在这儿忙活。

接下来,咱们一个屋一个屋地逛!🚶‍♂️

2. 虚拟机栈(VM Stack):你的私人工作台 🛠️

大家写代码时,最常用的就是方法调用。这个过程就在里完成。
  我看很多教程说“栈管运行,堆管存储”,这话对,但不完全对。

栈是什么? 它是线程私有的。每当你调用一个方法,JVM 就会在栈里压入一个栈帧(Stack Frame)。这栈帧里存了啥?

  • 局部变量表:你方法里定义的 int a = 1; 就在这儿。
  • 操作数栈:计算过程中的临时数据,比如 a + b 的时候,数据就在这儿进进出出。
  • 动态链接 & 方法出口:记录你从哪儿来,计算完要回哪儿去。

生动比喻
  这就好比你在厨房切菜(执行方法)。就是你的案板。你拿一个盘子(栈帧)放在案板上切土豆,切完了把盘子端走(出栈),再拿一个新盘子切西红柿(入栈)。这案板是你独享的,旁边的厨师(其他线程)不能乱拿你的刀。

💣 常见车祸现场:StackOverflowError
  如果你的盘子堆得太高,顶到天花板了,那就爆栈了!通常发生在你写了死递归的时候。

👨‍💻 代码实战(请勿在生产环境尝试):

public class StackBoom {
    private static int count = 0;

    public static void recursiveMethod() {
        count++;
        // 没写终止条件的递归,这就是在玩火🔥
        recursiveMethod();
    }

    public static void main(String[] args) {
        try {
            recursiveMethod();
        } catch (StackOverflowError e) {
            System.out.println("哎呀!栈爆了!💥");
            System.out.println("就在第 " + count + " 层的时候扛不住了...");
            // 栈深度是有限的,默认可能几千到几万层,看你JVM参数怎么设
        }
    }
}

3. 堆(Heap):乱糟糟的公共大仓库 📦

讲真,这是咱们跟 JVM 打交道最多的地方,也是最容易出幺蛾子的地方。
  几乎所有你 new 出来的对象(new User(), new ArrayList()),都堆在这里。

堆的特点
  1. :它是内存里最大的一块地儿。
  2. :所有线程的对象都混在一起。
  3. GC 的主战场:为了管理这么大的地方,JVM 把它分成了新生代(Young Gen)老年代(Old Gen)

生动比喻
  新生代就像是幼儿园,每天都有无数新对象(小孩)进来。大部分对象生命周期极短(朝生夕死),比如一个 HTTP 请求里的临时对象,用完就扔,所以新生代的 GC(Minor GC)非常频繁,像保洁阿姨一样天天扫地。
  老年代就像是养老院。如果一个对象在幼儿园里经过了 15 次(默认值)大扫除还没被扫走,说明它是个“老顽固”,就会被移送进老年代。这里存的都是生命力顽强的常驻对象(比如 Spring 的 Bean,连接池对象)。

💣 常见车祸现场:OutOfMemoryError (Heap space)
  如果养老院也住满了,保洁阿姨(Full GC)拼了老命也腾不出地儿来,那 JVM 只能两手一摊:挂了。

👨‍💻 代码实战(手写一个 OOM):

import java.util.ArrayList;
import java.util.List;

public class HeapBoom {
    static class HeavyObject {
        // 弄个大点的字节数组,占内存
        byte[] data = new byte[1024 * 1024]; // 1MB
    }

    public static void main(String[] args) {
        List<HeavyObject> list = new ArrayList<>();
        System.out.println("开始往堆里疯狂塞东西...🚒");
        
        try {
            while (true) {
                list.add(new HeavyObject());
                // 这里加个Thread.sleep能让你看着内存一点点飙升,更刺激
                // Thread.sleep(10); 
            }
        } catch (OutOfMemoryError e) {
            System.out.println("完犊子了!堆溢出!😱");
            // 此时你的 Java 进程基本上也就废了
        }
    }
}

4. 方法区(Method Area):存放“蓝图”的档案馆 📚

这也是个共享区域。很多新手容易把它和堆搞混。
  这里存的是类的信息(Class 结构)、常量静态变量(static)。

简单说,里存的是房子(对象实例),而方法区里存的是房子的图纸(类定义)。你得先有图纸,才能盖房子,对吧?

这里有个重要的历史演变(敲黑板!)
  * JDK 7 及以前:这块叫永久代(PermGen)。它其实在堆内存里偷了一块地。有个很大的坑是它大小固定,很容易满(PermGen space 错误),调优极难。
  * JDK 8 及以后:Oracle 大手一挥,把永久代废了!改名叫元空间(Metaspace)。最屌的是,元空间不再占用 JVM 内存,而是直接使用本地物理内存!理论上,只要你物理内存够大,它就能无限膨胀(当然我们一般会设个上限)。

💣 常见车祸现场:Metaspace OOM
  虽然改成了元空间,但如果你疯狂地动态生成类(比如用 CGLib 或动态代理狂搞),也会把它撑爆。

5. 直接内存(Direct Memory):不讲武德的 VIP 通道 🚀

这块区域其实不属于 JVM 运行时数据区的一部分,但咱们全栈开发必须得知道!
  它是 NIO(Non-blocking IO)出现后才火起来的。

为什么需要它?
  传统的 IO 操作(比如读文件),需要把数据从操作系统内核缓冲区复制到Java 堆内存,这中间有一次数据拷贝,多累啊!
  直接内存就是直接在堆外分配内存,操作系统能访问,Java 也能直接操作(通过 DirectByteBuffer)。这就实现了零拷贝(Zero Copy),速度快得飞起!🚀 像 Netty 这种高性能框架,底层全是这玩意儿。

有啥缺点?
  分配和回收的成本很高!而且因为它不受 GC 直接管理(虽然会有虚引用帮忙),如果用不好,很容易造成堆外内存泄漏,这种 Bug 查起来能让你怀疑人生。🤯

6. 总结:做个懂“风水”的开发 🧘‍♂️

你看,把 JVM 拆解开来看,是不是也没那么神秘?
  咱们来个一句话总结:

栈是打工人的工位(私有、处理逻辑),堆是公司的仓库(共享、存数据),方法区是公司的规章制度墙(存类信息),直接内存是公司的特快专递通道(高性能 IO)。

了解这些有什么用?
  当你下次遇到服务器变慢、CPU 飙高或者内存溢出时,你就不再是那个只会重启服务器的小白了。你会知道:

  • StackOverflow?查查是不是有死递归。
  • Heap OOM?查查是不是有大对象没释放,或者内存泄漏。
  • Metaspace OOM?查查是不是动态加载了太多类。

… …

文末

好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。

… …

学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!

wished for you successed !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。


版权声明:本文由作者原创,转载请注明出处,谢谢支持!

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。