JavaScript

前言

很多人都知道v8引擎,v8引擎是一种js引擎的实现。在开始介绍v8之前,先搞清JavaScript引擎是什么,这里简单引用

JavaScript引擎是执行JavaScript代码的程序或解释器。javaScript引擎可以实现为标准解释器或即时编译器,它以某种形式将JavaScript编译为字节码。

  • V8 - 开源,由Google开发,用C ++编写
  • Rhin- 由Mozilla基金会开源,完全用Java开发
  • SpiderMonkey 第一个JavaScript引擎,Netscape Navigator,Firefox
  • JavaScriptCore 苹果公司为Safari开发
  • KJS 最初由Harri Porten为KDE项目的Konqueror网络浏览器开发
  • Chakra** (JScript9) Microsoft Edge
  • Chakra** (JavaScript) Microsoft IE9-IE11
  • Nashorn 作为OpenJDK的一部分,由Oracle Java语言和工具组编写
  • JerryScript 一个物联网的轻量级引擎

数据表示

JavaScript是一种无类型语言,在编译时并不能准确知道变量的类型,只可以在运行时确定,这就不像c++或者java等静态类型语言,在编译时候就可以确切知道变量的类型。然而,在运行时计算和决定类型,会严重影响语言性能,这也就是JavaScript运行效率比C++或者JAVA低很多的原因之一。

在JavaScript中,除boolean,number,string,null,undefined这个五个简单变量外,其他的数据都是对象,V8使用一种特殊的方式来表示它们,进而优化JavaScript的内部表示问题。

隐藏类

在执行C++代码时,仅凭几个指令即可根据偏移信息获取变量信息,而JavaScript里需要通过字符串匹配来查找属性值的,这就需要更多的操作才能访问到变量信息,而代码量变量存取是十分频繁的,这也就制约了JavaScript的性能。V8借用了类和偏移位置的思想,将本来通过属性名匹配来访问属性值的方法进行了改进,使用类似C++编译器的偏移位置机制来实现,这就是隐藏类。

隐藏类将对象划分成不同的组,对于组内对象拥有相同的属性名和属性值的情况,将这些组的属性名和对应的偏移位置保存在一个隐藏类中,组内所有对象共享该信息。同时,也可以识别属性不同的对象。

隐藏类转换取决于将属性添加到对象的顺序.----非常重要

下面代码中p1,p2的属性添加顺序不一样,结果就是p1,p2会有两个不同的隐藏类。这种情况下还是最好采用相同的初始化顺序,以便系统可以复用隐藏类,帮助系统提升性能。

function Point(x, y) {
    this.x = x;
    this.y = y;
}
var p1 = new Point(1, 2);
p1.a = 5;
p1.b = 6;
var p2 = new Point(3, 4);
p2.b = 7;
p2.a = 8;
1
2
3
4
5
6
7
8
9
10

内联缓存

正常访问对象属性的过程是:首先获取隐藏类的地址,然后根据属性名查找偏移值,然后计算该属性的地址。虽然相比以往在整个执行环境中查找减小了很大的工作量,但依然比较耗时。能不能将之前查询的结果缓存起来,供再次访问呢?当然是可行的,这就是内嵌缓存。

内嵌缓存的大致思路就是将初次查找的隐藏类和偏移值保存起来,当下次查找的时候,先比较当前对象是否是之前的隐藏类,如果是的话,直接使用之前的缓存结果,减少再次查找表的时间。当然,如果一个对象有多个属性,那么缓存失误的概率就会提高,因为某个属性的类型变化之后,对象的隐藏类也会变化,就与之前的缓存不一致,需要重新使用以前的方式查找哈希表。

比如以下代码

function Point(x,y){
    this.x=x;
    this.y=y;
}
var p1 = new Point(1,2);
var p2 = new Point(3,4);
p2.x="hello world";
1
2
3
4
5
6
7

当执行p1=new Point(1,2)p2 = new Point(3,4)时,使用的同一个隐藏类,当执行p2.x="hello world"时x的类型由int转化成了String,就会新建一个新的隐藏类,所以代码中应该尽量避免此类操作。

内存管理

Node中通过JavaScript使用内存时就会发现只能使用部分内存(64位系统下约为1.4 GB,32位系统下约为0.7 GB),其深层原因是 V8 垃圾回收机制的限制所致(如果可使用内存太大,V8在进行垃圾回收时需耗费更多的资源和时间,严重影响JS的执行效率)。

内存的管理组要由分配和回收两个部分构成。V8的内存划分如下:

  • Zone:管理小块内存。其先自己申请一块内存,然后管理和分配一些小内存,当一块小内存被分配之后,不能被Zone回收,只能一次性回收Zone分配的所有小内存。当一个过程需要很多内存,Zone将需要分配大量的内存,却又不能及时回收,会导致内存不足情况。
  • 堆:管理JavaScript使用的数据、生成的代码、哈希表等。为方便实现垃圾回收,堆被分为三个部分:
    • 年轻分代:为新创建的对象分配内存空间,经常需要进行垃圾回收。为方便年轻分代中的内容回收,可再将年轻分代分为两半,一半用来分配,另一半在回收时负责将之前还需要保留的对象复制过来。
    • 年老分代:根据需要将年老的对象、指针、代码等数据保存起来,较少地进行垃圾回收。
    • 大对象:为那些需要使用较多内存对象分配内存,当然同样可能包含数据和代码等分配的内存,一个页面只分配一个对象。

垃圾回收

新生代垃圾回收

新生代内存中的垃圾回收主要通过 Scavenge 算法进行,具体实现时主要采用了 Cheney 算法。Cheney 将内存空间一分为二,每部分都叫做一个 Semispace,这两个 Semispace 一个处于使用,一个处于闲置。处于使用中的 Semispace 也叫作 From,处于闲置中的 Semispace 也叫作 To。

在垃圾回收运行时时,会检查 From 中的对象,当某个对象需要被回收时,将其留在 From 空间,剩下的对象移动到 To 空间,然后进行反转,将 From 空间和 To 空间互换。进行垃圾回收时,会将 To 空间的内存进行释放。

简而言之,就是 From 空间中存放不需要被回收的对象,To 空间中存放需要被回收的对象,当垃圾回收运行时,将 To 空间中的对象全部进行回收。

前面说过,新生代内存空间用来存放存活时间较短的对象,老生代内存空间用来存放存活时间较长的对象。新生代中的对象可以晋升到老生代中,具体有两种方式:

  • 在垃圾回收的过程中,如果发现某个对象之前被清理过,那么会将其晋升到老生代内存空间中
  • 在 From 空间和 To 空间进行反转的过程中,如果 To 空间中的使用量已经超过了 25%,那么就将 From 中的对象直接晋升到老生代内存空间中

老生代垃圾回收

对于老生代中的对象,由于存活对象占较大比重,再采用上面的方式会有两个问题:一个是存活对象较多,复制存活对象的效率将会很低;另一个问题依然是浪费一半空间的问题。为此,V8在年老分代中主要采用了Mark-Sweep(标记清除)标记清除和Mark-Compact(标记整理)相结合的方式进行垃圾回收。

Mark Sweep 是将需要被回收的对象进行标记,在垃圾回收运行时直接释放相应的地址空间,Mark Compact将存活的对象移动到一边,将需要被回收的对象移动到另一边,然后对需要被回收的对象区域进行整体的垃圾回收。

触发条件

作用域:能形成作用域的函数调用、with 语句 以及 全局作用域。 闭包:V8 无法主动回收内存中的闭包引用和全局变量引用。

内存泄漏原因

  • 全局变量引起的内存泄漏
  • 闭包引起的内存泄漏
  • dom清空或删除时,事件未清除导致的内存泄漏

总结

  1. 对类的初始化最好都通过构造函数
  2. 对类的属性的类型一开始就确定好,不要随便对属性进行类型更改以便更好的使用隐藏类
  3. 减少全局变量的定义
  4. 正确的使用闭包,不要无意义地使用闭包
  5. 对dom绑定事件,不使用之后要进行赋null

参考资料:

  1. V8 内存管理和垃圾回收机制总结
  2. Chrome V8引擎介绍
Last Updated: 11/28/2019, 10:26:13 PM