一、V8引擎是什么?

V8引擎是驱动 Google Chrome 的 JavaScript 引擎的名称。是 Chrome浏览器和edge浏览器获取我们的 JavaScript 代码并执行代码的东西。
V8 提供了 JavaScript 执行的运行时环境。 DOM 和其他 Web 平台 API 由浏览器提供。
JavaScript 引擎独立于它所在的浏览器。 这个关键特性促成了 Node.js 的兴起。 早在 2009 年,V8 就被选为驱动 Node.js 的引擎,随着 Node.js 的流行,V8 现在成为大量使用 JavaScript 编写的服务器端代码提供驱动的引擎。
Node.js 底层代码主要是为C++,这个跟后续内容有关。Node.js生态系统非常庞大,这要归功于 V8,它还支持桌面应用程序,例如 Electron 等项目。
其它JS引擎:
Firefox 使用 SpiderMonkey
Safari 使用 JavaScriptCore(也称为 Nitro)
Edge 最初基于 Chakra,但现在已经使用 Chromium 和 V8 引擎重建。
等等其它引擎
所有引擎都采用ECMA ES-262 标准,即 ECMAScript(JavaScript 使用的标准)。

二、内存

2.1、内存生命周期:(这个不同的程序语言基本一样)

1、分配你所需要的内存
2、使用分配到的内存(读、写)
3、不需要时将其释放归还

2.2、JavaScript的内存管理

与其他需要手动管理内存的语言不同,在JavaScript中,当我们创建变量(对象,字符串等)的时候,系统会自动给对象分配对应的内存。
系统发现这些变量不再被使用的时候,会自动释放(垃圾回收)这些变量的内存,开发者不用过多的关心内存问题。
在JavaScript中,数据类型分为两类,简单类型和引用类型,对于简单类型,内存是保存在栈(stack)空间中,复杂数据类型,内存是保存在堆(heap)空间中。
基本类型:这些类型在内存中分别占有固定大小的空间,他们的值保存在栈空间,我们通过按值来访问的
引用类型:引用类型,值大小不固定,栈内存中存放地址指向堆内存中的对象。是按引用访问的
而对于栈的内存空间,只保存简单数据类型的内存,由操作系统自动分配和自动释放。而堆空间中的内存,由于大小不固定,系统无法无法进行自动释放,这个时候就需要JS引擎来手动的释放这些内存。
 

2.3、为什么要关注内存?

1、防止页面占用内存过大,引起客户端卡顿,甚至无响应
(网页卡顿,电脑配置低,网络差,浏览器内存占用过多,http协议,服务器后台)
2、Node 使用的是V8,内存对于后端服务的性能至关重要,因为服务的持久性,后端更容易造成内存溢出
 
(谷歌浏览器 node内核V8)

2.4、V8引擎的内存分配

 
在V8引擎中,可以先粗犷的分为两个部分 。 那栈指的就是 调用栈,首先栈的特点后进先出,同时栈空间是连续的,在需要分配空间和销毁空间操作时,只需要移动下指针,所以非常适合管理函数调用。
而正因为栈空间是连续的,那它的空间就注定是非常有限的,所以不方便存放大的数据,这时我们就使用了 内存堆 来管理保存一些大数据。
 
 
 
  • 大型对象空间(Large object space):大于其他空间大小限制的对象存放在这里.存放体积超越其他区大小的对象,主要为了避免大对象的拷贝,使用该空间专门存储大对象。
 
  • 代码空间(Code-space):这是即时编译器(JIT)存储已经编译的代码块的地方。这是唯一可执行内存的空间(尽管代码可能被分配到大型对象空间(Large object space),那也是可以执行的)。
 
  • 单元空间(Cell Space),属性单元空间(Property Cell Space)和映射空间(Map Space):这些空间分别存放 Cell,PropertyCell 和 Map(隐藏类)。这些空间包含的对象大小相同,并且对对象类型有些限制,可以简化回收工作。
 
新生代(新空间):分为Semi space From和Semi space To,且两个区域的空间严格对半分。
老生代(老空间):分为Old pointer space和Old data space,是连续的区域,如果一个对象有指针引用或者指向其它对象,大多数会保存在Old pointer space里面;如果一个对象是原始对象,没有指针引用,会保存在Old data space里面,而且所有老生代的对象全部会由新生代晋升而来。
 

三、垃圾回收机制

3.1、垃圾回收算法

V8当前垃圾回收机制
2011年,V8应用了增量标记机制。直至2018年,Chrome64和Node.js V10启动并发标记(Concurrent),同时在并发的基础上添加并行(Parallel)技术,使得垃圾回收时间大幅度缩短。

 
变量的存储路径:
变量 –> 新生代 –> 老生代
新生代简单来说就是copy(复制),使用Scavenge算法(新生代互换);
老生代就是标记整理清除:早期用Mark-Sweep(标记清除),现在用Mark-Compact(标记整理)

3.2、详解垃圾回收

  新生代垃圾回收:
Scavange算法:
这里我们假设创建了一个对象 obj
  • 首先obj会被分配到 新生代 中两个space中其中一个space,这里我们假设分配到了from space中。
  • 程序继续执行会不断的向from space中添加新的对象信息,这时from space将要达到了存储的上限(16MB),V8的垃圾回收机制会开始清理from中不再被使用的对象(即没有被指向的对象)。
  • 清理后,将所有仍然存活的对象(我们假设obj还存活),会被复制到to space然后删除所有from space中的对象。
  • 这时,程序继续运行,如果有新创建的对象会不断的分配到to space中,当to space快要满了重复执行上面说的复制转移的工作。
也就是说创建的对象会在to space 和 from space 之间转移,
也就是所谓的 to –> from, from –> to 的角色互换过程。
 
  • 1、标记活动对象和非活动对象
  • 2、复制from-space的活动对象到to-space中并进行排序
  • 3、清除from-space中的非活动对象
  • 4、将from-spaceto-space进行角色互换,以便下一次的Scavenge算法垃圾回收

通过以上的流程图,我们可以很清楚地看到,Scavenge算法的垃圾回收过程主要就是将存活对象在From空间和To空间之间进行复制,同时完成两个空间之间的角色互换,因此该算法的缺点也比较明显,浪费了一半的内存用于复制。
 
新生代中的对象什么时候变成老生代的对象呢?
当一个对象在经过多次复制之后依旧存活,那么它会被认为是一个生命周期较长的对象,在下一次进行垃圾回收时,该对象会被直接转移到老生代中,这种对象从新生代转移到老生代的过程我们称之为
晋升
对象晋升的条件主要有以下两个(满足其一即可):
  • 对象是否经历过一次Scavenge算法
 
 
 
 
 
 
 
 
 
 
 
  • To空间的内存占比是否已经超过25%
 
设置25%这个限制值的原因是当这次Scavenge回收完成后,这个To空间将变成From空间,接下来的内存分配将在这个空间中进行。如果占比过高,会影响后续的内存分配。

复制算法在对象存活率较高时,需要较多的复制操作,效率会降低,尤其是该算法会浪费掉一部分内存,所以不适合老年代

老生代垃圾回收(标记整理清除)
早期用Mark-Sweep(标记清除),现在用Mark-Compact(标记整理)

标记清除 (Mark-Sweep)

Mark-Sweep处理时分为两阶段,标记阶段和清理阶段,看起来与Scavenge类似,不同的是,Scavenge算法是复制活动对象,而由于在老生代中活动对象占大多数,所以Mark-Sweep在标记了活动对象和非活动对象之后,直接把非活动对象清除。
标记阶段:对老生代进行第一次扫描,标记活动对象
清理阶段:对老生代进行第二次扫描,清除未被标记的对象,即清理非活动对象
这里存在一个问题,被清除的对象遍布于各内存地址,产生很多内存碎片,所以后面改进 了机制,采用标记整理方法(Mark-Sweep)。

标记整理(Mark-Compact)

由于Mark-Sweep完成之后,老生代的内存中产生了很多内存碎片,若不清理这些内存碎片,如果出现需要分配一个大对象的时候,这时所有的碎片空间都完全无法完成分配,就会提前触发垃圾回收,而这次回收其实不是必要的。
为了解决内存碎片问题,Mark-Compact被提出,它是在是在 Mark-Sweep的基础上演进而来的,相比Mark-Sweep,Mark-Compact添加了活动对象整理阶段,将所有的活动对象往一端移动,移动完成后,直接清理掉边界外的内存(先整理,再清除)。
 

增量标记(Incremental Marking)

为了减少全停顿的时间,V8对标记进行了优化,将一次停顿进行的标记过程,分成了很多小步。每执行完一小步就让应用逻辑执行一会儿,这样交替多次后完成标记。
长时间的GC,会导致应用暂停和无响应,将会导致糟糕的用户体验。从2011年起,V8就将「全暂停」标记换成了增量标记。改进后的标记方式,最大停顿时间减少到原来的1/6

三色标记

  • 白色:没有检查(或者检查过了,确实没有引用指向它了)
  • 灰色:自身被检查了,成员没被检查完(可以认为访问到了,但是正在被检查,就是图的遍历里那些在队列中的节点)
  • 黑色:自身和成员都被检查完了

 

 

引用计数

早期的浏览器最常使用的垃圾回收方法叫做”引用计数“(reference counting):语言引擎有一张”引用表“,保存了内存里面所有资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。
const user1 = {age: 11}
const user2 = {age: 22}
const user3 = {age: 33}
const userList = [user1.age, user2.age, user3.age]
上面这段代码,当执行过一遍后,user1、user2、user3都是被userList引用的,所以它们的引用计数不为零,就不会被回收
function fn() {
const num1 = 1
const num2 = 2
}

fn()
上面代码中fn函数执行完毕,num1、num2都是局部变量,执行过后,它们的引用计数就都为零,所有这样的代码就会被当做“垃圾”,进行回收
 
引用计数算法有一个比较大的问题: 无法回收循环引用的对象
function fn() {
    const stone1 = {}
    const stone2 = {}
    stone1.name = stone2
    stone2.name = stone1
fn()
无法回收循环引用的对象, 因为在如下代码中,在函数执行结束后,虽然 stone1 和 stone2 都无法被根访问到了,但是由于他们自身互相引用所以根据引用计数法的回收条件,是无法被回收的。
 
引用计数算法其实还有一个比较大的缺点,就是我们需要单独拿出一片空间去维护每个变量的引用计数,这对于比较大的程序,空间开销还是比较大的。
 
引用计数算法优点:
  • 引用计数为零时,发现垃圾立即回收;
  • 最大限度减少程序暂停;
引用计数算法缺点:
  • 无法回收循环引用的对象;
  • 空间开销比较大;
 

四、v8是如何处理变量的

1、如何查看内存使用情况
通过浏览器来查看内存:
    F12调试工具查看;performance
    控制台输入:window.performance // 可以查看到当前页面的各种内存情况
 
  通过node来查看内存使用情况:process.memoryUsage();
    {
    rss: 22347776, // v8申请到的总占用空间   
    heapTotal: 9682944, // 堆总内存     
   heapUsed: 5401712, // 已经使用了的堆内存   
    external: 16901 // node底层是C++,他可以申请到一些C++的内存. max_old_space_size
    }
 
2、内存处理
  内存主要就是存储变量等数据结构的
  局部变量当程序执行结束,并且引用的时候就会随着消失
  全局对象会始终存活到程序运行结束
var size = 20*1024*1024;
function a(){
    let arr1 = new Array(size);
    let arr2 = new Array(size);
    let arr3 = new Array(size);
    let arr4 = new Array(size);
    let arr5 = new Array(size);
    let arr6 = new Array(size);
    let arr7 = new Array(size);
    let arr8 = new Array(size);
}
a();
getmem();    // Process: heapTotal 1288.83MB  heapUsed  1283.34MB  rss  1299.79MB
var  arr9 = new Array(size);
// 有返回值的函数
var size = 20*1024*1024;
function a(){
    let arr1 = new Array(size);
    let arr2 = new Array(size);
    let arr3 = new Array(size);
    let arr4 = new Array(size);
    let arr5 = new Array(size);
    let arr6 = new Array(size);
    let arr7 = new Array(size);
    let arr8 = new Array(size);
    return [arr1,arr2,arr3,arr4,arr5,arr6,arr7,arr8];
}
a();
getmem();    // Process: heapTotal 1288.83MB  heapUsed  1283.34MB  rss  1299.76MB
let  arr9 = new Array(size);
 let  c = a();
// 局部变量没有引用之后会被销毁. 没有返回值,函数调用完了变量就会被销毁了 // 函数只是return 了一个值,最后的执行结果与第一种情况没很大的差别 // 当将返回值赋值给一个变量后,变量保留了这一系列数组的引用,最后崩了

、我们能做些什么

1、尽可能少地创建全局变量
 
2、手动清除定时器
const numbers = []; 
const foo = function() {   
  for(let i = 0;i < 100000;i++) {  
         numbers.push(i);  
   }
 }; 
 
 window.setInterval(foo, 1000);
3、少用闭包
function foo() {    
   let local = 123;   
   return function() {   
      return local;    
   }
} 
 const bar = foo(); 
 console.log(bar()); // -> 123
一般情况下,当foo函数执行完毕后,它的作用域会被销毁,但是由于存在变量引用其返回的匿名函数,导致作用域无法得到释放,也就导致local变量无法回收,只有当我们取消掉对匿名函数的引用才会进入垃圾回收阶段
 
…….减少循环体中的活动
 

原文地址:http://www.cnblogs.com/xiaoeshuang/p/16806946.html

1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长! 2. 分享目的仅供大家学习和交流,请务用于商业用途! 3. 如果你也有好源码或者教程,可以到用户中心发布,分享有积分奖励和额外收入! 4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解! 5. 如有链接无法下载、失效或广告,请联系管理员处理! 6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需! 7. 如遇到加密压缩包,默认解压密码为"gltf",如遇到无法解压的请联系管理员! 8. 因为资源和程序源码均为可复制品,所以不支持任何理由的退款兑现,请斟酌后支付下载 声明:如果标题没有注明"已测试"或者"测试可用"等字样的资源源码均未经过站长测试.特别注意没有标注的源码不保证任何可用性