你是否曾经遇到过浏览器突然卡顿,甚至崩溃的情况?尤其是在打开多个标签页或运行复杂的网页应用时,浏览器似乎变得异常脆弱。这种崩溃的背后,往往与内存管理息息相关。
一、内存管理
底层语言(如 C 语言)拥有手动的内存管理原语,例如:ree()。相反,JavaScript 是在创建对象时自动分配内存,并在不再使用时自动释放内存(垃圾回收)。这种自动化机制虽然方便,但也容易让我们产生误解,认为不需要关心内存管理,从而忽略潜在的内存问题。
二、内存生命周期
分配内存:根据需求分配所需的内存。
使用内存:对分配的内存进行读写操作。
释放内存:在内存不再需要时将其释放。
在底层语言中,内存的分配和释放是显式的,开发者需要手动管理。而在高级语言如 JavaScript 中,内存的分配和释放大多是隐式的,由垃圾回收机制自动处理。
2.1 内存分配
2.1.1 值的初始化
为了不让我们费心内存分配,JavaScript 在值初次声明时自动分配内存。
const n = 28; // 为数值分配内存
const s = "yongtao"; // 为字符串分配内存
const o = {
a: 1,
b: null,
}; // 为对象及其包含的值分配内存
// 为数组及其包含的值分配内存(就像对象一样)
const a = [1, null, "yongtao"];
function f(a) {
return a + 2;
} // 为函数(可调用的对象)分配内存
// 函数表达式也会分配内存
someElement.addEventListener(
"click",
function () {
someElement.style.backgroundColor = "blue";
}
);
2.2.1 通过函数调用分配内存
const d = new Date(); // 为 Date 对象分配内存
const e = document.createElement("div"); // 为 DOM 元素分配内存
const s = "azerty";
const s2 = s.substr(0, 3); // s2 是一个新的字符串
// 因为字符串是不可变的值,
// JavaScript 可能决定不分配内存,
// 只是存储了 [0-3] 的范围。
const a = ["yeah yeah", "no no"];
const a2 = ["generation", "no no"];
const a3 = a.concat(a2);
// 有四个元素的新数组,由 a 和 a2 其中的元素连接而成。
2.2 变量读取
2.3 内存回收(垃圾回收)
三、V8 的垃圾回收
垃圾回收的核心任务是识别内存中的“死区”,即不再使用的内存。一旦识别出这些区域,它们可以被重新用于新的内存分配或释放回操作系统。一个对象如果不再被根对象或活跃对象引用,则被视为“死的”。根对象通常是活跃的,例如局部变量、全局对象或浏览器对象(如 DOM 元素)。
function f() {
var obj = { x: 12 };
g(); // might contain an infinite loop.
return obj.x;
}
3.1 V8 内存结构

-
堆内存(Heap):
堆内存是 V8 中用于动态分配内存的区域,存储 JavaScript 对象、闭包、函数等数据。堆内存进一步分为以下几个区域: -
新生代:用于存储生命周期较短的对象(如临时变量、局部变量等)。分为两个半空间(From Space 和 To Space),采用 Scavenge 算法进行垃圾回收。新生代空间较小,垃圾回收频率较高。 -
老生代::用于存储生命周期较长的对象(如全局变量、闭包等)。采用标记-清除(Mark-Sweep)和标记-整理(Mark-Compact)算法进行垃圾回收。老生代空间较大,垃圾回收频率较低。 -
代码空间:专门用于存储 JIT(Just-In-Time)编译生成的机器代码。代码空间与其他空间分离,因为代码的生命周期通常较长,且需要高效访问。 -
大对象空间:用于存储较大的对象(如大数组、大字符串),避免频繁复制。采用标记-清除和标记-整理算法进行垃圾回收。 -
单元空间、属性单元空间和映射空间:些空间分别包含 Cells、PropertyCells 和 Maps。每个空间都包含大小相同的对象,并且对它们指向的对象类型有一定的限制,从而简化了垃圾回收。 -
栈内存(Stack)
栈内存用于存储函数调用时的局部变量、参数和返回地址。栈内存的特点是分配和释放速度快,但空间有限。
3.2 V8 垃圾回收机制
3.2.1 栈数据的垃圾回收

为什么需要区分“堆”和“栈”两个存储空间?为什么不将所有数据直接存放在栈中?
3.2.2 堆数据的垃圾回收
代际假说是垃圾回收领域的一个重要理论,V8 的垃圾回收策略正是基于这一假说。代际假说包含两个核心观点:
-
大多数对象的生命周期很短,分配后很快变得不可访问。 -
少数对象会存活较长时间。
基于此,V8 将堆内存分为新生代和老生代两个区域。新生代存放生命周期短的对象,老生代存放生命周期长的对象。V8 的垃圾回收器分为主垃圾回收器和副垃圾回收器。
副垃圾回收器:
副垃圾回收器主要负责新生代的垃圾回收。由于大多数小对象都分配在新生代,因此该区域的垃圾回收频率较高。
新生代采用 Scavenge 算法 进行垃圾回收。该算法将新生代空间对半划分为对象区域和空闲区域。
新加入的对象存放在对象区域。当对象区域快满时,副垃圾回收器会执行以下步骤:
-
标记对象区域中的存活对象。 -
将存活对象复制到空闲区域,并有序排列,消除内存碎片。 -
角色翻转:对象区域变为空闲区域,空闲区域变为对象区域。
主垃圾回收器:


全停顿和增量标记

如果在执行垃圾回收的过程中,占用主线程时间过久,就像上面图片展示的那样,花费了 200 毫秒,在这 200 毫秒内,主线程是不能做其他事情的。比如页面正在执行一个 JavaScript 动画,因为垃圾回收器在工作,就会导致这个动画在这 200 毫秒内无法执行的,这将会造成页面的卡顿现象。

四、内存泄漏与优化
4.1 常见的内存泄漏场景及优化方案
4.1.1 意外的全局变量
function leak() {
leakedVar = 'This is a global variable'; // 意外的全局变量
}
始终使用 var、let 或 const 声明变量。启用严格模式("use strict"),避免意外创建全局变量。
4.1.2 未清理的定时器或回调函数
let data = getData();
setInterval(() => {
process(data); // data 一直被引用,无法释放
}, 1000);
使用 clearInterval 或 clearTimeout 清除定时器。在组件销毁或页面卸载时清理定时器。
4.1.3 未解绑的事件监听器
const button = document.getElementById('myButton');
button.addEventListener('click', () => {
console.log('Button clicked');
});
// 如果 button 被移除,但未解绑事件监听器,会导致内存泄漏
使用 removeEventListener 解绑事件监听器。在组件销毁或页面卸载时解绑事件。
4.1.4 闭包中的引用
function createClosure() {
let largeData = new Array(1000000).fill('data');
return function() {
console.log(largeData[0]); // largeData 一直被闭包引用
};
}
const closure = createClosure();
免在闭包中捕获不必要的变量。在不再需要闭包时,手动解除引用(例如将闭包设置为 null)。
4.1.5 DOM 引用未释放
let element = document.getElementById('myElement');
document.body.removeChild(element); // 从 DOM 中移除
// element 仍然被引用,无法释放
在移除 DOM 元素后,将其引用设置为 null:
element = null;
4.1.6 缓存未清理
const cache = new Map();
function setCache(key, value) {
cache.set(key, value);
}
// 如果缓存未清理,会持续增长
使用 WeakMap 或 WeakSet,它们不会阻止键对象的垃圾回收。定期清理缓存。
4.2 内存泄漏检查
4.2.1 使用 Chrome 任务管理器

-
打开 Chrome 任务管理器:点击 Chrome 右上角的三个点(菜单按钮) > 更多工具 > 任务管理器。 -
查看内存占用:关注内存占用异常高的任务(如标签页、扩展程序、辅助框架等)。 -
检查内存增长:观察某个任务的内存占用是否持续增长(即使页面没有操作)。如果某个任务的内存占用不断增加,可能是内存泄漏。
4.2.2 使用 Chrome 开发者工具

步骤:
-
打开开发者工具:右键点击页面,选择 检查,或者使用快捷键:Ctrl + Shift + I(Windows/Linux)或 Cmd + Option + I(Mac)。 -
使用 Memory 面板:切换到 Memory 标签。选择以下工具之一进行分析: -
Heap Snapshot:拍摄堆内存快照,分析内存分配情况。 -
Allocation instrumentation on timeline:记录内存分配的时间线,查看内存增长情况。 -
Allocation sampling:通过采样分析内存分配。 -
分析内存泄漏: -
拍摄多个堆内存快照,比较快照之间的内存变化。 -
查找未被释放的对象(如 DOM 节点、事件监听器等)。 -
检查 Retainers(持有者),找到导致内存泄漏的代码。
4.2.3 使用第三方工具
-
Lighthouse:Chrome 的 Lighthouse 工具可以检测页面性能问题,包括内存泄漏。 -
MemLab:Facebook 开源的 JavaScript 内存分析工具,专门用于检测内存泄漏。
五、从崩溃到优化:内存管理的终极目标
浏览器的崩溃往往源于内存管理的不足,而 V8 引擎的内存管理机制正是解决这一问题的关键。通过理解 V8 的内存分配、垃圾回收机制以及常见的内存泄漏场景,我们可以更好地优化代码,避免内存浪费和性能瓶颈。无论是开发者还是普通用户,了解这些原理都能帮助我们更好地应对浏览器崩溃问题,提升应用的整体性能和用户体验。
六、 总结
本文通过从常见的浏览器崩溃场景引出本篇文章的分享主题:V8的内存管理, 文章主要介绍了V8垃圾回收的原理、常见的内存泄漏场景及其预防方案。
最后,最重要的一点:欢迎评论区互动,一起交流学习,共同成长

本文分享自微信公众号 - 京东云开发者(JDT_Developers)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。