你是否曾经遇到过浏览器突然卡顿,甚至崩溃的情况?尤其是在打开多个标签页或运行复杂的网页应用时,浏览器似乎变得异常脆弱。这种崩溃的背后,往往与内存管理息息相关。
一、内存管理
底层语言(如 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垃圾回收的原理、常见的内存泄漏场景及其预防方案。
最后,最重要的一点:欢迎评论区互动,一起交流学习,共同成长


