一、内存泄露
1、定义
不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)
2、常见的内存泄露场景
2.1 隐式全局变量
function foo(arg) {
bar = "暴露到全局的变量";
}
function foo() {
this.variable = "默认指向全局";
}
foo();
问题:定义全局变量,或者指向全局变量,且没有做删除该全局变量处理,不会被垃圾回收
解决:
- 不要定义全局变量
- '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
- 如果没有treeRef,leafRef的引用,移除tree,leaf节点,就会直接移除,且不会保留在内存
- 如果有treeRef,leafRef的引用,把tree的引用和节点移除,但leafRef的引用仍然会导致通过他就能找到(访问)其它所有节点,都给标记上活跃,不会被清除
- 就是说,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
标记清除算法步骤如下:
-
GC维护一个root列表,root通常是代码中持有引用的全局变量。JS中,window对象就是一例作为root的全局变量。window对象一直存在,所以GC认为它及其所有孩子一直存在(非垃圾)
-
所有root都会被检查并标记为活跃(非垃圾),其所有孩子也被递归检查。能通过root访问到的所有东西都不会被当做垃圾
-
所有没被标记为活跃的内存块都被当做垃圾,GC可以把它们释放掉归还给操作系统
现代GC技术对这个算法做了各种改进,但本质都一样:可访问的内存块被这样标记出来后,剩下的就是垃圾
2. Shallow Size
对象自身占用内存的大小,比如字符串和数组
3. Retained Size
对象自身及依赖它的对象(从GC root无法再访问到的对象)被删掉后释放的内存大小
五、Chrome控制台性能优化调试
- Performance 性能
主要看内存的变化情况,若一直上升,则有内存泄露的问题
5.png
-
Memory 内存
主要是看内存中存储什么东西
比如数组,对象,函数,闭包等,看哪个占用大,并且内容是什么
2.png
-
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);
- 初步看Memory内存面板,发现Select JavaScript VM Instance的数值一值在变化,变化的规律是,20M左右的递增,在100M到200M不定时的被垃圾回收,回到初始水平。最大可涨到400M。
- 接着看Performance性能面板,点击开始记录,发现记录10秒以上也不会停,此时手动停止。看到内存线性递增,没有下降的地方。点击JS Heap的高处,都是定位到Timer Fired,说明是定时器有问题,定时器下有个Function Call,说明是他调用的函数有问题。
- 接着我们又回到Memory面板,点击开始记录,发现记录会自动停止,生成报告。此时内存已经涨到700M!了,连续点击了几记开始记录,生成了多份报告,发现每份报告的大小在500M左右。然后看Shadow Size 和 Retained Size 的数值,发现 Shadow Size中 (string)的值占比98%,Retained Size的前5个占比98%。然后我展开(string),发现大量1M左右的字符串,看来这个就是originalThing保存在内存中,而且保存了非常多份。
- 最后,我们把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/
网友评论