Loading...
Loading...
自从前段时间了解了V8的一些底层原理后,我对这个高性能的JavaScript引擎就充满了好奇,刚好最近在学习实习相关的内容,看了很多面经,发现前端面试的八股中经常会被问到JS的内存管理,这一高频问题的本质其实就是在问面试者对于V8 GC原理的理解,联想到我本来就想多点了解V8的机制原理,于是就去网上找了一些资料学了一下有关V8内存管理的原理,但由于这部分内容实在是比较复杂,因此还是决定写一篇博客来加深一下我的印象,也方便后续复习。
在上一篇有关V8的博客中,我们提到了,V8底层中对于栈和堆两种内存有两种不同的作用:
之前我们说过,当一个存储在堆内存中的变量不被任何其它变量引用时(下文简称引用解除),GC会在下一次触发时回收这片内存。但之前我们没有讲到的是,堆内存中的结构是怎么样的,GC又是如何“知道”每个变量有没有被其它变量引用的。
在本节中,我们首先来讲讲,堆内存中的结构是什么样的,以及V8是如何分配堆内存中的空间的。
一个正在运行的程序是由 V8 进程分配的内存来表示的,这被称为 Resident Set(常驻集)。这些内存会进一步划分成不同的部分:
min_semi_space_size(Initial) 和 max_semi_space_size(Max) ,分别表示最小(最初)分配的大小,以及最大的大小。initial_old_space_size(Initial) 和 max_old_space_size(Max) 。在这其中,我们重点要讨论的就是新空间和旧空间,因为只有这两个类型的堆内存才会受到GC的管理。
因为在内存中,栈内存是被操作系统管理的,当一个函数或其它调用栈执行完毕后,它的作用域及其内部的所有可回收数据会随着栈顶的移动而被自动弹出、失效;但堆内存却不会被这样管理,因此,在JS中,我们需要一种机制来接管堆内存,于是V8设计出了一个叫 Orinoco 的垃圾回收器,也就是GC。而GC在新空间和旧空间中,管理内存的方式是不一样的,它们分别被叫做Minor GC和Major GC。
简单来说,新空间内部有一个分配指针,表示目前已有的内存的边界,每当有新的对象被分配到了新空间内时,分配指针就会递增,而当指针到达了新空间的末端时,就会触发一次Minor GC,这个过程也被称为Scavenger,它实现了一个名为Cheney的算法。因为新空间的变化频率非常高,而且新空间本身比较小,所以这个算法会采用并行的辅助线程进行处理,速度非常快。
新空间会被分为两个等大的部分,分别是from-space和to-space,其中from-space是所有新对象加入内存时被插入的地方。
我们假设这样一种情景:from-space中现在已经有内容了,分别存储了01~06六个对象,此时来了一个对象07,V8发现from-space中已经没有足够的空间分配给这个新对象07了,那么它就会触发一次Minor GC。
具体的过程就是,GC首先会从Root指针(又称GC Root,主要是上下文中的局部变量、全局对象、内置对象和DOM引用等)开始,递归检查from-space中的内容。找到那些正在被使用,或者说是还活着的对象,GC为它们打上一个“被引用”的标签,接着将它们转移到to-space空间中去,更新它们的指针引用,并且会执行一次压缩,以减少内存中的碎片空间。
这一过程完成后,from-space中剩余的对象,很显然就是那些已经被解除引用的“垃圾”了,GC就会来回收它们。
接下来,一个非常聪明的操作就出现了:V8会交换from-space和to-space的名称,然后将对象07插入到from-space中,现在,from-space依然存着01到07的所有数据,而to-space也还是空的,这样就保证了内存的紧凑性,而不至于是这边一点、那边一点,拖累程序的执行效率。
这样一系列过程运行后,V8就完成了一次Minor GC,这也就是前文提到的“一个Minor GC周期”,它为新的对象分配了足够的空间,同时还顺带整理和清理了一下新空间中的旧数据。
但是,新空间的内存大小是有限的,并且比较小,它注定不能长期存储数据,当很多个Minor GC周期(一般是两个)后,还停留在新空间中的数据该怎么处理呢?如果新空间满了又该如何处理呢?
当一个新空间中的数据在两个Minor GC中存活下来时,它就会被V8认作是“旧数据”,并被转移到旧空间中,这个过程被称为对象晋升。
此时,如果旧空间没有足够的空间分配给这个新晋升的旧数据时,一个和新空间类似的垃圾回收操作就会产生,它被称为Major GC。
Major GC的原理和Minor GC是不一样的,因为Minor GC的Cheney算法尽管在小空间和小数据量中非常完美、效率很高,但对于大空间(比如旧空间)来说是不切实际的——因为它有不可忽视的内存开销。
因此,V8对于旧空间的内存管理,有一套新的机制,就是Mark-Sweep-Compact(标记-清扫-压缩),顾名思义,它分为三个阶段:
这种类型的 GC 也被称为 stop-the-world GC(世界停止GC),因为它在执行的过程中会引入暂停时间。为了避免这种情况,V8 使用了如下技术:
最后,让我们来完整的看一下 Major GC 的过程:
在学习完这一系列过程后不由得感慨,V8真是一群聪明人设计出来的聪明引擎,它的很多流程和设计都体现出了开发者在设计这一过程时的思考,理解这些思想对于后续学习JS的很多底层实现和原理都是十分有帮助的。
这篇博客写的很潦草,仅仅是匆匆讲了一下内存管理的核心思想,也许读起来会感觉”也就那样“,但这并不是因为它本来就这样,而是因为我的表达水平实在有限,实际的GC原理会更加复杂,也牵扯到更多深层次的概念,如果有机会,我也还会继续进一步学习更多V8相关的原理。
最后,感谢网络上的各位大佬能将高深莫测的V8底层原理翻译为人类可以听得懂的语言,这无疑降低了很多我的学习和理解成本,如果直接对着V8官方技术博客的英文生肉啃的话,我有限的脑容量也许这辈子也理解不了它的思路(