이 기사는 NodeJS V8 엔진의 메모리 및 가비지 수집기(GC)에 대한 심층적인 이해를 제공할 것입니다. 도움이 되기를 바랍니다!
Garbage Collection Program
(이하 Garbage Collection) GC라고 함) 자동으로 할당 및 해제됩니다.소프트웨어 개발 초기나 일부 언어에서는 C
,C++ 등 수동으로 힙 메모리를 할당하고 해제했습니다. 메모리를 정확하게 작동하고 최상의 메모리 사용을 달성할 수 있지만 개발 효율성이 매우 낮고 메모리 작동이 제대로 이루어지지 않는 경향이 있습니다. [관련 튜토리얼 추천 : nodejs 영상 튜토리얼, Programming Teaching]기술의 발전으로 고급 언어 (예: Java
Node)에서는 개발자가 메모리를 수동으로 조작할 필요가 없습니다. , 프로그래밍 언어는 자동으로 공간을 할당하고 해제합니다. 동시에 메모리 해제 및 정리를 돕기 위해 GC(Garbage Collection) 가비지 컬렉터도 탄생했습니다. 대부분의 경우 개발자는 메모리 자체에 신경 쓸 필요 없이 사업 개발에만 집중할 수 있습니다. 다음 문서에서는 주로 힙 메모리와 GC에 대해 설명합니다. 2. GC 개발
Phase 1 단일 스레드 GC(대표: 직렬)
해야 합니다. GC 초기 단계이고 성능도 최악Phase two 병렬 멀티스레드 GC(대표 : Parallel Scavenge, ParNew)
3단계 다중 스레드 동시 동시 GC( 대표자: CMS (Concurrent Mark Sweep) G1)
이전 두 개발 단계의 GC 알고리즘은 완전히 STW이며 동시 GC에서는 GC 스레드의 일부 단계가 비즈니스 코드와 동시에 실행될 수 있으므로 STW 시간이 단축됩니다. 하지만 이 모드에서는 GC 프로세스 중에 새로운 개체가 들어올 수 있기 때문에 표시 오류가 발생합니다. 물론 알고리즘 자체가 이 문제를 수정하고 해결합니다위의 세 단계가 GC가 반드시 다음 중 하나여야 한다는 의미는 아닙니다. 위에서 설명한 세 가지. 다양한 프로그래밍 언어로 된 GC는 다양한 필요에 따라 다양한 알고리즘 조합을 사용하여 구현됩니다.
3. v8 메모리 파티션과 GC
Node.js에서 GC는 세대별 전략을 채택하고 있으며 대부분의 메모리 데이터는 이 두 영역에 있습니다.
New Generation은 어린 나이의 물체를 저장하는 작고 빠른 메모리 풀입니다. 두 개의 반 공간(semi-space)으로 나뉘며, 그 중 절반은 무료입니다(space로 호출됨). ), 공간의 나머지 절반은 (공간에서 호출된) 데이터를 저장합니다.
객체가 처음 생성되면 1세의 하프스페이스에서 from 젊은 세대에 할당됩니다. from 공간이 부족하거나 특정 크기를 초과하면 Minor GC(복사 알고리즘 Scavenge 사용)가 트리거됩니다. 이때 GC는 애플리케이션 실행을 일시 중지합니다(STW, stop-the). -world) 및 마크(from 공간의 모든 활성 개체)는 정렬되어 새 세대의 다른 여유 공간(to space)으로 지속적으로 이동됩니다. 마침내 원본 from 공간의 모든 메모리가 해제되어 여유 공간이 됩니다. 두 공간은 from과 to의 교환을 완료합니다. 복사 알고리즘은 시간을 위해 공간을 희생하는 알고리즘입니다.
신세대의 공간은 더 작으므로 이 공간은 GC를 더 자주 트리거합니다. 동시에 스캔 공간도 더 작고, GC 성능 소모도 더 적으며, GC 실행 시간도 더 짧습니다.
마이너 GC가 완료될 때마다 살아남은 개체의 수명은 +1입니다. 여러 번의 마이너 GC(N보다 큰 수명)에서 살아남은 개체는 이전 세대 메모리 풀로 이동됩니다.
Old Generation은 수명이 긴 객체를 저장하는 데 사용되는 대규모 메모리 풀입니다. 구세대 메모리는 Mark-Sweep 및 Mark-Compact 알고리즘을 사용합니다. 그 중 하나를 Mayor GC라고 합니다. Old Generation의 객체가 특정 비율, 즉 전체 객체에 대한 생존 객체의 비율이 특정 임계값을 초과하면 markclear또는 markcompression이 트리거됩니다.
공간이 더 크기 때문에 GC 실행 시간도 더 길고, 빈도도 신세대보다 낮습니다. 이전 세대가 GC 재활용을 완료한 후에도 여전히 공간이 부족한 경우 V8은 시스템에서 더 많은 메모리를 적용합니다.
global.gc() 메서드를 수동으로 실행하고, 다양한 매개변수를 설정하고, GC를 적극적으로 트리거할 수 있습니다. 하지만 Node.js에서는 이 메서드가 기본적으로 비활성화되어 있다는 점에 유의해야 합니다. 활성화하려면 Node.js 애플리케이션을 시작할 때 --expose-gc 매개변수를 추가하여 활성화할 수 있습니다. 예:
node --expose-gc app.js
V8 이전 세대에서는 Mark-Sweep 및 Mark -Compact는 주로 복합 가비지 수집에 사용됩니다.
Mark-Sweep은 마크 스윕을 의미하며 마크와 스윕의 두 단계로 나뉩니다. Mark-Sweep 마킹 단계에서는 힙에 있는 모든 개체를 순회하고 활성 개체를 표시합니다. 후속 지우기 단계에서는 표시되지 않은 개체만 지워집니다.
Mark-Sweep 가장 큰 문제는 마크 스윕 및 재활용 후에 메모리 공간이 불연속적이 된다는 것입니다. 이러한 종류의 메모리 단편화는 큰 객체를 할당해야 할 가능성이 높기 때문에 후속 메모리 할당에 문제를 일으킬 수 있습니다. 이때 단편화된 모든 공간은 할당을 완료할 수 없으며 가비지 수집이 시작됩니다. 사전에 이러한 재활용이 필요하지 않습니다.
Mark-Sweep의 메모리 조각화 문제를 해결하기 위해 Mark-Compact이 제안되었습니다. Mark-Compact는 Mark-Sweep을 기반으로 한 마크 편집을 의미합니다. 차이점은 물체가 죽은 것으로 표시된 후 청소 과정에서 살아있는 물체가 한쪽 끝으로 이동한 후 경계 밖의 메모리가 직접 지워진다는 것입니다. V8은 또한 일정량의 여유 메모리를 해제하고 이를 특정 논리에 따라 시스템에 반환합니다.
대형 개체는 대형 개체 공간에서 직접 생성되며 다른 공간으로 이동되지 않습니다. 그렇다면 신세대 from 영역이 아닌 대형 객체 공간에서 직접 생성되는 객체는 얼마나 될까요? 정보와 소스코드를 참고한 끝에 마침내 답을 찾았습니다. 기본적으로는 256K이며, V8은 수정 명령을 노출하지 않는 것 같습니다. 패키징 시 소스 코드의 v8_enable_hugepage 구성을 설정해야 합니다.
// 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이 사용되었는지 고려하지 않겠습니다.
새 버전이 출시된 후 열흘 넘게 관찰한 결과, 평균 메모리는 100M 남짓으로 유지되었으며, GC에서는 정상적으로 일시적으로 늘어난 메모리를 재활용하여 물결 모양의 곡선을 보여주었으며 더 이상 누수 현상이 발생하지 않았습니다.
지금까지 메모리 누수는 메모리 스냅샷을 사용해 분석하고 해결했습니다. 물론 실제 분석에는 약간의 우여곡절이 필요합니다. 이 메모리 스냅샷의 내용은 이해하기 쉽지 않고 그리 간단하지도 않습니다. 스냅샷 데이터 표시는 유형 집계로, 몇 가지 단서를 찾으려면 자신의 코드에 대한 포괄적인 분석과 결합하여 다양한 생성자와 내부 데이터 세부 정보를 살펴봐야 합니다. 예를 들어 당시 제가 받은 메모리 스냅샷을 보면 클로저, 문자열, 몽고 모델 클래스, Timeout, Object 등을 포함해 엄청난 양의 데이터가 있었습니다. 사실 이러한 증분 데이터는 문제가 있는 코드에서 나온 것이므로 처리할 수 없었습니다. GC 재활용.
6. 마지막으로및 Go: Java:
JVM(Node V8에 해당)을 이해하고 Java도 생성을 사용함) 전략, 신세대에는 eden 영역도 있으며, 이 영역에서 새로운 개체가 생성됩니다. 그리고 V8 신세대에는 eden 영역이 없습니다. Go: 마크 클리어링 사용, 3색 마킹 알고리즘
언어마다 GC 구현 방식이 다르지만 기본적으로는 모두 서로 다른 알고리즘의 조합을 사용하여 구현됩니다. 성능 측면에서 서로 다른 조합은 모든 측면에서 서로 다른 성능 효율성을 가져오지만 모두 상충되며 서로 다른 애플리케이션 시나리오에 편향될 뿐입니다.
노드 관련 지식을 더 보려면
nodejs 튜토리얼위 내용은 Node V8 엔진의 메모리와 GC에 대한 자세한 그래픽 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!