【前端知识相关分享】内存泄漏与垃圾回收机制 (上)
- 1. 内存的生命周期
- 1.1 内存生命周期的一般流程
- 1.2 C++,JS和python内存分配和释放的区别
- 2. JS中的内存管理
- 2.1 两种数据类型与两种内存
- 一个思考
- 2.2 两种内存空间的区别对比
- 3. 内存泄漏的定义、原因及危害
- 3.1 定义
- 3.2 危害
- 3.3 可能的原因
- 3.3.1 滥用全局变量,隐式的全局变量
- 3.3.2 没有正确的销毁定时器,闭包,回调函数等
- 3.3.3 没有正确的监听和销毁事件
- 3.3.4 DOM清空时,还存在引用(存在没有清理的DOM引用)
- 3.3.5 被遗漏的Map对象和Set对象
- 3.3.6 未被清理的Console输出
- 4. 内存怎么分析and内存泄漏如何定位
- 4.1 内存分析的方法
- Chrome Devtools中Memory工具的简单介绍
- 4.2 内存泄漏的识别
- 4.2.1 浏览器方法
- 4.2.2 命令行方法
- 5. 笔记篇章下
五一假期第一天,随机分享自己关于前端得一份笔记(上篇)吧。
本文为原创,未经同意请勿转载
由于篇幅有点长,所以笔者将我关于这部分的笔记分为上下两个篇章(文章后面附录下篇链接),避免读者的阅读疲倦感😵,同时也方便大家的阅读啦🤗。下面的笔记笔者也结合了自己所学的东西和平常使用的情况进行了相关的拓展。如果下面笔记中存在错误,欢迎大家及时指正,学习与讨论。
- 上章:介绍内存的生命周期,管理及内存泄漏的相关概念,分析内存泄漏可能的原因。
- 下章:内存泄漏的解决方法,垃圾回收机制的相关知识,内存管理的优化等。
1. 内存的生命周期
1.1 内存生命周期的一般流程
无论什么程序语言,内存的生命周期都是一致的:内存分配→内存使用(读/写)→内存释放。其中,第二部分一般来说都是我们平常都有在显式使用的,也是明确的,但是第一和第三部分在底层语言(比如C++,C语言)等是明确的,而向JS,python这种高级语言中,大部分是隐含的。
这里笔者我主要讲一下C++,JS和python中内存分配和释放之间的区别。
1.2 C++,JS和python内存分配和释放的区别
-
C++
对于C++,C语言等底层语言,它们一般都包含有底层的内存管理接口,可以进行显式地请求内存分配和内存释放,以此避免内存泄漏。比如:
(1)对于内存分配,C++提供了new操作符和malloc()函数
(2)对于内存释放,C++有delete操作符和free()函数来释放已分配的内存
因此,一旦动态内存分配被申请且不使用时,C++并没有垃圾回收机制负责自动释放内存,而是由程序员负责手动进行内存管理。 -
JS
在Javascript中,创建变量是会自动分配内存,并在不再使用时自动释放内存。也就是说JS的内存分配和内存释放是通过JS引起进行自动垃圾回收的。因此,JS中没有手动释放内存的概念,因此无法显式的进行内存释放的操作,javascript没有手动释放内存的概念,因此无法进行显式的内存释放。因此,在写代码时,我们需要格外注意是否可能导致内存泄漏的问题。 -
python
python在内存分配方面具有类似于JS的动态类型语言的特点。当创建一个新对象时,python会从堆栈中分配内存,而且这个内存分配是由python解释器自动执行的。同时python跟JS类似,也有自动垃圾回收机制。它采用的是引用计数的垃圾回收算法来实现内存的自动释放。但引用计数会存在无法处理循环引用的垃圾回收问题,因此,python中还使用了一种循环收集的算法来处理循环引用的情况。通过上述两种算法,python实现了自动的垃圾回收,以确保内存不会被泄漏。
💡 一些小小的总结:
虽然像JS,python这些高级编程语言存在自动的垃圾回收GC机制,但是由于程序本身的逻辑问题,外部的因素,或者在处理大型数据集和复杂的应用程序时,内存泄漏是难以避免的。在这种情况下,我们需要进行手动的进行内存管理,以此避免内存占用过多导致程序崩溃。比如在深度学习中,我们训练模型时,经常有时候就会遇到out of memory的问题,在受限于显卡大小的情况下,我们其实可以通过内存及时释放来做优化。比如我们在test和val时,就可以利用with torch.no_grad() 来限制自动求导的占用内存,使用.detach() 让张量与计算图分离,及时释放不需要的资源。对于一些比较大的中间变量在不需要使用的时候还可以手动del删除。这样子就可以减少内存的占用。
2. JS中的内存管理
根据前面的介绍,我们知道JS会自动进行内存的分配与释放,那JS中对变量的内存分配是怎么样的?
2.1 两种数据类型与两种内存
我们也都知道在JS中数据类型主要分为两种:
- 基本数据类型:String、Number、Boolean、Undefined、Null和Symbol
- 引用数据类型:Object、Function、Array
那为什么我们要这么区别呢?其实这跟JS对数据的内存分配有关。在JS中,它的内存空间也分为两种,分别为栈内存和堆内存。栈空间具有运行效率高的优势,而堆空间具有空间大的优势。因此,一般将简单的数据类型放在栈内存中,而复杂的数据类型值放在堆空间。具体地:
- 对于基本数据类型的数据:其地址和具体内容都会在栈空间进行内存分配,因此,栈内存是按值引用,所以复制的方式为值传递,即复制的时一个新值,时将存储在栈上的变量值直接复制到新的内存位置上。
- 对于引用数据类型的数据:其地址存储在栈空间而具体内容存储在堆空间,因此查找引用类型值得时候先去栈查找再去堆查找,堆内存是按指针引用,所以复制的方式是指针传递,只是复制指向堆空间对象的指针,并不会复制对象本身,相当于一个副本。
注:全局变量以及被闭包引用的变量(即使是原始类型)均储存在堆内存中。
在JS中,可以通过逃逸分析辨别出变量需要存储的空间是什么。逃逸分析是在一种是JS引擎中自动执行的优化技术,用于确定函数内的变量是否会在函数调用结束后继续被使用。简单来说,逃逸分析就是用来分析变量作用域的机制。一个变量逃逸,也就是指它不能被局部优化掉并且需要在函数作用域外访问。在这里也需要注意一个点,因为JS是解释性语言,所以它其实跟Java的逃逸分析不一样,它不是编译时逃逸分析。
其实,在一些文章中,JS空间也还包含池得概念,但一般池会归类为栈,所以上面我就没有介绍了。总的来说就是:栈存放变量,堆存放复杂对象,池存放常量。
一个思考
🤔为什么全局变量和闭包中的数据需要存储在堆空间中,这跟变量本身的特征与堆存储的适配性有关:
-
生命周期长:全局变量和闭包中的变量具有较长的生命周期,它们的值需要在内存中一直存在,直到程序结束才会被释放。如果存储在栈空间上,当函数执行完毕时变量所占用的内存会被立即释放,导致后续访问该变量时出现错误。
-
动态分配内存:在闭包中,变量通常通过动态分配内存来存储,并且无法确定内存的大小。此外,在 JS中,对象和数组也是动态分配内存的,它们的大小也无法预先确定。因此,将这些变量存储在堆空间上可以更好地管理和利用内存资源。
-
多个作用域访问:与栈空间不同,堆空间的内存不受作用域的限制。因此,全局变量和闭包中的变量可以在多个作用域中进行访问,而不会出现作用域链问题。
总之,全局变量和闭包中的变量需要存储在堆空间上,主要是由于它们的生命周期通常较长,需要动态分配内存,并且在多个作用域中进行访问。
2.2 两种内存空间的区别对比
栈内存是由操作系统直接管理(分配和释放)的一段连续的内存空间,采用栈的结构进行数据的存储,结构简单直接,访问和操作的速度快,但内存容量小,主要用于存放生命周期短,占用空间小且固定的数据。其中,栈内存中存储的变量一般都是临时性的,也就是说在该变量的当前执行环境结束后就会被销毁,被垃圾回收机制回收。
堆内存是动态且不连续的内存空间,访问速度较慢,但内存容量大,主要用于存放生命周期长,占用空间大或者占用空间不固定的数据。堆内存空间的分配和释放通常由程序员手动控制。在堆空间中的变量一般不会在其当前执行环境结束后就被销毁,因为其通常存储的是引用类型,不确定其他地方是否对它还有引用。因此,堆内存中的变量只有在其所有引用结束后才会被回收。
3. 内存泄漏的定义、原因及危害
3.1 定义
指不再使用的内存,没有被释放,也就是没有被垃圾回收机制回收。动态开辟的内存空间,在使用结束后未释放,结果导致一致占据该内存空间,直到程序结束。
3.2 危害
程序内存空间占用大,可能导致性能变差,程序崩溃卡死,或者程序莫名挂掉的情况,比如Out of MemoryError
常见的一些表现:CPU资源耗尽、进程耗尽、硬盘耗尽、内存耗尽。
3.3 可能的原因
3.3.1 滥用全局变量,隐式的全局变量
对于全局变量,很难判断什么时候不再被使用,因此什么时候自动释放空间也很难判断,所以通常全局变量不会被回收,我们可以使用全局变量,但应该避免一些额外的全局变量和一些不太被注意到的隐式全局变量,比如下面其实就采用了隐式的全局变量:
function fn(){
// 没有声明从而制造了隐式全局变量test1
test1 = '字符串'
// 函数内部this指向window,制造了隐式全局变量test2
this.test2 = '字符串'
}
fn()
解决方法:及时置空null。比如在上面fn()之后,将test1和test2置空。
3.3.2 没有正确的销毁定时器,闭包,回调函数等
(1)遗漏的定时器
在像节流,防抖,轮询和其他常见的场景中,我们经常需要用到定时器,那JS中常见的定时器有哪些?——setTimeout,setInterval,requestAnimationFrame。这三者都是JS中进行时间相关的操作函数,其中:
- setTimeout:倒计时定时器,在指定的间隔时间之后运行指定的代码或者函数,ES6推荐使用箭头函数来代替SetTimeout的传统回调。
- setInterval:定时周期执行器,用于周期性地执行指定的代码或者函数,每隔一段时间就会调用一次执行函数(将其放入执行队列中,并没有指定什么时候应该执行),直到被取消。
- requestAnimationFrame:是一个浏览器提供的API, 它可以用于执行动画(2Dcanvas动画,webGL动画等)及其他高性能应用。其要求在浏览器在下次重绘之前调用指定的回调函数来更新动画。
requestAnimationFrame(callback)
接受一个函数作为参数,对该函数在下次重绘之前进行回调操作。其中回调函数执行次数与浏览器的刷新频率有关,因此其与系统保持同步,一般不会出现丢帧、卡顿的情况。与setTimeout和setInterval相比,requestAnimationFrame最主要的优势在于它的CPU节能性能。因为当页面隐藏或最小化的时候,setTimeout还是会在后台运行,此时刷新动画没用,但是requestAnimationFrame和系统保持同步,页面未激活的状态下,requestAnimationFrame会停止执行;页面激活,会从上次停止的位置开始执行,这样子就可以达到节能的效果。
🤔那三者有什么区别?常用的场景?
- 回调函数执行时机不同:从上面的描述,我们也可以看出,
setTimeout
和setInterval
是在指定的延迟时间之后执行回调函数,而且延迟时间会受到浏览器其他操作的影响而可能出现丢帧现象。而requestanimationframe
是在浏览器下一次重绘之前调用的回调函数,可以保证不会出现丢帧的情况。 - 时间精度不同:
requestanimationframe
的精度更高,可达到每秒60帧的刷新率(这里是跟电脑的刷新频率有关,因为电脑一般刷新频率为60Hz),而setTimeout
和setInterval
则为 4ms 左右。 - 调用次数不同:
requestanimationframe
需要主动调用才会执行回调函数,而setTimeout
和setInterval
则可以自动多次调用同一个回调函数。 - 主要应用场景不同:根据上述的区别,这三者的主要应用场景也有所不同。
requestanimationframe
主要用于动画和其他需要高帧率更新 ui 等频繁重绘任务,可以保证动画的流畅度和性能,同时如果处于后台运行状态还可以节省CPU开销。setTimeout
的用法常常是实现一些特殊效果或虽然是异步但并无需立即响应的任务,比如模拟延迟加载,执行一次性定时任务等。setInterval
最常用于轮询应用程序和数据源,例如即时通讯、在线游戏等。
😂三者怎么不遗漏而清除?
在不需要定时器的时候应该及时的结束定时器的调用,清除定时器避免内存泄漏:
- setTimeout:clearTimeout()
- setInterval:clearInterval()
- requestAnimationFrame:cancelAnimationFrame()
(2)不正当的闭包
🤔 闭包为什么会导致内存泄漏?什么样子的闭包使用会导致内存泄漏?
这里笔者我先介绍一下闭包的概念。闭包是指有权访问另一个函数作用域中的变量的函数。也就是闭包会将函数内部的局部变量带到函数外部,而且前面我们也提到了闭包中的变量是存储在堆内存中的。那堆内存就是当你对该变量的引用全部结束之后才会释放,这样子就会导致垃圾收集器不能及时地判断这些变量释放可以被收集处理了,所以闭包过多会导致内存占用而久久不能释放,从而导致内存泄漏。
当然并不是所有的闭包函数都会导致内存的泄漏。闭包导致内存泄漏的核心在于匿名函数可以访问父级作用域的变量,也就是将闭包内的局部变量带到函数外部。怎么理解,请看下面的例子:
// 例子1:内部的变量没有到外部去,JS可以自动回收该变量,不会导致内存泄漏。
function fn1(){
let test = '字符串'
return function(){
console.log('不会内存泄漏')
}
}
let fn1Child = fn1()
fn1Child()
// 例子2:内部的变量到外部去了,JS不可以自动回收该变量,可能导致内存泄漏
function fn2(){
let test = '字符串'
return function(){
console.log(test) // 这里引用了,内部变量会到外部
return test
}
}
let fn2Child = fn2
fn2Child()
🤔 怎么解决闭包内存泄漏问题?
根据前面的讲述,其实我们也知道关键在于当内部变量使用结束后,怎么及时解除内部变量在外部的引用。其实最简单的就是null
,也就是使用结束后,把外部的引用关系置空就行,比如在上面的代码fn2Child()
后面加一行fn2Child = null
就可以了。
3.3.3 没有正确的监听和销毁事件
(1)遗忘的事件监听器
- 原因:当事件监听器
addEventListener
在组件内挂载相关的事件处理函数,而在组件销毁时不主动将其清除时,那么其中引用的变量或者函数都被认为是需要的而不会进行垃圾回收,当内存变量占用内存过多时就可能导致内存泄漏。 - 解决方法:对Vue组件而言,只需要在
beforeDestroy
组件销毁生命周期里将对应的事件监听器清除即可,也就是removeEventListener
。
(2)遗忘的监听者模式
- 原因:以为Vue为例子,当我们使用监听者模式并在组件内挂载相关的事件处理函数,比如
eventBus.on
,而在组件销毁时不主动将其清除时,那么其中引用的变量或者函数都被认为是需要的而不会进行垃圾回收,跟前面的事件监听器一样也可能导致内存泄漏。 - 解决方法:跟前面事件监听器一样,只需要在
beforeDestroy
组件销毁生命周期里将对应的事件处理销毁便可,也就是eventBus.off
。
3.3.4 DOM清空时,还存在引用(存在没有清理的DOM引用)
- 原因:在JS中,我们创建一个DOM元素时,会使用一个变量来缓存对该DOM节点元素的引用,它会一直保存在内存中,直到该引用被清理。如果我们在移除DOM节点的时候,没有同步释放缓存引用,也就是说没有完全删除相应的元素,这个变量没有被及时的销毁或者赋值为空,那么就会产生游离DOM引用,其对应的子树也不会被释放,那么浏览器无法释放这些元素占用的内存空间,这些元素也就不会被垃圾回收,因此就可能导致内存泄漏。跟前面一样解决方法就是置空所有对应的元素。
- 例子加深理解:
<div id="root"> <ul id="ul"> <li></li> <li id="liry"></li> <li ></li> </ul> </div> <script> let root = document.querySelector('#root') let ul = document.querySelector('#ul') let liry = document.querySelector('#liry') // 这里虽然在DOM树上使用 removechild 方法从根元素中删除了 ul 元素 // 但是由于ul变量存在,所以整个ul及其子元素都不能垃圾回收 root.removeChild(ul) // 虽然这里我们进一步置空了ul变量,但是因为liry变量仍然引用了ul的子节点 // 说明对应元素还没有完全置空,所以ul元素依旧不能被垃圾回收 ul = null // 到这里,所有对应的变量都置空,无引用 // 此时可以立即垃圾回收掉整个 ul 元素及其子元素。 liry = null </script>
3.3.5 被遗漏的Map对象和Set对象
前面我们讲到了引用类型,但其实在JS中数据也存在类似Java那样的弱引用和强引用的区分,只不过没有像Java那样子直接的概念罢了。那具体是什么呢?什么又是强引用,什么又是弱引用。
🤔 上面讲到的引用必须等到不存在才会垃圾回收是指什么引用呢?
在JS中,当使用Map或者Set存储对象时,跟Object一样都是强引用,如果主动清除引用,可能会造成内存泄漏的情况,也就是我们上面所讲到的引用类型数据。所以前面我们所的引用数据类型其实就是“强引用”。相对于Map或者Set的“强引用”数据结构,JS中弱引用可以使用WeakMap、WeakSet和WeakRef来实现。其中,Weak Set中的对象引用是弱引用。WeakMap中的键也是弱引用(值不是)。
🤔 那么强引用和弱引用的区别是什么?
这里其实可以借助Java中直接的概念来理解:在Java中:
- 强引用:指一个指针或者变量,它将对象所在的内存地址保存在自己的内存中。只要这个引用存在,对象就会一直存在内存中,无法被垃圾回收器回收。
- 弱引用:指一个指向对象的指针或变量,但它不会阻止垃圾回收器回收所引用的对象。在垃圾回收机制中通常会对不可达的数据进行回收。当一个对象只被弱引用对象引用时,则会被认为时不可达(弱访问/不可访问),因此可以被垃圾回收。
- 总结:因此,强引用和弱引用的主要区别在于其是否在使用过程中会被自动垃圾回收。
🤔 那弱引用有什么作用?
弱引用可以允许在不影响垃圾回收的情况下实现对对象集合的存储和管理,当仅身下一个对象的弱引用时,则这些对象会立即成为垃圾回收器的候选对象,并且在必要时自动回收。相比于强引用,弱引用可以避免一些内存泄漏的问题,而且可以实现缓存等功能,进行性能的优化。但需要注意的是,在JS中,当对象被回收后,其对应的弱引用并不为null,而是被置为null,因此需要在使用弱引用前判断它是否为null。比如:
let myObject = { name: "xiaobai_Ry" };
// 创建弱引用
let myWeakRef = new WeakRef(myObject);
//释放myObject的强引用
myObject = null;
//通过myWeakRef尝试访问myObject
console.log(myWeakRef.deref());
// 上面输出:{ name: 'xiaobai_Ry' },不为null
// weakRef并未变成null, 要使用前需要进行判断
if (myWeakRef.deref() === null) {
console.log('对象已被清除');
} else {
console.log('对象仍然存在');
}
🤔 WeakMap、WeakSet和Set,Map区别
Set、WeakSet都是元素的集合,而Map和WeakMap是键值对的集合。这几种都是ES6中引入的数据类型。下面分别说一下它们之间的区别:
-
Set和WeakSet的区别
相同点:都是元素集合,里面元素唯一。
区别:
(1)垃圾回收机制不同:Set是强引用,WeakSet是弱引用。因此,Set中存储的对象在内存中被引用时不能被回收器回收;而weakset是弱引用,可以被回收器回收。
(2)存储数据的类型不同:Set中存储的可以是任意类型的数据,包括基本类型和对象类型等;而WeakSet中的元素只能是对象。
(3)可迭代性不同:Set是可迭代的,可以使用for…of、foreach等方法来遍历;而WeakSet是不可迭代的,不能循环,因为其成员随时可能随时被垃圾回收。
(4)使用方法的不同:WeakSet支持的方法比较少。WeakSet不支持 size 属性, 不支持 entries(), keys() 和 values()方法,也不支持 foreach。WeakSet仅支持添加新对象到集合中并判断某个对象是否在该集合中。它没有 clear(), delete()等用于删除读取某个元素的方法。 -
Map和WeakMap的区别
相同点:都是键值对的集合,Map和WeakMap的键必须唯一。
区别:
(1)垃圾回收机制不同:Map使用强引用来保存添加进去的键和值,而WeakMap使用弱引用来保存键。因此,只要Map存在,其键和值就都不会被垃圾回收,而对于WeakMap,当某个键值没有强引用指向它时,该健和对应的值都会被回收。
(2)数据类型不同:Map中键可以是任意类型的值;而WeakMap中键必须是对象(null 除外)。
(3)可迭代性不同:Map可以迭代遍历键,可以使用for…of、foreach等方法来遍历;而WeakMap不可迭代遍历键,不能循环,因为其成员随时可能随时被垃圾回收。
(4)使用方法的不同:WeakMap支持的方法比较少。Map有has()、get()、set()、delete()和clear()等方法,而WeakMap只有has()、get()和set()方法。另外,无法直接获取WeakMap中存储的所有键名,也无法获取其大小。Weakmap没有size属性。
3.3.6 未被清理的Console输出
在开发环境下,我们经常也会使用console.log()来输出一些数据来调试代码。但是console其实在某些情况下也可能会导致内存泄漏。只是我们平常也应该不会大量的console,所以也就不会注意到这一点。我们之所以在控制台能看到数据输出,是因为浏览器保存了我们输出对象的信息数据引用,也正是因此它也可能会造成内存泄漏。
当控制台写入大量数据时,它将占用内存并降低系统性能。如果不刷新缓冲区,这些数据可能会长时间留在内存中,直到程序结束才得以释放。在某些场景下,例如循环中频繁写入控制台输出,这些buff中的数据可能会长期存在,消耗大量内存。
为了避免这种情况,可以使用定时清空或分段输出控制台日志等方式来减少占用内存的情况,并在必要时禁用console输出。这也跟其他代码平台时一样的,比较常见的是我在matlab调试时,经常就会在控制台clear,这跟上面是一个道理。所以,开发环境下我们可以使用控制台输出来便于我们调试,但是在生产环境下,一定要及时清理掉输出(注释掉没有必要的console.log)。
4. 内存怎么分析and内存泄漏如何定位
4.1 内存分析的方法
内存怎么进行分析,其实就是借助一些辅助工具或者命令行来查看内存的占用情况,通过占用情况的分析找到内存泄漏的位置。比如在linux中,我们训练模型时,有时候也会用nvidia-smi
来监控GPU使用情况。
Chrome Devtools中Memory工具的简单介绍
那在前端中,通常使用的内存分析则是浏览器开发工具(Devtools
)中的Memory
了。在我们平常的项目中,用的最多的是谷歌开发工具(Chrome Devtools
)中的Memory
工具,中文应该是叫做内存面板,如下图所示:
在Memory面板上,我们其实可以看到有3种记录内存的情况:
- Heap snapshot:堆快照
- Allocation instrumentation on timeline:内存分配时间轴
- Allocation sampling:内存分配采样
点击1处便可以进行堆快照的内存分析,点击完,就会生成右边这样子的报告:
在堆快照结果页面中,可以有四种视图来观察:
- Summary:摘要视图,按照构造函数进行分组,捕获对象和其使用内存的情况,可理解为一个内存摘要,用于跟踪定位DOM节点的内存泄漏。
- Comparison:比较视图 ,对比某个操作前后的内存快照区别,分析操作前后内存释放情况等,便于确认内存是否存在泄漏及造成原因。
- Containment:包含视图,探测堆的具体内容,提供一个视图来查看对象结构,有助分析对象引用情况,可分析闭包及更深层次的对象分析
- Statistics:统计视图
比如下面的图片就是统计视图,上面的图片就是摘要视图。
更多详细的内存分析可以查看下面这篇博客:
博客:JavaScript 内存详解 & 分析指南
这个博主介绍得很详细,必须点个赞👍。
4.2 内存泄漏的识别
内存泄漏的识别其实跟内存分析是一致的,通过分析内存,我们可以定位内存泄漏可能的位置。那我们一般是怎么识别内存泄漏的呢?
经验法则是:如果连续五次垃圾回收之后,内存占用的一次比一次大,就有内存泄漏。
前端中,内存泄漏识别的方法一般可以分为辅助工具法(浏览器方法)和命令行法。
4.2.1 浏览器方法
利用辅助工具,比如JProfiler,Chrome DevTools等。
谷歌开发工具DevTools,Record之后的事件日志Event log中的GC日志,记录页面活动的概况,生成可视化的分析结果,可以用Comparison视图或者filter按需查看快照之间的差异,可以从时间轴上直观的看到内存泄漏的情况。
- 具体的操作:
- 打开开发者工具,选择 Performance ⾯板
- 在顶部勾选 Memory
- 点击左上角的 record 按钮
- 在页面上进行各种操作,模拟用户的使用情况
- 一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况
4.2.2 命令行方法
使用 Node 提供的 process.memoryUsage
方法。比如:
console.log(process.memoryUsage());
// 输出
{
rss: 27709440, // resident set size,所有内存占用,包括指令区和堆栈
heapTotal: 5685248, // "堆"占用的内存,包括用到的和没用到的
heapUsed: 3449392, // 用到的堆的部分
external: 8772 // V8 引擎内部的 C++ 对象占用的内存
}
判断内存泄漏,以heapUsed字段为准。
- Node.js查看方法
- 方法1:可以带上- -inspect参数,启动Node.js便可利用Chrome DevTools来查看
- 方法2:可借助第三方包heapdump来生成快照文件,导入谷歌开发工具之后进行Memory的快照对比
- 方法3:带上- - expose-gc参数,可调用global.gc()来触发垃圾回收,从而借助process.memoryUsage().heapUsed来检查内存的大小
5. 笔记篇章下
通过上面的知识,我们也对内存管理和内存泄漏有了一个大概的了解,所以在下篇中,笔者我将围绕内存泄漏的解决方法,什么是垃圾回收机制,垃圾回收机制常见算法展开详细的介绍以及内存管理应该如何优化。
笔记链接:【前端知识】内存泄漏与垃圾回收机制 (下)
码字不易,可能当中存在某些字体错误(笔者我没有发现),如果有错误,欢迎大家指正。🤗