无论是前端的JavaScript、后端的的Java,还是移动端的Android,只要涉及到内存管理,就绕不开[垃圾回收 (Garbage Collection), 简称GC]。
很多开发者在日常的开发中,只关注业务逻辑,却忽略了垃圾回收的底层逻辑——直到遇到内存泄漏、页面卡顿、内存溢出才意识到它的重要性。
这篇文章从是什么、为什么、怎么做三个维度来把垃圾回收机制讲透。
一、先搞懂: 什么是垃圾回收?
我们先从最基础的概念入手,一句话讲明白核心:垃圾回收,就是程序自动识别并释放“不在使用的内存空间”的过程。
举个例子:你在电脑上打开了一个文件,系统会给这个文件分配一块内存;当你关闭文件,这块内存就“闲置”了,如果不及时释放,打开的文档越多,闲置的内存就越多,最终电脑会越来越卡。
开发也是一样:我们定义变量、创建对象、调用函数时,系统会自动分配内存;当这些变量/对象不再被使用(比如函数执行完成、变量被赋值为null),它们占用的内存就变成了“垃圾”。
如果不及时清理这些”垃圾“,内存会被持续占用,最终页面卡顿、帧率下降,甚至浏览器崩溃;
二、如何识别垃圾回收算法
垃圾回收的第一步,也是最核心的一步:如何准确判断一个内存空间是否”不再被使用“?
目前主流的垃圾回收算法有两种,几乎所有现代编程语言的GC,都是基于这两种算法衍生而来的,我们重点讲最常用的两种:
1.引用计数法 (Reference Counting)
这是最简单、最直观的算法,核心逻辑:给每个对象分配一个”引用技术器“,记录当前对象被多少个地方引用。
- 当对象被引用一次,计数器+1
- 当引用失效 (比如变量被赋值为null、函数执行完毕),计数器-1
- 当计数器的值为0时,说明这个对象不再被使用,标记为”垃圾“,等待回收。
// 1. 创建对象, 引用计数器=1
let obj = {name:'垃圾回收'};
// 2. 新增一个引用,计数器+1
let obj1 = obj
// 3. 释放obj的引用,计数器-1
obj = null
// 4. 释放obj1的引用,计数器再-1 为0,标记为垃圾
obj1 = 0
优点: 实现简单、回收及时,只要计数为0就立即回收,不会产生内存堆积;
缺点:无法解决循环引用问题——两个对象互相引用,计数器都不为0,但它们都不再被其他地方引用,导致内存泄漏。
let a = {};
let b = {};
// 循环引用,a引用b,b引用a
a.b = b;
b.a = a;
// 此时a和b的计数器都是1,即使后续将a和b赋值为null,计数器仍为1,无法被回收
a = null
b = null
正是因为这个缺点,现在几乎没有编程语言单独使用引用计数法。
2. 标记清除法 (Mark Sweep)
标记清除法是目前主流的垃圾回收识别算法,解决了引用计数法的循环引用问题,核心逻辑分两步: 标记 + 清除
第一步: 标记
从根对象(Root Object)出发,遍历所有可达的对象,给这些对象打上”存活“标记;而那些无法被根对象遍历到的对象,就是”垃圾“。根对象在JS中就是全局对象(window、global)。
第二步:清除
遍历整个内存空间,将没有被打上”存活“标记的对象(垃圾)释放,回收它们占用的内存空间。
优点:解决了循环引用问题,适用范围广,是现代GC的核心算法;
缺点:清除后会产生”内存碎片“——就像往箱子里放大小不一的物品,拿走一部分后,箱子里会留下很多零散的空隙,后续如果需要分配大块内存,可能无法找到连续的空间,只能触发更频繁的垃圾回收。
Java的老年代GC(比如CMS、G1)、Python的GC,都用到了标记-整理法的思想。
JS的垃圾回收机制,结合了「引用计数法」和「标记-清除法」,不同引擎(V8、SpiderMonkey)实现略有差异,其中V8引擎(Chrome、Node.js)最具代表性。
V8的GC策略:分代回收(基于“大部分对象存活时间很短”的经验规律),将内存分为「新生代」和「老年代」,分别采用不同的回收算法,提高回收效率。
- 新生代(Young Generation):存放存活时间短的对象(比如临时变量、函数内的局部对象),采用「复制算法」(将新生代内存分为两个区域,From和To,存活对象复制到To区,然后清空From区,切换两者角色),回收速度快;
- 老年代(Old Generation):存放存活时间长的对象(比如全局变量、长期缓存的对象),采用「标记-清除法+标记-整理法」,回收频率较低,但回收耗时较长。
前端常见内存泄漏场景:
- 全局变量未清理(比如未声明的变量、挂载在window上的变量);
- 闭包滥用(闭包引用了外部变量,且闭包未被释放);
- DOM元素引用未清理(比如删除DOM后,仍保留其引用);
- 定时器/事件监听器未销毁(比如setInterval未clear、addEventListener未remove)。