JavaScript内存泄露排查、垃圾回收理解、性能优化调试

一、内存泄露

1、定义

不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)

2、常见的内存泄露场景
2.1 隐式全局变量
function foo(arg) {
    bar = "暴露到全局的变量";
}
function foo() {
    this.variable = "默认指向全局";
}
foo();

问题:定义全局变量,或者指向全局变量,且没有做删除该全局变量处理,不会被垃圾回收
解决:

  1. 不要定义全局变量
  2. 'use strict' 严格模式,调用全局this的变量,能将错误报出。
2.2 忘记关闭定时器
var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

问题:node引用被定时器使用,node无法自动被垃圾回收。
解决:用完定时器后,关闭定时器

2.3 忘记关闭事件回调
   var element = document.getElementById('button');

    function onClick(event) {
        element.innerText = 'text';
    }

    element.addEventListener('click', onClick);

问题:element引用被事件回调函数使用,element无法自动被垃圾回收。
解决:移除节点之前应该先移除节点身上的事件监听器,因为IE6没处理DOM节点和JS之间的循环引用(因为BOM和DOM对象的GC策略都是引用计数),可能会出现内存泄漏,现代浏览器已经不需要这么做了,如果节点无法再被访问的话,监听器会被回收掉

2.4 忘记释放游离DOM的引用
var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image'),
    text: document.getElementById('text')
};

function doStuff() {
    image.src = 'http://some.url/image';
    button.click();
    console.log(text.innerHTML);
}

function removeButton() {
    // The button is a direct child of body.
    document.body.removeChild(document.getElementById('button'));

    // 即使我们移除了button,但因为没有释放他的引用,所以仍然可以使用elements.button来操作,并且不会被垃圾回收
}
var select = document.querySelector;
var treeRef = select("#tree");
var leafRef = select("#leaf");
var body = select("body");

body.removeChild(treeRef);
//#tree不会被垃圾回收

treeRef = null;
//即使treeRef已经删除,但因为leafRef仍然存在,所以#tree仍然不会被垃圾回收

leafRef = null;
//现在#tree 和 #leaf都以被垃圾回收

游离子树上任意一个节点引用没有释放的话,整棵子树都无法释放,因为通过一个节点就能找到(访问)其它所有节点,都给标记上活跃,不会被清除

我的理解

<button id="tree">
    <span id="leaf">leaf</span>
</button>

var treeRef = document.querySelector("#tree");
var leafRef = document.querySelector("#leaf");
var body = document.querySelector("body");
body.removeChild(treeRef);
treeRef = null;
// 即使#tree已经被移除且引用也置为空,但因为leaf的存在,内存中必然会保存整个tree的DOM树
console.log(leafRef.parentNode) 
// 正常显示#tree
  1. 如果没有treeRef,leafRef的引用,移除tree,leaf节点,就会直接移除,且不会保留在内存
  2. 如果有treeRef,leafRef的引用,把tree的引用和节点移除,但leafRef的引用仍然会导致通过他就能找到(访问)其它所有节点,都给标记上活跃,不会被清除
  3. 就是说,tree下,如果有任何leaf没有解除引用占用,则这棵tree的DOM节点对象无法被垃圾回收

解决方法:当决定要移除某个DOM时,他以及所有子节点的引用都要释放掉,比如:

body.removeChild(treeRef);
treeRef=null;
leafRef=null; 
2.5 闭包导致
var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing)
      console.log("hi");
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log(someMessage);
    }
  };
};
setInterval(replaceThing, 1000);

执行结果:内存以10M左右的速度一直增加,增加到400M之后,浏览器端会回到12M,再持续增加。

原因:originalThing 一直在被保存到内存,而不是覆盖存。unused形成的闭包会保存originalThing,也一直是复制保存,而不是覆盖存。

解释:因为闭包的典型实现方式是每个函数对象都有一个指向字典对象的关联,这个字典对象表示它的词法作用域。如果定义在replaceThing里的函数都实际使用了originalThing,那就有必要保证让它们都取到同样的对象,即使originalThing被一遍遍地重新赋值,所以这些(定义在replaceThing里的)函数都共享相同的词法环境。但V8已经聪明到把不会被任何闭包用到的变量从词法环境中去掉了,所以如果把unused删掉(或者把unused里的originalThing访问去掉),就能解决内存泄漏。只要变量被任何一个闭包使用了,就会被添到词法环境中,被该作用域下所有闭包共享。这是闭包引发内存泄漏的关键

解决:把unused闭包删除,内存占用从400M下降置200M,再把originalThing删除,内存从200M下降到正常。

二. 内存膨胀

内存膨胀是说占用内存太多了,chrome是400M就会处理一次

三、频繁GC

频繁GC很影响体验(页面暂停的感觉,因为Stop-The-World),可以通过Task Manager内存大小数值或者Performance趋势折线来看:

  • Task Manager中如果内存或JS使用的内存数值频繁上升下降,就表示频繁GC
  • 趋势折线中,如果JS堆大小或者节点数量频繁上升下降,表示存在频繁GC

解决方案:可以通过优化存储结构(避免造大量的细粒度小对象)、缓存复用(比如用享元工厂来实现复用)等方式来解决频繁GC问题

四、Chrome控制台中的一些术语概念

1. Mark-and-sweep

JS相关的GC算法主要是引用计数(IE的BOM、DOM对象)和标记清除(主流做法),各有优劣:

引用计数回收及时(引用数为0立即释放掉),但循环引用就永远无法释放

标记清除不存在循环引用的问题(不可访问就回收掉),但回收不及时需要Stop-The-World

标记清除算法步骤如下:

  1. GC维护一个root列表,root通常是代码中持有引用的全局变量。JS中,window对象就是一例作为root的全局变量。window对象一直存在,所以GC认为它及其所有孩子一直存在(非垃圾)

  2. 所有root都会被检查并标记为活跃(非垃圾),其所有孩子也被递归检查。能通过root访问到的所有东西都不会被当做垃圾

  3. 所有没被标记为活跃的内存块都被当做垃圾,GC可以把它们释放掉归还给操作系统

现代GC技术对这个算法做了各种改进,但本质都一样:可访问的内存块被这样标记出来后,剩下的就是垃圾

2. Shallow Size

对象自身占用内存的大小,比如字符串和数组

3. Retained Size

对象自身及依赖它的对象(从GC root无法再访问到的对象)被删掉后释放的内存大小

五、Chrome控制台性能优化调试
  1. Performance 性能
    主要看内存的变化情况,若一直上升,则有内存泄露的问题
    5.png
  1. Memory 内存
    主要是看内存中存储什么东西
    比如数组,对象,函数,闭包等,看哪个占用大,并且内容是什么


    2.png
  1. FPS 帧率
    主要测试动画页面刷新的频率,频率越高,动画越流畅
    在 more tools > rendering > FPS meter 打开


    3.png

五、排查步骤

1.确认问题,找出可疑操作

先确认是否真的存在内存泄漏:

切换到Performance面板,开始记录(有必要从头记的话)

开始记录 -> 操作 -> 停止记录 -> 分析 -> 重复确认

确认存在内存泄漏的话,缩小范围,确定是什么交互操作引起的

也可以进一步通过Memory面板的内存分配时间轴来确认问题,Performance面板的优势是能看到DOM节点数和事件监听器的变化趋势,甚至在没有确定是内存问题拉低性能时,还可以通过Performance面板看网络响应速度、CPU使用率等因素

2.分析堆快照,找出可疑对象

锁定可疑的交互操作后,通过内存快照进一步深入:

切换到Memory面板,截快照1

做一次可疑的交互操作,截快照2

对比快照2和1,看数量Delta是否正常

再做一次可疑的交互操作,截快照3

对比3和2,看数量Delta是否正常,猜测Delta异常的对象数量变化趋势

做10次可疑的交互操作,截快照4

对比4和3,验证猜测,确定什么东西没有被按预期回收

3.定位问题,找到原因

锁定可疑对象后,再进一步定位问题:

该类型对象的Distance是否正常,大多数实例都是3级4级,个别到10级以上算异常

看路径深度10级以上(或者明显比其它同类型实例深)的实例,什么东西引用着它

4.释放引用,修复验证

到这里基本找到问题源头了,接下来解决问题:

想办法断开这个引用

梳理逻辑流程,看其它地方是否存在不会再用的引用,都释放掉

修改验证,没解决的话重新定位

当然,梳理逻辑流程在一开始就可以做,边用工具分析,边确认逻辑流程漏洞,双管齐下,最后验证可以看Performance面板的趋势折线或者Memory面板的时间轴

六、记录一次性能调试过程

浏览器执行以下脚本

var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing)
      console.log("hi");
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log(someMessage);
    }
  };
};
setInterval(replaceThing, 1000);
  1. 初步看Memory内存面板,发现Select JavaScript VM Instance的数值一值在变化,变化的规律是,20M左右的递增,在100M到200M不定时的被垃圾回收,回到初始水平。最大可涨到400M。
  2. 接着看Performance性能面板,点击开始记录,发现记录10秒以上也不会停,此时手动停止。看到内存线性递增,没有下降的地方。点击JS Heap的高处,都是定位到Timer Fired,说明是定时器有问题,定时器下有个Function Call,说明是他调用的函数有问题。
  3. 接着我们又回到Memory面板,点击开始记录,发现记录会自动停止,生成报告。此时内存已经涨到700M!了,连续点击了几记开始记录,生成了多份报告,发现每份报告的大小在500M左右。然后看Shadow Size 和 Retained Size 的数值,发现 Shadow Size中 (string)的值占比98%,Retained Size的前5个占比98%。然后我展开(string),发现大量1M左右的字符串,看来这个就是originalThing保存在内存中,而且保存了非常多份。
  4. 最后,我们把unused闭包删除,发现内存占用大概下降了一半,再把originalThing删除,就正常了。

参考原文链接:http://www.ayqy.net/blog/js%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E6%8E%92%E6%9F%A5%E6%96%B9%E6%B3%95/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,366评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,521评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,689评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,925评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,942评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,727评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,447评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,349评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,820评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,990评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,127评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,812评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,471评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,017评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,142评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,388评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,066评论 2 355

推荐阅读更多精彩内容