Chrome V8系列--浅析Chrome V8引擎中的垃圾回收机制和内存泄露优化策略

V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制。所以,V8 将内存(堆)分为新生代和老生代两部分。javascript

 

1、前言html

V8的垃圾回收机制:JavaScript使用垃圾回收机制来自动管理内存。垃圾回收是一把双刃剑,其好处是能够大幅简化程序的内存管理代码,下降程序员的负担,减小因 长时间运转而带来的内存泄露问题。vue

但使用了垃圾回收即意味着程序员将没法掌控内存。ECMAScript没有暴露任何垃圾回收器的接口。咱们没法强迫其进 行垃圾回收,更没法干预内存管理java

内存管理问题:在浏览器中,Chrome V8引擎实例的生命周期不会很长(谁没事一个页面开着几天几个月不关),并且运行在用户的机器上。若是不幸发生内存泄露等问题,仅仅会 影响到一个终端用户。且不管这个V8实例占用了多少内存,最终在关闭页面时内存都会被释放,几乎没有太多管理的必要(固然并不表明一些大型Web应用不需 要管理内存)。但若是使用Node做为服务器,就须要关注内存问题了,一旦内存发生泄漏,长此以往整个服务将会瘫痪(服务器不会频繁的重启)。node

 

2、chrome内存限制react

2.1存在限制git

Chrome限制了所能使用的内存极限(64位为1.4GB,32位为1.0GB),这也就意味着将没法直接操做一些大内存对象。程序员

2.2为什么限制github

Chrome之因此限制了内存的大小,表面上的缘由是V8最初是做为浏览器的JavaScript引擎而设计,不太可能遇到大量内存的场景,而深层次的缘由 则是因为V8的垃圾回收机制的限制。因为V8须要保证JavaScript应用逻辑与垃圾回收器所看到的不同,V8在执行垃圾回收时会阻塞 JavaScript应用逻辑,直到垃圾回收结束再从新执行JavaScript应用逻辑,这种行为被称为“全停顿”(stop-the-world)。 若V8的堆内存为1.5GB,V8作一次小的垃圾回收须要50ms以上,作一次非增量式的垃圾回收甚至要1秒以上。这样浏览器将在1s内失去对用户的响 应,形成假死现象。若是有动画效果的话,动画的展示也将显著受到影响redis

 

3、chrome V8的堆构成

V8的堆其实并不仅是由老生代和新生代两部分构成,能够将堆分为几个不一样的区域:

一、新生代内存区:大多数的对象被分配在这里,这个区域很小可是垃圾回特别频繁;

二、老生代指针区:属于老生代,这里包含了大多数可能存在指向其余对象的指针的对象,大多数重新生代晋升的对象会被移动到这里;

三、老生代数据区:属于老生代,这里只保存原始数据对象,这些对象没有指向其余对象的指针;

四、大对象区:这里存放体积超越其余区大小的对象,每一个对象有本身的内存,垃圾回收其不会移动大对象;

五、代码区:代码对象,也就是包含JIT以后指令的对象,会被分配在这里。惟一拥有执行权限的内存区;

六、Cell区、属性Cell区、Map区:存放Cell、属性Cell和Map,每一个区域都是存放相同大小的元素,结构简单。

每一个区域都是由一组内存页构成,内存页是V8申请内存的最小单位,除了大对象区的内存页较大之外,其余区的内存页都是1MB大小,并且按照1MB对 齐。内存页除了存储的对象,还有一个包含元数据和标识信息的页头,以及一个用于标记哪些对象是活跃对象的位图区。另外每一个内存页还有一个单独分配在另外内 存区的槽缓冲区,里面放着一组对象,这些对象可能指向其余存储在该页的对象。垃圾回收器只会针对新生代内存区、老生代指针区以及老生代数据区进行垃圾回收。

 

4、chrome V8的垃圾回收机制

4.1如何判断回收内容

如何肯定哪些内存须要回收,哪些内存不须要回收,这是垃圾回收期须要解决的最基本问题。咱们能够这样假定,一个对象为活对象当且仅当它被一个根对象 或另外一个活对象指向。根对象永远是活对象,它是被浏览器或V8所引用的对象。被局部变量所指向的对象也属于根对象,由于它们所在的做用域对象被视为根对 象。全局对象(Node中为global,浏览器中为window)天然是根对象。浏览器中的DOM元素也属于根对象。

 

4.2如何识别指针和数据

垃圾回收器须要面临一个问题,它须要判断哪些是数据,哪些是指针。因为不少垃圾回收算法会将对象在内存中移动(紧凑,减小内存碎片),因此常常须要进行指针的改写:

目前主要有三种方法来识别指针:
1. 保守法:将全部堆上对齐的字都认为是指针,那么有些数据就会被误认为是指针。因而某些实际是数字的假指针,会背误认为指向活跃对象,致使内存泄露(假指针指向的对象多是死对象,但依旧有指针指向——这个假指针指向它)同时咱们不能移动任何内存区域。
2. 编译器提示法:若是是静态语言,编译器可以告诉咱们每一个类当中指针的具体位置,而一旦咱们知道对象时哪一个类实例化获得的,就能知道对象中全部指针。这是JVM实现垃圾回收的方式,但这种方式并不适合JS这样的动态语言
3. 标记指针法:这种方法须要在每一个字末位预留一位来标记这个字段是指针仍是数据。这种方法须要编译器支持,但实现简单,并且性能不错。V8采用的是这种方式。V8将全部数据以32bit字宽来存储,其中最低一位保持为0,而指针的最低两位为01

 

4.3 V8回收策略

自动垃圾回收算法的演变过程当中出现了不少算法,可是因为不一样对象的生存周期不一样,没有一种算法适用于全部的状况。因此V8采用了一种分代回收的策 略,将内存分为两个生代:新生代和老生代

新生代的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。分别对新生代和老生代使用 不一样的垃圾回收算法来提高垃圾回收的效率。对象起初都会被分配到新生代,当新生代中的对象知足某些条件(后面会有介绍)时,会被移动到老生代(晋升)。

 

5、新生代算法

新生代中的对象通常存活时间较短,使用 Scavenge GC 算法。在Scavenge的具体实现中,主要是采用一种复制的方式的方法--cheney算法。

在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,一定有一个空间是使用的,另外一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,若是有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。

 

6、老生代算法

老生代中的对象通常存活时间较长且数量也多,使用了两个算法,分别是标记清除算法标记压缩算法

在讲算法前,先来讲下什么状况下对象会出如今老生代空间中:

一、新生代中的对象是否已经经历过一次 Scavenge 算法,若是经历过的话,会将对象重新生代空间移到老生代空间中。

二、To 空间的对象占比大小超过 25 %。在这种状况下,为了避免影响到内存分配,会将对象重新生代空间移到老生代空间中。

老生代中的空间很复杂,有以下几个空间:

enum AllocationSpace {
  // TODO(v8:7464): Actually map this space's memory as read-only. RO_SPACE, // 不变的对象空间 NEW_SPACE, // 新生代用于 GC 复制算法的空间 OLD_SPACE, // 老生代常驻对象空间 CODE_SPACE, // 老生代代码对象空间 MAP_SPACE, // 老生代 map 对象 LO_SPACE, // 老生代大空间对象 NEW_LO_SPACE, // 新生代大空间对象 FIRST_SPACE = RO_SPACE, LAST_SPACE = NEW_LO_SPACE, FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE, LAST_GROWABLE_PAGED_SPACE = MAP_SPACE };

在老生代中,如下状况会先启动标记清除算法:

一、某一个空间没有分块的时候

二、空间中被对象超过必定限制

三、空间不能保证新生代中的对象移动到老生代中

Mark Sweep 是将须要被回收的对象进行标记,在垃圾回收运行时直接释放相应的地址空间,以下图所示(红色的内存区域表示须要被回收的区域):

Mark Compact 的思想有点像新生代垃圾回收时采起的 Cheney 算法:将存活的对象移动到一边,将须要被回收的对象移动到另外一边,而后对须要被回收的对象区域进行总体的垃圾回收。

在这个阶段中,会遍历堆中全部的对象,而后标记活的对象,在标记完成后,销毁全部没有被标记的对象。在标记大型对内存时,可能须要几百毫秒才能完成一次标记。这就会致使一些性能上的问题。为了解决这个问题,2011 年,V8 从 stop-the-world 标记切换到增量标志。在增量标记期间,GC 将标记工做分解为更小的模块,可让 JS 应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿状况。但在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术可让 GC 扫描和标记对象时,同时容许 JS 运行。

清除对象后会形成堆内存出现碎片的状况,当碎片超过必定限制后会启动压缩算法。在压缩过程当中,将活的对象像一端移动,直到全部对象都移动完成而后清理掉不须要的内存。

 

7、内存泄露和优化

7.1 什么是内存泄露?

存泄露是指程序中已分配的堆内存因为某种缘由未释放或者没法释放,形成系统内存的浪费,致使程序运行速度减慢甚至系统奔溃等后果。。

7.2 常见的内存泄露的场景

7.2.1 缓存

js开发时候喜欢用对象的键值来缓存函数的计算结果,可是缓存中存储的键越多,长期存活的对象就越多,致使垃圾回收在进行扫描和整理时,对这些对象作了不少无用功。

7.2.2 做用域未释放(闭包)

var leakArray = []; exports.leak = function () { leakArray.push("leak" + Math.random()); }

模块在编译执行后造成的做用域由于模块缓存的缘由,不被释放,每次调用 leak 方法,都会致使局部变量 leakArray 不停增长且不被释放。

闭包能够维持函数内部变量驻留内存,使其得不到释放。

 

7.2.3 没有必要的全局变量

声明过多的全局变量,会致使变量常驻内存,要直到进程结束才可以释放内存。

 

7.2.4 无效的DOM引用

//dom still exist function click(){ // 可是 button 变量的引用仍然在内存当中。 const button = document.getElementById('button'); button.click(); } // 移除 button 元素 function removeBtn(){ document.body.removeChild(document.getElementById('button')); }

 

7.2.5 定时器未清除

// vue 的 mounted 或 react 的 componentDidMount componentDidMount() { setInterval(function () { // ...do something }, 1000) }

vue 或 react 的页面生命周期初始化时,定义了定时器,可是在离开页面后,未清除定时器,就会致使内存泄漏。

 

7.2.6 事件监听为空白

componentDidMount() {
    window.addEventListener("scroll", function () { // do something... }); }

在页面生命周期初始化时,绑定了事件监听器,但在离开页面后,未清除事件监听器,一样也会致使内存泄漏。

 

7.3 内存泄露优化

7.3.1 解除引用

确保占用最少的内存可让页面得到更好的性能。而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据。一旦数据再也不有用,最好经过将其值设置为 null 来释放其引用——这个作法叫作解除引用(dereferencing)

function createPerson(name){ var localPerson = new Object(); localPerson.name = name; return localPerson; } var globalPerson = createPerson("Nicholas"); // 手动解除 globalPerson 的引用 globalPerson = null;

解除一个值的引用并不意味着自动回收该值所占用的内存。解除引用的真正做用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收

 

7.3.2 提供手动清空变量的方法

var leakArray = []; exports.clear = function () { leakArray = []; }

 

7.3.3 其余方法

一、在业务不须要的用到的内部函数,能够重构到函数外,实现解除闭包。

二、避免建立过多的生命周期较长的对象,或者将对象分解成多个子对象。

三、避免过多使用闭包。

四、注意清除定时器和事件监听器。

五、nodejs中使用stream或buffer来操做大文件,不会受nodejs内存限制。

六、使用redis等外部工具来缓存数据。

 

8、总结

js是一门具备自动回收垃圾收集的编程语言,在浏览器中主要是经过标记清除的方法回收垃圾,在nodejs中主要是经过分代回收,Scavenge,标记清除,增量标记等算法来回收垃圾。在平常开发中,有一些不引入注意的书写方式可能会致使内存泄露,多注意本身代码规范。

 

9、参考

一、V8的垃圾回收机制与内存限制

二、node 内存限制的问题

三、node内存控制

四、深刻浅出Nodejs

五、javascript高级程序设计

 

原文出处:https://www.cnblogs.com/chengxs/p/10919311.html

相关文章
相关标签/搜索