前言
垃圾回收是什么,你们是不是想到垃圾车来收垃圾了?耳边响起东方红?
是不是还有人想到想到的是废品回收:收电饭煲、高压窝、煤气灶~~~
其实今天我们要讲的是内存中·垃圾回收啦~
本文首发于【掘金-垃圾回收(GC)哪些事儿】
第一次了解垃圾回收是在一个公众号公众号看到的,当时讲了一下标记清除法和引用计数法,但是当时存在很多疑惑,比如可达不可达到底是什么?当时也没太在意。今天重新了解,用自己的话总结分享出来。希望能给您一些启发和思考。
这篇文章包含以下知识点:
- 什么是垃圾回收
- 如何判断是否为垃圾
- 可达性
- 垃圾回收算法有哪些,各自的特点
- 内存泄漏
在文章开始前要知道一个很重要的知识,JS的内存生命周期。今天要讲的就是那些关于内存释放时的故事。
- 1.为变量分配内存
- 2.使用分配的内存
- 3.不需要的时候将内存释放
一、什么是垃圾回收
第一次垃圾回收难理解我觉得一个原因是比较抽象,毕竟是浏览器内部的操作,是我们肉眼不可见的。
可以想象我们日常生活中那些废弃的没用的东西就是“垃圾”,为了占位置就要将生活垃圾清理掉。js内存管理也是如此,只不过他的内存管理是自动执行的,我们不可见的(但是在没有垃圾回收机制的语言中就需要人为管理内存,比如c语言)。
知识点1
JavaScript
引擎中有一个后台进程称为垃圾回收器,它监视所有对象,并删除那些不可访问的对象(垃圾)。
知识点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属性了。
上面的图片都来自
这里。我们继续来看看垃圾回收算法有哪些。
三、垃圾回收算法
这里主要介绍两种主要回收算法,如果想了解更多,比如标记压缩,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数组从新生代中晋升后被移到这里。
增量回收
如果有很多对象,并且我们试图一次遍历并标记整个对象集,那么可能会花费一些时间,并在执行中会有一定的延迟。因此,引擎试图将垃圾回收分解为多个部分。然后,各个部分分别执行。这需要额外的标记来跟踪变化,这样有很多微小的延迟,而不是很大的延迟。
空闲时间收集
垃圾回收器只在 CPU 空闲时运行,以减少对执行的可能影响。
内存泄露
由于内存泄露和内存没有被释放有关,所以这里简单介绍下什么时候会产生内存泄露吧!
知识点1:
什么是内存泄露,对于不再用到的内存如果没有及时释放就叫内存泄露。
这和泄露有半毛钱关系???
我是这样理解的,我们把内存比作手心里的沙子,当沙子从手里漏了出去,那么可用的内存就越来越少了,这个过程就是内存泄露。
手里握不住的沙,不如扬了它
但是内存泄露了还是要管一下的啦,如何对它负责,请接着往下看
四种内存泄露
理解了内存泄露的概念后,我们要知道以下几种情况会导致内存泄露
- 意外的全局变量(严格模式解决)
- 被遗忘的定时器和回调函数
- 脱离dom的引用
- 闭包(变量引用指向null解决)
第一个和第四个很好理解,如果有问题可以评论区找linglong,十分欢迎。主要讲讲2、3两个。
- 2、被遗忘的定时器和回调函数
当不需要setInterval或者setTimeout时,定时器没有被clear,定时器的回调函数以及内部依赖的变量都不能被回收,造成内存泄漏。
timeout = setTimeout(() => {
var node = document.getElementById('node');
if(node){
fn();
}
}, 1000);
解决方法: 在定时器完成工作的时候,手动清除定时器。timeout=null
- 3、脱离dom的引用
<body>
<div id="fa">
<button id="button"></button>
</div>
<script>
const div=document.getElementById('fa');
const button=document.getElementById('button');
document.body.removeChild(div); // dom删除了
//button=null //切断button对button的引用
console.log(div);
</script>
</body>
其他内存泄露情况
除了上面四种,如果有其他的内存泄露情况欢迎指出,一起学习,嘻嘻嘻
结果:
<div id="fa">
<button id="button"></button>
</div>
我们可以看到结果中button并没有被删除,这是因为代码中删除的button是dom树中的button,const button=document.getElementById('button');
这句代码中存在着button对button的引用。
solution:我们要通过button=null
将两次引用都切掉。
最后给大家一个思考题:
如何减少内存泄露?欢迎大家评论区各抒己见。
总结
看完了给自己一个大大的赞吧,可以问问自己:
- 什么是垃圾回收,什么是可达性
- 垃圾回收算法,哪两种
- 内存泄露常见的有哪些,如何解决
认真看完一定会有收获滴,比心~
玲珑觉得如果垃圾回收算法的话可以聊很多.
从内存机制开始讲起,什么是垃圾回收,垃圾回收算法,v8引擎如何回收等,内存泄露,以及ES6中国的Weakset和WeakMap这两个不计入垃圾回收机制的弱引用…
如果我有哪些理解不对的地方还请掘友们指正,如果误导大家就尴尬啦
另外文章封面“除了money
都是垃圾”是一句玩笑话啦,毕竟还是有很多东东高于毛爷爷的,比如你们的star~
和留言!