前 言
垃圾回收是什么,你们是不是想到垃圾车来收垃圾了?耳边响起东方红?
是不是还有人想到想到的是废品回收:收电饭煲、高压窝、煤气灶~~~
其实今天我们要讲的是内存中·垃圾回收啦~
第一次了解垃圾回收是在一个公众号看到的,当时讲了一下标记清除法和引用计数法,但是当时存在很多疑惑,比如可达不可达到底是什么?当时也没太在意。今天重新了解,用自己的话总结分享出来。希望能给您一些启发和思考。
这篇文章包含以下知识点:
什么是垃圾回收
如何判断是否为垃圾
垃圾回收算法有哪些,各自的特点
内存泄漏
在文章开始前要知道一个很重要的知识,JS的内存生命周期。今天要讲的就是那些关于内存释放时的故事。
为变量分配内存
使用分配的内存
不需要的时候将内存释放
什么是垃圾回收
首先我认为垃圾回收比较难理解的一个原因是:它比较抽象,毕竟是浏览器内部的操作,是我们肉眼不可见的。
可以想象我们日常生活中那些废弃的没用的东西就是“垃圾”,为了占位置就要将生活垃圾清理掉。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复制等可以看
https://www.jianshu.com/p/a8a04fd00c3c
引用计数法
引用计数法也很好理解,就是引用对引用的次数进行计数。如果引用了增加就加1,引用减少就减去1.当引用等于0将它清除。看一个例子
例3
//假如有一个计数器count=0let a ={ name:'linglong',//count==1}let b=a; //count==2b=null; //count==1a=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之前仍然还在用。现在用的较多的是后面介绍的标记清除法。
标记清除算法
标记清除法分为两大步,先标记然后清除没有被标记的。
垃圾回收器获取根并
标记
(记住)它们。1. 然后它访问并“标记”所有来自它们的引用。
2. 然后它访问标记的对象并标记它们的引用。所有被访问的对象都被记住,以便以后不再访问同一个对象两次。
3. 以此类推,直到有未访问的引用(可以从根访问)为止。
除标记的对象外,所有对象都被
清除
。
例如,对象结构如下:
我们可以清楚地看到右边有一个“不可到达的块”。现在让我们看看**“标记并清除”**垃圾回收器如何处理它。
第一步标记根
然后标记他们的引用
以及子孙代的引用
现在进程中不能访问的对象被认为是不可访问的,将被删除
标记清除算法数据结构
标记清除法利用到了堆、链表结构 标记阶段:从根集合出发,将所有活动对象及其子对象打上标记 清除阶段:遍历堆,将非活动对象(未打上标记)的连接到空闲链表上
这就是垃圾收集的工作原理。JavaScript引擎应用了许多优化,使其运行得更快,并且不影响执行。
V8引擎一些优化
分代回收
v8堆中对象对象分为两组:新生代 和 老生代
新生代:大多数对象的创建被分配在这里,这个区域很小,但垃圾回收非常频繁,独立于其它区存活期短,如临时变量字符串等
老生代
老生代指针区:包含大部分可能含有指向其它对象指针的对象。大多数从新生代晋升(存活一段时间)的对象会被移动到这里。
老生代数据:区包含原始数据对象(没有指针指向其它对象)。Strings、boxed numbers以及双精度unboxed数组从新生代中晋升后被移到这里。
增量回收
如果有很多对象,并且我们试图一次遍历并标记整个对象集,那么可能会花费一些时间,并在执行中会有一定的延迟。因此,引擎试图将垃圾回收分解为多个部分(18年提出)。然后,各个部分分别执行。这需要额外的标记来跟踪变化,这样有很多微小的延迟,而不是很大的延迟。
空闲时间收集
垃圾回收器只在 CPU 空闲时运行,以减少对执行的可能影响。
内存泄漏
由于内存泄露和内存没有被释放有关,所以这里简单介绍下什么时候会产生内存泄露吧!
知识点1:
什么是内存泄露,对于不再用到的内存如果没有及时释放就叫内存泄露。
这和泄露有半毛钱关系???
我是这样理解的,我们把内存比作手心里的沙子,当沙子从手里漏了出去,那么可用的内存就越来越少了,这个过程就是内存泄露。
手里握不住的沙,不如扬了它
但是内存泄露了还是要管一下的啦,如何对它负责,请接着往下看
四种内存泄露
理解了内存泄露的概念后,我们要知道以下几种情况会导致内存泄露
意外的全局变量(严格模式解决)
被遗忘的定时器和回调函数
脱离dom的引用
闭包(变量引用指向null解决)
第一个和第四个很好理解,主要讲讲2、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
的作者????????
1、浏览器中识别(Chrome浏览器的控制台Performance或Memory)
这里展示performance里面查看的方法。主要步骤如下:
在网页上右键, 点击“检查”打开控制台(Mac快捷键option+command+i);
选择Performance面板(下图的步骤1)
勾选Memory, 然后点击左上角的小黑点Record开始录制(下图的步骤2、3)
点击弹窗中的Stop结束录制, 面板上就会显示这段时间的内存占用情况。
完成前面三步开始录制就是下面这个样子。
第4部结束录制后的样子如下图。如果内存使用情况一直在做增量,就是内存泄露了。
ps:打开控制台中Memory面板也是可以检查的,在
LinDaiDai_霖呆呆
的“记录一次定时器及闭包问题造成的内存泄漏”一文中有详细讲解https://juejin.im/post/5d80854d5188253264365f11#heading-2
2、命令行方法
命令行可以使用 Node 提供的process.memoryUsage方法。玲珑对node.JS不熟悉,在这之前不知道这个方法的。经查阅官方文档后得知:
process 是一个全局变量,即 global全局对象下的一个的属性它用于描述当前Node.js 进程状态的对象。
memoryUsage是process下的一个方法,返回一个对象,描述了 Node 进程所用的内存状况,单位为字节。
接下来一起实践一下吧
新建一个main.js文件,输入以下代码
const fun = () => { console.log(__filename);//文件所在位置的绝对路径 console.log('下面是内存使用信息:'); console.log(process.memoryUsage()); } fun();
在终端输入node main.js回车可以看到执行后结果,如下图所示:
3. 结果说明
我们看到process.memoryUsage返回一个对象,包含了 Node 进程的内存占用信息。该对象包含四个字段,单位是字节,含义如下。
rss(resident set size):所有内存占用,包括指令区和堆栈。heapTotal:"堆"占用的内存,包括用到的和没用到的。heapUsed:用到的堆的部分。external:V8 引擎内部的 C++ 对象占用的内存。
注意:判断内存泄漏,以heapUsed
字段为准。
最后给大家一个思考题:如何减少内存泄露?
总 结
看完了给自己一个大大的赞吧,可以问问自己:
什么是垃圾回收,什么是可达性
垃圾回收算法,哪两种
内存泄露常见的有哪些,如何解决
认真看完一定会有收获滴,比心~
玲珑觉得如果垃圾回收算法的话可以聊很多. 从内存机制开始讲起,什么是垃圾回收,垃圾回收算法,v8引擎如何回收等,内存泄露,以及ES6中国的Weakset和WeakMap这两个不计入垃圾回收机制的弱引用....
参考文章
[译] 前端面试:谈谈 JS 垃圾回收机制 https://juejin.im/post/5ca16056f265da308868dfa6
推荐阅读
JS高程中的垃圾回收机制与常见内存泄露的解决方法
https://juejin.im/post/5b40581e5188251ac446c716#heading-13
关于本文
作者:LINGLONG
来源:掘金
原文链接:https://juejin.im/post/5e6de590f265da57616aacd7#heading-9
版权声明:版权归作者所有
小编最近新建了一个【前端技术及面试交流群】,后台回复“进群”,或直接添加微信 huangpei5011 拉你进群一起学习分享!
更多文章请点击“阅读原文”
喜欢本文点个“在看”哟!