ホームページ >ウェブフロントエンド >jsチュートリアル >JavaScriptのメモリリークを完全マスター(グラフィックとテキストで詳しく解説)
この記事では、メモリ リークとは何か、メモリ リークが発生する状況やその他の関連問題など、JavaScript のメモリ リークに関する関連知識を提供します。
プログラムを実行するにはメモリが必要です。プログラムがメモリを要求するたびに、オペレーティング システムまたはランタイムはメモリを提供する必要があります。
継続的に実行されるサービス プロセス (デーモン) の場合、使用されなくなったメモリは適時に解放する必要があります。そうしないと、メモリ使用量がますます高くなり、システムのパフォーマンスに影響を及ぼし、最悪の場合はプロセスがクラッシュする可能性があります。
使用されなくなり、期限内に解放されないメモリは、メモリ リークと呼ばれます。
一部の言語 (C 言語など) では手動でメモリを解放する必要があり、プログラマがメモリ管理を担当します。
char * buffer;buffer = (char*) malloc(42);// Do something with bufferfree(buffer);
上記はC言語のコードで、メモリの確保にはmallocメソッドを使用しており、使用後はfreeメソッドでメモリを解放する必要があります。
これは面倒なので、ほとんどの言語ではプログラマの負担を軽減するために自動メモリ管理が用意されており、これを「ガベージコレクタ」(ガベージコレクター)と呼びます。
フロントエンドにはガベージコレクション機構が備わっていますが、ガベージコレクション機構によって不要なメモリがガベージとみなされない場合、メモリリークが発生します。
1. 予期しないグローバル変数
グローバル変数は、ページが閉じられるまでのライフ サイクルが最も長くなります。生きているため、グローバル変数のメモリは決して再利用されません。
グローバル変数が不適切に使用されたり、時間内にリサイクルされなかったり (null を手動で割り当てたり)、スペル ミスにより変数がグローバル変数にマウントされた場合、メモリ リークが発生します。
2. 忘れられたタイマー
setTimeout と setInterval はブラウザ専用のスレッドによって維持されるため、特定のページでタイマーが使用されるとき、タイマーが使用されるとき、ページが使用されるとき、これらのタイマーが手動で解放されて消去されない場合、これらのタイマーはまだ生きています。
言い換えると、タイマーのライフサイクルはページに関連付けられていないため、現在のページの js でタイマーを介してコールバック関数が登録され、コールバック関数が現在のページを保持するとき特定の変数または特定の DOM 要素が使用されている場合、ページが破棄されても、タイマーがページへの部分参照を保持しているため、ページを正常にリサイクルできず、メモリ リークが発生します。
このとき、再度同じページを開くと、実際にはメモリ内に2ページ分のデータが存在することになり、何度も閉じたり開いたりすると、メモリリークはますます深刻になってしまいます。そして、タイマーを使用している人はクリアを忘れやすいため、このシナリオは起こりやすいです。
3. クロージャの不適切な使用
関数自体は、それが定義された字句環境への参照を保持しますが、通常、関数の使用後、関数は次のようになります。割り当てられたメモリは再利用されます。
しかし、関数内で関数が返された場合、返された関数は外部関数の字句環境を保持し、返された関数は他のライフサイクルのものによって保持されるため、外部関数は実行されていますが、メモリは再利用できません。
4. DOM 要素の欠落
DOM 要素の通常のライフサイクルは、DOM 要素が DOM ツリーにマウントされているかどうかによって異なります。破棄されリサイクルされます
ただし、DOM 要素が js 内でもその要素への参照を保持している場合、そのライフサイクルは js と DOM ツリー上にあるかどうかの両方によって決まります。削除するときは、すべての場所をクリーンアップする必要があることを忘れないでください。適切にリサイクルされる前に。
5. ネットワーク コールバック
一部のシナリオでは、ネットワーク リクエストが特定のページで開始され、コールバックが登録され、コールバック関数がページの一部のコンテンツを保持します。 、ページが破棄されると、ネットワーク コールバックはログアウトされる必要があります。ログアウトしないと、ネットワークがページ コンテンツの一部を保持するため、ページ コンテンツの一部はリサイクルされません。
メモリ リークは 2 つのカテゴリに分類できます。1 つはより深刻で、リークされたものは決して回復されません。もう 1 つは、わずかに深刻です。クリーンアップが間に合わなかったことが原因で メモリ リーク
が発生した場合でも、一定期間が経過すればクリーンアップできる可能性があるので、慎重に行ってください。
どちらの場合でも、開発者ツールで取得したメモリ マップを使用すると、メモリ使用量が一定期間にわたって直線的に減少し続けることがわかります。これは、GC が継続的に発生するためです。これはガベージコレクションが原因です。
メモリが不足すると連続 GC が発生し、GC がメイン スレッド
をブロックするため、ページのパフォーマンスに影響を与え、遅延が発生するため、メモリ リークの問題には引き続き注意する必要があります。
#シナリオ 1: 特定の関数でメモリの一部を適用し、その関数が短期間に連続して呼び出される#// 点击按钮,就执行一次函数,申请一块内存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 中国語 Web サイトの他の関連記事を参照してください。