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

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

ini 复制代码
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引擎会创建一个函数的活动对象【执行上下文】并推入调用栈的栈顶。

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

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

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

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

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

堆内存回收

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

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

如何判断非活跃对象

1、引用计数

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

2、可访问性分析法

V8采用了这种方法。

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

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

  • 全局的window对象
  • 文档DOM树。由可以通过遍历文档到达的所有原生 DOM 节点组成。
  • 存放栈上变量
graph TD 通过GCRoots标记活跃对象和非活跃对象 --> 回收非活动对象的内存 --> 整理不连续的内存空间

代际假说

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

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

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

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

如何避免内存泄漏

  • 少创建全局变量

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

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

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

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

    绑定事件回收

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

    • 特点:key必须是一个对象,不能是原始值
    • 优点:可以快速被垃圾回收
    • 缺点:不支持迭代以及keys(), values(),和entries()方法。
相关推荐
FliPPeDround3 天前
浏览器扩展 E2E 测试的救星:vitest-environment-web-ext 让你告别繁琐配置
e2e·浏览器·测试
SuperEugene3 天前
浏览器存储:localStorage / sessionStorage / cookie 应该怎么用
前端·javascript·面试·浏览器
宁雨桥3 天前
浏览器渲染原理
前端·浏览器·原理
YZ0995 天前
2026年如何批量保存小红书作者主页的视频、图片和文案?
经验分享·浏览器·插件
程序员ys5 天前
网页白屏的原理与优化
前端·性能优化·浏览器
Wect7 天前
从输入URL到页面显示的完整技术流程
前端·面试·浏览器
NEXT067 天前
从输入 URL 到页面展示的完整链路解析
网络协议·面试·浏览器
CappuccinoRose10 天前
CSS 语法学习文档(十五)
前端·学习·重构·渲染·浏览器
REDcker11 天前
Media Source Extensions (MSE) 详解
前端·网络·chrome·浏览器·web·js
x-cmd12 天前
Browser-Use:用自然语言控制浏览器,告别脆弱的自动化脚本
运维·ai·自动化·agent·浏览器·x-cmd