Dieser Artikel vermittelt Ihnen ein detailliertes Verständnis des Speichers und Garbage Collectors (GC) der NodeJS V8-Engine. Ich hoffe, er wird Ihnen hilfreich sein!
Garbage Collection-Programm
(im Folgenden als Garbage Collection bezeichnet). als GC) Automatisch zugeteilt und freigegeben.In den frühen Tagen der Softwareentwicklung oder einiger Sprachen wurde Heap-Speicher manuell zugewiesen und freigegeben, z. B. C
,C++. Obwohl es den Speicher präzise bedienen und die bestmögliche Speichernutzung erzielen kann, ist die Entwicklungseffizienz sehr gering und es besteht die Gefahr einer fehlerhaften Speicheroperation. [Verwandte Tutorial-Empfehlungen: nodejs-Video-Tutorial, Programmierlehre]Mit der Entwicklung der Technologie erfordern Hochsprachen (wie Java
Node) keine manuelle Bedienung des Speichers durch Entwickler , und Programmiersprachen weisen automatisch Speicherplatz zu und geben ihn frei. Gleichzeitig wurde auch der Müllsammler GC (Garbage Collection) geboren, der dabei hilft, Erinnerungen freizugeben und zu organisieren. In den meisten Fällen müssen sich Entwickler nicht um den Speicher selbst kümmern und können sich auf die Geschäftsentwicklung konzentrieren. Der folgende Artikel befasst sich hauptsächlich mit Heap-Speicher und GC. 2. Der GC-Vorgang verbraucht CPU-Ressourcen. Der GC-Vorgang löst STW (Stop-the-World) aus, um den Geschäftscode-Thread anzuhalten. Dadurch soll sichergestellt werden, dass es während des GC-Prozesses nicht zu Konflikten mit neu erstellten Objekten kommt.
Phase 3 Multi-Thread gleichzeitiger gleichzeitiger GC ( Vertreter: CMS (Concurrent Mark Sweep) G1)
Heap-Speicherdesign und GC-Design sind eng miteinander verbunden. V8 unterteilt den Heap-Speicher in mehrere Hauptbereiche und verfolgt eine Generationsstrategie.3. v8-Speicherpartition und GC
Neuer Raum oder junge Generation: Der Raum ist klein und in zwei Halbräume unterteilt, und die Datenüberlebenszeit ist kurz.
Large-Object-Space
: Standardmäßig werden Objekte mit mehr als 256 KB darunter angezeigt Speicherplatz, das Folgende erklärtCode-Bereich: Der Just-in-Time-Compiler (JIT) speichert hier kompilierten Code
Die neue Generation ist ein kleiner, schneller Speicherpool, der junge Objekte speichert. Er ist in zwei Halbräume (Halbraum) unterteilt, von denen die Hälfte frei ist (als Raum bezeichnet). Die andere Hälfte des Speicherplatzes speichert Daten (aus dem Weltraum aufgerufen).
Wenn Objekte zum ersten Mal erstellt werden, werden sie der jungen Generation aus dem Halbraum zugeordnet, der ein Alter von 1 hat. Wenn der from-Speicherplatz nicht ausreicht oder eine bestimmte Größe überschreitet, wird Minor GC (unter Verwendung des Kopieralgorithmus Scavenge) ausgelöst. Zu diesem Zeitpunkt unterbricht der GC die Ausführung der Anwendung (STW, stop-the). -Welt) und Markierung ( Alle aktiven Objekte im Von-Raum) werden dann sortiert und kontinuierlich in einen anderen freien Raum (in den Raum) der neuen Generation verschoben. Schließlich wird der gesamte Speicher im ursprünglichen from-Bereich freigegeben und wird zu freiem Speicherplatz. Die beiden Leerzeichen vervollständigen den Austausch von from und to. Der Kopieralgorithmus ist ein Algorithmus, der Platz für Zeit opfert.
Der Raum der neuen Generation ist kleiner, sodass dieser Raum häufiger GC auslöst. Gleichzeitig ist der gescannte Raum kleiner, der GC-Leistungsverbrauch ist ebenfalls geringer und die GC-Ausführungszeit ist ebenfalls kürzer.
Jedes Mal, wenn ein Minor GC abgeschlossen ist, beträgt das Alter der überlebenden Objekte +1. Objekte, die mehrere Minor GCs überstanden haben (Alter größer als N), werden in den Speicherpool der alten Generation verschoben.
Die alte Generation ist ein großer Speicherpool, der zum Speichern langlebiger Objekte verwendet wird. Der Speicher der alten Generation verwendet den Mark-Sweep und den Mark-Compact-Algorithmus. Eine Ausführung davon heißt Mayor GC. Wenn die Objekte der alten Generation einen bestimmten Anteil ausfüllen, d. h. das Verhältnis der überlebenden Objekte zur Gesamtzahl der Objekte einen bestimmten Schwellenwert überschreitet, wird eine „Markierung löschen“ oder eine „Markierungskomprimierung“ ausgelöst. Da der Speicherplatz größer ist, ist auch die GC-Ausführungszeit länger und die Häufigkeit geringer als bei der neuen Generation. Wenn nach Abschluss des GC-Recyclings der alten Generation immer noch nicht genügend Speicherplatz vorhanden ist, beantragt V8 mehr Speicher vom System. Sie können die Methode global.gc() manuell ausführen, verschiedene Parameter festlegen und GC aktiv auslösen. Es ist jedoch zu beachten, dass diese Methode in Node.js standardmäßig deaktiviert ist. Wenn Sie es aktivieren möchten, können Sie es aktivieren, indem Sie beim Starten der Node.js-Anwendung den Parameter --expose-gc hinzufügen, zum Beispiel:
node --expose-gc app.js
In der alten Generation
Mark-Sweepund Mark -Kompakt werden hauptsächlich zur kombinierten Müllabfuhr verwendet. Mark-Sweep bedeutet Mark Sweep, das in zwei Stufen unterteilt ist: Mark und Sweep.
Mark-SweepIn der Markierungsphase durchläuft es alle Objekte im Heap und markiert die lebenden Objekte. In der anschließenden Löschphase werden nur nicht markierte Objekte gelöscht. Mark-Sweep Das größte Problem besteht darin, dass nach einer Mark-Sweep-Wiederherstellung der Speicherplatz diskontinuierlich wird. Diese Art der Speicherfragmentierung führt zu Problemen bei der späteren Speicherzuweisung, da die Wahrscheinlichkeit groß ist, dass ein großes Objekt zugewiesen werden muss. Zu diesem Zeitpunkt kann die Speicherbereinigung nicht vollständig abgeschlossen werden, und die Speicherbereinigung wird im Voraus ausgelöst Dieses Recycling ist unnötig.
Um das Speicherfragmentierungsproblem von Mark-Sweep zu lösen, wurde
Mark-Compactvorgeschlagen. Mark-Compact bedeutet Markenzusammenstellung, die auf Mark-Sweep basiert. Der Unterschied besteht darin, dass die lebenden Objekte während des Reinigungsvorgangs an ein Ende verschoben werden, nachdem das Objekt als tot markiert wurde. Nach Abschluss der Bewegung wird der Speicher außerhalb der Grenze direkt gelöscht. V8 gibt außerdem eine bestimmte Menge freien Speichers frei und gibt ihn basierend auf einer bestimmten Logik an das System zurück. 3.2 Großer Objektraum Großer Objektraum
, V8 scheint keine Änderungsbefehle verfügbar zu machen, die v8_enable_hugepage-Konfiguration im Quellcode sollte beim Packen festgelegt werden. chromium.googlesource.com/v8/v8.git/+…
source.chromium.org/chromium/ch…// 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.Nach dem Login kopieren
(1 << (18 - 1)) 的结果 256K (1 << (19 - 1)) 的结果 256K (1 << (21 - 1)) 的结果 1M(如果开启了hugPage)Nach dem Login kopieren四、V8 新老分区大小
4.1 老生代分区大小
在v12.x 之前:
为了保证 GC 的执行时间保持在一定范围内,V8 限制了最大内存空间,设置了一个默认老生代内存最大值,64位系统中为大约1.4G,32位为大约700M,超出会导致应用崩溃。
如果想加大内存,可以使用 --max-old-space-size 设置最大内存(单位:MB)
node --max_old_space_size=Nach dem Login kopieren在v12以后:
V8 将根据可用内存分配老生代大小,也可以说是堆内存大小,所以并没有限制堆内存大小。以前的限制逻辑,其实不合理,限制了 V8 的能力,总不能因为 GC 过程消耗的时间更长,就不让我继续运行程序吧,后续的版本也对 GC 做了更多优化,内存越来越大也是发展需要。
如果想要做限制,依然可以使用 --max-old-space-size 配置, v12 以后它的默认值是0,代表不限制。
参考文档:nodejs.medium.com/introducing…
4.2 新生代分区大小
新生代中的一个 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。
五、 内存分析相关API
5.1 v8.getHeapStatistics()
执行 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 }Nach dem Login kopieren到 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 5.2 process.memoryUsage
process.memoryUsage() { rss: 35438592, heapTotal: 6799360, heapUsed: 4892976, external: 939130, arrayBuffers: 11170 }Nach dem Login kopieren通过它可以查看当前进程的内存占用和使用情况 heapTotal、heapUsed,可以定时获取此接口,然后绘画出折线图帮助分析内存占用情况。以下是 Easy-Monitor 提供的功能:
建议本地开发环境使用,开启后,尝试大量请求,会看到内存曲线增长,到请求结束之后,GC触发后会看到内存曲线下降,然后再尝试多次发送大量请求,这样往复下来,如果发现内存一直在增长低谷值越来越高,就可能是发生了内存泄漏。
5.3 开启打印GC事件
使用方法
node --trace_gc app.js // 或者 v8.setFlagsFromString('--trace_gc');Nach dem Login kopieren
- --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 requestedNach dem Login kopierenGCType <heapUsed before> (<heapTotal before>) -> <heapUsed after> (<heapTotal after>) MBNach dem Login kopieren上面的 Scavenge 和 Mark-sweep 代表GC类型,Scavenge 是新生代中的清除事件,Mark-sweep 是老生代中的标记清除事件。箭头符号前是事件发生前的实际使用内存大小,箭头符号后是事件结束后的实际使用内存大小,括号内是内存空间总值。可以看到新生代中事件发生的频率很高,而后触发的老生代事件会释放总内存空间。
- --trace_gc_verbose
展示堆空间的详细情况
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 msNach dem Login kopieren
- --trace_gc_nvp
每次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=124Nach dem Login kopieren5.4 内存快照
const { writeHeapSnapshot } = require('node:v8'); v8.writeHeapSnapshot()Nach dem Login kopieren打印快照,将会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 清除。
Leckauflösung und -optimierung
Durch die Code-Logikanalyse haben wir schließlich das Problem gefunden. Es handelte sich um ein Problem mit der Auslösebedingung von clearInterval, das dazu führte, dass der Timer nicht gelöscht wurde und die Schleife fortgesetzt wurde. Der Timer wird weiterhin ausgeführt. Dieser Code und die darin enthaltenen Daten befinden sich noch im Abschluss und können von GC nicht recycelt werden, sodass der Speicher immer größer wird, bis er die Obergrenze erreicht und abstürzt.
Die Verwendung von setInterval ist hier übrigens unvernünftig, es wurde geändert, um für die sequenzielle Ausführung in der Warteschlange zu verwenden, um eine große Anzahl gleichzeitiger Zugriffe zu vermeiden, und der Code sollte viel sein klarer. Da dieser Codeabschnitt relativ alt ist, werde ich nicht darauf eingehen, warum setInterval überhaupt verwendet wurde.
Nach mehr als zehn Tagen Beobachtung nach der Veröffentlichung der neuen Version blieb der durchschnittliche Speicher bei knapp über 100 MB. Der GC recycelte normalerweise den vorübergehend erhöhten Speicher und zeigte eine wellenförmige Kurve, und es traten keine Lecks mehr auf. Bisher wurde der Speicherverlust mithilfe von Speicher-Snapshots analysiert und behoben. Natürlich erfordert die eigentliche Analyse einige Wendungen. Der Inhalt dieses Erinnerungsschnappschusses ist nicht leicht zu verstehen und nicht so einfach. Bei der Anzeige von Snapshot-Daten handelt es sich um eine Typaggregation. Um einige Hinweise zu finden, müssen Sie sich verschiedene Konstruktoren und interne Datendetails ansehen und Ihren eigenen Code umfassend analysieren. Aus dem Speicher-Snapshot, den ich damals erhalten habe, geht beispielsweise hervor, dass es eine große Menge an Daten gab, darunter Abschlüsse, Zeichenfolgen, Mongo-Modellklassen, Zeitüberschreitungen, Objekte usw. Tatsächlich stammten diese inkrementellen Daten aus dem problematischen Code und konnten nicht seinGC Recycling. 6. Schließlich haben
verschiedene Sprachen unterschiedliche GC-Implementierungen, wie Java
undGo: Java: Verstehen Sie JVM
(entsprechend Node V8),Java verwendet auch Generierung Strategie, es gibt auch einen eden-Bereich in seiner neuen Generation, und in diesem Bereich werden neue Objekte erstellt. Und V8 neue Generation hat keinen eden Bereich. Los: Verwendung von Markierungslöschung, dreifarbiger Markierungsalgorithmus
Verschiedene Sprachen haben unterschiedliche GC-Implementierungen, aber im Wesentlichen werden sie mithilfe einer Kombination verschiedener Algorithmen implementiert. In Bezug auf die Leistung führen unterschiedliche Kombinationen in allen Aspekten zu unterschiedlichen Leistungseffizienzen, aber alle gehen Kompromisse ein und sind lediglich auf unterschiedliche Anwendungsszenarien ausgerichtet. Weitere Informationen zu Knoten finden Sie unter:nodejs-Tutorial
!
Das obige ist der detaillierte Inhalt vonDetaillierte grafische Erläuterung des Speichers und des GC der Node V8-Engine. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!