浏览器崩溃的第一性原理:内存管理的艺术
你是否曾经遇到过浏览器突然卡顿,甚至崩溃的情况?尤其是在打开多个标签页或运行复杂的网页应用时,浏览器似乎变得异常脆弱。这种崩溃的背后,往往与内存管理息息相关。
一、内存管理
底层语言(如 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源创计划”,欢迎正在阅读的你也加入,一起分享。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Node.js技术原理分析系列6——基于 V8 封装一个自己的 JavaScript 运行时
Node.js 是一个开源的、跨平台的JavaScript运行时环境,它允许开发者在服务器端运行JavaScript代码。Node.js 是基于Chrome V8引擎构建的,专为高性能、高并发的网络应用而设计,广泛应用于构建服务器端应用程序、网络应用、命令行工具等。 本系列将分为9篇文章为大家介绍 Node.js 技术原理:从调试能力分析到内置模块新增,从性能分析工具 perf_hooks 的用法到 Chrome DevTools 的性能问题剖析,再到 ABI 稳定的理解、基于 V8 封装 JavaScript 运行时、模块加载方式探究、内置模块外置以及 Node.js addon 的全面解读等主题,每一篇都干货满满。 在上一节中我们探讨了 Node.js 中的 ABI 稳定相关内容,在本节中则主要分享《基于 V8 封装一个自己的 JavaScript 运行时》相关内容,本文内容为本系列第6篇,以下为正文内容。 前言 Google 推出的 V8 引擎,自 2008 年随 Chrome 浏览器面世以来,大幅提升了 JavaScript 性能,重新定义了其应用范畴。作为 Chrome 和 ...
- 下一篇
MiniMax GenAI 可观测性分析 :基于阿里云 SelectDB 构建 PB 级别日志系统
"阿里云SelectDB作为MiniMax日志存储服务的核心支撑,为在线和离线业务提供了高效、稳定的查询与聚合分析能力。其支持实时物化视图、租户资源隔离、冷热分离等企业级特性,不仅有效解决了日志场景下PB级别数据查询的性能瓶颈,还通过智能化的资源调度与存储优化,实现了成本与效率的最佳平衡,为业务的高效运转提供了坚实保障。" ------MiniMax可观测架构师 香克斯 可观测日志系统的探索与挑战 近年来,MiniMax在多模态与文本模型领域持续发力,凭借其技术突破和应用创新能力,迅速成为全球人工智能领域的焦点。25年1月,MiniMax发布了多项重磅成果:支持主体参考功能的视频新模型S2V-01、基于大规模线性注意力机制的开源模型MiniMax-01系列,以及支持17种语言音频合成的T2A-01系列语音模型。作为一家成立仅三年但估值已突破数十亿美元的初创企业,MiniMax已然跻身人工智能领域最具潜力的独角兽企业之列。 为了深入洞察模型训练迭代和 AI应用的运行状态,精准定位潜在问题以持续优化模型和业务系统的性能,可观测系统的建设成为MiniMax底层基础设施建设中不可或缺的关键环节...
相关文章
文章评论
共有0条评论来说两句吧...