本文主要就下面三块内容展开
栈和堆空间
垃圾回收
解释器和编译器
一.栈空间和堆空间
先回顾下基本知识
1.静态语言就是强类型?二者的关系
(1-1)静态语言, 使用前 要确定数据类型
(1-2)动态语言, 运行中 检查数据类型;
(2-1)弱类型语言,支持隐式类型转换
(2-2)强类型,不支持 隐式转换;
C是静态语言,但是它支持隐式转换,是弱类型
2.JS的内存空间
(1)分三块,代码空间,栈stack空间,堆heap空间;
(2) 原始 类型的数据值都是直接保存在“栈”中的, 引用 类型的值是存放在“堆”中的
栈空间比较小,堆空间比较大;
(3)为什么不放在一起?
放在一起,会影响执行上下文切换和执行效率;
(4)闭包的内存模型,Closure
比如下面这段代码,foo中有个闭包对象,有两个属性值 myName和test1,存到堆中;
|
|
执行到foo 函数中“return innerBar” 的调用栈情况,如下图
二.垃圾回收机制
1.常用的垃圾回收机制如下:
1-1.标记清除法;
给存储在内存中的变量都加上标记,判断哪些变量没有在执行环境中引用,进行删除;
1-2.引用计数法;
(1)跟踪记录每个值被引用的次数
(2)当声明变量并将一个引用类型的值赋值给该变量时,则这个值的引用次数加1,
(3)同一值被赋予另一个变量,该值的引用计数加1 。
(4)当引用该值的变量被另一个值所取代,则引用计数减1,
(5)当计数为 0 的时候,说明无法在访问这个值了,系统将会收回该值所占用的内存空间。
缺点 :循环引用的时候,引用次数不为0,不会被释放;
调用栈中的数据的GC
1.调用栈还有一个记录当前执行状态的 指针 (称为 ESP)
JS引擎通过下移ESP指针来销毁 栈顶 某个执行上下文的过程。
如下图,上面按个已经是无效的,有新的会直接覆盖;
堆空间中数据的GC
1.代际假说和分代收集~新生代,老生代;
(0)提高垃圾回收的效率,V8将堆分为 新生代 和 老生代 两个部分,
(1)其中新生代为存活时间较短的对象(需要经常进行垃圾回收),内存占用小,GC频繁;
只支持1-8M容量;使用副垃圾回收器;
(2)而老生代为存活时间较长的对象(垃圾回收的频率较低),内存占用多,GC不频繁;
使用主垃圾回收器;
2.新生代的GC算法
新生代的对象通过 Scavenge 算法进行GC。在 Scavenge 的具体实现中,主要采用了 Cheney 算法。
(1)Cheney 算法是一种采用 停止复制(stop-copy) 的方式实现的垃圾回收算法。
(2)它将堆内存一分为二,每一部分空间成为 semispace-半空间。
(3)在这两个 semispace 空间中,只有一个处于使用中,另一个处于闲置中。
(4)处于使用中的 semispace 空间成为 From 对象空间,处于闲置状态的空间成为 To 空闲空间。
(5)当我们分配对象时,先是在 From 空间中进行分配。当开始进行垃圾回收时,会检查 From 空间中的存活对象,这些存活对象将被复制到 To 空间中,同时还会将这些对象有序的排列起来~~相当于内存整理,所以没有内存碎片,而非存活对象占用的空间将被释放。
完成复制后,From空间和To空间的角色发生对换。
(6)Scavenge 是典型的 空间换取时间 的算法,而且复制需要时间成本,无法大规模地应用到所有的垃圾回收中,但非常适合应用在新生代中进行快速频繁清理。
3.对象晋升策略
(1)对象从新生代中移动到老生代中的过程称为晋升。
(2)晋升条件主要有两个:
(1)对象是否经历过 两次 Scavenge 回收都未清除,则移动到老生代
(2)To 空间已经使用超过 25% ,To 空间对象移动到老生代
因为这次 Scavenge 回收完成后,这个 To 空间将变成 From 空间,接下来的内存分配将在这个空间中进行,如果占比过高,会影响后续的内存分配
4.写屏障
写缓冲区中有一个列表(CrossRefList),列表中记录了所有老生区对象指向新生区的情况
这样可以快速找到指向新生代该对象的老生代对象,根据他是否活跃,来清理这个新生代对象;
5.老生代的GC
(1)老生代的内存空间较大且存活对象较多,使用新生代的Scavenge 复制算法,会耗费很多时间,效率不高;而且还会浪费一半的空间;
(2)为此V8使用了 标记-清除算法 (Mark-Sweep) 进行垃圾回收,并使用 标记-压缩算法 (Mark-Compact) 整理内存碎片,提高内存的利用率。步骤如下:
1) 对老生代进行第一遍扫描,标记存活的对象
从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据;
2) 对老生代进行第二次扫描,清除未被标记的对象
3) 标记-整理算法,标记阶段一样是递归遍历元素,整理阶段是将存活对象往内存的一端移动
4) 清除掉存活对象边界外的内存
注意,不管哪种,整理内存后只要地址有变化,需要及时更新到调用栈的;
6.全停顿Stop-The-World
(1)JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World);
(2)新生代因为内存小,活动对象少,全停顿影响不大,但是老生代可能会造成卡顿明显;
(3)解决办法~ 增量标记算法Incremental Marking
V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成;
三.编译器和解释器
1.编译器Compiler和解释器Interpreter
按语言的执行流程,可以把语言划分为编译型语言和解释型语言。
1-1.编译型语言
在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了。比如 C/C++、GO 等都是编译型语言。
1-2.解释型语言
在每次运行时都需要通过解释器对程序进行动态解释和执行。比如 Python、JavaScript 等都属于解释型语言。
2.V8执行一段代码流程图
2-1.生成抽象语法树(AST)和执行上下文
2-1-1.AST
(1)编译器或解释器,他们不理解高级语言,只可以理解AST;
(2)简单demo,在线AST网站
https://resources.jointjs.com/demos/javascript-ast
(3)可以把 AST 看成代码的结构化表示;
(4)应用非常广泛
1)Babel:
Babel 的工作原理就是先将 ES6 源码转换为 AST,然后再将 ES6 语法的 AST 转换为 ES5 语法的 AST,最后利用 ES5 的 AST 生成 JavaScript 源代码;
2)ESLint:
利用 AST 来检查代码规范化的问题。
2-1-2.AST生成过程
(1)分词 tokenize ,或词法分析
将一行行的源码拆解成一个个 token。
token,指的是语法上不可能再分的、最小的单个字符或字符串;
比如:
|
|
关键字“var” ~ keyword、
标识符“myName” ~ identifier、
赋值运算符“=” ~ assignment、
字符串“悬笔e绝” ~ Literal
四个都是 token,而且它们代表的属性还不一样;
(2)解释 parse ,或语法分析
上一步生成的 token 数据,根据语法规则转为 AST。如果源码符合语法规则,这一步就会顺利完成。但如果源码存在语法错误,这一步就会终止,并抛出一个“语法错误”。
2-1-3.执行上下文
前面介绍过,主要是代码在执行过程中的环境信息
2-2.生成字节码
(1)解释器Ignition
根据AST生成 字节码 ,并解释执行字节码;
(2)历史,一开始直接把AST转成机器码,
效率性能很高,但是机器码占用内存很大,小内存手机上内存占用问题明显,
V8团队画了快4年时间,引入字节码,抛弃编译器,才有现在的架构;
(3)字节码
字节码就是介于 AST 和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。
2-3.执行代码
(1)如果有一段第一次执行的字节码,解释器 Ignition 会逐条解释执行。
解释器 Ignition 除了负责生成字节码之外,还有一个作用,就是解释执行字节码。
(2)在 Ignition 执行字节码的过程中,如果发现有 热点代码(HotSpot) ,比如一段代码被重复执行多次,这种就称为热点代码,后台的编译器 TurboFan 就会把它编译为高效的 机器码 ,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。
(3)V8 的解释器和编译器的取名
解释器Ignition ~ 点火器;
编译器TurboFan ~ 涡轮增压发动机;
寓意着代码启动时通过点火器慢慢发动,一旦启动,涡轮增压介入,其执行效率随着执行时间越来越高效率;
(4)即时编译 JIT
字节码配合解释器和编译器的技术;
Java 和 Python 的虚拟机,苹果的 SquirrelFish Extreme 和 Mozilla 的 SpiderMonkey 也是基于JIT这个技术;
3.V8做的性能优化措施
(1)脚本流
一边下载,一边解析,节省时间;
(2)字节码缓存
访问同一个页面时直接复用之前的字节码,不再重新编译生成;
(3)内联
将主函数中调用的函数,直接换成要执行的语句;加快执行速度;
(4)隐藏类
通过隐藏类快速定位到动态加入的属性;注意:动态加入的属性顺序不一样,会造成生成不同的隐藏类,我们动态赋值同一个构造函数对象的时候,尽量保证顺序也是一致的。
(5)JIT即时编译,将热点代码编译成机器码
执行越久,越多的代码编译成机器码,速度越快;
常用的函数传入的类型保持固定。并且对象的属性越稳定,越有利于性能。
总结
这篇只是V8引擎的入门介绍,关于V8的其他知识,后续会补充其他的文章
未完待续……