V8内存管理
- 程序运行需要分配内存
- V8也会申请内存,申请的内存又会分为堆内存和栈内存
栈
- 栈用于存放JS中的基本类型和引用类型指针
- 栈的空间是连续的,增加删除只需要移动指针,操作速度非常快
- 栈的空间是有限的,当栈满了,就会抛出一个错误
- 栈一般是在执行函数时创建的,在函数执行完毕后,栈就会被销毁
堆
- 如果不需要连续空间,或者申请的内存较大,可以使用堆
- 堆主要用于存储JS中的引用类型
- 源码
javascript
const v8 = require('v8');
const heapSpace = v8.getHeapSpaceStatistics();
function format(size) {
return `${(size / 1024 / 1024).toFixed(2)}M`.padEnd(10, ' ');
}
console.log(`${"空间名称".padEnd(20, ' ')} 空间大小 已用空间大小 可用空间大小 物理空间大小`);
for (let i = 0; i < heapSpace.length; i++) {
const space = heapSpace[i];
console.log(`${space.space_name.padEnd(23, ' ')}`,
`${format(space.space_size)}`,
`${format(space.space_used_size)}`,
`${format(space.space_available_size)}`,
`${format(space.physical_space_size)}`);
}
堆空间分类
新生代(new space)
- 新生代内存用于存放一些生命周期比较短的对象数据
老生代(old space)
-
老生代内存用于存放一些生命周期比较长的对象数据
-
当
new space
的对象进行两个周期的垃圾回收后,如果数据还存在new space
中,则将他们存放到old space
中 -
old space又可以分为两部分,分别是Old pointer space和Old data space
- Old pointer space 存放GC后surviving的指针对象
- Old data space 存放GC后surviving的数据对象
-
Old Space使用标记清除和标记整理的方式进行垃圾回收
Code space
- 用于存放JIT已编译的代码
- 唯一拥有执行权限的内存
Large object space
- 为了避免大对象的拷贝,使用该空间专门存储大对象
- GC 不会回收这部分内存
Map space
- 存放对象的Map信息,即隐藏类
- 隐藏类是为了提升对象属性的访问速度的
- V8 会为每个对象创建一个隐藏类,记录了对象的属性布局,包括所有的属性和偏移量
什么是垃圾
- 在程序运行过程中肯定会用到一些数据,这些数据会放在堆栈中,但是在程序运行结束后,这些数据就不会再被使用了,那些不再使用的数据就是垃圾
ini
global.a = { name: 'a' };
global.a.b = { name: 'b1' };
global.a.b = { name: 'b2' };
新生代的垃圾回收
-
新生代内存有两个区域,分别是对象区域(from) 和 空闲区域(to)
-
新生代内存使用
Scavenger
算法来管理内存,垃圾回收的入口- 广度优先遍历 From-Space 中的对象,从根对象出发,广度优先遍历所有能到达的对象,把存活的对象复制到 To-Space
- 遍历完成后,清空 From-Space
- From-Space 和 To-Space 角色互换
-
复制后的对象在 To-Space 中占用的内存空间是连续的,不会出现碎片问题
-
这种垃圾回收方式快速而又高效,但是会造成空间浪费
-
新生代的 GC 比较频繁
-
新生代的对象转移到老生代称为晋升Promote,判断晋升的情况有两种
- 经过一次 GC 还存活的对象
- 对象复制到 To-Space 时,To-Space 的空间达到一定的限制
ini
global.a={};
global.b = {e:{}}
global.c = {f: {},g:{h:{}}}
global.d = {};
global.d=null;
rust
bool Heap::ShouldBePromoted(Address old_address) {
Page* page = Page::FromAddress(old_address);
Address age_mark = new_space_->age_mark();
return page->IsFlagSet(MemoryChunk::NEW_SPACE_BELOW_AGE_MARK) &&
(!page->ContainsLimit(age_mark) || old_address < age_mark);
}
老生代的垃圾回收
- 老生代里的对象有些是从新生代晋升过来的,有些是比较大的对象直接分配到老生代里的,所以老生代的对象空间大,活的长
- 如果使用Scavenge算法,浪费一半空间不说,复制如此大块的内存消耗时间将会相当长。所以Scavenge算法显然不适合
- V8在老生代中的垃圾回收策略采用Mark-Sweep(标记清除)和Mark-Compact(标记整理)相结合
Mark-Sweep(标记清除)
- 标记清除分为标记和清除两个阶段
- 在标记阶段需要遍历堆中的所有对象,并标记那些活着的对象,然后进入清除阶段。在清除阶段总,只清除没有被标记的对象
- V8采取的是黑色和白色来标记数据,垃圾收集之前,会把所有的数据设置为白色,用来标记所有的尚未标记的对象,然后会从GC根出发,以深度优先的方式把所有的能访问到的数据都标记为黑色,遍历结束后黑色的就是活的数据,白色的就是可以清理的垃圾数据
- 由于标记清除只清除死亡对象,而死亡对象在老生代中占用的比例很小,所以效率较高
- 标记清除有一个问题就是进行一次标记清楚后,内存空间往往是不连续的,会出现很多的内存碎片。如果后续需要分配一个需要内存空间较多的对象时,如果所有的内存碎片都不够用,就会出现内存溢出的问题
Mark-Compact(标记整理)
- 标记整理正是为了解决标记清除所带来的内存碎片的问题
- 标记整理在标记清除的基础进行修改,将其的清除阶段变为紧缩极端
- 在整理的过程中,将活着的对象向内存区的一段移动,移动完成后直接清理掉边界外的内存
- 紧缩过程涉及对象的移动,所以效率并不是太好,但是能保证不会生成内存碎片,一般10次标记清理会伴随一次标记整理
优化
-
在执行垃圾回收算法期间,JS脚本需要暂停,这种叫Stop the world(全停顿)
-
如果回收时间过长,会引起卡顿
-
性能优化
- 把大任务拆分小任务,分步执行,类似fiber
- 将一些任务放在后台执行,不占用主线程
diff
JavaScript执行 垃圾标记、垃圾清理、垃圾整理 JavaScript执行
-------------- ---------------->
Parallel(并行执行)
- 新生代的垃圾回收采取并行策略提升垃圾回收速度,它会开启多个辅助线程来执行新生代的垃圾回收工作
- 并行执行需要的时间等于所有的辅助线程时间的总和加上管理的时间
- 并行执行的时候也是全停顿的状态,主线程不能进行任何操作,只能等待辅助线程的完成
- 这个主要应用于新生代的垃圾回收
lua
-------辅助线程----->
-------辅助线程----->
-------辅助线程----->
--------- --------------------------->
增量标记
-
老生代因为对象又大又多,所以垃圾回收的时间更长,采用增量标记的方式进行优化
-
增量标记就是把标记工作分成多个阶段,每个阶段都只标记一部分对象,和主线程的执行穿插进行
-
为了支持增量标记,V8必须可以支持垃圾回收的暂停和恢复,所以采用了
黑白灰
三色标记法- 黑色表示这个节点被GC根引用到了,而且该节点的子节点都已经标记完成了
- 灰色表示这个节点被 GC根引用到了,但子节点还没被垃圾回收器标记处理,也表明目前正在处理这个节点
- 白色表示此节点还没未被垃圾回收器发现,如果在本轮遍历结束时还是白色,那么这块数据就会被收回
-
引入了灰色标记后,就可以通过判断有没有灰色节点来判断标记是否完成了,如果有灰色节点,下次恢复的应该从灰色节点继续执行
diff
---------开始标记---增量标记---增量标记---清理---整理----------------->
Write-barrier(写屏障)
- 当黑色指向白色节点的时候,就会触发写屏障,这个写屏障会把白色节点设置为灰色
ini
global.a = { name: 'a' };
global.a.b = { name: 'b1' };
//执行标记工作
global.a.b = { name: 'b2' };
//继续执行标记工作
Lazy Sweeping(惰性清理)
- 当增量标记完成后,如果内存够用,先不清理,等JS代码执行完慢慢清理
concurrent(并发回收)
- 其实增量标记和惰性清理并没有减少暂停的总时间
- 并发回收就是主线程在执行过程中,辅助线程可以在后台完成垃圾回收工作
- 标记操作全都由辅助线程完,清理操作由主线程和辅助线程配合完成
rust
----辅助线程标记----> -----清理整理---->
----辅助线程标记----> -----清理整理---->
----辅助线程标记----> -----清理整理---->
-----------------------------执行JS>-----清理整理--->--------------------------->
并发(concurrent)和并行(parallel)
- 并发和并行都是同时执行任务
- 并行的
同时
是同一时刻可以多个进程在运行 - 并发的
同时
是经过上下文快速切换,使得看上去多个进程同时都在运行的现象
V8执行过程
- V8引擎是一个JavaScript引擎实现
语言的分类
解释执行
- 先将源代码通过解析器转成中间代码,再用解释器执行中间代码,输出结果
- 启动快,执行慢
编译执行
- 先将源代码通过解析器转成中间代码,再用编译器把中间代码转成机器码,最后执行机器码,输出结果
- 启动慢,执行快
V8执行过程
- V8采用的是解释和编译两种方式,这种混合使用的方式称为JIT技术
- 第一步先由解析器生成抽象语法树和相关的作用域
- 第二步根据AST和作用域生成字节码,字节码是介于AST和机器码的中间代码
- 然后由解释器直接执行字节码,也可以让编译器把字节码编译成机器码后再执行
- jsvu可以快速安装V8引擎
- V8源码编译出来的可执行程序名为d8d8,d8是V8自己的开发工具shell
抽象语法树
- astexplorer可以查看抽象语法树
ini
var a = 1;
var b = 2;
var c = a + b;
ini
d8 --print-ast 4.js
[generating bytecode for function: ]
--- AST ---
FUNC at 0
. KIND 0
. SUSPEND COUNT 0
. NAME ""
. INFERRED NAME ""
. DECLS
. . VARIABLE (00000278B965EB88) (mode = VAR) "a"
. . VARIABLE (00000278B965EC78) (mode = VAR) "b"
. . VARIABLE (00000278B965EDB0) (mode = VAR) "c"
. BLOCK NOCOMPLETIONS at -1
. . EXPRESSION STATEMENT at 8
. . . INIT at 8
. . . . VAR PROXY unallocated (00000278B965EB88) (mode = VAR) "a"
. . . . LITERAL 1
. BLOCK NOCOMPLETIONS at -1
. . EXPRESSION STATEMENT at 20
. . . INIT at 20
. . . . VAR PROXY unallocated (00000278B965EC78) (mode = VAR) "b"
. . . . LITERAL 2
. BLOCK NOCOMPLETIONS at -1
. . EXPRESSION STATEMENT at 32
. . . INIT at 32
. . . . VAR PROXY unallocated (00000278B965EDB0) (mode = VAR) "c"
. . . . ADD at 34
. . . . . VAR PROXY unallocated (00000278B965EB88) (mode = VAR) "a"
. . . . . VAR PROXY unallocated (00000278B965EC78) (mode = VAR) "b"
作用域
- 作用域是一个抽象的概念,它描述了一个变量的生命周期,比如哪些变量是在哪里声明的,哪些变量是在哪里使用的
less
d8 --print-scopes 1.js
Global scope:
global { // (000001D42188BEC0) (0, 38)
// will be compiled
// 1 stack slots
// temporary vars:
TEMPORARY .result; // (000001D42188C730) local[0]
// local vars:
VAR c; // (000001D42188C670)
VAR b; // (000001D42188C538)
VAR a; // (000001D42188C448)
}
字节码
- 字节码是机器码的抽象表示
- 源代码直接编译成机器码编译时间太长,体积太大,不适合移动端
- 编译成字节码编译时间短,体积小
- bytecodes.h
- FeedBack Vector slot(反馈向量槽)是一个数组,是用来给优化编译器提供信息的
- 字节码执行过程
ini
var a = 10;
var b = 20;
var c = a + b;
css
d8 --print-bytecode 4.js
LdaConstant [0] 从常量池中加载索引0的常量到累加寄存器中
Star r1 把累加器的值保存到目标寄存器中
LdaZero 把0保存到累加寄存器中
Star r2 把累加器的值0保存到目标寄存器中
Mov <closure>, r3 保存r3寄存器的值
CallRuntime [DeclareGlobals], r1-r3
StackCheck 检查栈是否溢出
LdaSmi [10] 加载10到累加寄存器中
StaGlobal [1] 把累加寄存器的值保存到常量池索引1处
LdaSmi [20] 加载20到累加寄存器中
StaGlobal [2] 把累加寄存器的值保存到常量池索引2处
LdaGlobal [1] 从常量池加载索引1到累加寄存器
Star r1 把累加器的值10保存到目标计数器中
LdaGlobal [2] 从常量池加载索引2的值20到累加寄存器
Add r1 把r1寄存器的值加到累加寄存器中,累加寄存器值为30
StaGlobal [3] 把累加寄存器的值保存到常量池索引3处
LdaUndefined 把Undefined保存到累加寄存器中
Return 返回累加寄存器中的值
编译器优化
ini
function sum() {
let a = 1;
let b = 2;
return a + b;
}
for (let i = 0; i < 10000; i++) {
sum();
}
sql
d8 --trace-opt sum.js
[marking 0x02ccc2ba2279 <JSFunction (sfi = 000002CCC2BA2091)> for optimized recompilation, reason: small function, ICs with typeinfo: 4/4 (100%), generic ICs: 0/4 (0%)]
[marking 0x02ccc2ba2339 <JSFunction sum (sfi = 000002CCC2BA2141)> for optimized recompilation, reason: small function, ICs with typeinfo: 1/1 (100%), generic ICs: 0/1 (0%)]
[compiling method 0x02ccc2ba2339 <JSFunction sum (sfi = 000002CCC2BA2141)> using TurboFan]