还在手动 free 内存?你知道你的 Java 程序是怎么在垃圾堆里“捡破烂”续命的吗?

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

开篇语

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

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

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

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

0. 前言:从 malloc 的噩梦说起

如果你写过 C/C++,那你肯定有过被 Segmentation Fault 支配的恐惧。那时候咱们就是内存的保姆,借了东西(malloc)必须得还(free),要是忘了还,那就是内存泄漏;要是还错人了,那就是野指针。那时候我就在想,要是有个田螺姑娘能自动帮我收拾屋子该多爽啊!😭

后来 Java 来了,带着它的 JVM 和 GC(Garbage Collection)来了!它就像那个宠坏你的田螺姑娘:“你只管造(new),剩下的脏活累活我来干!”

但是!田螺姑娘也是有脾气的! 😤 如果你不知道她是怎么打扫卫生的,拼命往地上扔垃圾,她忙不过来的时候,直接就给你来个 Stop The World (STW) —— 所有人都不许动,老娘要大扫除了!这时候你的系统就像断网了一样尴尬。

所以,今天咱们就来扒一扒 GC 的三大神技:标记-清除、复制、标记-整理。看看它们到底是怎么把你的内存打理得井井有条(或者乱七八糟)的。🧹


1. 标记-清除(Mark-Sweep):最原始的“贴条法”

这是最基础、最老牌的算法,就像是咱们打扫卫生时的第一直觉。

怎么玩呢?分两步走:

  1. 标记(Mark):GC 从根节点(GC Roots,比如栈里的变量)出发,顺藤摸瓜。只要是能摸到的对象,就给它脑门上贴个条:“这玩意儿还有用,别扔!”🏷️
  2. 清除(Sweep):全堆扫描,凡是脑门上没贴条的,统统干掉!👋

听起来很完美对吧?简单粗暴!但是…

这就好比你在切披萨。 🍕 你把不爱吃的火腿抠掉了,剩下的披萨虽然还能吃,但是这就这就这就… 千疮百孔啊!

它的致命缺陷叫:内存碎片化(Fragmentation)。
  想象一下,你的内存被清理后变成了瑞士奶酪,到处都是小洞洞。这时候,你需要分配一个连续的大对象(比如一个大数组),虽然这些小洞洞加起来空间够,但因为没有一块完整的连续地盘,JVM 只能两手一摊:“臣妾做不到啊!” 结果只能再次触发 GC,简直是恶性循环。🔄

💻 伪代码模拟逻辑

# 假装这是内存管理器
def mark_and_sweep(memory_heap):
    # 第一步:标记
    # 这里的 root_objects 是你正在用的变量
    active_objects = set()
    for root in root_objects:
        mark(root, active_objects) # 递归把引用的都标记上

    # 第二步:清除
    # 遍历堆里所有对象,不在 active_objects 里的就是垃圾
    for obj in memory_heap:
        if obj not in active_objects:
            memory_heap.free(obj) # 直接释放,留下一块空地
            # 此时 memory_heap 里充满了不连续的空洞... 🕳️

2. 复制算法(Copying):富二代的“双拼别墅”玩法

为了解决碎片化的问题,有人想出了一个极端的招数:复制算法

这思路简直就是土豪行为!💰 它把内存一分为二,分成 A区B区
  平时咱们只在 A区 蹦跶。当 A 区满了,垃圾回收开始:

  1. 把 A 区里所有活着的对象,整整齐齐地搬到 B区 去。
  2. 搬完之后,直接把 A 区一把火烧光!🔥
  3. 下次就在 B 区玩,满了再搬回 A 区。

爽不爽?太爽了! 根本不需要考虑碎片问题,每次搬家都是紧凑排列的,分配内存时只需要移动指针,速度快得飞起!🚀

但是(万恶的转折来了)…
  这种玩法的代价是:你得有一半的内存常年空着! 😱
  比如你买了 8G 内存,结果系统告诉你:“不好意思,为了快,你只能用 4G,另外 4G 要留着搬家用。”
  这谁顶得住啊?这不是败家子吗?所以,这种算法通常只用在对象死得快的区域(比如 Java 的新生代),因为存活的对象少,搬运成本低。

💻 伪代码模拟逻辑

# 假装这是新生代 GC
def copying_gc(from_space, to_space):
    free_pointer = 0 # to_space 的起始位置
    
    for obj in from_space:
        if is_alive(obj):
            # 搬家!而且是紧凑地搬过去
            copy(obj, to_space[free_pointer])
            free_pointer += size(obj)
            
    # 这一行代码简直霸气侧漏
    clear_all(from_space) # 旧家直接推平!💥
    
    # 交换角色,下次从 to_space 搬回 from_space
    swap(from_space, to_space)

3. 标记-整理(Mark-Compact):强迫症患者的福音

既然“标记-清除”有碎片,“复制算法”太浪费空间,那有没有折中方案?
  这就轮到标记-整理出场了。它结合了前两者的优点,特别适合老年代这种“老不死”对象扎堆的地方。👴

怎么玩?

  1. 标记:和第一种一样,先把活着的找出来。
  2. 整理(Compact):这一步最关键!它不是直接清理垃圾,而是让所有存活的对象都向一端移动
  3. 清理:把边界以外的内存全部清空。

这就好比咱们玩“俄罗斯方块”! 🧱
  不管中间有多少空隙,我把所有方块都往底下一压,上面剩下的空间就全是干净、连续的了!

优点:没有碎片,也不浪费一半空间。
  缺点累啊!慢啊! 😩
  你想想,如果内存里有几万个对象,你要把它们一个个挪位置,还得更新它们之间错综复杂的引用地址,这工作量简直爆炸。所以在整理的过程中,系统不得不暂停很长时间(STW),你的网页可能就在那儿转圈圈。💫


4. 深度拓展:分代收集才是王道(大乱炖)

既然这三种算法各有优劣,那咱们能不能成年人不做选择,全都要?
  这就是现代 JVM 的分代收集理论。它根据对象存活周期的不同,把内存分成了几块,因地制宜!🗺️

  • 新生代(Young Gen): 这里全是刚 new 出来的“小鲜肉”。据统计,98% 的对象在这里那是“朝生夕死”。

    • 策略:用复制算法的改良版!
    • 并不是 1:1 分,而是弄一个大的 Eden 区和两个小的 Survivor 区(通常 8:1:1)。每次只浪费 10% 的空间,是不是聪明多了?😏
  • 老年代(Old Gen): 这里全是经过多次 GC 还没死掉的“老油条”,或者是那种大到塞不进新生代的“巨无霸”。

    • 策略:这种对象很难死,搬家又贵,所以用标记-整理或者标记-清除

🛠️ 实战演示:如何看懂 GC 日志?

光说不练假把式。咱们写一段 Java 代码,故意制造点垃圾,看看 GC 是怎么工作的。

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

// 运行参数建议:-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails
// 意思就是:给堆内存限制在 20M,新生代 10M,打印 GC 详情
public class GCDemo {
    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();
        int i = 0;
        try {
            while (true) {
                // 不断制造 1MB 的垃圾对象
                // 这就像是你疯狂买买买,家里马上就堆满了
                list.add(new byte[1 * 1024 * 1024]); 
                i++;
                System.out.println("分配了 " + i + "MB 内存...");
            }
        } catch (OutOfMemoryError e) {
            System.out.println("💀 完蛋!OOM 了!");
            System.out.println("一共存活了 " + i + " 次分配");
        }
    }
}

当你运行这段代码,你会看到类似这样的日志(主要看心情):

[GC (Allocation Failure) [PSYoungGen: 8192K->1000K(9216K)] 8192K->5024K(19456K), 0.0042314 secs]

老哥帮你翻译一下:

  • Allocation Failure:新生代(Eden)满了,放不下了,触发 GC。
  • PSYoungGen: 8192K->1000K:新生代原本占了 8M,GC 用复制算法一顿操作猛如虎,把活着的搬到 Survivor 区,剩下 1M。
  • 你看! 这就体现了复制算法“倒腾”的过程。
  • 等到最后 OOM(内存溢出)的时候,就是老年代也塞满了,标记-整理也救不了你了,直接崩溃。💥

5. 写在最后:垃圾回收也是人生哲学

你看,GC 算法其实特别像咱们的人生规划:

  • 复制算法像年轻时候,精力旺盛,哪怕浪费点时间去试错(浪费空间),也要追求快节奏,不行就换个地方重头再来。
  • 标记-整理像中年以后,东西多了,羁绊深了,不能随便搬家了,只能在现有的圈子里慢慢整理,虽然动作慢了(效率低),但是稳重、紧凑,每一步都算数。

所以啊,兄弟们,别光顾着埋头写代码,偶尔也看看这些底层的智慧。哪怕是为了面试的时候能跟面试官侃大山,把这些原理搞透也是稳赚不赔的买卖!

好了,今天的“垃圾分类”课程就到这儿。我要去手动 System.gc() 一下我的脑子了,咱们下期见!👋🚀

… …

文末

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

… …

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

wished for you successed !!!


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

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


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

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

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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