你的代码把内存吃光了?V8引擎的“垃圾清理工”有话说

张开发
2026/4/13 20:14:24 15 分钟阅读

分享文章

你的代码把内存吃光了?V8引擎的“垃圾清理工”有话说
你写了一个页面打开时内存占用200MB用了一天涨到2GB最后浏览器崩了。谁偷了你的内存今天我们就钻进V8引擎的肚子里看看那个专门负责“打扫卫生”的垃圾回收器是怎么工作的。弄懂它你就能写出不卡顿、不泄漏的代码。前言想象你有一个房间内存。你不断往里面搬家具创建对象但从来不扔忘记释放。慢慢的房间堆满杂物连门都开不了。这时候你需要一个清洁工定期把没用的东西清走。在JavaScript里这个清洁工就是垃圾回收器GC。它会在后台默默运行找到那些“不再需要”的对象然后回收它们占用的内存。但清洁工不是万能的有时候它也会犯迷糊或者你藏了它找不到的垃圾。今天我们就来当一回“卫生监督员”看看GC怎么工作以及怎么避免内存泄漏。一、V8引擎JS的“跑车引擎”V8是Google开发的JS引擎用在Chrome和Node.js里。它的工作是把JS代码编译成机器码然后执行。顺便也负责内存管理——分配内存、垃圾回收。V8的内存分两部分新生代Young Generation存放刚创建、存活时间短的对象。比如函数里的临时变量。老生代Old Generation存放存活久、大块头的对象。比如全局变量、长期缓存。为什么分两个区域因为大部分对象都是“朝生暮死”分开管理能提高回收效率。二、垃圾回收的核心思想可达性GC怎么知道一个对象“不再需要”它靠的是可达性从根Roots出发能访问到的对象就是活的否则就是垃圾。根包括全局对象window/global、当前函数的局部变量和参数、调用栈上的变量、DOM引用等。比如letobj{name:张三};// 对象被obj引用可达objnull;// 没有引用指向它了不可达等待回收关键不是所有“没有变量指向”的对象都会被立刻回收。GC会周期性运行找出不可达的对象然后清理。三、新生代回收Scavenge算法快如闪电新生代空间很小通常1~8MB分成两个半区From和To。对象先分配到From区。当From快满时GC启动标记From区中存活的对象。把存活对象复制到To区并紧凑排列消除碎片。清空From区。交换From和To角色。这个过程很快因为新生代存活对象少。如果一个对象经历了两次回收还活着或者To区空间不够它就会被晋升到老生代。四、老生代回收标记-清除与标记-整理老生代空间大对象存活率高不能用复制算法太浪费。V8用标记-清除和标记-整理。标记-清除Mark-Sweep标记阶段从根出发遍历所有对象把能访问到的标记为“存活”。清除阶段遍历老生代把没标记的对象回收内存变成碎片。问题清除后留下很多不连续的空洞内存碎片可能放不下一个大对象。标记-整理Mark-Compact为了解决碎片问题标记-整理会在标记后把存活对象向一端移动然后清理边界外的内存。这样内存就连续了。V8会按需使用这两种算法。标记-清除快但有碎片标记-整理慢但能整理空间。五、全停顿与增量标记别让清洁工“霸占”房间早期GC是全停顿的清洁工干活时JS代码暂停页面卡住。对于老生代一次回收可能几十毫秒甚至几百毫秒用户明显感觉到卡顿。V8后来引入了增量标记把标记阶段拆成多个小步骤每步只做一点点中间让JS执行一会儿。这样用户几乎感觉不到停顿。还有并发标记在后台线程里标记不阻塞主线程。所以现在你很少因为GC而卡顿了除非你疯狂创建对象。六、常见内存泄漏清洁工找不到的垃圾即使有GC你还是可能写出内存泄漏——因为有些对象你以为没用了但GC觉得它还可达。常见场景1. 意外的全局变量functionleak(){name张三;// 没有var/let/const变成全局变量}leak();// 全局的name永远可达不会被回收解决严格模式use strict会阻止这种隐式全局。2. 被遗忘的定时器或回调letdatanewArray(1000000);setInterval((){console.log(data.length);},1000);// 即使你不再需要data定时器还引用着它data不会被回收解决不需要时clearInterval并把引用置null。3. DOM引用残留constelementdocument.getElementById(btn);document.body.removeChild(element);// DOM移除了console.log(element);// 变量还保留着DOM对象无法回收解决移除DOM后把变量也设为null。4. 闭包滥用functionouter(){letbigDatanewArray(1000000);returnfunctioninner(){// 没有使用bigData但闭包保留了整个作用域链console.log(hello);};}constfnouter();// bigData一直被保留解决只在闭包里引用需要的数据或者用完后解除引用。5. 离屏DOM树constdivdocument.createElement(div);div.innerHTMLspan.../span;// 大量DOM// 但div没添加到页面也没变量引用实际上div本身在内存中// 如果你忘了释放它就一直在解决确保无用DOM被设为null。七、实战如何排查内存泄漏Chrome DevTools → Memory 面板可以拍堆快照。拍一张快照。执行你怀疑有泄漏的操作比如打开关闭弹窗。再拍一张快照。对比两张快照看哪些对象没被释放。也可以用Performance面板录制内存变化看看是不是一直涨。八、优化建议帮清洁工省点力及时解除引用变量不用了赋null尤其是大数组、大对象。避免不必要的全局变量。手动清理定时器、事件监听。使用弱引用WeakMap、WeakSet、WeakRef。它们的键是弱引用不影响垃圾回收。letcachenewWeakMap();letobj{name:张三};cache.set(obj,some data);objnull;// obj被回收后cache里的键值对也会自动消失控制数组大小不要无限push。用requestAnimationFrame做动画时注意清理。九、总结和清洁工做朋友V8内存分新生代短命对象和老生代长命对象。新生代用Scavenge复制算法快。老生代用标记-清除和标记-整理避免碎片。全停顿→增量标记→并发标记卡顿越来越少。内存泄漏常来自全局变量、定时器、闭包、DOM残留。用WeakMap、及时置null、清理监听器。垃圾回收不是“自动挡”就能乱开你得配合它。写代码时想想“这个对象现在没用了GC能认出它是垃圾吗”如果你觉得今天的“清洁工”故事够生动点个赞让更多人看到。明天我们将聊聊网络安全看看XSS和CSRF是怎么偷你数据的以及怎么防。我们明天见

更多文章