对于C/C++等底层语言,内存需要手动进行申请,使用完后手动进行释放。而对于javascript语言使用者来说,因为有垃圾回收器的工作,在使用中通常不需要关心内存的使用情况。但有时不当的代码会意外的导致变量未被垃圾回收器回收,积少成多后造成内存泄漏,潜在的提高应用卡顿的风险。本文从垃圾回收器的工作原理进行分析,总结可能造成内存泄漏的几个典型场景,避免工作中出现内存泄漏造成应用卡顿。
无论哪种编程语言,内存的生命周期都是差不多的:申请内存、使用内存(读写)、释放或归还内存。
显而易见,用户设备内存是有限的,只申请不释放,内存被占满时,就无法给新创建的对象分配内存。这里类比我们去公司食堂吃饭的场景:打饭后找到空位(申请内存)、在空位吃饭(使用内存)、吃完饭收拾餐盘放回回收区(释放内存)。想象一下,我们吃完不收拾餐盘(释放内存),后来的人就没有餐桌可以吃饭了(程序崩溃)。
对于大多数学校和公司食堂,都是使用者吃完饭释放餐桌(收拾餐盘放回回收区),和C/C++等底层语言类似,使用者申请内存空间,使用完毕再释放内存。
如果我们去外边餐馆吃饭,也是同样的流程。只不过不需要自己找餐桌,由引导服务员给分配,使用后,不需要关心留在餐桌上的餐盘,由回收餐盘服务员去回收。对于JS 来说,垃圾回收器(Garbage Collector)就在做类似于餐盘服务员垃圾回收的工作:将不再使用的内存进行释放回收,从而能够循环利用有限的内存空间。
function grow() {
var x = []
let str = new Array(100000).join('x');
// 1亿个
for (let i=0; i<100000000; i++) {
x.push(str)
}
}
document.getElementById('grow').addEventListener('click', grow);
以上面这段代码为例,点击 grow按钮后,会向数组x中存入大量的(一亿个)字符串,然后这个tab就崩溃了。看到下图,大概率是内存超过了浏览器单 tab的内存上限。以chrome为例,其单tab内存上限在32位系统上为 512M,64位系统上为1.4GB左右。
JS中变量分为原始类型和引用类型,不同的变量类型存储方式不同。我们先回顾一下 JS 是如何存储变量的。原始类型直接存储在栈(Stack)中,引用类型存储在堆(Heap)中。
var a = 1;
function doSomething() {
let b = 2;
let obj = { c: 3}
console.log(a, b);
}
doSomething();
以上面的一段代码为例,全局执行上下文中存在一个值类型变量 a,doSomething 函数执行上下文中存在一个值类型变量b,一个引用类型变量obj。从下面的内存分配图可以看到,值类型直接存储在栈中,引用类型存储在堆中。
栈内存回收相对来说很简单,函数执行完毕后,该函数执行上下文从栈中弹出,存储在执行上下文中的变量立即被回收掉。还是以上面的一段代码为例,当 doSomething 执行完毕后,内存结构如下图:
doSomething 执行上下文被弹出,该执行上下文中所有变量都被销毁回收。对于值类型b来说,就直接释放了其占用的内存,对于引用类型obj来说,销毁的只是变量obj对堆内存地址 1001 的引用,obj的值 { c: 3 } 依然存在于堆内存中。那么堆内存中的变量如何进行回收呢?
代际假说认为,大部分新对象的生存时间比较短,在一次垃圾回收周期内被回收。
基于此,V8 将堆内存分为新生代和老生代。新生代又将内存分为 Nursery 和 Intermediate两个区域。新对象存放到Nursery区域中,经过一次垃圾回收,存活的对象被复制到 Intermediate 区域。经过两次垃圾回收仍然存活的对象将被移动到老生代中。有点像我们上学的过程,从幼儿园到小学到中学。
垃圾回收器有一些基本的任务:识别活动对象(marking)、回收或重用垃圾对象内存(sweeping)、整理碎片内存(defragment)。
标记阶段通过变量是否可达(reachability),判断是否为活动对象。通常为从一个根对象进行递归遍历,所有遍历到的对象都是可达的,为活动对象。没有遍历到的对象为非活动对象,需要进行回收。
var obj1 = { a: 1};
var obj2 = = { b: 2};
执行如下代码后obj2失去对 1002 的引用,在垃圾回收器遍历完之后发现没有对 1002 这块内存的引用变量,标记为其非活动变量。
obj2 = null;
GC会维护一个 freeList 列表,将非活动对象占用的内存片段地址添加到 freeList。有新对象申请内存时,freeList里有合适大小的内存块,会优先分配给新对象。
这个阶段是可选的。内存在经过垃圾回收之后,活动对象将内存块分割的很零碎,这个时候会进行整理,将活动对象复制到相同连续的内存区域内。
副垃圾回收器负责新生代垃圾回收。主要有四个步骤:标记、复制、更新指针、切换角色。新生代将内存分为 from space(Nursery) 和 to space (Intermediate)。当有新对象申请内存,会分配from space 区域中的地址,to space 区域为备用区域。
标记阶段同主垃圾回收器,将可达对象标记为活动对象。
复制阶段将from space中标记的活动对象复制到 to space区域,并给活动对象做标记,此时其已经位于 intermediate中,下一次垃圾回收时如果仍为活动对象,就要被复制到老生代中。
将活动对象复制到 to space 中之后,需要更新指针引用地址,这样原引用才能保证正确的指向。
最后切换 from space 和 to space 的角色。在下一次垃圾回收周期后,存活两次的对象会被复制到老生代区域。
在最初,GC运行在主线程,与 JS交替执行。在GC执行阶段,主线程停止JS代码执行,这称为全停顿(Stop-the-World)。如果垃圾回收器需要处理(标记-复制-整理)的对象比较多,就需要比较长的时间才能完成一次周期内的任务。在这期间如果有更高优的任务需要执行,是无法及时响应的,比如用户输入、动画的执行,给用户的感觉就是卡顿。
Goal: Free Main Thread
Orinoco是 Google 垃圾回收器(Garbage Collector)的项目代号,致力于研究如何提高垃圾回收效率。经过多年的发展,产出了三种能有效提高垃圾回收效率的方案:并行(Parallel)、增量标记(Incremental)、并发(Concurrent)。
在主线程执行垃圾回收任务的同时,开几个辅助线程同时进行,这样可以大大减少主线程全停顿(Stop the World)的时间。
将主线程垃圾回收任务分成多个小任务,与JS交替执行。这种方式并没有缩短GC工作的时间,但是给了JS响应高优任务的时间,避免了出现卡顿。
并发是主线程专注执行JS, 开启辅助线程进行垃圾回收。这种方式没有了全停顿,完全解放主线程,实现了 Free Main Thread 的目标。
通过了解V8垃圾回收机制,我们知道垃圾回收器会和JS线程争夺资源和时间。V8也在不断通过更先进的技术来减少全停顿(Stop the World)的时间。对于我们开发者来说,能做的就是尽量减少GC的工作负担。总结来说就是,变量不用之后立即释放。下面我们总结了几种容易造成内存泄漏的bad case,大家在工作中可以规避。
下面这段代码,函数作用域中变量未使用关键字声明,导致非严格模式下挂载到全局作用域。这样foo()函数执行完毕之后,由于 window.bar的引用一直存在,导致被GC识别为活动对象。这样只要程序在运行,该对象的内存就会一直存在无法被回收,增加垃圾回收器的工作负担。
// 非严格模式下,bar会被挂在全局上
function foo(arg) {
bar = { a: 1 };
this.obj = { b: 1};
console.log(bar, obj);
}
foo();
对于这种情况建议开启严格模式,或者使用 lint工具检查这种错误。
有了React和Vue这种UI库,我们就很少直接操作DOM了。在我们业务中,需要对富文本内的一些内容进行操作中,有很多直接操作DOM的场景。在操作完DOM之后,需及时清掉对DOM节点的引用,不然也会造成对内存的泄露。
<body>
<input type="text" id="input">
<div id="node"></div>
<script>
let node = document.getElementById('node');
node.parentNode.removeChild(node);
console.log('node', node) // 对node节点操作完成之后,内存中仍然保存着node节点
node = null ; // 通过将 node 赋值为null,切掉对 DOM 节点的引用
</script>
</body>
在我们业务中经常需要在组件挂载后给元素添加事件监听。这时需要在组件卸载时将监听事件移除,来避免无用的内存消耗。
componentDidMount() {
this.myScaleBar?.addEventListener('mousedown', this.handleMouseDown);
document.addEventListener('mousemove', this.handleMouseMove);
document.addEventListener('mouseup', this.handleMouseUp);
}
componentDidMount() {
this.myScaleBar?.addEventListener('mousedown', this.handleMouseDown);
document.addEventListener('mousemove', this.handleMouseMove);
document.addEventListener('mouseup', this.handleMouseUp);
}
chrome devtools 中的 performance 面板可以记录内存使用的timeLine, 在录制之前选中内存,报告中会有内存的使用情况。我们主要关注JS堆中内存的使用情况。
我们以下面这段代码为例,通过点击grow按钮,会向grow 函数内的变量x 内添加大量的长度为100000的字符串。
<!DOCTYPE html>
<html lang="en">
<head>
<title>内存测试</title>
</head>
<body>
<div>
<button id="grow">grow</button>
</div>
<script>
function grow() {
var x = [];
const str = new Array(100000).join('x');
for (let i=0; i<100000000; i++) {
x.push(str)
}
}
document.getElementById('grow').addEventListener('click', grow);
</script>
</body>
</html>
记录开始后先点击【强制垃圾回收】,然后点击grow,记录一段时间后再点击【强制垃圾回收】后查看报告。可以看到第二次垃圾回收与操作之前的内存相等,说明没有垃圾泄漏。
我们再稍微改一下代码,看一下内存的使用情况。
function grow() {
x = [];
let str = new Array(100000).join('x');
for (let i=0; i<100000000; i++) {
x.push(str)
}
}
记录发现强制垃圾回收之后,内存的占用要高于grow函数执行之前。与上面第一次记录的区别是,grow内变量 x 的声明没有使用关键字声明,非严格模式下直接挂载到window上。这样grow函数执行完毕,全局对 x依然 的引用,GC无法回收 x 占用的内存。
V8垃圾回收器帮助JS使用者周期性的回收不再使用的内存。过多的对象会对垃圾回收器造成额外的负担,甚至影响到主线程JS的执行,造成页面的卡顿。作为开发者应该有意识的减少全局变量的数量、及时移除不再使用DOM引用、事件监听及计时器,来减少垃圾回收器的负担。
[1]Trash talk: the Orinoco garbage collector · V8: https://v8.dev/blog/trash-talk
[2]代际假说: https://www.memorymanagement.org/glossary/g.html#term-generational-hypothesis
[3]代际垃圾回收器: https://www.memorymanagement.org/glossary/g.html#term-generational-garbage-collection
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/wT8_cz82Y5Ur74xGKpPgtQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。