V8 memory structure
v8内存结构
由于JS是单线程的,每一个JS上下文都会对应一个单独的V8进程,所有当时在你的应用中使用 service worker
时,每一个worker都会有一个V8进程。在V8中,一个运行中的程序表现为有很多已分配的内存,这被称为Resident Set。
Heap Memory
堆内存是存储对象或者动态数据的地方。这是最大的内存区块的地方,也是GC工作的地方。完整的堆内存是不会被回收的,只有新老空间是被GC管理的。
堆内存
- New space 新空间或者新生代是新对象存活的地方或者大多数对象短暂存活的地方。这块区域比较小,并且有两块
semi-space
,这块区域是被**Scavenger(Minor GC)**
管理的。这块区域的大小是被--min_semi_space_size
(初始大小)和--max_semi_space_size
(最大)来控制的。 - Old space 老空间或者老生代是对象复活的地方,或者从新空间经过两次
minor GC
循环移入的地方。这块区域是被**Major GC(Mark-Sweep & Mark-Compact)**
管理的。这块区域的大小是被--initial_old_space_size
(初始大小)和--max_old_space_size
(最大)这些V8标记来控制的。这块地方又被分为:- old pointer space 包含了复活对象,并且有指针指向了这些对象。
- old data space 包含对象,仅仅是包含数据(没有指针指向这些对象)。字符串,装箱number,未装箱的双精度数组,在经历两次
minor GC
循环从在new space
中复活被移入。
- Large Object space 这个地方是存储 在其他空间超出了空间大小限制的活跃对象。每一个对象都有它自己的内存区域。大型的对象是不会被垃圾回收器移动的。
- Code-space 这里是Just In Time(JIT) 编译器存放已编译代码区块的地方。这里是唯一有可执行内存的区域(尽管代码可能被分配在
**Large Object space**
,这些也同样是可执行的) - Cell space, property cell space, and map space:这个地方包含
Cells
,PropertyCells
, 和Maps
。每一个这些空间存储的对象都是同样的大小,并且有关于他们指向的何种对象的限制,简化的集合。Stack
栈
这是栈内存空间,并且每一个V8进程都有一个栈。这也是静态数据包括方法和函数帧,原始值,指向对象的指针存储的地方。栈内存的限制可以通过设置--stack_size
V8标识来设置。V8 memory usage (Stack vs Heap)
```javascript class Employee { constructor(name, salary, sales) { this.name = name; this.salary = salary; this.sales = sales; } }
const BONUS_PERCENTAGE = 10;
function getBonusPercentage(salary) { const percentage = (salary * BONUS_PERCENTAGE) / 100; return percentage; }
function findEmployeeBonus(salary, noOfSales) { const bonusPercentage = getBonusPercentage(salary); const bonus = bonusPercentage * noOfSales; return bonus; }
let john = new Employee(“John”, 5000, 5);
john.bonus = findEmployeeBonus(john.salary, john.sales);
console.log(john.bonus);
```
V8_memory_use.pdf
通过上面的slides你可以看到:
- 全局作用域
(Global Scope)
是在栈中保持的全局帧‘Global frame’
。 - 每一次函数调用被添加到栈内存作为一个帧块。
- 所有的本地变量包括参数和返回值都是被保存在栈中的函数帧块里。
- 所有的原始类型包括
int
和string
都是直接保存在栈上的。这对于全局作用域也是如此,JS把string作为基础类型。 - 所有的对象类型如
Employee
和Function
是在堆上创建的,并且在镇中有栈指针作为引用。函数在JS中也是对象。这同样也适用于于全局作用域。 - 从当前函数来的函数调用会被push到栈的最顶层。
- 当一个函数返回它的帧 是已被从栈中移除了。
- 当主程序完成了,堆中的对象没有任何栈中的指针并且变的‘孤单’。
- 除非你做精确的拷贝,所有对象内对其他对象的引用都是通过引用指针。
栈是自动的被操作系统管理和完成的而不是V8本身。所以我们不需要担心栈。堆,从另一方面讲,是自动的被操作系统管理的,由于他是最大的内存空间,并且保持有动态数据,随着时间的增长这可能导致指数增长,把内存耗尽。随着时间的增长导致程序分裂减缓应用程序。这也是GC存在的原因。
有区别的指针和堆上的数据对于GC是非常重要的,V8使用'Tagged pointers'
这种方式,在这种方式下,v8会保留一个字节在每一个单词的结尾来预示这是指针还是数据。这种方式需要有限制的编译器支持,
V8 Memory management: Garbage collection
现在我们都知道V8是如何分配内存的,我们看下V8是如何管理堆内存的,这对于应用程序的性能是非常重要的。当一个应用程序试图在堆上分配更多的内存,比可用的内存更多(这取决于V8标识设定)我们会遇到内存超出错误。一个不正确的内存管理会引起内存泄露。
V8通过GC管理堆内存。简而言之,它释放孤单对象释放的内存,这里的孤单对象指的是不再有栈中的直接引用,或者非直接(通过一个其他对象的引用)为新对象的创建开辟空间。
Orinoco is the codename of the V8 GC project to make use of parallel, incremental and concurrent techniques for garbage collection, to free the main thread.
V8中的GC用于重新声明未使用的内存来重复使用。
V8的GC是分代的(堆中的对象是按照时期分组,在不同的阶段被清除),有两个阶段和三种不同的算法来实现GC。
Minor GC (Scavenger)
这种类型的GC会保持新生代空间紧凑和整洁。对象在new space
被分配空间,这是相对小的(在1M和8M之间,依赖于行为探索)。在 ‘new space’
中分配内存空间是非常简单的,无论什么时候我们想为新的对象保留空间,有一个分配指针增加。当这个分配指针到达了new space的尽头时,minor GC
就被触发了。这个过程也叫做Scavenger,它实现了这个算法。它是定期运行,使用平行帮助线程,非常快。(It occurs frequently and uses parallel helper threads and is very fast)。new space
被划分为大小相等的semi-space
:**to-space **
和**from-space**
。大多数的内存分配是在**from-space**
操作的(除了特定种类的对象,例如可执行代码,一般被分配在**old-space**
)。当**from-space**
填满,**minor GC**
就触发了。
V8_minor_GC.pdf
看一下minor GC
的过程:
- 假使当我们开始的时候,已经有对象在
‘from-space’
(块01到06被标记为已使用内存)。 - 这个过程创建了新的对象(07)。
- V8试图从from-space 获取需要的内存,但是没有足够的空闲空间来容纳新对象,最后触发了
minor GC
。 - Minor GC 会递归遍历对象图在
‘from -space’
,从栈指针开始(GC roots),来发现那些对象是活动的或者已使用的。这些对象会被移动到页面的'to-space'
。被这些对象引用的任何对象也会被移动到这个页面的'to-space'
,并且指针会被更新。这个过程会被重复执行直到’from-space‘
的对象都被扫描到。最后,’to-space‘
会被自动压实以减少分裂。 - Minor GC清空
’from -space‘
,并且剩下的对象就是垃圾对象。 - Minor GC 会交换
'from-space'
和’to-space‘
,所有的对象现在在’from-space‘
,并且'to-space'
是空的。 - 对象在 ’from-space‘ 被分配内存。
- 假使现在已经过了一段时间,有更多的对象在
' from-space'
(块07到09被标记为已用内存) - 应用创建对象(09)
- V8试图从
'from-space'
获取需要的内存,但是没有足够的空间,v8就触发第二次minor GC。 - 上述过程会被重复,并且任何已存活对象在第二次Minor GC后都被移动到 ’old space‘。第一次的幸存者会被移动到
’to-space‘
,剩余的对象会被从 ’from-space‘中清除。 - Minor GC 交换 ‘ to-space’ 和 ‘from-space’,这是所有的对象在 ’from-space‘, ‘to-space’是空的。
- 新对象在 ’from-space‘分配内存。
现在我们看到了minor GC是如何从新生代中回收内存的,并且保持它紧凑的。这是一个暂停的过程,但是它是高效的,它的消耗可以忽略不计。由于这个进程不会扫描“old space”中的对象以获取“new space”中的任何引用,所以它使用一个寄存器来存储从old space到 new space的所有指针。这被一个进程记录到存储缓冲区中叫做写屏障。
Major GC
这种类型的GC会保持老生代空间紧凑和整洁。当V8发现没有足够的**old space**
的时候,就触发了,基于动态的计算限制,当它从minor GC循环装满后。
Scavenger算法对于小的数据大小是完美的,但对于大堆是不切实际的,因为他有内存开销,所以**major GC**
是用Mark-Sweep-Compact
算法完成。有一个**tri-color **
(white-grey-black)标记系统。所以**major GC**
有三步 步骤,第三步的执行是依赖于一个分裂启发式方法。
- Marking:第一步,对于两种算法都是同样的,首先垃圾回收器会识别出哪个对象正在使用,哪个没有在用。正在使用的对象,或者从GC Root(栈指针)递归触达的被标记为活动对象。从技术上讲,这是堆的深度优先搜索,可被看作是定向图。
- Sweeping:垃圾回收器遍历堆,并且记录下不活动的任何对象的内存地址。并在空闲列表标记这个空间是空闲的,可以用于存储其他对象。
- Compacting:在被清除之后,如果有需要的话,所有的已存活对象都会被移动到一起。这会降低分段存储,并且会提升给新对象分配内存空间的性能。
- incremental GC:GC是在多个增量步骤中完成的而不是一个。
- Concurrent marking:标记被通过多个helper线程同步完成的,而不会影响JS的主线程。当helper正在同步标记,写屏障用于跟踪JS创建的对象之间的新引用。
- Concurrent sweeping/compacting:清除和压实是在helper线程同步执行的,而不影响JS主线程。
- Lazy sweeping:延迟清除包括延迟页面的垃圾清除直到,有内存需要。
Major GC的过程:
- 假使现在很多的minor GC 循环已经通过了,old pace已经满了,V8决定触发
‘major GC’
- Major GC递归遍历对象图,从栈指针开始来标记对象,仍被使用的是活动对象(已占用的内存),在old space剩余的对象被作为垃圾。这个过程是通过多个helper线程同步完成的,每一个helper都有一个指针。这不会影响JS的主线程。
- 当同步标记已经完成,或者如果内存限制已经达到了,GC会使用主线程做最后一个标记步骤,这会导致一个暂停
- Major GC 现在会使用同步清除线程来标记无用内存空间是空闲的。同步压实任务也会触发,会移动相关联的内存块到同一个页面来避免分裂。在执行这些步骤期间,指针也会更新。