垃圾回收(GC)那些事儿

前言

垃圾回收是什么,大家是否是想到垃圾车来收垃圾了?耳边响起东方红?前端

是否是还有人想到想到的是废品回收:收电饭煲、高压窝、煤气灶~~~node

其实今天咱们要讲的是内存中·垃圾回收啦~面试

第一次了解垃圾回收是在一个公众号看到的,当时讲了一下标记清除法和引用计数法,可是当时存在不少疑惑,好比可达不可达究竟是什么?当时也没太在乎。今天从新了解,用本身的话总结分享出来。但愿能给您一些启发和思考。算法

这篇文章包含如下知识点:数组

  • 什么是垃圾回收
  • 如何判断是否为垃圾
    • 可达性
  • 垃圾回收算法有哪些,各自的特色
  • 内存泄漏

在文章开始前要知道一个很重要的知识,JS的内存生命周期。今天要讲的就是那些关于内存释放时的故事。浏览器

  • 1.为变量分配内存
  • 2.使用分配的内存
  • 3.不须要的时候将内存释放

1、什么是垃圾回收

首先我认为垃圾回收比较难理解的一个缘由是:它比较抽象,毕竟是浏览器内部的操做,是咱们肉眼不可见的。bash

能够想象咱们平常生活中那些废弃的没用的东西就是“垃圾”,为了占位置就要将生活垃圾清理掉。js内存管理也是如此,只不过他的内存管理是自动执行的,咱们不可见的(可是在没有垃圾回收机制的语言中就须要人为管理内存,好比c语言)。数据结构

知识点1闭包

JavaScript 引擎中有一个后台进程称为垃圾回收器,它监视全部对象,并删除那些不可访问的对象(垃圾)。dom

知识点2

咱们在建立一个字符串、数组等都看做对象(无论是基本类型仍是引用类型)都会为这个对象开辟一个内存空间来保存这个变量。若是访问不到这个对象的时候(没用了)就是垃圾

那么你可能会问, 不可访问的对象是什么呢?怎么知道对象是否能够访问呢?下面就由我一一为你们道来

2、如何判断垃圾

如何判断垃圾前面说过就是看这个对象可否被访问,那如何知道对象可否被访问?有一个专业的词叫可达性。根据对象是否可达来判断。

可达性

JavaScript 中内存管理的主要概念是可达性。

简单地说,“可达性” 值就是那些以某种方式可访问或可用的值,它们被保证存储在内存中。先看一个例子吧

//定义一个user对象,引用name属性
const user={
    name:"john"
}
复制代码

这里箭头表示一个对象引用。全局变量user引用对象 {name:“John”} ,user 的 “name” 属性存储一个基本类型,所以它被绘制在对象中。

若是 user 的值被覆盖,则引用丢失:

例1

//让user的引用为空
user=null
复制代码

这个时候经过user就没有办法访问到name这个属性,更没办法获得属性值。

图中的箭头表示引用,第一个图user引用name属性,第二图让user指向空,箭头消失,user没法引用name属性,js引擎将 {name:“John”}回收到垃圾桶处理掉,释放了内存空间。

圈个重点👇

user能够访问到name属性,那name是可达的;没法访问那么name就是不可达的。

看了上面这个例子不知道对可达性是否有基础的认识了呢?接着咱们继续深刻可达性。

  • 有一组基本的固有可达值,因为显而易见的缘由没法删除, 例如:

    • 本地函数的局部变量和参数

    • 当前嵌套调用链上的其余函数的变量和参数

    • 全局变量

    • 还有一些其余的,内部的

上面这些值称为根。

  • 若是引用或引用链能够从根访问任何其余值,则认为该值是可访问的。

如今咱们又多了一个概念那就是。接着来看一个例子。

例2

// user具备对象的引用
let user = {
  name: "John"
};

let admin = user;
复制代码

咱们建立一个全局新对象admin,它和user同样引用了同一个变量。此时name是可达的,若是咱们进行下面的操做,它仍是可达的吗?

admin=null;
复制代码

结果是name属性仍是可达的,为何呢?不是已经删除了admin对name的引用吗?

缘由是:虽然admin没有办法引用name,可是user仍是能够引用name属性的,所以能够从根访问到name属性,所以他仍是可达的。

若是再让user=null;那name才会变成不可达。这个时候没法从引用name属性了。

上面的图片都来自 这里。咱们继续来看看垃圾回收算法有哪些。

3、垃圾回收算法

这里主要介绍两种主要回收算法,若是想了解更多,好比标记压缩,GC复制等能够点击这里

引用计数法

引用计数法也很好理解,就是引用对引用的次数进行计数。若是引用了增长就加1,引用减小就减去1.当引用等于0将它清除。看一个例子

  • 例3
//假若有一个计数器count=0
let a ={
    name:'linglong',//count==1
}
let b=a;           //count==2
b=null;            //count==1
a=null;            //count==0,被清除
复制代码

假若有一个引用计数的计数器count,依次进行上面四步操做,对于name的引用从0->1->2->1->0。最后被回收。

引用计数的问题

引用计数有一个致命的问题就是循环引用,若是两个对象互相引用,尽管再也不使用可是会进入一个无限循环,垃圾回收器不会对他进行回收。看下面代码

  • 例4
function cycle(){
    var o1={};
    var o2={};
    o1.a=o2;
    o2.a=o1;
}
cycle();
复制代码

这个代码中cycle函数执行完后不须要了,因此o1和o2的内存应该被释放,可是他们互相引用致使内存不会被回收,如今通常不会使用这个方法,可是ie9以前仍然还在用。如今用的较多的是后面介绍的标记清除法。

标记清除算法

标记清除法分为两大步,先标记而后清除没有被标记的。

  • 垃圾回收器获取根并标记(记住)它们。
    • 而后它访问并“标记”全部来自它们的引用。
    • 而后它访问标记的对象并标记它们的引用。全部被访问的对象都被记住,以便之后再也不访问同一个对象两次。
    • 以此类推,直到有未访问的引用(能够从根访问)为止。
  • 除标记的对象外,全部对象都被清除

例如,对象结构以下:

咱们能够清楚地看到右边有一个“不可到达的块”。如今让咱们看看**“标记并清除”**垃圾回收器如何处理它。

  • 第一步标记根

- 而后标记他们的引用

-以及子孙代的引用:

  • 如今进程中不能访问的对象被认为是不可访问的,将被删除:

标记清除算法数据结构

标记清除法利用到了堆、链表结构 标记阶段:从根集合出发,将全部活动对象及其子对象打上标记 清除阶段:遍历堆,将非活动对象(未打上标记)的链接到空闲链表上

这就是垃圾收集的工做原理。JavaScript引擎应用了许多优化,使其运行得更快,而且不影响执行。

V8引擎一些优化:

分代回收

v8堆中对象对象分为两组:新生代和老生代

  • 新生代:大多数对象的建立被分配在这里,这个区域很小,但垃圾回收很是频繁,独立于其它区存活期短,如临时变量字符串等
  • 老生代
    • 老生代指针区:包含大部分可能含有指向其它对象指针的对象。大多数重新生代晋升(存活一段时间)的对象会被移动到这里。
    • 老生代数据:区包含原始数据对象(没有指针指向其它对象)。Strings、boxed numbers以及双精度unboxed数组重新生代中晋升后被移到这里。
增量回收

若是有不少对象,而且咱们试图一次遍历并标记整个对象集,那么可能会花费一些时间,并在执行中会有必定的延迟。所以,引擎试图将垃圾回收分解为多个部分(18年提出)。而后,各个部分分别执行。这须要额外的标记来跟踪变化,这样有不少微小的延迟,而不是很大的延迟。

空闲时间收集

垃圾回收器只在 CPU 空闲时运行,以减小对执行的可能影响。

内存泄露

因为内存泄露和内存没有被释放有关,因此这里简单介绍下何时会产生内存泄露吧!

知识点1

什么是内存泄露,对于再也不用到的内存若是没有及时释放就叫内存泄露。

这和泄露有半毛钱关系???

我是这样理解的,咱们把内存比做手内心的沙子,当沙子从手里漏了出去,那么可用的内存就愈来愈少了,这个过程就是内存泄露。

手里握不住的沙,不如扬了它

可是内存泄露了仍是要管一下的啦,如何对它负责,请接着往下看

四种内存泄露

理解了内存泄露的概念后,咱们要知道如下几种状况会致使内存泄露

  • 意外的全局变量(严格模式解决)
  • 被遗忘的定时器和回调函数
  • 脱离dom的引用
  • 闭包(变量引用指向null解决)

第一个和第四个很好理解,若是有问题能够评论区找linglong,十分欢迎。主要讲讲二、3两个。

  • 二、被遗忘的定时器和回调函数

当不须要setInterval或者setTimeout时,定时器没有被clear,定时器的回调函数以及内部依赖的变量都不能被回收,形成内存泄漏。

timeout = setTimeout(() => {
 var node = document.getElementById('node');
    if(node){
          fn();
    }
    }, 1000);
复制代码

解决方法: 在定时器完成工做的时候,手动清除定时器。timeout=null

  • 三、脱离dom的引用
<body>
    <div id="fa">
        <button id="button"></button>
    </div>
    <script>
        let div=document.getElementById('fa');
        document.body.removeChild(div); // dom删除了
        //div=null          //切断div对div的引用
        console.log(div);
    </script>
</body>
复制代码

结果:

<div id="fa">
        <button id="button"></button>
 </div>
复制代码

咱们能够看到结果中div并无被删除,这是由于代码中删除的div是dom树中div,let div=document.getElementById('fa');这句代码中存在着div对div的引用。

solution:咱们要经过div=null将两次引用都切掉。

其余内存泄露状况

除了上面四种,若是有其余的内存泄露状况欢迎指出,一块儿学习,嘻嘻嘻

内存泄露的识别方法

内存泄露识别方法的内容来自LinDaiDai_霖呆呆小哥哥的建议,超级nice的做者😃😉

一、浏览器中识别(Chrome浏览器的控制台Performance或Memory)

这里展现performance里面查看的方法。主要步骤以下:

  1. 在网页上右键, 点击“检查”打开控制台(Mac快捷键option+command+i);
  2. 选择Performance面板(下图的步骤1)
  3. 勾选Memory, 而后点击左上角的小黑点Record开始录制(下图的步骤二、3)
  4. 点击弹窗中的Stop结束录制, 面板上就会显示这段时间的内存占用状况。
    完成前面三步开始录制就是下面这个样子。
    第4部结束录制后的样子以下图。若是内存使用状况一直在作增量,就是内存泄露了。
    ps:打开控制台中Memory面板也是能够检查的,在LinDaiDai_霖呆呆记录一次定时器及闭包问题形成的内存泄漏一文中有详细讲解

二、命令行方法

命令行可使用 Node 提供的process.memoryUsage方法。玲珑对node.JS不熟悉,在这以前不知道这个方法的。经查阅官方文档后得知:

process 是一个全局变量,即 global全局对象下的一个的属性它用于描述当前Node.js 进程状态的对象。

memoryUsage是process下的一个方法,返回一个对象,描述了 Node 进程所用的内存情况,单位为字节。

接下来一块儿实践一下吧

  1. 新建一个main.js文件,输入如下代码
const fun = () => {
    console.log(__filename);//文件所在位置的绝对路径
    console.log('下面是内存使用信息:');
    console.log(process.memoryUsage());
}
fun();
复制代码
  1. 在终端输入node main.js回车能够看到执行后结果,以下图所示:

3. 结果说明

咱们看到process.memoryUsage返回一个对象,包含了 Node 进程的内存占用信息。该对象包含四个字段,单位是字节,含义以下。

rss(resident set size):全部内存占用,包括指令区和堆栈。
heapTotal:"堆"占用的内存,包括用到的和没用到的。
heapUsed:用到的堆的部分。
external: V8 引擎内部的 C++ 对象占用的内存。
复制代码

注意:判断内存泄漏,以heapUsed字段为准

最后给你们一个思考题:如何减小内存泄露?能够评论区留言哦

总结

看完了给本身一个大大的赞吧,能够问问本身:

  • 什么是垃圾回收,什么是可达性
  • 垃圾回收算法,哪两种
  • 内存泄露常见的有哪些,如何解决

认真看完必定会有收获滴,比心~

玲珑以为若是垃圾回收算法的话能够聊不少. 从内存机制开始讲起,什么是垃圾回收,垃圾回收算法,v8引擎如何回收等,内存泄露,以及ES6中国的Weakset和WeakMap这两个不计入垃圾回收机制的弱引用....

若是我有哪些理解不对的地方还请掘友们指正,若是误导你们就尴尬啦

另外文章封面“除了money都是垃圾”是一句玩笑话啦,毕竟仍是有不少东东高于毛爷爷的,好比大家的star~和留言!

再次感谢LinDaiDai_霖呆呆小哥哥的鼓励和帮助!

参考文章

[译] 前端面试:谈谈 JS 垃圾回收机制

推荐阅读

咱们下篇文章见...

相关文章
相关标签/搜索