이 기사는 메모리 누수가 무엇인지, 메모리 누수가 발생할 수 있는 상황 및 기타 관련 문제를 포함하여 JavaScript의 메모리 누수에 대한 관련 지식을 제공합니다.
프로그램을 실행하려면 메모리가 필요합니다. 프로그램이 요청할 때마다 운영 체제나 런타임은 메모리를 제공해야 합니다.
지속적으로 실행되는 서비스 프로세스(데몬)의 경우 더 이상 사용되지 않는 메모리를 적시에 해제해야 합니다. 그렇지 않으면 메모리 사용량이 점점 더 높아져 시스템 성능에 가장 큰 영향을 미치고 최악의 경우 프로세스가 중단될 수 있습니다.
더 이상 사용되지 않고 제때 해제되지 않는 메모리를 메모리 누수라고 합니다.
일부 언어(예: C 언어)에서는 메모리를 수동으로 해제해야 하며, 메모리 관리는 프로그래머가 담당합니다.
char * buffer;buffer = (char*) malloc(42);// Do something with bufferfree(buffer);
위는 C 언어 코드입니다. 메모리를 적용할 때는 malloc 방식을 사용하고, 메모리를 해제하려면 반드시 free 방식을 사용해야 합니다.
이것이 번거롭기 때문에 대부분의 언어에서는 프로그래머의 부담을 줄이기 위해 자동 메모리 관리 기능을 제공합니다. 이를 "가비지 컬렉터"라고 합니다.
프런트 엔드에는 가비지 수집 메커니즘이 있지만 쓸모 없는 메모리 조각이 가비지 수집 메커니즘에 의해 가비지로 간주될 수 없는 경우 메모리 누수가 발생합니다.
1. 예상치 못한 전역 변수
전역 변수는 페이지가 닫힐 때까지 가장 긴 수명 주기를 가지므로 전역 변수의 메모리는 절대 재활용되지 않습니다.
전역 변수를 부적절하게 사용하거나, 제때에 재활용되지 않거나(null 수동 할당), 철자 오류로 인해 변수가 전역 변수에 마운트되면 메모리 누수가 발생합니다.
2. 잊혀진 타이머
setTimeout 및 setInterval은 수명 주기를 유지하기 위해 브라우저 전용 스레드에 의해 유지되므로 특정 페이지에서 타이머가 사용되고 페이지가 삭제되면 수동으로 해제하거나 정리할 필요가 없습니다. 타이머가 있는 경우 해당 타이머는 아직 활성 상태입니다.
즉, 타이머의 생명주기가 페이지에 붙어 있지 않기 때문에 현재 페이지의 js에 타이머를 통해 콜백 함수가 등록되어 있고 콜백 함수가 변수를 보유하고 있거나 특정 DOM 요소가 사용하면 페이지가 삭제되더라도 타이머가 페이지에 대한 부분 참조를 보유하고 있어 메모리 누수가 발생하므로 페이지를 정상적으로 재활용할 수 없습니다.
이 때 같은 페이지를 다시 열면 실제로 메모리에 이중 페이지 데이터가 있게 됩니다. 여러 번 닫았다가 열면 메모리 누수가 점점 더 심각해집니다. 타이머를 사용하는 사람들은 타이머를 지우는 것을 쉽게 잊어버릴 수 있기 때문에 이 시나리오는 쉽게 발생합니다.
3. 클로저의 부적절한 사용
함수 자체는 자신이 정의된 어휘 환경에 대한 참조를 보유하지만 일반적으로 함수가 사용된 후에는 함수에서 요청한 메모리가 재활용됩니다.
하지만 함수 내에서 함수가 반환되면 반환된 함수는 외부 함수의 어휘 환경을 보유하고 반환된 함수는 다른 생명주기 사물에 의해 보유되므로 외부 함수가 실행되더라도 메모리를 사용할 수 없습니다. 재활용.
4. DOM 요소 누락
DOM 요소의 일반적인 수명 주기는 DOM 트리에서 제거되면 폐기되고 재활용될 수 있습니다. 요소는 또한 js에 참조를 보유하며, 해당 수명 주기는 js와 DOM 트리에 있는지 여부에 따라 결정됩니다. 제거할 때 정상적으로 재활용하려면 두 위치를 모두 정리해야 합니다.
5. 네트워크 콜백 일부 시나리오에서는 특정 페이지에서 네트워크 요청이 시작되고 콜백이 등록되며 페이지의 일부 내용이 콜백 함수에 보관됩니다. 네트워크는 콜백을 로그아웃해야 합니다. 그렇지 않으면 네트워크가 페이지 콘텐츠의 일부를 보유하므로 페이지 콘텐츠의 일부가 재활용되지 않습니다.
内存泄漏
,一段时间后还是可以被清理掉。
不管哪一种,利用开发者工具抓到的内存图,应该都会看到一段时间内,内存占用不断的直线式下降,这是因为不断发生 GC,也就是垃圾回收导致的。
内存不足会造成不断 GC,而 GC 时是会阻塞主线程
어느 쪽이든 개발자 도구로 캡쳐한 메모리 그래프를 사용해보면 일정 시간이 지나면서 메모리 사용량이 계속해서 선형적으로 감소하는 것을 볼 수 있는데, 이는 GC, 즉 가비지가 지속적으로 발생하기 때문입니다. 수집.
메모리가 부족하면 연속 GC가 발생하고 GC가 메인 스레드를 차단
하므로 페이지 성능에 영향을 미치고 지연이 발생하므로 메모리 누수 문제는 여전히 주의가 필요합니다.
// 点击按钮,就执行一次函数,申请一块内存startBtn.addEventListener("click", function() { var a = new Array(100000).fill(1); var b = new Array(20000).fill(1);});
一个页面能够使用的内存是有限的,当内存不足时,就会触发垃圾回收机制去回收没用的内存。
而在函数内部使用的变量都是局部变量,函数执行完毕,这块内存就没用可以被回收了。
所以当我们短时间内不断调用该函数时,可以发现,函数执行时,发现内存不足,垃圾回收机制工作,回收上一个函数申请的内存,因为上个函数已经执行结束了,内存无用可被回收了。
所以图中呈现内存使用量的图表就是一条横线过去,中间出现多处竖线,其实就是表示内存清空,再申请,清空再申请,每个竖线的位置就是垃圾回收机制工作以及函数执行又申请的时机。
场景二:在某个函数内申请一块内存,然后该函数在短时间内不断被调用,但每次申请的内存,有一部分被外部持有。
// 点击按钮,就执行一次函数,申请一块内存var arr = [];startBtn.addEventListener("click", function() { var a = new Array(100000).fill(1); var b = new Array(20000).fill(1); arr.push(b);});
看一下跟第一张图片有什么区别?
不再是一条横线了吧,而且横线中的每个竖线的底部也不是同一水平了吧。
其实这就是内存泄漏
了。
我们在函数内申请了两个数组内存,但其中有个数组却被外部持有,那么,即使每次函数执行完,这部分被外部持有的数组内存也依旧回收不了,所以每次只能回收一部分内存。
这样一来,当函数调用次数增多时,没法回收的内存就越多,内存泄漏的也就越多,导致内存使用量一直在增长
另外,也可以使用 performance monitor
工具,在开发者工具里找到更多的按钮,在里面打开此功能面板,这是一个可以实时监控 cpu,内存等使用情况的工具,会比上面只能抓取一段时间内工具更直观一点:
梯状上升的就是发生内存泄漏了,每次函数调用,总有一部分数据被外部持有导致无法回收,而后面平滑状的则是每次使用完都可以正常被回收。
这张图需要注意下,第一个红框末尾有个直线式下滑,这是因为,我修改了代码,把外部持有函数内申请的数组那行代码去掉,然后刷新页面,手动点击 GC 才触发的效果,否则,无论你怎么点 GC,有部分内存一直无法回收,是达不到这样的效果图的。
以上,是监控是否发生内存泄漏的一些工具,但下一步才是关键,既然发现内存泄漏,那该如何定位呢?如何知道,是哪部分数据没被回收导致的泄漏呢?
分析内存泄漏的原因,还是需要借助开发者工具的 Memory
功能,这个功能可以抓取内存快照,也可以抓取一段时间内,内存分配的情况,还可以抓取一段时间内触发内存分配的各函数情况。
利用这些工具,我们可以分析出,某个时刻是由于哪个函数操作导致了内存分配,分析出大量重复且没有被回收的对象是什么。
这样一来,有嫌疑的函数也知道了,有嫌疑的对象也知道了,再去代码中分析下,这个函数里的这个对象到底是不是就是内存泄漏的元凶,搞定。
先举个简单例子,再举个实际内存泄漏的例子:
场景一:在某个函数内申请一块内存,然后该函数在短时间内不断被调用,但每次申请的内存,有一部分被外部持有
// 每次点击按钮,就有一部分内存无法回收,因为被外部 arr 持有了var arr = [];startBtn.addEventListener("click", function() { var a = new Array(100000).fill(1); var b = new Array(20000).fill(1); arr.push(b);});
可以抓取两份快照,两份快照中间进行内存泄漏操作,最后再比对两份快照的区别,查看增加的对象是什么,回收的对象又是哪些,如上图。
也可以单独查看某个时刻快照,从内存占用比例来查看占据大量内存的是什么对象,如下图:
还可以从垃圾回收机制角度出发,查看从 GC root 根节点出发,可达的对象里,哪些对象占用大量内存:
从上面这些方式入手,都可以查看到当前占用大量内存的对象是什么,一般来说,这个就是嫌疑犯了。
当然,也并不一定,当有嫌疑对象时,可以利用多次内存快照间比对,中间手动强制 GC 下,看下该回收的对象有没有被回收,这是一种思路。
这个方式,可以有选择性的查看各个内存分配时刻是由哪个函数发起,且内存存储的是什么对象。
当然,内存分配是正常行为,这里查看到的还需要借助其他数据来判断某个对象是否是嫌疑对象,比如内存占用比例,或结合内存快照等等。
这个能看到的内容很少,比较简单,目的也很明确,就是一段时间内,都有哪些操作在申请内存,且用了多少。
总之,这些工具并没有办法直接给你答复,告诉你 xxx 就是内存泄漏的元凶,如果浏览器层面就能确定了,那它干嘛不回收它,干嘛还会造成内存泄漏
所以,这些工具,只能给你各种内存使用信息,你需要自己借助这些信息,根据自己代码的逻辑,去分析,哪些嫌疑对象才是内存泄漏的元凶。
例子1:
var t = null;var replaceThing = function() { var o = t var unused = function() { if (o) { console.log("hi") } } t = { longStr: new Array(100000).fill('*'), someMethod: function() { console.log(1) } }}setInterval(replaceThing, 1000)
先说说这代码用途,声明了一个全局变量 t 和 replaceThing 函数,函数目的在于会为全局变量赋值一个新对象,然后内部有个变量存储全局变量 t 被替换前的值,最后定时器周期性执行 replaceThing 函数
我们先利用工具看看,是不是会发生内存泄漏:
三种内存监控图表都显示,这发生内存泄漏了:反复执行同个函数,内存却梯状式增长,手动点击 GC 内存也没有下降,说明函数每次执行都有部分内存泄漏了。
这种手动强制垃圾回收都无法将内存将下去的情况是很严重的,长期执行下去,会耗尽可用内存,导致页面卡顿甚至崩掉。
既然已经确定有内存泄漏了,那么接下去就该找出内存泄漏的原因了。
首先通过 sampling profile,我们把嫌疑定位到 replaceThing 这个函数上
接着,我们抓取两份内存快照,比对一下,看看能否得到什么信息:
比对两份快照可以发现,这过程中,数组对象一直在增加,而且这个数组对象来自 replaceThing 函数内部创建的对象的 longStr 属性。
其实这张图信息很多了,尤其是下方那个嵌套图,嵌套关系是反着来,你倒着看的话,就可以发现,从全局对象 Window 是如何一步步访问到该数组对象的,垃圾回收机制正是因为有这样一条可达的访问路径,才无法回收。
其实这里就可以分析了,为了多使用些工具,我们换个图来分析吧。
我们直接从第二份内存快照入手,看看:
为什么每一次 replaceThing 函数调用后,内部创建的对象都无法被回收呢?
因为 replaceThing 的第一次创建,这个对象被全局变量 t
持有,所以回收不了。
后面的每一次调用,这个对象都被上一个 replaceThing 函数
内部的 o 局部变量
持有而回收不了。
而这个函数内的局部变量 o 在 replaceThing 首次调用时被创建的对象的 someMethod 方法持有,该方法挂载的对象被全局变量 t 持有,所以也回收不了。
这样层层持有,每一次函数的调用,都会持有函数上次调用时内部创建的局部变量,导致函数即使执行结束,这些局部变量也无法回收。
相关推荐:javascript学习教程
위 내용은 JavaScript 메모리 누수를 완전히 마스터하세요(자세한 그래픽 및 텍스트 설명)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!