首頁  >  文章  >  web前端  >  完全掌握JavaScript記憶體洩漏(圖文詳解)

完全掌握JavaScript記憶體洩漏(圖文詳解)

WBOY
WBOY轉載
2022-01-28 18:00:113746瀏覽

這篇文章為大家帶來了關於JavaScript中記憶體外洩的相關知識,其中包括記憶體外洩是什麼,那些情況會造成記憶體外洩等相關問題,希望對大家有幫助。

完全掌握JavaScript記憶體洩漏(圖文詳解)

js 記憶體洩漏

什麼是記憶體洩漏?

程式的運作需要記憶體。只要程式提出要求,作業系統或運行時(runtime)就必須供給記憶體。

對於持續運行的服務進程(daemon),必須及時釋放不再用到的記憶體。否則,記憶體佔用越來越高,輕則影響系統效能,重則導致進程崩潰。
完全掌握JavaScript記憶體洩漏(圖文詳解)
不再用到的內存,沒有及時釋放,就叫做記憶體洩漏(memory leak)。

有些語言(例如 C 語言)必須手動釋放內存,程式設計師負責記憶體管理。

char * buffer;buffer = (char*) malloc(42);// Do something with bufferfree(buffer);

上面是 C 語言程式碼,malloc方法用來申請內存,使用完畢之後,必須自行用free方法釋放內存。

這很麻煩,所以大多數語言提供自動記憶體管理,減輕程式設計師的負擔,這被稱為"垃圾回收機制"(garbage collector)。

雖然前端有垃圾回收機制,但當某塊無用的內存,卻無法被垃圾回收機制認為是垃圾時,也就發生內存洩漏了。

哪些情況會造成記憶體洩漏

1. 意外的全域變數

全域變數的生命週期最長,直到頁面關閉前,它都存活著,所以全域變數上的記憶體一直都不會被回收。

當全域變數使用不當,沒有及時回收(手動賦值 null),或拼字錯誤等將某個變數掛載到全域變數時,也就發生記憶體洩漏了。

2. 被遺忘的計時器
setTimeout 和setInterval 是由瀏覽器專門執行緒來維護它的生命週期,所以當在某個頁面使用了計時器,當該頁面銷毀時,沒有手動去釋放清理這些定時器的話,那麼這些定時器還是存活著的。

也就是說,計時器的生命週期並不掛靠在頁面上,所以當在當前頁面的js 裡透過定時器註冊了某個回呼函數,而該回呼函數內又持有當前頁面當某個變數或某些DOM 元素時,就會導致即使頁面銷毀了,由於定時器持有該頁面部分引用而造成頁面無法正常被回收,從而導致記憶體洩漏了。

如果此時再次打開同個頁面,記憶體中其實是有雙份頁面資料的,如果多次關閉、打開,那麼記憶體洩漏會越來越嚴重。而且這種場景很容易出現,因為使用定時器的人很容易遺忘清除。

3. 使用不當的閉包
函數本身會持有它定義時所在的詞法環境的引用,但通常情況下,使用完函數後,函數所申請的記憶體都會被回收了。

但當函數內再傳回一個函數時,由於傳回的函數持有外部函數的詞法環境,而傳回的函數又被其他生命週期東西所持有,導致外部函數雖然執行完了,但內存卻無法被回收。

4. 遺漏的DOM 元素
DOM 元素的生命週期正常是取決於是否掛載在DOM 樹上,當從DOM 樹上移除時,也可以被銷毀回收了
但如果某個DOM 元素,在js 中也持有它的引用時,那麼它的生命週期就由js 和是否在DOM 樹上兩者決定了,記得移除時,兩個地方都需要去清理才能正常回收它。

5. 網路回呼
某些場景中,在某個頁面發起網路請求,並註冊一個回呼,且回呼函數內持有該頁面某些內容,那麼,當該頁面銷毀時,應該註銷網路的回調,否則,因為網路持有頁面部分內容,也會導致頁面部分內容無法被回收。


如何監控記憶體洩漏

記憶體洩漏是可以分成兩類的,一種是比較嚴重的,洩漏的就一直回收不回來了,另一種嚴重程度稍微輕點,就是沒有及時清理導致的記憶體洩漏,一段時間後還是可以被清理掉。

不管哪一種,利用開發者工具抓到的記憶體圖,應該都會看到一段時間內,記憶體佔用不斷的直線式下降,這是因為不斷發生GC,也就是垃圾回收導致的。

記憶體不足會造成不斷 GC,而 GC 時是會阻塞主執行緒的,所以會影響到頁面效能,造成卡頓,所以記憶體洩漏問題還是需要關注的。

場景一:在某個函數內申請一塊內存,然後該函數在短時間內不斷被呼叫

// 点击按钮,就执行一次函数,申请一块内存startBtn.addEventListener("click", function() {
	var a = new Array(100000).fill(1);
	var b = new Array(20000).fill(1);});

完全掌握JavaScript記憶體洩漏(圖文詳解)

一个页面能够使用的内存是有限的,当内存不足时,就会触发垃圾回收机制去回收没用的内存。

而在函数内部使用的变量都是局部变量,函数执行完毕,这块内存就没用可以被回收了。

所以当我们短时间内不断调用该函数时,可以发现,函数执行时,发现内存不足,垃圾回收机制工作,回收上一个函数申请的内存,因为上个函数已经执行结束了,内存无用可被回收了。

所以图中呈现内存使用量的图表就是一条横线过去,中间出现多处竖线,其实就是表示内存清空,再申请,清空再申请,每个竖线的位置就是垃圾回收机制工作以及函数执行又申请的时机。

场景二:在某个函数内申请一块内存,然后该函数在短时间内不断被调用,但每次申请的内存,有一部分被外部持有。

// 点击按钮,就执行一次函数,申请一块内存var arr = [];startBtn.addEventListener("click", function() {
	var a = new Array(100000).fill(1);
	var b = new Array(20000).fill(1);
    arr.push(b);});

完全掌握JavaScript記憶體洩漏(圖文詳解)

看一下跟第一张图片有什么区别?

不再是一条横线了吧,而且横线中的每个竖线的底部也不是同一水平了吧。

其实这就是内存泄漏了。

我们在函数内申请了两个数组内存,但其中有个数组却被外部持有,那么,即使每次函数执行完,这部分被外部持有的数组内存也依旧回收不了,所以每次只能回收一部分内存。

这样一来,当函数调用次数增多时,没法回收的内存就越多,内存泄漏的也就越多,导致内存使用量一直在增长
另外,也可以使用 performance monitor 工具,在开发者工具里找到更多的按钮,在里面打开此功能面板,这是一个可以实时监控 cpu,内存等使用情况的工具,会比上面只能抓取一段时间内工具更直观一点:
完全掌握JavaScript記憶體洩漏(圖文詳解)

梯状上升的就是发生内存泄漏了,每次函数调用,总有一部分数据被外部持有导致无法回收,而后面平滑状的则是每次使用完都可以正常被回收。

这张图需要注意下,第一个红框末尾有个直线式下滑,这是因为,我修改了代码,把外部持有函数内申请的数组那行代码去掉,然后刷新页面,手动点击 GC 才触发的效果,否则,无论你怎么点 GC,有部分内存一直无法回收,是达不到这样的效果图的。

以上,是监控是否发生内存泄漏的一些工具,但下一步才是关键,既然发现内存泄漏,那该如何定位呢?如何知道,是哪部分数据没被回收导致的泄漏呢?

如何分析内存泄漏,找出有问题的代码

分析内存泄漏的原因,还是需要借助开发者工具的 Memory 功能,这个功能可以抓取内存快照,也可以抓取一段时间内,内存分配的情况,还可以抓取一段时间内触发内存分配的各函数情况。
完全掌握JavaScript記憶體洩漏(圖文詳解)
利用这些工具,我们可以分析出,某个时刻是由于哪个函数操作导致了内存分配,分析出大量重复且没有被回收的对象是什么。

这样一来,有嫌疑的函数也知道了,有嫌疑的对象也知道了,再去代码中分析下,这个函数里的这个对象到底是不是就是内存泄漏的元凶,搞定。

先举个简单例子,再举个实际内存泄漏的例子:

场景一:在某个函数内申请一块内存,然后该函数在短时间内不断被调用,但每次申请的内存,有一部分被外部持有

// 每次点击按钮,就有一部分内存无法回收,因为被外部 arr 持有了var arr = [];startBtn.addEventListener("click", function() {
	var a = new Array(100000).fill(1);
	var b = new Array(20000).fill(1);
  arr.push(b);});

完全掌握JavaScript記憶體洩漏(圖文詳解)
可以抓取两份快照,两份快照中间进行内存泄漏操作,最后再比对两份快照的区别,查看增加的对象是什么,回收的对象又是哪些,如上图。

也可以单独查看某个时刻快照,从内存占用比例来查看占据大量内存的是什么对象,如下图:
完全掌握JavaScript記憶體洩漏(圖文詳解)
还可以从垃圾回收机制角度出发,查看从 GC root 根节点出发,可达的对象里,哪些对象占用大量内存:
完全掌握JavaScript記憶體洩漏(圖文詳解)
从上面这些方式入手,都可以查看到当前占用大量内存的对象是什么,一般来说,这个就是嫌疑犯了。

当然,也并不一定,当有嫌疑对象时,可以利用多次内存快照间比对,中间手动强制 GC 下,看下该回收的对象有没有被回收,这是一种思路。

  • 抓取一段时间内,内存分配情况。
    完全掌握JavaScript記憶體洩漏(圖文詳解)

这个方式,可以有选择性的查看各个内存分配时刻是由哪个函数发起,且内存存储的是什么对象。

当然,内存分配是正常行为,这里查看到的还需要借助其他数据来判断某个对象是否是嫌疑对象,比如内存占用比例,或结合内存快照等等。

  • 抓取一段时间内函数的内存使用情况
    完全掌握JavaScript記憶體洩漏(圖文詳解)

这个能看到的内容很少,比较简单,目的也很明确,就是一段时间内,都有哪些操作在申请内存,且用了多少。

总之,这些工具并没有办法直接给你答复,告诉你 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 函数

  • 发现问题

我们先利用工具看看,是不是会发生内存泄漏:
完全掌握JavaScript記憶體洩漏(圖文詳解)
三种内存监控图表都显示,这发生内存泄漏了:反复执行同个函数,内存却梯状式增长,手动点击 GC 内存也没有下降,说明函数每次执行都有部分内存泄漏了。

这种手动强制垃圾回收都无法将内存将下去的情况是很严重的,长期执行下去,会耗尽可用内存,导致页面卡顿甚至崩掉。

  • 分析问题

既然已经确定有内存泄漏了,那么接下去就该找出内存泄漏的原因了。
完全掌握JavaScript記憶體洩漏(圖文詳解)
首先通过 sampling profile,我们把嫌疑定位到 replaceThing 这个函数上

接着,我们抓取两份内存快照,比对一下,看看能否得到什么信息:
完全掌握JavaScript記憶體洩漏(圖文詳解)
比对两份快照可以发现,这过程中,数组对象一直在增加,而且这个数组对象来自 replaceThing 函数内部创建的对象的 longStr 属性。

其实这张图信息很多了,尤其是下方那个嵌套图,嵌套关系是反着来,你倒着看的话,就可以发现,从全局对象 Window 是如何一步步访问到该数组对象的,垃圾回收机制正是因为有这样一条可达的访问路径,才无法回收。

其实这里就可以分析了,为了多使用些工具,我们换个图来分析吧。

我们直接从第二份内存快照入手,看看:
完全掌握JavaScript記憶體洩漏(圖文詳解)

为什么每一次 replaceThing 函数调用后,内部创建的对象都无法被回收呢?

因为 replaceThing 的第一次创建,这个对象被全局变量 t 持有,所以回收不了。

后面的每一次调用,这个对象都被上一个 replaceThing 函数内部的 o 局部变量持有而回收不了。

而这个函数内的局部变量 o 在 replaceThing 首次调用时被创建的对象的 someMethod 方法持有,该方法挂载的对象被全局变量 t 持有,所以也回收不了。

这样层层持有,每一次函数的调用,都会持有函数上次调用时内部创建的局部变量,导致函数即使执行结束,这些局部变量也无法回收。

相关推荐:javascript学习教程

以上是完全掌握JavaScript記憶體洩漏(圖文詳解)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:csdn.net。如有侵權,請聯絡admin@php.cn刪除