前端工程师岗位,JavaScript是一门必须掌握的语言,面试官也常常会问你其的垃圾回收机制,考察你对JavaScript的掌握情况。
- 我们先从,垃圾回收机制存在的必要性说起。在浏览器输入,window.performance 可查看程序所占用的内存情况。
十分清楚的看得到,js的内存限制,以及目前所使用的内存情况。你也可以在node环境中,使用 process.memoryUsage(),查看node进程所占用的内存空间。一个优秀的前端程序员,必定是注重内存占用问题的,包括内存泄漏和性能优化等。
言归正传,垃圾是如何存在的呢?
var arr = [1,2,3,4,5];
arr = [];
arr = []的操作,确实可以将arr 数组 成为一个 空数组, 但实际上,arr = [],其实是,新建一个空数组,再将arr 指向了这个新数组;
那原来的 [1,2,3,4,5]这个数组呢,它依旧存在于内存中,因此就造成了 [1,2,3,4,5] 的内存碎片,这就是垃圾的产生。那如何对arr 数组 实现复用呢?
var arr = [1,2,3,4,5];
arr.length = 0; // []
arr.length = 0,这是一种快捷且实用的数组复用方式。对于对象而言,尽可能做到复用,不再使用时,将对象 = null,尽快触发垃圾回收。
那垃圾回收机制的必要性显而易见:当程序运行时,会造成大量的内存碎片(失去引用的对象或数组或一些不再使用的其他变量),如果不及时进行内存的回收,就可能造成内存占用过高,导致系统的崩溃。
- 上边能打印监测到 程序 对内存的占用状态,那他们究竟如何占用内存的呢?我们先从数据类型说起(这里仅考虑JavaScript)
javascript 中,数据类型分为:基本类型 和 引用类型
基本类型:Undefined、Null、Boolean、Number、String、Symbol
引用类型:Object,Array ,Function, Date, RegExp等
那当我们 定义一个 变量时,它们是怎么存储的呢?我们再介绍一下,栈和堆
对于基本类型的变量而言,它们存储时,是直接存储的数值本身;而引用类型的变量,他们在存储时,存储的仅仅是地址;具体存储方式请看:
栈:由操作系统自动分配释放存放函数的参数值、局部变量的值等,其操作方式类似于数据结构中的栈;
简单数据类型直接存放到栈里面。
堆:存储复杂类型,一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收;
复杂数据类型引用放在栈里面,实际数据存放到堆里面。
原因:1、基本类型的数据简单,所占用空间比较小,内存由系统自动分配;
2、引用类型数据比较复杂,复杂程度是动态的,计算机为了较少反复的创建和回收引用类型数据所带来的损耗, 就先为其开辟另外一部分空间—— 即堆内存,以便于这些占用空间较大的数据重复利用;
3、堆内存中的数据不会随着方法的结束立即销毁,有可能该对象会被其它方法所引用, 直到系统的垃圾回收机制检索到该对象没有被任何方法所引用的时候才会对其进行回收。
上图解:
这个图片很清楚的,告诉我们不同数据类型存储的方式~~
可以看到obj2 = obj 之后,在栈中,仅仅是将 obj 的存储地址值赋值给 obj2了。
如果继续添加代码 obj.name = ‘李四’;
则 console.log(obj2.name); // 李四
输出 的 肯定也是 李四。
好了,我们接着讲解垃圾得产生:
当执行 代码 obj.b = []; 时,javascript会在 堆内 新创建一个 Array,并将 obj.b 的值 改为新数组的地址值。
但这时候,原数组[1,2,3,4,5]就失去了 引用,但他依旧在占用内存,这就是垃圾的产生。
那如果,obj对象重新赋值呢?
当obj = {} 时,堆内新建一个对象,并将所在地址值赋值给 obj。 但原来的 obj2 依旧在是 原来的地址值,堆内的 第一个Object 依旧在被引用,所以 第一个 Object 并不是’垃圾’
如果我们在此时,再次打印 obj2.b 呢? 肯定输出 [] ,obj2.name 输出为 ‘张三’
再次将obj 赋值 给 obj2 后,obj2 在栈 中的值 也变为 0x100,这时候,堆内0x001所在的Object,失去引用,则变为’垃圾’,而0x011所在的Array 虽然被 Object 所引用,但是 它失去了 根引用,也变为了’垃圾’。
这下很清楚的可以看到,基本类型的值直接存储 在 栈中,而对于引用类型的数据,他们的地址值 存放在 栈中,实际值 存放在 堆中。当一些不在引用或不在使用的变量没有被及时清理占用的内存时,就造成了内存泄漏,垃圾回收迫在眉睫~~
- 当数据失去引用时,就会变为‘垃圾’,无论时栈中 还是 堆中。那我们应该如何进行回收呢?我们以V8为例,进行介绍。
在 64位 电脑中,V8的 内存默认被限制到 1.4G,他们不像Java 和 php等 其他语言,有很大的内存空间,这么做的原因是,在 v8 进行垃圾回收时,应用逻辑处于 等待状态,当如果内存分配很大,进行垃圾回收 需要耗费大量时间,非常影响性能。
V8引擎将内存空间,分为新生代(副垃圾回收器)和老生代(主垃圾回收器)。
副垃圾回收器是一种牺牲空间换时间的方式,其分为 from 空间和 to空间,所有的数据存放在from空间内,当需要进行垃圾回收时,拷贝from 空间内还存活的对象,拷贝至to 空间,之后to 空间叫 from空间,原from 空间 叫to 空间,进行如此循环。
新生代内数据晋升至老生代的条件:
1. 当某个数据经历5次回收,依旧存在则,此数据进入老生代;
2. 当某次垃圾回收后,to空间占比达到 25%,则,to内所有数据,都进入老生代;
3. 那v8又是如何知道要回收的是哪些数据呢?---->引用计数。
var obj = { name:'张三'} // 计数 obj = 1
var obj2 = obj // 计数 obj = 2
obj2 = null // 计数 obj - 1 = 1
obj = null // 计数 obj - 1 = 0
当 obj 对象 计数 === 0,则下次副垃圾回收器 在回收时,会将obj 清除。
对于老生代来说,像新生代那种,直接拷贝的方案就不再适用了,因为老生代也一分为二的话,浪费了太多空间,其次拷贝较大数据时…耗时。那老生代采用的垃圾回收方案是------ 标记-清除(Mark-Sweep)
在主垃圾回收器,进行回收时,其采用深度遍历的方案,其清理机制是,深度遍历后清除不再被引用的对象。
如上代码中,深度遍历:obj—>obj.a(obj2)—>obj2.a(obj3), obj2,obj3,obj4
当如果执行代码obj2 =null 和 obj3 = null时,主垃圾回收器进行深度遍历是:
obj,obj4
这时候,发现两个未被遍历到的对象,则对它们进行标记清除。
在清除未被遍历到的对象后,可以发现,空间占用并不是很理想,Object 和 Object4 之间的内存空间,无法放下比这块更大的数据,但是放个小的数据,又不能完全利用。可以想到,如果只是这样分配老生代的空间,多次回收后,全都是大大小小的内存空间。那如何解决这种情况呢?----->标记-整理(Mask-Compact)。
如果在深度遍历后,将遍历到的对象,移动到老生代空间区的一端,而未被遍历到对象移至另一端,这时直接清理掉未被遍历到的对象所占用的空间,就行啦~~
当然,由于老生代垃圾回收时长较长,且在回收时,程序完全停止,对性能影响极大。所以V8又引入了增量标记的方式,我们把这种垃圾回收的方式成为增量垃圾回收。但是我们在这里就不做过多介绍了,各位可以查找相应的文献进行了解~
垃圾回收机制就介绍到这里啦,有什么问题,欢迎各位私信探讨~~