震惊!这个代码片段竟然会让 V8 内存无法回收?!

简介: 如你的应用已经遇到类似的内存泄漏问题,请尽快升级到最新的nodejs或alinode。

开门见山,这是一段可以搞崩掉服务器的代码片段,如果你的代码也这样,那一定要注意啦~

try {
        obj = JSON.parse(data);
    } catch (err) {
        // ignore
    }

你肯定很好奇,这段看似平淡的代码片段究竟是怎样搞崩掉服务器的?

这是一个"真实"的故事,就发生在几天前......

某晚一办公大楼警铃大作,电话那头某应用函数报告某应用系统异常, 从监控上看到,内存增长呈现阶梯式爆炸式增长,短短几个小时就消耗完了系统内存。

image.png

内存监控

咋一看,这是普通的不能再普通的内存泄漏问题,这对训练有素的士兵们已经不算什么。按照常规方法,取heapdump进行分析,占用最多的对象一般都能分析个八九不离十了。

但是 。。。

heapdump竟然看不出什么。。。只看到一个影子,一个吃了几百兆内存的影子,这是什么鬼?

image.png

Heapdump

此时,报警还在持续,办公室报警声不断,但又非常安静,弥漫着诡异的气氛。

监控上,应用一个个逼近系统极限,OOM一个个成为尸体,但是都留下相同的影子 。。。

时间在一分一秒的过去,"我们必须尽快抓到'影子',好给大家一个交代",数班长急促的声音透露着坚定。

"'影子'可能有个代号script_list,但是我们目前掌握的就只有那么多信息了",Y说到。

Y是班里最牛的信息兵,他有着最敏锐的洞察力,并掌握着最精准的信息,但是这一次,他也感到困惑。

"M,你跟我立刻去一趟基地,我们要进去抓'影子'" 班长说。"是,长官"。

作为特种兵的M,平时就接受了缺少粮食、缺少装备的高强度训练,他可以在极简的配置下,执行最底层的特殊任务。

image.png

M近照

"如果'影子'是个人,他应该还在基地里",M说。

"你能找到他么?",班长问。

"能!他只能从指定的门进去,并且注册登记,吃成这么胖,应该很容易被发现。"

"如果是妖呢?"

"下次好莱坞的电影可以用这个做题材,这是人类历史上首次捉到妖",

班长一脚踢向了M,"少TM扯淡,走!"

"带上这个,或许会用到。" 临走时,P塞给了M一卷图纸。走的匆忙,M也没来得及看一眼,就丢在了包里。

image.png

一卷图纸

班长和M离开了办公大楼,去往基地。

基地在不远的地方,门口有门卫守护,但是地方很大,要在基地找到'影子'并不是容易的事情。

基地内戒备森严,并还有巡逻的卫兵,巡视着基地内各个房间,并清理一些不必要的垃圾出来。

基地已经运作了很多很多年,可能有过一些异类后来被清理了,但是从来没有遇到过'妖怪'?

到了基地, "M,你进去吧,我还有个会议要参加,要给排长作简报,等你好消息哟~", "是,长官",M背着包就进了基地。

M的包里除了P塞的图纸,还有gdb和llnode两个工具。"真实的师傅领进门",M心里默想。

gdb 用来定位和分析v8/node的c++实现,大部分没啥用,但有把叉子总比啥都没有的强。

llnode 用来定位和分析v8的object,虽然绝大部分都是unkown,但能看个东西总比眼瞎的强。

基地内被分割了很多个营地,每个营地都有自己独立的管理人员。M面临的第一个问题,是如何找到各营地的管理人员,因为管理人员通常不固定在一个地方,而且他又没有电话号码可以联系。

但是每一个营地在建设的时候,都保留了一个设计图纸,里面标注了这个营地营长的办公室。

"P给我塞的难道是营地图纸",M嘀咕着,

拿出图纸一看,真的是Isolate第一营地的地方标注,他径直走了进去。

关于进程内存中定位Isolate node支持多个Isolate,通过

image.png

node::per_process::v8_platform.platform_.per_isolate_ 可以获取到所有v8::Isolatenode binary会在固定内存的地址上存放了一些很重要的数据用以分析,比如下面的v8_platform

00000000029ae600 B node::per_process::v8_platform

除此之外还有 nodedbg、v8dbg开头的常量符号用于mdb(Modular Debugger), 被收进llnode中,用来给v8和node定位corefile,也被称作 postmortem (验尸)。

"长管,我是NODE特种兵M,请问您是Isolate的营长么?"

"我是"

"我受上级命令,来调查一个叫'影子'的人,这个人很危险关系到人民的利益,影响到群众用TB了"

"'影子'?从来没听过这个人",营长一脸困惑

"这个人可能很胖,你能给我讲一下我怎么能查到所有的人,我相信我能找到他"

"可以是可以,你得这样来 。。。",营长给M讲了一下营地的结构。

原来营地分为很多个区域,

  • 新兵区,刚来的新兵都在这个区域进行训练,有些新兵呆满2年就退伍转业了,有些新兵则可能留在部队晋升到老兵区了。
  • 老兵区,老兵通常有着更丰富的经验,并且比新兵更加沉稳,愿意效忠,退伍意愿并不强烈。
  • 还有器械区,摆放了各种武器,虽然武器最后会分发给各个士兵,但是都存放在这里。

image.png

Node内存

"每一个人,每一把枪,都在账本上有登记,你也可以查看宿舍和仓库。我现在带你去见H长官",营长说。

H长官负责所有营地的人或物件的管理,任何进出都需由H长官许可。

Isolate->heap_ 管理了v8所有的对象。

在H长官的带领下,M检查了新兵区和老兵区的登记,没有发现任何异样,完全没有异常体重的人。

M走进了大型器械仓库,看到一个超级大的架子,

"这是什么?",M问道,

"这是武器架,任何武器都存放在这个架子上,每个武器存放一格"。

"这有多长",M接着问道,

"700多m",

"你们有多少武器"

"10w件",

"那要这么大的架子么?",M表示疑问。

image.png

从 0xbec56a80138 - 0xbec81f55660,存放了一个LargeObject,占用了726M内存空间

M拿出了GDB仔细检查了这个架子,发现700m的架子上,只有头上和中间部分集中摆放了一些武器,其余部分都是空的。

"为什么会这样?",M问H长官。

"这是按规定的,我们有一个账本,记录了进来的武器,每次进来一件,我就会从架子上分配一个格子,如果没有格子了,我就问上级需求一个新的架子。我们这里需求很大,你看,现在已经分配到66626945格了。"

"那些取出的武器呢?",M问

"放心,GC卫兵会来清点的,如果架子后面都是空的,他会标注最后一个有武器的格子,然后我会从下一个空格子分配。这个系统已经运作很久了,从来没有出过问题",H长官有些不耐烦。

"这个架子有代号么?",

"有,叫script_list"。

image.png

中间 (0x00002090 - 0x056dc5c0), ( 0x056dc610 - 0x1fc48d60) 都是0x0000000000000003(v8空指针) 空洞占了绝大多内存空间,由于v8指针压缩技术的存在,写脏的页面导致很大的内存开销。每一个js都会创建一个script添加到script_list上。

听到这,M已经理解为啥这个营地需要那么多的架子存放武器了。

因为只要架子后面有一把武器没有被拿走,新来的武器只能存放在他的后面。所以这个架子已经接到700多m,并且600多m都是空的。

M走到架子中间,随手拿起中部架子上第一部武器,是一把手枪。M拿出了LLNODE,仔细检查了这把手枪。M注意到手枪上面印有"[object Object]"的字样。

image.png

这个script含有特征字符 "[object Object]"和3个smi数字(1,2,6596938),但无法判断是什么script

"这是谁的枪?",M问道,

"士兵使用不同的枪械,这种类型的'[object Object]'手枪属于很多个兵种,一排二排都是,但是不知道具体谁的。",长官答道,

M拍了拍上面的灰尘说,"这把枪应该很久没有人来拿过了,要不现在开始,所有的入库都需要检查一下,看看谁还有这把手枪?"

没过多久,有个叫Json士兵来到架子前,M用GDB查看了他的手枪,上面写着"[object Object]"。

"有个长官让我更换这个枪的枪托,我更换时发现这个枪托根本拆不开,按照部队规定我就给送到这里来了",Json解释到,

"这把是不是也你的?",M问道,

"可能是我上次忘了吧,", Json答道,

"你们有没有流程记录送到这里的枪械,然后会全部取回么?",M问道,

"没有,忙起来就忘了"。

image.png

利用gdb的数据断点,可以捕获向script_list添加script的调用栈。

这个捕获的调用栈显示了在处理JSON异常时,会向v8::script_list增加script,并且这个script含有特征字符串"[object Object]"。

JS的代码呢?你没看错,就是片头的范例。

image.png

M拨通了数班长的电话,"我找到'影子'了"。

几个月后,

node基地从v12.18.2开始,对script_list的入库,都采用了新账本来管理这些入库的武器, 那些freed的格子都被填满了武器。

终——

这是一个复合型的内存泄漏案例。

v8::script_list的实现是在WeakArrayList的末尾添加新的script,并在执行完成之后由GC回收缩短队列,

JSON.parse()在遇到异常时,会有少量的内存泄漏并可能遗留script的对象在script_list中,

泄漏的script对象造成了v8::script_list出现空洞而无法回缩,从而放大了对内存的消耗。

  • node-v12.18.2以前所有的v12版本都受这个问题影响。
  • 但v10不受这个问题影响。

最后:如你的应用已经遇到类似的内存泄漏问题,请尽快升级到最新的nodejs或alinode。

关注「淘系技术」微信公众号,一个有温度有内容的技术社区~

image.png

相关实践学习
阿里云图数据库GDB入门与应用
图数据库(Graph Database,简称GDB)是一种支持Property Graph图模型、用于处理高度连接数据查询与存储的实时、可靠的在线数据库服务。它支持Apache TinkerPop Gremlin查询语言,可以帮您快速构建基于高度连接的数据集的应用程序。GDB非常适合社交网络、欺诈检测、推荐引擎、实时图谱、网络/IT运营这类高度互连数据集的场景。 GDB由阿里云自主研发,具备如下优势: 标准图查询语言:支持属性图,高度兼容Gremlin图查询语言。 高度优化的自研引擎:高度优化的自研图计算层和存储层,云盘多副本保障数据超高可靠,支持ACID事务。 服务高可用:支持高可用实例,节点故障迅速转移,保障业务连续性。 易运维:提供备份恢复、自动升级、监控告警、故障切换等丰富的运维功能,大幅降低运维成本。 产品主页:https://www.aliyun.com/product/gdb
相关文章
|
18天前
|
程序员 开发者
分代回收和手动内存管理相比有何优势
分代回收和手动内存管理相比有何优势
|
1月前
|
算法 Java 程序员
内存回收
【10月更文挑战第9天】
44 5
|
1月前
|
Java 测试技术 Android开发
让星星⭐月亮告诉你,强软弱虚引用类型对象在内存足够和内存不足的情况下,面对System.gc()时,被回收情况如何?
本文介绍了Java中四种引用类型(强引用、软引用、弱引用、虚引用)的特点及行为,并通过示例代码展示了在内存充足和不足情况下这些引用类型的不同表现。文中提供了详细的测试方法和步骤,帮助理解不同引用类型在垃圾回收机制中的作用。测试环境为Eclipse + JDK1.8,需配置JVM运行参数以限制内存使用。
32 2
|
5月前
|
NoSQL Java Redis
Redis系列学习文章分享---第十八篇(Redis原理篇--网络模型,通讯协议,内存回收)
Redis系列学习文章分享---第十八篇(Redis原理篇--网络模型,通讯协议,内存回收)
84 0
|
1月前
|
算法 Java
JVM进阶调优系列(3)堆内存的对象什么时候被回收?
堆对象的生命周期是咋样的?什么时候被回收,回收前又如何流转?具体又是被如何回收?今天重点讲对象GC,看完这篇就全都明白了。
|
3月前
|
存储 NoSQL 算法
Redis内存回收
Redis 基于内存存储,性能卓越,但单节点内存不宜过大,以免影响持久化或主从同步。可通过配置 `maxmemory` 限制最大内存。内存达到上限时,Redis采用两种策略:内存过期策略和内存淘汰策略。过期策略包括惰性删除和周期删除,后者分为 SLOW 和 FAST 模式。内存淘汰策略有八种,如 LRU、LFU 和随机淘汰等,用于在内存不足时释放空间。官方推荐使用 LFU 算法。
Redis内存回收
|
3月前
|
JavaScript 前端开发 算法
js 内存回收机制
【8月更文挑战第23天】js 内存回收机制
39 3
|
2月前
|
数据安全/隐私保护 虚拟化
基于DAMON的内存能回收 【ChatGPT】
基于DAMON的内存能回收 【ChatGPT】
|
4月前
|
Web App开发 前端开发 JavaScript
React的内存回收方式有哪些
【7月更文挑战第15天】 React内存管理依赖JS的垃圾回收,利用标记-清除算法释放无用对象。组件卸载时,通过`componentWillUnmount`(类组件)或`useEffect`(函数组件)执行清理。Hooks如`useMemo`和`useCallback`减少不必要的内存分配。避免内存泄漏的关键是及时清理副作用和资源。使用Chrome DevTools进行内存分析可提升性能和应用稳定性。
87 7
|
4月前
|
NoSQL 算法 Linux
【内附完整redis配置文件】linux服务器命令设置redis最大限制内存大小,设置redis内存回收机制,redis有哪些回收机制
【内附完整redis配置文件】linux服务器命令设置redis最大限制内存大小,设置redis内存回收机制,redis有哪些回收机制
120 0