NodeJS中V8的内存控制
V8的垃圾回收机制与内存限制
V8的内存限制
v8引擎最开始是用在浏览器上,其内存控制策略需要提前申请一块内存空间, 后续不会再扩展内存空间。考虑浏览器的使用场景,v8的团队为v8预设了一个合理的内存空间,原因如下:
- 需要申请的内存越大,每次执行垃圾回收的时间成本很增大。
看下文的V8垃圾回收机制就能知道了,V8启动时就已经设置好了要用多大的内存,之后的垃圾回收都在这块内存里面做。 因此无论是新生代的内存交换还是老生代的标记、整理策略,使用的内存块越大每次垃圾回收的时间就会变长。
- v8限制的内存,对浏览器来说绰绰有余
对于网页来说,V8的限制值已经绰绰有余。深层原因是V8的垃圾回收机制的限制。按官方的说法,以1.5 GB的垃圾回收堆内存为例,V8做一次小的垃圾回收需要50毫秒以上,做一次非增量式的垃圾回收甚至要1秒以上。这是垃圾回收中引起JavaScript线程暂停执行的时间,在这样的时间花销下,应用的性能和响应能力都会直线下降。
限制解除,v8还是提供了方案来解除这个限制的:
--max-new-space-size
可以用来调整新生代堆内存的大小
--max-old-space-size
用来调整老生代内存的大小
v8的垃圾回收策略
分代式垃圾回收机制
根据对象的存活时间来对对象划分“阶级”,针对不同的阶级采用不同的垃圾回收策略。 v8将内存划分为新生代区
和老生代区
这针对这两块内存区间采用不同的垃圾回收算法,新生代区还有一套晋升机制,将对象晋升为老生代
新生代垃圾回收策略
使用Scavenge算法(Minor GC),将新生代的内存区一分为二,分别叫做form区
和to区
。代码中新建的对象会优先分配到form区
,当form区
存满时,触发复制策略,将form区
的内存对象复制到to区
,此时form区
和to区
翻转,原来的form变成to,to变成form。
对象的晋升策略:
- 如果一个对象经过一轮的GC任然存活,那么这个对象就会晋升到老生代内存区
- 如果复制对象时to区的占用超过了25%,那么这对象直接晋升到老生代区
💡为什么会有to区25%这个限额?
一轮的GC后to和from会翻转,如果新的from区内存占用比过高,会影响后续的内存分配。
Scavenge算法是一个典型的空间换时间的算法,直接将存活的内存完全复制过去,内存区一次清空,且内存总是连片的,不需要经历内存重排也不会出现“缝隙”。基于这个特性,它就非常适合处理活的比较短的新生代内存对象(这类对象存活时间较短,不会长时间占用内存),当然缺点也很明显,它将一个内存区划分成两半,只用了一半的内存,如果这个策略放在对象存活较久的老生代区,那就白白浪费了另一半内存而且每次复制都会有一堆存活的对象。
老生代垃圾回收策略
采用Mark-Sweep(下文简称MS) & Mark-Compact(下文简称MC)两种策略。
-
Mark-Sweep标记清除
清理死亡的对象。经历两个阶段,第一个阶段遍历堆中所有的对象,标记活着的对象 第二个阶段开始清除没有被标记的对象,也就是“死掉”的对象。
Mark-Sweep的缺点也很明显,它会在堆中创造一堆不连续的内存块,因此需要结合Mark-Compact来处理这些碎块
-
Mark-Compact标记整理 把活着的对象移动到一起,整理出一大块连续的内存空间。
MS和MC何时使用?
由于MC会对内存进行重排调整,是上述三种策略中最慢最复杂的。因此当对象晋升到老生代的时候,如果堆内存以及没有这个对象的容身之所才会启动MC这个最慢的策略,来重新调整出一大块连续的内存,这样能够保证程序的执行效率
Incremental Marking (增量标记)
为了避免垃圾回收的时候垃圾回收器看到的和实际js线程的情况不一致,上述三种算法的执行都需要暂停主线程来进行垃圾回收。
待执行完垃圾回收后再恢复执行应用逻辑,这种行为被称为“全停顿"
新生代的对象处理较快,一般停顿时间也不会很久。老生代的处理较慢,停顿较久。因此需要对这个流程进行优化,v8采用的是一种步进式的策略,拆成多个小步骤执行,每次执行完了就让js线程运行一下。
将传统“标记”阶段拆成如下过程:
1. 初始标记(Initial Mark)
暂停 JS 执行,标记根对象(Root Set),例如全局对象、栈上的引用。
停顿时间短,快速完成。
2. 增量标记(Incremental Marking)
恢复 JS 执行,后台线程在 JS 执行过程中穿插标记任务。
每执行一小段 JS,就跑一点标记逻辑。
这时会记录 JS 程序运行中新创建/修改的对象引用(Write Barrier)。
3. 最终标记(Final Mark / Stop-the-world)
再次暂停 JS 执行,处理增量阶段未完成的部分。
这部分也尽量精简,提高响应速度。
4. 清除阶段(Sweep)
回收未被标记的对象,占用的内存归还堆空间。
V8在经过增量标记的改进后,垃圾回收的最大停顿时间可以减少到原本的1/6左右。V8后续还引入了延迟清理(lazy sweeping)与增量式整理(incremental compaction),让清理与整理动作也变成增量式的。同时还计划引入并行标记与并行清理,进一步利用多核性能降低每次停顿的时间。