垃圾回收机制的理解

做一个总结和学习跟大家分享,希望对大家也有所帮助,共同成长进步💪~

如果大家喜欢,可以点赞或留言哈💕~~~~,谢谢大家⭐️⭐️⭐️~~~

垃圾回收的必要性

下面这段话引自《JavaScript权威指南(第四版)》

由于字符串、对象和数组没有固定大小,所以当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。

这段话解释了为什么需要系统需要垃圾回收,JavaScript不像C/C++,它有自己的一套垃圾回收机制。

JavaScript垃圾回收的机制很简单:找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是时时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行。

什么是垃圾回收?

因为内存泄漏了,所以引擎才会去回收这些没有用的变量,这一过程就叫垃圾回收

什么是内存泄漏?

不再用到的内存,没有得到释放就是内存泄露

垃圾回收运行机制

在说这个话题前,我们先回顾下,在 JavaScript 由什么组成的,JavaScript 的数据类型可分为基本类型和引用类型。基本类型存在栈内存,引用类型存在堆内存

但是我们那时没有解释为什么基本类型要存在栈中,引用类型要存在堆中。只是介绍,因为基本类型所花销的内存小,而引用类型所花销的内存大,而这恰恰是分两个空间存放不同数据的原因

在 JavaScript 中,引擎需要用栈来维护程序执行时的上下文状态(即执行上下文),如果栈空间大了的话,所有数据存放在栈空间中,会影响到上下文切换的效率,从而影响整个程序的执行效率,所以占内存大的数据会放在堆空间中,引用它的地址来表示这个变量

堆内存的分类

一个 V8 进程的内存通常由以下部分组成

  • 新生代内存区(new space)
  • 老生代内存区(old space)
  • 大对象区(large object space)
  • 代码区(code space)
  • map 区(map space)

其他几个不重要,关键是新生代(内存)和老生代(内存)。针对新生代和老生代,引擎采用了两种不同的垃圾回收机制

新生代与老生代的垃圾回收

新生代中存放生存时间短的对象,老生代存放生存时间久的对象

为此,新生代区通常只支持1~8M 的容量,而老生代区会支持更大的容量,而针对这两块区域,V8 分别使用两个不同的垃圾回收器

  • 主垃圾回收器,负责老生代的垃圾回收
  • 副垃圾回收器,负责新生代的垃圾回收

新生代内存回收

新生代采用的是 Scavenge 算法,所谓 Scavenge 算法,是把新生代空间对半分为两个区域,一半是对象区域(from),一半是空闲区域(to)。如下图所示:

新的对象会首先被分配到对象(from)空间,当对象区域被占满时,就需要执行一次垃圾清理操作。当进行垃圾收回时,先将 from 空间中存活的对象复制到空闲(to)空间进行保存,对未存活的空间进行回收。复制完成后,对象空间和空闲空间进行角色调换,空闲空间变成新的对象空间,原来的对象空间则变成空闲空间。这样就完成了垃圾对象的回收操作,同时这种角色调换的操作能让新生代中的这两块区域无限重复使用下去

而当一个对象在两次变换中还存在时,就会从新生代区"晋升"到"老生代区"。这一过程被称为对象晋升策略

老生代内存回收

主垃圾回收器负责老生代区的垃圾回收。其中的对象包括新生代区"晋升"的对象和一些大的对象。因此老生代区中的对象有两个特点,对象占用空间大,对象存活时间长

它不会像新生代区那样使用 Scavenge 算法,因为复制大对象所花费的时间长,执行效率并不高。所以它采用标记 - 清除(Mark - Sweep)进行垃圾回收

简单来说,先标记,然后清除,但是内存空间里的对象还是不连续,所以引入整理。这就是老生代区的垃圾回收过程 标记 - 清除 - 整理。先标记哪些是要回收的变量,再进行回收(清除),然后将内存空间整理(到一边),这样空间就大了

因为老生代区的对象相对大,虽然采用"标记-清除"算法会比 Scavenge 更快,但架不住卡顿问题。为什么会卡顿?因为 JavaScript 是单线程。为此,V8 将标记过程分为一个个子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,这一算法被称为增量标记算法

而这一行为,与 React Fiber 的设计思路类似,将大任务分割成小任务,因为小,所以执行快,让人察觉不到卡顿

新生代 VS 老生代

  • 新生代垃圾回收是临时分配的内存,存活时间短;老生代垃圾回收是常驻内存,存活时间长
  • 新生代垃圾回收由副垃圾回收器负责;老生代垃圾回收由主垃圾回收器负责
  • 新生代采用 Scavenge 算法;老生代采用「标记-清除」算法
    • Scavenge 算法:将空间分为两半,一半是 from 空间,一半是 to 空间。新加入的对象会放在 from 空间,当空间快满时,执行垃圾清理;再角色调换,再当调换完后的 from 空间快蛮时,再执行垃圾清理,如此反复
    • 标记-清理-整理:此为两个算法,「标记-清理」算法和 「标记-整理」算法
      • 标记-清理:标记用不到的变量,清理掉
      • 标记-整理:清理完内存后,会产生不连续的内存空间,为节省空间,整理算法会将内存排序到一处空间,空间就变大了

引用计数

在 《JavaScript 高级程序设计》中介绍了另一种垃圾回收的机制------引用计数

简单来说:引擎会有张"引用表",保存了内存里面的资源的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放

但后来这个机制被放弃了,因为它会遇到一个严重的问题:循环引用,从而导致内存泄漏,所以被放弃了

哪些情况会引起内存泄漏?

1. 意外的全局变量

ini 复制代码
function foo(arg) {
    bar = "this is a hidden global variable";
}

bar没被声明,会变成一个全局变量,在页面关闭之前不会被释放。

另一种意外的全局变量可能由 this 创建:

csharp 复制代码
function foo() {
    this.variable = "aaaaa";
}
// foo 调用自己,this 指向了全局对象(window)
foo();

在 JavaScript 文件头部加上 'use strict',可以避免此类错误发生。启用严格模式解析 JavaScript ,避免意外的全局变量。

2. 被遗忘的计时器或回调函数

ini 复制代码
var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // 处理 node 和 someResource
        node.innerhtml = json.stringify(someResource));
    }
}, 1000);

这样的代码很常见,如果id为Node的元素从DOM中移除,该定时器仍会存在,同时,因为回调函数中包含对someResource的引用,定时器外面的someResource也不会被释放。

3. 闭包

javascript 复制代码
function bindEvent(){
  var obj=document.createElement('xxx')
  obj.onclick=function(){
    // Even if it is a empty function
  }
}

闭包可以维持函数内局部变量,使其得不到释放。上例定义事件回调时,由于是函数内定义函数,并且内部函数--事件回调引用外部函数,形成了闭包。

javascript 复制代码
// 将事件处理函数定义在外面
function bindEvent() {
  var obj = document.createElement('xxx')
  obj.onclick = onclickHandler
}
// 或者在定义事件处理函数的外部函数中,删除对dom的引用
function bindEvent() {
  var obj = document.createElement('xxx')
  obj.onclick = function() {
    // Even if it is a empty function
  }
  obj = null
}

解决之道,将事件处理函数定义在外部,解除闭包,或者在定义事件处理函数的外部函数中,删除对dom的引用。

4. 没有清理的DOM元素引用

有时,保存 DOM 节点内部数据结构很有用。假如你想快速更新表格的几行内容,把每一行 DOM 存成字典(jsON 键值对)或者数组很有意义。此时,同样的 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中。将来你决定删除这些行时,需要把两个引用都清除。

javascript 复制代码
var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image'),
    text: document.getElementById('text')
};
function doStuff() {
    image.src = 'http://some.url/image';
    button.click();
    console.log(text.innerhtml);
}
function removeButton() {
    document.body.removeChild(document.getElementById('button'));
    // 此时,仍旧存在一个全局的 #button 的引用
    // elements 字典。button 元素仍旧在内存中,不能被 GC 回收。
}

虽然我们用removeChild移除了button,但是还在elements对象里保存着#button的引用,换言之,DOM元素还在内存里面。

结合以上四点简单来说

  • 第一种情况是由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。

  • 第二种情况是设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。

  • 第三种情况是不合理的使用闭包,从而导致某些变量一直被留在内存当中。

  • 第四种情况是获取一个 DOM 元素的引用,而后面这个元素被删除,由于我们一直保留了对这个元素的引用,所以它也无法被回收。

相关推荐
_.Switch1 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光1 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   1 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   1 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web1 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常1 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇2 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr2 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho3 小时前
【TypeScript】知识点梳理(三)
前端·typescript
安冬的码畜日常4 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js