浏览器V8是怎么进行垃圾回收的

面试相关问题解答

1、浏览器V8是怎么进行垃圾回收的

浏览器的内存占用是有限制的:

64位系统:
	物理内存 > 16G =>  最大堆内存限制为4G
	物理内存 < 16G =>  最大堆内存限制为2G
32位系统:
	最大堆内存限制为1G

为什么浏览器要对占用内存做限制呢?

在程序运行的过程中,我们要将不用的内存释放出来,否则复杂的程序会占用很大的内存,导致程序运行缓慢或者卡顿。JavaScript的运行时单线程的,在执行垃圾回收时会阻塞JavaScript应用执行,直到垃圾回收结束才继续执行JavaScript应用逻辑,这种阻塞被称为“全停顿”(stop-the-world)

若V8的内存为1.5G,执行一次小的垃圾回收在50ms以上,做一次非增量级的垃圾回收要1s以上,这样浏览器将在这1s内失去对用户的响应,造成假死想象。如果有动画的话,动画也会收到印象,这样会造成严重的影响。

chrome是很占内存的

chrome很占内存是因为浏览器使用了多进程机制,浏览器的每一个标签页以及扩展都独立占一个进程。访问一个网站,至少要占4个进程:1个浏览器进程,一个GPU进程,一个渲染进程和一个网络进程,除此之外,可能还有多个插件组成的进程架构。而最新的Chrome浏览器运行一个网站包括:1个浏览器进程,一个GPU进程,一个网络进程,多个渲染进程和多个插件进程。【多进程架构优点是更稳定、流畅、安全】【缺点是架构复杂(各模块耦合性高、扩展性差)、占用资源高】

我们的javascript是运行在渲染进程中的。

内存空间

在javascript执行过程中,有三种类型的空间:代码空间、栈空间、堆空间

  • 代码空间:存储可执行代码
  • 栈空间:调用栈,用来存储可执行上下文,原始类型的数据值和引用类型的地址都是直接存在栈空间
  • 堆空间:存储引用类型的值
引用类型为什么一定要存在堆空间中,直接存在堆空间中行不行?

答案是不可以,javascript引擎用栈来维护程序执行期间上下文状态,如果栈空间过大,所有的数据都存在栈空间中,会影响到上下文切换的效率,进而影响到整个程序的执行效率

调用栈切换执行上下文状态

浏览器V8是怎么进行垃圾回收的_第1张图片

栈内存回收

调用一个函数时,V8引擎会创建一个函数的活动对象【执行上下文】并推入调用栈的栈顶。

活动对象包含这个函数的参数、局部变量、返回值

调用完成后,活动对象从栈中弹出,释放内存。继续执行当前执行环境下剩余的代码

当分配的调用栈空间被占满时,会引发“堆栈溢出”问题

一开始调用栈为空,直到函数被调用,便自动的加入调用栈,执行完成,调用栈自动弹出这个函数。依此类推

综上所述,栈内存是随着函数执行完成调用栈弹出活动对象时自动释放的。函数执行完毕,立即释放,节省内存空间。

堆内存回收

堆内存主要用来存放对象和动态数据的地方。是程序对于内存空间最大的一个地方。同时我们常说的垃圾回收就是指堆内存的垃圾回收。

V8使用垃圾回收机制来管理我们的堆内存。简单来说就是释放孤立(非活跃)对象使用的内存。

如何判断非活跃对象
1、引用计数

每当有引用的地方,就加一,去掉引用就减一,这种方式无法解决循环引用的问题,引用计数都无法为0,导致无法GC,所以V8没有使用这种方法

2、可访问性分析法

V8采用了这种方法。

将一个成为GCRoots的对象(在浏览器环境中,GC Roots可以包括:全局的window对象,所有原生dom节点集合等等)做为所有初始存活的对象集合,从这个对象出发,进行遍历,遍历到就认为是可访问的,标记为活动对象,需要保留,如果没有访问到,就是非活动对象,可能会被垃圾回收

在浏览器环境中,GCRoots有很多,通常包含一下几种(实际更多):

  • 全局的window对象
  • 文档DOM树。由可以通过遍历文档到达的所有原生 DOM 节点组成。
  • 存放栈上变量

浏览器V8是怎么进行垃圾回收的_第2张图片

代际假说

代际假说是垃圾回收的一个关键术语,它有两个特点:

  1. 大部分变量对象的生命周期很短,比如函数内部的变量,块级作用域中的变量。这些代码在函数执行完成就可以清除。
  2. 少量的对象会存活很久,比如全局的window、Dom、全局api等对象

我们将生命周期短的对象叫新生代,生命周期长的称为老生代

浏览器V8是怎么进行垃圾回收的_第3张图片

对于这两种不同的活动对象,V8分别采用不同的垃圾回收机制,达到高效垃圾回收的目的

副垃圾回收器:主要负责新生代的垃圾回收

主垃圾回收器:主要负责老生代的垃圾回收

新生代 - Scavenger算法

新生代分为两个区域,From区-To区,或者称为激活区(new space)-未激活区**(inactive new space)**,这两块区域大小相同,称为Semispace

这是一个牺牲空间换取时间的算法

1、新的对象存放在from-space

2、垃圾回收时,将还活跃的对象复制到to-space

3、清空from-space

4、将from-space和to-space互换,依此类推

scavenger算法需要在每次执行时将存活的对象复制到空闲区域,但是复制需要时间成本,如果新生区空间太大了,复制时间会比较久,所以为了执行效率,新生区空间一般都比较小。

晋升机制
  • 【第一次回收->nursery子代】 =》【第二次回收 ->intermediate子代】 =》【第三次回收->晋升到老生代】
  • to空间超过25%
老生代

主要使用【标记-清除】和【标记-整理】两个算法

Mark-Sweep【标记清除】

也就是上面提到的可访问性分析

遍历堆中的所有对象,递归调用这组跟元素,标记存活和未存活的对象,标记完成后,将未存活对象进行清除。因为这里都是生命周期长的对象,未存活对象比较少,所以效率比较高。

Mark-Compact【标记整理】

标记清除后,导致不连续的存储空间比较多,产生大量不连续的内存碎片,碎片过多无法分配大对象。

标记整理是将所有存活对象向一端移动,然后直接释放掉端边界以外的内存。从而让活动对象占连续的内存。

垃圾回收引起的性能问题

JavaScript是运行在主线程上的,为了避免JavaScript应用逻辑与垃圾回收产生不一致的冲突,垃圾回收执行时,就会占用JavaScript引擎,正在执行的JavaScript脚本会被暂停。

在V8的分代式垃圾回收中,新生代内存比较小,对应的活动对象也比较少,所以执行速度快,全停顿影响也不大。老生代内存比较大,且活动对象比较多,全堆的活动对象标记、清除、整理耗费的时间就比较长,造成的停顿会比较严重。

如何避免内存泄漏

  • 少创建全局变量

    • 全局变量会在页面关闭时才回收,所以避免创建全局变量,和没有声明的变量(变量提升,变成全局变量)
  • 手动清除定时器

    var someResource = getData();
    setInterval(function() {
        var node = document.getElementById('Node');
        if(node) {
            node.innerHTML = JSON.stringify(someResource));
        }
    }, 1000);
    someResource = null; // 定时器依然在引用变量无法回收 
    
  • 少用闭包

    var leaks = (function(){
        var leak = 'xxxxxx';// 闭包中引用,不会被回收
        return function(){
            console.log(leak);
        }
    })()
    
  • 清除DOM引用

    var element = {
      image: document.getElementById('image'),
      button: document.getElementById('button')
    };
    document.body.removeChild(document.getElementById('image'));
    // 如果element没有被回收,这里移除了 image 节点也是没用的,image 节点依然留存在内存中. 
    

    绑定事件回收

    let oDiv = document.querySelector('div');
    oDiv.onclick = function(){
                alert(111111111)
            }
    document.body.removeChild(oDiv);
    oDiv.onclick = null; // 解除事件绑定,触发垃圾回收 
    
  • 弱引用(WeakMap和WeakSet)

    • 特点:key必须是一个对象,不能是原始值
    • 优点:可以快速被垃圾回收
    • 缺点:不支持迭代以及keys(), values(),和entries()方法。

你可能感兴趣的:(前端,前端,浏览器)