浅谈JS内存机制

发表于 2年以前  | 总阅读数:335 次

前言

随着web的发展与普及,前端页面不仅只加载在浏览器上,也慢慢流行于各种app的webview里。尤其在如今设备性能越来越好的条件下,前端页面更是开始在app中担任重要的角色。如此一来,前端页面的停留时间变得更长,我们理应越发重视前端的内存管理,防止内存泄露,提高页面的性能。

想要打造高性能前端应用,防止崩溃,就必须得搞清楚JS的内存机制,其实就是弄清楚JS内存的分配与回收。

JS数据存储机制

内存空间

从图中可以看出, 在 JavaScript 的执行过程中, 主要有三种类型内存空间,分别是代码空间、栈空间和堆空间。

代码空间:用来存放可执行代码

栈空间:一块连续的内存区域,容量较小,读取速度快,被设计成先进后出结构

堆空间:不连续的内存区域,容量较大,用于储存大数据,读取速度慢

数据类型

JavaScript 发展至今总共有八种数据类型,其中 Object 类型称为引用类型,其余七种称为基本类型,Object 是由其余七种基本类型组成的kv结构数据。

栈空间和堆空间

栈空间其实就是 JavaScript 中的调用栈,是用来储存执行上下文,以及存储执行上下文中的一些基本类型中的小数据,如下图所示:

image.png

变量环境: 存放var声明与函数声明的变量空间,编译时就能确定,不受块级作用域影响

词法环境: 存放let与const声明的变量空间,编译时不能完全确定,受块级作用域影响

而堆空间,则是用来储存大数据如引用类型,然后把他们的引用地址保存到栈空间的变量中,所以多了这一道中转,JavaScript 对堆空间数据的读取自然会比栈空间数据的要慢,可以用下图表示两者关系:

通常情况下,栈空间都不会设置太大,这是因为 JavaScript 引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了的话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率。

闭包

内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包

闭包中的数据会组成一个对象,然后保存在堆空间中,如:

可以利用开发者工具查看闭包情况,其中括号中的名称就是产生闭包的函数名。一般我们会认为闭包是返回的内部函数引用的变量集合,但闭包有一个较为迷惑的情况,如下:

可以理解为,如果函数存在闭包,其所有内部函数都会拥有一个指向这个闭包的引用,即所有内部函数会共享同一个闭包,只要任意内部函数有引用外部函数中声明的变量,这个变量都会被纳入闭包内,而且最内部的函数会持有所有外部的闭包。

堆栈存放的数据类型

原始类型的数据是存放在栈中,引用类型的数据是存放在堆中?

上面这句话是用来描述栈中数据的存储情况,调用栈中的引用类型存放在堆中,相信大家都没有问题,但是原始类型真的都存放在栈中吗?

数字

V8把数字分成两种类型:smi 和 heapNumber

smi是范围为 :-2³¹ 到 2³¹-1的整数,在栈中直接存值;除了smi,其余数字类型都是heapNumber,需要另外开辟堆空间进行储存,变量保存其引用。

var times = 50000;
var smi_in_stack = 1;
var heap_number = 1.1;

// about 1.5~1.6ms, fast
console.time('smi_in_stack');
for (let i = 0; i < times; i++) {
  smi_in_stack++;
}
console.timeEnd('smi_in_stack');

// about 2.1~2.5ms, slow
console.time('heap_number');
for (let i = 0; i < times; i++) {
  heap_number++;
}
console.timeEnd('heap_number');

同时我们可以通过heap snapshots观察到heap_number的存在,所以验证了栈中的heapNumber值是存在堆中,smi值是直接存在栈中。

更基本的基本类型

V8定义了一种 oddball[1] 类型,属于 oddball 类型的有null、undefined、true和false

function BasicType() {
  this.oddBall1 = true;
  this.oddBall2 = false;
  this.oddBall3 = undefined;
  this.oddBall4 = null;
  this.oddBall5 = '';
}
const obj1 = new BasicType();
const obj2 = new BasicType();

这里可以看到oddball类型以及空字符串的堆引用全部都是一个固定值,代表在V8跑起来的第一时间,不管我们有没有声明这些基本类型,他们都已经在堆中被创建完毕了。由此猜想栈中这些类型使用的也是堆中的地址。

function Obj() {
  this.string = 'str';
  this.num1 = 1;
  this.num2 = 1.1;
  this.bigInt = BigInt('1');
  this.symbol = Symbol('1');
}
const obj = new Obj();
debugger;
obj.string = 'other str';
obj.num1 = 2;
obj.num2 = 1;
obj.bigInt = BigInt('2');
obj.symbol = Symbol('2');

debugger后内存快照

其中bigInt、string、symbol的内存地址都进行了更换,由此可以猜想是因为这三种类型占用的内存大小不是一个固定值,需要根据其值进行动态分配,所以内存地址会进行更换;而heapNumber的内存地址并没有发生变化,这个更换值的操作还是在原来的内存空间中进行。因为栈是一块连续的内存空间,不希望运行中会产生内存碎片,由此可以得出bigInt、string、symbol这些内存大小不固定的类型在栈中也是保存其堆内存的引用。同时我们在栈中可以声明很大的string,如果string存放在栈中明显也不合理

故栈空间中的基本类型储存位置如下:

类型 储存位置
Number smi储存栈中,heapNumber储存堆中
String
Boolean
Null
undefined
BigInit
Symbol

上述结论主要是从heap snapshots和栈的特性中得出,毕竟最正确的答案是在源码中获得,如有不当,请指正。

JS内存回收

栈内存回收

function fn1() {
  //....
  function fn2() {
    //...
  }
  fn2();
}
fn1();

调用栈中有一个记录当前执行状态的指针(称为 ESP),随着函数的执行,函数执行上下文被压入调用栈中,执行上下文中的数据会按照前面说的JS数据存储机制被分配到堆栈中,ESP会指向最后压栈的执行上下文,如左图所示的fn2函数。当fn2函数调用完毕,JS 会把ESP指针下移至fn1函数,这个指针下移的操作就是销毁fn1函数执行上下文的过程。最后fn1函数执行上下文所占用的区域会变成无效区域,下一个函数执行上下文压入调用栈的时候会直接覆盖其内存空间。简而言之,只要函数调用结束,该栈内存就会自动被回收,不需要我们操心。刚刚我们也聊到闭包,如果出现闭包的情况,闭包的数据就会组成一个对象保存在堆空间里。

堆内存回收

内存垃圾回收领域中有个重要术语:代际假说,其有以下两个特点:

1 . 大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问;

2 . 不死的对象,会活得更久。

基于代际假说,JS 把堆空间分成新生代和老生代两个区域,新生代中存放的是生存时间短的对象,通常只支持 1~8M 的容量;老生代中存放的生存时间长的对象,一些大的数据也会被直接分配到老生区中。而针对这两个区域,JS 存在两个垃圾回收器:主垃圾处理器和副垃圾处理器。这里先说说垃圾回收一般都有相同的执行流程:

1 . 标记空间中活动对象和非活动对象

2 . 回收非活动对象所占据的内存

3 . 内存整理,这步是可选的,因为有的垃圾回收器工作过程会产生内存碎片,这时就需要内存整理防止不够连续空间分配给大数据

副垃圾回收器

副垃圾回收器主要是采用 Scavenge 算法进行新生区的垃圾回收,它把新生区划分为两个区域:对象区域和空闲区域,新加入的对象都会存放到对象区域,当对象区域快被写满时,会对对象区域进行垃圾标记,把存活对象复制并有序排列至空闲区域,完成后让这两个区域角色互转,由此便能无限循环进行垃圾回收。同时存在对象晋升策略,也就是经过两次垃圾回收依然还存活的对象,会被移动到老生区中。

主垃圾回收器

由于老生区空间大,数据大,所以不适用 Scavenge 算法,主要是采用标记-整理算法,其工作流程是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。接着让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。垃圾回收工作是需要占用主线程的,必须暂停JS脚本执行等待垃圾回收完成后恢复,这种行为称为全停顿。 由于老生代内存大,全停顿对性能的影响非常大,所以出现了增量标记的策略进行老生区的垃圾回收。

JS内存泄漏

由于栈内存会随着函数调用结束而被释放(覆盖),所以JS中的内存泄漏一般发生在堆中。之前有同学分享过一篇关于内存泄漏的文章 ,里面讲到一些常见内存泄漏的原因和监测手段,这里我就不赘述,但是可以根据最近的IM工作讲一些实践:

确认是否有内存泄漏的情况

1 . 本地打包一个去掉压缩、拥有sourcemap及没有任何console的生产版本(console会保留对象引用,阻碍销毁;去掉压缩和保留sourcemap有利于定位源码)

2 . 启动本地服务器,使cef访问本地项目

3 . 不断操作和记录heap snapshots,观察snapshots和timeline情况

4 . 最终内存从22.5m上升至34.6m,conversation实例从443上升至1117,message实例从443上升至1287,而该用户实际只有221个会话

5 . 不断在会话间切换,通过timeline看到有内存没被释放,而且生成detached dom

通过上述观测,可以判断为有内存泄漏情况。

确定内存泄漏排查方式

IM页分为:会话列表,会话顶栏,消息列表,输入框四部分。使用逐一排查法缩小排查范围,排查各个部分内存情况。如:先保留会话列表,注释其余三个部分,操作会话列表并使用timeline和heap snapshots进行内存排查。按照这一方法逐步排查四个部分组件,并针对各个组件进行优化。可以简单归纳成一个通用步骤:

1 . 使用timeline进行录制,观察是否像上面那样有不被释放的内存区域

2 . 选择不被释放的区域进行查看,先找自己项目中的锚点物:像我们IM数据都是用conversation和messsage对象进行储存,所以可以先进行这两个对象的搜索查看

3 . 如果没有好的锚点物也没关系,接着查看detached dom(毕竟很多事件绑定在dom中,事件中引用着数据,造成无法被释放)和 string

有些detached dom可能是react虚拟dom的数据,但像上面的Detached HTMLAudioElement会随着操作一直增加,所以这个是不正常的。

像这里string的重复,经排查是有相同conversation和message对象引起

堆快照里包含太多运行时、上下文等信息,实在太难从中找到有用的信息,所以会把目标放在锚点物、detached dom和string上

4 . 利用heap snapshot 的comparison模式过滤出操作阶段内存变更情况,更有利于查找影响位置

上面是个人进行内存泄露排查整理的方法,如果你有更好的方法,欢迎交流∠(°ゝ°)

React中一个需要注意的内存泄漏问题

现象: 当组件被销毁后,仍有一些异步事件调用组件中setState方法

原理: 组件销毁后,再调用setstate方法会保留相关引用,造成内存泄漏

// 测试代码
const [test, setTest] = useState(null);
useEffect(() => {
  (async () => {
    // 这里表达一个异步操作如:xhr、fetch、promise等等
    await sleep(3000);
    const obj = new TestObj();
    setTest(obj);
  })();
}, []);

如果把代码改成这样,就不会造成内存泄漏:

const [test, setTest] = useState(null);
useEffect(() => {
  let unMounted = false;
  (async () => {
    await sleep(3000);
    if (unMounted) return;
    const obj = new TestObj();
    setTest(obj);
  })();
  return () => {
    unMounted = true;
  };
}, []);

这是在开发环境测试的,翻看源码发现react只会在开发模式保留这些引用,然后抛出warning来提醒开发者这里可能有内存泄漏的问题(如这些setState是注册在全局事件里或者setInterval里的调用),生产环境是不会对其进行引用,所以不需要额外进行处理也不会造成内存泄漏

react18更是直接把这个报错给干掉,以免误导开发者使用刚刚说的类似手段来进行避免报错,这里有做解释:https://github.com/facebook/react/pull/22114

总结

本文先是讲述js类型在内存空间的储存位置,接着探讨堆栈中的内存是如何进行回收,最后描述内存泄漏确定和排查的方法,也补充一个react中有关setState造成“内存泄漏”的例子。内存泄漏在复杂应用中是难以避免的,个人排查也只能是解决一些比较明显的内存泄漏现象。所以为了更好地解决这个应用内内存泄漏问题,必须做好线上监控,利用广大用户操作数据,发现内存泄漏问题,进而不断改善应用的性能。

参考资料

1 . https://developer.chrome.com/docs/devtools/memory-problems/memory-101/

2 . https://www.cnblogs.com/goloving/p/15352261.html

3 . https://hashnode.com/post/does-javascript-use-stack-or-heap-for-memory-allocation-or-both-cj5jl90xl01nh1twuv8ug0bjk

4 . https://www.ditdot.hr/en/causes-of-memory-leaks-in-javascript-and-how-to-avoid-them

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/uxSoXkmi5KIGNPsyd5cXrA

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237227次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8063次阅读
 目录