背景 (书接上回)
开始学习Chrome devTools查看内存情况
打开Chrome的无痕模式, 这个操作是为了屏蔽Chrome插件对之后的测试内存占用情况产生的影响, 从而避免产生干扰, 接下来要去打开开发者工具, 找到Performance这一栏, 在这其中可以发现带有功能按钮, 这些按钮包括开始录制按钮, 刷新页面按钮, 清空记录按钮, 还有那个用于记录并可视化js内存以及节点和事件监听器的相关按钮, 另外还需要注意有个能够触发垃圾回收机制的按钮。
简单地录个百度那个页面的屏, 瞅一瞅咱们到底能搞出来点啥东西来嘛, 就跟底下那张动图上面画的一模一样:
从上图里, 我们能够看到那些内容。在页面从零变成加载完成的那个期间。JS Heap、documents、Nodes、Listeners、GPU memory这几个指标。它们的最低值是多少。最高值是多少。随着时间变化的走势曲线是什么样的。这些点也是我们主要的关注对象。
大家不妨查看一下开发者工具里的那个名叫Memory的栏目, 它主要是用来记录页面堆内存的具体情况,同时也是用来展示js的堆内存是如何随着加载时间线的推移而进行动态分配的。
堆快照这种操作, 其作用大致等同于照相机拍照的功能, 它有能力记录与你当前页面有关的堆内存实际状况, 而且只要执行一次快照生成动作, 那么结果上就会导致出现一条属于该次操作的快照记录。
如图所示, 我们首次执行了快照操作, 记录下的当时堆内存空间的占用情况为33.7MB。随后, 我们在页面中点击了一些按钮, 接着再次执行了一次快照, 此时记录下来的堆内存空间占用则为32.5MB。
此外, 我们可以点击对应的这些快照记录, 以此查看在时间点所有存在于内存中的变量的具体情况, 例如它们的具体结构, 以及它们占用的总内存比例即百分比等数据。
在我们开始记录之后, 我们可以看到图的右上角存在着起伏的蓝色与灰色的柱形图, 其中蓝色的部分表示当前时间线下占用着的内存空间;而灰色的部分则表示之前占用的内存空间已经被清除释放了。
当获悉内存泄漏现象存在之际, 人们可以转而运用memory这一手段, 以便对问题进行更加确定的确认操作以及问题定位工作。
第一步可以使用基于时间线的分配检测工具来核实问题所在, 具体的展现形式可以参考下面所展示的图片内容。
内存泄漏的场景
1. 闭包使用不当引起内存泄漏
可以使用Performance这个工具还有Memory这个工具, 这两样东西一起来查看一下那些因为闭包的使用而引发的内存方面的问题, 也就是内存泄漏的问题。
function fn1 () {
let a = new Array(10000) // 这里设置了一个很大的数组对象
let b = 3
function fn2() {
let c = [1, 2, 3]
}
fn2()
return a
}
let res = []
function myClick() {
res.push(fn1())
}
当 fn1 函数执行退出后, 里面的变量 a 本来应该被认为没有用, 接着被回收系统当成垃圾清掉。但是因为 fn1 函数最终把变量 a 返回去了, 并且把它赋值给了全局变量 res, 这样就让那个返回值对原来的变量 a 保持了引用关系。
结果就是变量 a 被标记成了还在活动中的一种状态, 所以它会一直占着它原来那部分内存空间不放。要是假设一下, 后来的代码里根本就用不到这个 res 变量了, 那么这种让内存长期占用的情况, 就可以算作是一个闭包功能被不小心用坏了的典型案例。
我们已经设置了一个按钮, 每次点击这个按钮执行操作的时候, 就会把函数fn1返回的结果加入到全局数组变量res里面去, 这样的目的就是为了可以在performacne的性能曲线图上看出实际的效果变化, 具体的情况如图所示。
以上就是一个判断闭包带来内存泄漏问题并简单定位的方法了
2. 全局变量
全局的变量通常是不会被垃圾回收器回收掉的, 但是这并不代表所有的变量都不能放在全局作用域里面去, 只是说有时候会因为人们的疏忽大意, 从而导致一些不该在全局的变量流失到了全局环境当中, 比如没有先声明某个变量, 就急着去直接对这个变量进行赋值操作, 这样的行为就会导致这个变量在全局的作用域下被创建出来, 下面的代码就是这种情况的具体表现。
function fn1() {
// 此处变量name未被声明
name = new Array(99999999)
}
fn1()
function fn1() {
'use strict';
name = new Array(99999999)
}
fn1()
3. 分离的DOM节点
假设你手动移除了某个dom节点, 本以为这样就能释放掉该dom节点所占用的内存, 但是因为你的疏忽, 导致在代码的某处地方, 仍然保存着对这个已经被移除下来的节点的引用,所以最终就会出现这个节点所占据的内存没有办法被释放出来的这种情况。
我是子元素
let btn = document.querySelector('button')
let child = document.querySelector('.child')
let root = document.querySelector('#root')
btn.addEventListener('click', function() {
root.removeChild(child)
})
这个代码执行的操作, 就是在点击按钮之后, 把那个.child的子节点给删掉。
尽管在点击操作发生后, 这个子节点已经从DOM树里被移除掉了, 但是因为全局的变量child还保留着对这个节点的引用, 所以这就导致了该节点占用的内存一直都不能得到释放。大家可以用Memory的快照功能去做检测, 具体的样子就像图片里展示的那样。
我们需要先记录下初始状态下的那个快照, 接着去点击移除按钮, 然后还要再点击一次快照。在这个时候, 从内存的大小方面来看, 我们是看不出有什么变化的, 这是因为被移除的那个节点所占用到的内存实在是太小了, 小到可以不予考虑。
不过, 我们是可以去点击第二条快照记录的, 同时要在筛选框里面输入detached这个字符串, 这样的话, 就会呈现出所有那些已经脱离了状态但却还没有被清除掉的节点对象。
你可以参照下面的图片所示, 来获取相应的解决办法。
我是子元素
let btn = document.querySelector('button')
btn.addEventListener('click', function() {
let child = document.querySelector('.child')
let root = document.querySelector('#root')
root.removeChild(child)
})
改动其实非常简单, 就是把对那些名为 child 的节点的引用, 转移到了 click 事件的回调函数里面去。这样做的结果是, 当节点被移除了, 并且也退出了该回调函数的执行上下文之后, 系统就会自动清除对那个节点的引用。
既然引用已经不存在了, 那么自然也就不可能存在内存泄漏这个问题了。接着我们来做一个验证, 大家看看下面这幅图的样子。
结果很明显,这样处理过后就不存在内存泄漏的情况了
4. 控制台的打印
document.querySelector('button').addEventListener('click', function() {
let obj = new Array(1000000)
console.log(obj);
})
我们在那个按钮的点击时候所发生的回调动作里面, 新建一个体量非常大的数组类型对象, 并且把这个对象给打印出来, 与此同时, 使用performance这个工具来对其进行验证工作。
开始进行录制操作, 首先触发一次垃圾回收以清除初始状态下的内存数据, 接着连续点击按钮三次, 意味着成功执行了两次以上的点击事件行为, 随后再次触发垃圾回收处理机制, 在查看最终的录制结果时发现JS堆内存曲线呈现阶梯式上升的形态, 并且最终维持的高度远远超出了初始设定的基准线水平, 这一现象充分说明每次执行点击动作时所创建的大型数组对象obj均因为控制台输出功能而被浏览器强制保存下来, 导致其处于无法被自动回收的状态。
接下来, 将console.log这一行给注释掉, 之后再瞧一眼所得到的结果如何。
document.querySelector('button').addEventListener('click', function() {
let obj = new Array(1000000)
// console.log(obj);
})
我们可以观察到, 在打印操作完成之后, 每一次被创建出来的
事实上, 大家其实也同样可以使用memory来进一步进行验证, 同时console.log这种方式也是可以存在的。
未注释 console.log
注释掉了console.log
最后来做一个简要的总结: 在开发环境这个场景下, 开发人员可以利用控制台打印信息的方式来方便地进行代码调试;然而, 一旦进入生产环境这个至关重要的阶段, 就应该尽可能避免在控制台中进行数据打印的操作。正因为如此, 我们会经常在实际的代码库中看到类似下面这种处理方式的代码片段。
// 如果在开发环境下,打印变量obj
if(isDev) {
console.log(obj)
}
通过这样做, 就可以防止在生产环境当中无用的变量被打印出来而占用内存空间。还有另外一点就是, 除了使用 console.log 这个方法之外, 还要做到在 production 环境下不要使用 console.error、console.info、console.dir 这些方法。
5. 遗忘的定时器
定时器这个问题也是平时许多人常常会忽略掉的一个点, 打个比方来说, 有些人一旦定义好了定时器之后就不再会去思考清除定时器这一件事情了, 这种做法其实实际上也会在一定程度之上引发某些内存泄漏问题的出现, 接下来就让我们一起来看看下面这一些代码方面的示例内容。
function fn1() {
let largeObj = new Array(100000)
setInterval(() => {
let myObj = largeObj
}, 1000)
}
document.querySelector('button').addEventListener('click', function() {
fn1()
})
当用户点击了这个按钮以后, 程序就会去运行fn1这个函数, 在fn1函数的内部, 会创建一个非常大的数组对象, 这个对象的名称叫做largeObj, 同时, 这里还会创建一个定时器, 这个定时器是由setInterval来创建的, 那个定时器的回调函数里面, 只是非常简单地引用了一下largeObj这个变量, 接下来, 我们一块儿去看看整个过程中内存的分配情况到底是什么样子的吧。
按道理来说, 点击按钮执行fn1函数后, 应该会退出该函数的执行上下文, 紧接着函数体内的局部变量应该被清除, 但是图中performance的录制结果显示似乎是存在内存泄漏问题的, 也就是最终曲线高度比基准线高度要高, 那么接下来再用Memory来确认一次:
function fn1() {
let largeObj = new Array(100000)
let index = 0
let timer = setInterval(() => {
if(index === 3) clearInterval(timer);
let myObj = largeObj
index ++
}, 1000)
}
document.querySelector('button').addEventListener('click', function() {
fn1()
})
现在我们再一次通过对performance以及memory这两个方面来进行观察, 从而去审视是否存在内存泄漏这一种状况, 看看究竟会不会还存在着这样的一种问题。
从这次录制的结果来看, 最终的曲线高度与初始基准线的高度保持一致, 这就表明了不存在内存泄漏这种情况。
这里做一个解释, 图中刚开始出现的蓝色柱形是因为我在录制后刷新了页面, 可以忽略;然后我们点击了按钮, 看到又出现了一个蓝色柱形, 此时就是为fn1函数中的变量largeObj分配了内存, 3s后该内存又被释放了, 即变成了灰色柱形。所以我们可以得出结论, 这段代码不存在内存泄漏的问题。
咱们来做个简短的总结, 平时在使用定时器的时候, 如果不再需要那个定时器了, 那么就一定要把它给清除掉。不这样的话, 就会出现像这个例子里面所描述的这种状况。
不光只是 setTimeout 和 setInterval 这两个可能会出问题, 浏览器同时还提供了一个 API 也存在有可能存在同样的问题情况, 这个东西的名称叫做 requestAnimationFrame 。
好了, 各位兄弟, 内存泄漏这个知识点, 你们现在已经学会了吗。