还在手动 free 内存?你知道你的 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):最原始的“贴条法”
这是最基础、最老牌的算法,就像是咱们打扫卫生时的第一直觉。
怎么玩呢?分两步走:
- 标记(Mark):GC 从根节点(GC Roots,比如栈里的变量)出发,顺藤摸瓜。只要是能摸到的对象,就给它脑门上贴个条:“这玩意儿还有用,别扔!”🏷️
- 清除(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 区满了,垃圾回收开始:
- 把 A 区里所有活着的对象,整整齐齐地搬到 B区 去。
- 搬完之后,直接把 A 区一把火烧光!🔥
- 下次就在 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):强迫症患者的福音
既然“标记-清除”有碎片,“复制算法”太浪费空间,那有没有折中方案?
这就轮到标记-整理出场了。它结合了前两者的优点,特别适合老年代这种“老不死”对象扎堆的地方。👴
怎么玩?
- 标记:和第一种一样,先把活着的找出来。
- 整理(Compact):这一步最关键!它不是直接清理垃圾,而是让所有存活的对象都向一端移动。
- 清理:把边界以外的内存全部清空。
这就好比咱们玩“俄罗斯方块”! 🧱
不管中间有多少空隙,我把所有方块都往底下一压,上面剩下的空间就全是干净、连续的了!
优点:没有碎片,也不浪费一半空间。
缺点:累啊!慢啊! 😩
你想想,如果内存里有几万个对象,你要把它们一个个挪位置,还得更新它们之间错综复杂的引用地址,这工作量简直爆炸。所以在整理的过程中,系统不得不暂停很长时间(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 !!!
⭐️若喜欢我,就请关注我叭。
⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。
版权声明:本文由作者原创,转载请注明出处,谢谢支持!
- 点赞
- 收藏
- 关注作者
评论(0)