ホームページ >ウェブフロントエンド >jsチュートリアル >Node V8 エンジンのメモリと GC の詳細な図による説明
この記事では、NodeJS V8 エンジンのメモリとガベージ コレクター (GC) について詳しく説明します。お役に立てば幸いです。
プログラム アプリケーションはメモリを使用する必要があり、メモリの 2 つのパーティションがよく使用されます。概念: スタック領域とヒープ領域。
スタック領域は関数の終了とともに自動的に解放されるリニアキューであり、ヒープ領域は動的メモリの空き領域です。ヒープメモリは手動で割り当ておよび解放されるか、ガベージコレクションプログラム#によって行われます。 ##(ガベージコレクション(以下、GC)は自動的に割り当てられ、解放されます。
ソフトウェア開発や一部の言語の初期には、C、C など、ヒープ メモリの割り当てと解放が手動で行われていました。メモリを正確に操作し、可能な限り最適なメモリ使用量を実現できますが、開発効率が非常に低く、不適切なメモリ操作が発生する傾向があります。 [関連チュートリアルの推奨事項: nodejs ビデオ チュートリアル 、プログラミング教育 ]
テクノロジーの発展に伴い、高級言語 (Java# など) ##Node ) では、開発者が手動でメモリを操作する必要はなく、プログラミング言語が自動的にスペースを割り当て、解放します。同時に、メモリの解放と整理を支援するために、GC (ガベージ コレクション) ガベージ コレクターも誕生しました。ほとんどの場合、開発者はメモリ自体を気にする必要がなく、ビジネス開発に集中できます。次の記事では主にヒープ メモリと GC について説明します。
2. GC 開発GC は主にメモリ サイズの増加とともに開発および進化します。大きく分けて代表的な 3 つのステージに分かれます。
フェーズ 1 シングルスレッド GC (シリアルを表す)他のすべてのワーカー スレッドを完全に一時停止する必要がありますフェーズ 2 の並列マルチスレッドGC (Parallel Scavenge、ParNew を表します)。これは GC の初期段階であり、パフォーマンスが最も悪くなります。
他のすべての作業スレッドを完全に一時停止しますフェーズ 3 マルチスレッド同時並行 GC (代表: CMS (Concurrent Mark Sweet) G1) )
3. v8 メモリ パーティションと GC前の 2 つの開発段階の GC アルゴリズムは完全に STW ですが、同時 GC では、GC スレッドの一部の段階がビジネス コードと同時に実行できるため、STW 時間が短縮されます。ただし、GC プロセス中に新しいオブジェクトが入ってくる可能性があるため、このモードではマーキング エラーが発生します。もちろん、アルゴリズム自体がこの問題を修正して解決します。
上記の 3 つの段階では、マーキング エラーが発生しません。 GC は上記の通りである必要があることを意味します。3 つのタイプのいずれかです。さまざまなプログラミング言語の GC は、さまざまなニーズに応じてさまざまなアルゴリズムの組み合わせを使用して実装されます。
盗まれた写真:
Node.js では、GC が世代世代戦略を採用しています。は新世代領域と旧世代領域に分かれており、ほとんどのメモリデータはこの 2 つの領域にあります。
新世代は、古いオブジェクトを格納する小型で高速なメモリ プールであり、2 つのハーフスペース (セミスペース) に分割されています。スペースの半分は空き (スペースに呼び出されます)、スペースの残りの半分はデータを保存します (スペースから呼び出されます)。
オブジェクトが最初に作成されると、オブジェクトは年齢が 1 の若い世代 from 半空間に割り当てられます。 from が不足しているか、一定のサイズを超えている場合、Minor GC (コピー アルゴリズム Scavenge を使用) がトリガーされ、このとき GC はアプリケーションの実行 (STW、ストップ・ザ・ワールド)、(スペースから) 内のすべてのアクティブなオブジェクトをマークし、それらを整理して新しい世代の別の空きスペース (スペースへ) に継続的に移動します。最後に、元の from スペースのすべてのメモリが解放され、空きスペースになり、2 つのスペースの from と to の交換が完了します。アルゴリズムとは、時間を犠牲にして空間を犠牲にするアルゴリズムです。
新しい世代のスペースは小さいため、このスペースにより GC がより頻繁にトリガーされます。同時に、スキャンされるスペースが小さくなり、GC パフォーマンスの消費も小さくなり、GC 実行時間も短くなります。
マイナー GC が完了するたびに、生き残ったオブジェクトの年齢は 1 になります。複数のマイナー GC を乗り越えたオブジェクト (年齢が N より大きい) は、旧世代のメモリ プールに移動されます。
旧世代は、存続期間の長いオブジェクトを格納するために使用される大規模なメモリ プールです。旧世代のメモリは、Mark-Soup および Mark-Compact アルゴリズムを使用します。この実行の 1 つは Mayor GC と呼ばれます。古い世代のオブジェクトが特定の割合を占めると、つまり、全オブジェクトに対する生き残ったオブジェクトの割合が特定のしきい値を超えると、マーク クリア または マーキング圧縮 がトリガーされます。
空間が広いため、GCの実行時間も長くなり、頻度も新世代に比べて低くなります。古い世代が GC リサイクルを完了した後もスペースがまだ不十分な場合、V8 はシステムから追加のメモリを申請します。
global.gc() メソッドを手動で実行し、さまざまなパラメータを設定して、GC をアクティブにトリガーできます。 ただし、このメソッドは Node.js ではデフォルトで無効になっていることに注意してください。これを有効にしたい場合は、Node.js アプリケーションの起動時に --expose-gc パラメーターを追加することで有効にできます。例:
node --expose-gc app.js
V8 Mark is主に旧世代で使用されているガベージコレクションは、-SweatとMark-Compactを組み合わせて実行されます。
Mark-Soup はマーク スイープを意味し、マークとスイープの 2 段階に分かれています。 マーク-スイープ マーキング フェーズでは、ヒープ内のすべてのオブジェクトが走査され、ライブ オブジェクトがマークされます。後続のクリア フェーズでは、マークされていないオブジェクトのみがクリアされます。
マークスイープ 最大の問題は、マークスイープの実行後にメモリ空間が不連続になってしまうことです。この種のメモリの断片化は、大きなオブジェクトを割り当てる必要がある可能性が非常に高いため、その後のメモリ割り当てに問題を引き起こします。この時点では、すべての断片化された領域では割り当てを完了できず、事前にガベージ コレクションがトリガーされます。このリサイクルは不要です。
Mark-Soup のメモリ断片化問題を解決するために、Mark-Compact が提案されました。 Mark-Compact は、Mark-Soup に基づくマーク コンパイルを意味します。それらの違いは、オブジェクトが死んだものとしてマークされた後、クリーニングプロセス中に生きているオブジェクトが一方の端に移動され、移動が完了した後、境界の外側のメモリが直接クリアされることです。 V8 また、一定量の空きメモリを解放し、特定のロジックに基づいてシステムに返します。
from 領域ではなく、ラージ オブジェクト空間に直接作成されるのでしょうか?情報とソースコードを調べた結果、ついに答えが見つかりました。デフォルトでは 256K です。V8 は変更コマンドを公開していないようです。ソース コード内の v8_enable_hugepage 構成はパッケージ化時に設定する必要があります。
chromium.googlesource.com/v8/v8.git/ …
// There is a separate large object space for objects larger than // Page::kMaxRegularHeapObjectSize, so that they do not have to move during // collection. The large object space is paged. Pages in large object space // may be larger than the page size.
(1 << (18 - 1)) 的结果 256K (1 << (19 - 1)) 的结果 256K (1 << (21 - 1)) 的结果 1M(如果开启了hugPage)
在v12.x 之前:
为了保证 GC 的执行时间保持在一定范围内,V8 限制了最大内存空间,设置了一个默认老生代内存最大值,64位系统中为大约1.4G,32位为大约700M,超出会导致应用崩溃。
如果想加大内存,可以使用 --max-old-space-size 设置最大内存(单位:MB)
node --max_old_space_size=
在v12以后:
V8 将根据可用内存分配老生代大小,也可以说是堆内存大小,所以并没有限制堆内存大小。以前的限制逻辑,其实不合理,限制了 V8 的能力,总不能因为 GC 过程消耗的时间更长,就不让我继续运行程序吧,后续的版本也对 GC 做了更多优化,内存越来越大也是发展需要。
如果想要做限制,依然可以使用 --max-old-space-size 配置, v12 以后它的默认值是0,代表不限制。
参考文档:nodejs.medium.com/introducing…
新生代中的一个 semi-space 大小 64位系统的默认值是16M,32位系统是8M,因为有2个 semi-space,所以总大小是32M、16M。
--max-semi-space-size
--max-semi-space-size 设置新生代 semi-space 最大值,单位为MB。
此空间不是越大越好,空间越大扫描的时间就越长。这个分区大部分情况下是不需要做修改的,除非针对具体的业务场景做优化,谨慎使用。
--max-new-space-size
--max-new-space-size 设置新生代空间最大值,单位为KB(不存在)
有很多文章说到此功能,我翻了下 nodejs.org 网页中 v4 v6 v7 v8 v10的文档都没有看到有这个配置,使用 node --v8-options 也没有查到,也许以前的某些老版本有,而现在都应该使用 --max-semi-space-size。
执行 v8.getHeapStatistics(),查看 v8 堆内存信息,查询最大堆内存 heap_size_limit,当然这里包含了新、老生代、大对象空间等。我的电脑硬件内存是 8G,Node版本16x,查看到 heap_size_limit 是4G。
{ total_heap_size: 6799360, total_heap_size_executable: 524288, total_physical_size: 5523584, total_available_size: 4340165392, used_heap_size: 4877928, heap_size_limit: 4345298944, malloced_memory: 254120, peak_malloced_memory: 585824, does_zap_garbage: 0, number_of_native_contexts: 2, number_of_detached_contexts: 0 }
到 k8s 容器中查询 NodeJs 应用,分别查看了v12 v14 v16版本,如下表。看起来是本身系统当前的最大内存的一半。128M 的时候,为啥是 256M,因为容器中还有交换内存,容器内存实际最大内存限制是内存限制值 x2,有同等的交换内存。
所以结论是大部分情况下 heap_size_limit 的默认值是系统内存的一半。但是如果超过这个值且系统空间足够,V8 还是会申请更多空间。当然这个结论也不是一个最准确的结论。而且随着内存使用的增多,如果系统内存还足够,这里的最大内存还会增长。
容器最大内存 | heap_size_limit |
---|---|
4G | 2G |
2G | 1G |
1G | 0.5G |
1.5G | 0.7G |
256M | 256M |
128M | 256M |
process.memoryUsage() { rss: 35438592, heapTotal: 6799360, heapUsed: 4892976, external: 939130, arrayBuffers: 11170 }
通过它可以查看当前进程的内存占用和使用情况 heapTotal、heapUsed,可以定时获取此接口,然后绘画出折线图帮助分析内存占用情况。以下是 Easy-Monitor 提供的功能:
建议本地开发环境使用,开启后,尝试大量请求,会看到内存曲线增长,到请求结束之后,GC触发后会看到内存曲线下降,然后再尝试多次发送大量请求,这样往复下来,如果发现内存一直在增长低谷值越来越高,就可能是发生了内存泄漏。
使用方法
node --trace_gc app.js // 或者 v8.setFlagsFromString('--trace_gc');
[40807:0x148008000] 235490 ms: Scavenge 247.5 (259.5) -> 244.7 (260.0) MB, 0.8 / 0.0 ms (average mu = 0.971, current mu = 0.908) task [40807:0x148008000] 235521 ms: Scavenge 248.2 (260.0) -> 245.2 (268.0) MB, 1.2 / 0.0 ms (average mu = 0.971, current mu = 0.908) allocation failure [40807:0x148008000] 235616 ms: Scavenge 251.5 (268.0) -> 245.9 (268.8) MB, 1.9 / 0.0 ms (average mu = 0.971, current mu = 0.908) task [40807:0x148008000] 235681 ms: Mark-sweep 249.7 (268.8) -> 232.4 (268.0) MB, 7.1 / 0.0 ms (+ 46.7 ms in 170 steps since start of marking, biggest step 4.2 ms, walltime since start of marking 159 ms) (average mu = 1.000, current mu = 1.000) finalize incremental marking via task GC in old space requested
GCType <heapUsed before> (<heapTotal before>) -> <heapUsed after> (<heapTotal after>) MB
上面的 Scavenge 和 Mark-sweep 代表GC类型,Scavenge 是新生代中的清除事件,Mark-sweep 是老生代中的标记清除事件。箭头符号前是事件发生前的实际使用内存大小,箭头符号后是事件结束后的实际使用内存大小,括号内是内存空间总值。可以看到新生代中事件发生的频率很高,而后触发的老生代事件会释放总内存空间。
展示堆空间的详细情况
v8.setFlagsFromString('--trace_gc_verbose'); [44729:0x130008000] Fast promotion mode: false survival rate: 19% [44729:0x130008000] 97120 ms: [HeapController] factor 1.1 based on mu=0.970, speed_ratio=1000 (gc=433889, mutator=434) [44729:0x130008000] 97120 ms: [HeapController] Limit: old size: 296701 KB, new limit: 342482 KB (1.1) [44729:0x130008000] 97120 ms: [GlobalMemoryController] Limit: old size: 296701 KB, new limit: 342482 KB (1.1) [44729:0x130008000] 97120 ms: Scavenge 302.3 (329.9) -> 290.2 (330.4) MB, 8.4 / 0.0 ms (average mu = 0.998, current mu = 0.999) task [44729:0x130008000] Memory allocator, used: 338288 KB, available: 3905168 KB [44729:0x130008000] Read-only space, used: 166 KB, available: 0 KB, committed: 176 KB [44729:0x130008000] New space, used: 444 KB, available: 15666 KB, committed: 32768 KB [44729:0x130008000] New large object space, used: 0 KB, available: 16110 KB, committed: 0 KB [44729:0x130008000] Old space, used: 253556 KB, available: 1129 KB, committed: 259232 KB [44729:0x130008000] Code space, used: 10376 KB, available: 119 KB, committed: 12944 KB [44729:0x130008000] Map space, used: 2780 KB, available: 0 KB, committed: 2832 KB [44729:0x130008000] Large object space, used: 29987 KB, available: 0 KB, committed: 30336 KB [44729:0x130008000] Code large object space, used: 0 KB, available: 0 KB, committed: 0 KB [44729:0x130008000] All spaces, used: 297312 KB, available: 3938193 KB, committed: 338288 KB [44729:0x130008000] Unmapper buffering 0 chunks of committed: 0 KB [44729:0x130008000] External memory reported: 20440 KB [44729:0x130008000] Backing store memory: 22084 KB [44729:0x130008000] External memory global 0 KB [44729:0x130008000] Total time spent in GC : 199.1 ms
每次GC事件的详细信息,GC类型,各种时间消耗,内存变化等
v8.setFlagsFromString('--trace_gc_nvp'); [45469:0x150008000] 8918123 ms: pause=0.4 mutator=83.3 gc=s reduce_memory=0 time_to_safepoint=0.00 heap.prologue=0.00 heap.epilogue=0.00 heap.epilogue.reduce_new_space=0.00 heap.external.prologue=0.00 heap.external.epilogue=0.00 heap.external_weak_global_handles=0.00 fast_promote=0.00 complete.sweep_array_buffers=0.00 scavenge=0.38 scavenge.free_remembered_set=0.00 scavenge.roots=0.00 scavenge.weak=0.00 scavenge.weak_global_handles.identify=0.00 scavenge.weak_global_handles.process=0.00 scavenge.parallel=0.08 scavenge.update_refs=0.00 scavenge.sweep_array_buffers=0.00 background.scavenge.parallel=0.00 background.unmapper=0.04 unmapper=0.00 incremental.steps_count=0 incremental.steps_took=0.0 scavenge_throughput=1752382 total_size_before=261011920 total_size_after=260180920 holes_size_before=838480 holes_size_after=838480 allocated=831000 promoted=0 semi_space_copied=4136 nodes_died_in_new=0 nodes_copied_in_new=0 nodes_promoted=0 promotion_ratio=0.0% average_survival_ratio=0.5% promotion_rate=0.0% semi_space_copy_rate=0.5% new_space_allocation_throughput=887.4 unmapper_chunks=124 [45469:0x150008000] 8918234 ms: pause=0.6 mutator=110.9 gc=s reduce_memory=0 time_to_safepoint=0.00 heap.prologue=0.00 heap.epilogue=0.00 heap.epilogue.reduce_new_space=0.04 heap.external.prologue=0.00 heap.external.epilogue=0.00 heap.external_weak_global_handles=0.00 fast_promote=0.00 complete.sweep_array_buffers=0.00 scavenge=0.50 scavenge.free_remembered_set=0.00 scavenge.roots=0.08 scavenge.weak=0.00 scavenge.weak_global_handles.identify=0.00 scavenge.weak_global_handles.process=0.00 scavenge.parallel=0.08 scavenge.update_refs=0.00 scavenge.sweep_array_buffers=0.00 background.scavenge.parallel=0.00 background.unmapper=0.04 unmapper=0.00 incremental.steps_count=0 incremental.steps_took=0.0 scavenge_throughput=1766409 total_size_before=261207856 total_size_after=260209776 holes_size_before=838480 holes_size_after=838480 allocated=1026936 promoted=0 semi_space_copied=3008 nodes_died_in_new=0 nodes_copied_in_new=0 nodes_promoted=0 promotion_ratio=0.0% average_survival_ratio=0.5% promotion_rate=0.0% semi_space_copy_rate=0.3% new_space_allocation_throughput=888.1 unmapper_chunks=124
const { writeHeapSnapshot } = require('node:v8'); v8.writeHeapSnapshot()
打印快照,将会STW,服务停止响应,内存占用越大,时间越长。此方法本身就比较费时间,所以生成的过程预期不要太高,耐心等待。
注意:生成内存快照的过程,会STW(程序将暂停)几乎无任何响应,如果容器使用了健康检测,这时无法响应的话,容器可能被重启,导致无法获取快照,如果需要生成快照、建议先关闭健康检测。
兼容性问题:此 API arm64 架构不支持,执行就会卡住进程 生成空快照文件 再无响应, 如果使用库 heapdump,会直接报错:
(mach-o file, but is an incompatible architecture (have (arm64), need (x86_64))
此 API 会生成一个 .heapsnapshot 后缀快照文件,可以使用 Chrome 调试器的“内存”功能,导入快照文件,查看堆内存具体的对象数和大小,以及到GC根结点的距离等。也可以对比两个不同时间快照文件的区别,可以看到它们之间的数据量变化。
一个 Node 应用因为内存超过容器限制经常发生重启,通过容器监控后台看到应用内存的曲线是一直上升的,那应该是发生了内存泄漏。
使用 Chrome 调试器对比了不同时间的快照。发现对象增量最多的是闭包函数,继而展开查看整个列表,发现数据量较多的是 mongo 文档对象,其实就是闭包函数内的数据没有被释放,再通过查看 Object 列表,发现同样很多对象,最外层的详情显示的是 Mongoose 的 Connection 对象。
到此为止,已经大概定位到一个类的 mongo 数据存储逻辑附近有内存泄漏。
再看到 Timeout 对象也比较多,从 GC 根节点距离来看,这些对象距离非常深。点开详情,看到这一层层的嵌套就定位到了代码中准确的位置。因为那个类中有个定时任务使用 setInterval 定时器去分批处理一些不紧急任务,当一个 setInterval 把事情做完之后就会被 clearInterval 清除。
コード ロジック分析を通じて、最終的に問題を発見しました。タイマーが停止しない原因は、clearInterval のトリガー条件でした。クリアされ、サイクルは継続します。タイマーは実行を続けます。このコードとその中のデータはまだクロージャ内にあり、GC によってリサイクルできないため、メモリは上限に達してクラッシュするまでどんどん大きくなります。
ここで setInterval を使う方法は無茶です ちなみに、大量のリクエストを避けるために await キュー順次実行を使用するように変更しましたコードもより明確になりました。このコードは比較的古いものであるため、そもそもなぜ setInterval が使用されたのかについては考慮しません。
新しいバージョンがリリースされてから 10 日以上観察した後、平均メモリは 100M 強にとどまり、GC は一時的に増加したメモリを正常に再利用し、波状の曲線を示し、リークは発生しなくなりました。
#これまで、メモリ リークはメモリ スナップショットを使用して分析され、解決されてきました。もちろん、実際の解析には多少の紆余曲折が必要であり、この記憶スナップショットの内容は、理解するのが容易ではなく、単純なものでもありません。スナップショット データの表示は型集約であるため、手がかりを見つけるには、独自のコードの包括的な分析と組み合わせて、さまざまなコンストラクターと内部データの詳細を確認する必要があります。 たとえば、当時取得したメモリ スナップショットから判断すると、クロージャ、文字列、mongo モデル クラス、タイムアウト、オブジェクト などを含む大量のデータがありました。実際、これらの増分データはすべて来ていました。問題のあるコードからのものであり、GC によってリサイクルすることはできません。
Java や Go:## など、言語ごとに GC 実装が異なります。 #Java:
JVM(ノード V8 に対応) を理解します。Java も世代戦略を採用しています。新しい世代には eden 領域もあります. 、新しいオブジェクトがこの領域に作成されます。 V8 新世代には eden 領域がありません。 Go: マーク除去の使用、3 色のマーキング アルゴリズム
言語ごとに GC 実装は異なりますが、基本的にはすべて、異なるアルゴリズムの組み合わせを使用して実装されます。パフォーマンスの点では、組み合わせが異なるとあらゆる面でパフォーマンス効率が異なりますが、それらはすべてトレードオフであり、異なるアプリケーション シナリオに偏っているだけです。
ノード関連の知識の詳細については、
nodejs チュートリアル以上がNode V8 エンジンのメモリと GC の詳細な図による説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。