导航
文章来源:https://segmentfault.com/a/1190000041218998
就是判断一个对象的引用数,引用数为 0
就回收,引用数大于 0
就不回收:
let obj1 = { name: '林三心', age: 22 }
let obj2 = obj1
let obj3 = obj1
obj1 = null
obj2 = null
obj3 = null
引用法是有缺点的,下面代码执行完后,按理说obj1和obj2都会被回收,但是由于他们互相引用,各自引用数都是1,所以不会被回收,从而造成内存泄漏
function fn () {
const obj1 = {}
const obj2 = {}
obj1.a = obj2
obj2.a = obj1
}
fn()
标记法就是,将可达的对象标记起来,不可达的对象当成垃圾回收。
那问题来了,可不可达,通过什么来判断呢?(这里的可达,可不是可达鸭)
言归正传,想要判断可不可达,就不得不说可达性了,可达性是什么?就是从初始的根对象(window或者global)的指针开始,向下搜索子节点,子节点被搜索到了,说明该子节点的引用对象可达,并为其进行标记,然后接着递归搜索,直到所有子节点被遍历结束。那么没有被遍历到节点,也就没有被标记,也就会被当成没有被任何地方引用,就可以证明这是一个需要被释放内存的对象,可以被垃圾回收器回收。
// 可达
var name = '林三心'
var obj = {
arr: [1, 2, 3]
}
console.log(window.name) // 林三心
console.log(window.obj) // { arr: [1, 2, 3] }
console.log(window.obj.arr) // [1, 2, 3]
console.log(window.obj.arr[1]) // 2
function fn () {
var age = 22
}
// 不可达
console.log(window.age) // undefined
普通的理解其实是不够的,因为垃圾回收机制(GC)其实不止这两个算法,想要更深入地了解V8垃圾回收机制,就继续往下看吧!!!
其实 JavaScript 内存的流程很简单,分为 3 步:
那么这些使用者是谁呢?举个例子:
var num = ''
var str = '林三心'
var obj = { name: '林三心' }
obj = { name: '林胖子' }
上面这些num,str,obj就是就是使用者,我们都知道,JavaScript数据类型分为基础数据类型和引用数据类型:
在Chrome中,V8被限制了内存的使用(64位约1.4G/1464MB , 32位约0.7G/732MB),为什么要限制呢?
前面说到栈内的内存,操作系统会自动进行内存分配和内存释放,而堆中的内存,由JS引擎(如Chrome的V8)手动进行释放,当我们的代码没有按照正确的写法时,会使得JS引擎的垃圾回收机制无法正确的对内存进行释放(内存泄露),从而使得浏览器占用的内存不断增加,进而导致JavaScript和应用、操作系统性能下降。
在JavaScript中,对象存活周期分为两种情况
那么问题来了,对于存活周期短的,回收掉就算了,但对于存活周期长的,多次回收都回收不掉,明知回收不掉,却还不断地去做回收无用功,那岂不是很消耗性能?
对于这个问题,V8做了分代回收的优化方法,通俗点说就是:V8将堆分为两个空间,一个叫新生代,一个叫老生代,新生代是存放存活周期短对象的地方,老生代是存放存活周期长对象的地方
新生代通常只有1-8M的容量,而老生代的容量就大很多了。对于这两块区域,V8分别使用了不同的垃圾回收器和不同的回收算法,以便更高效地实施垃圾回收
在JavaScript中,任何对象的声明分配到的内存,将会先被放置在新生代中,而因为大部分对象在内存中存活的周期很短,所以需要一个效率非常高的算法。在新生代中,主要使用Scavenge算法进行垃圾回收,Scavenge算法是一个典型的牺牲空间换取时间的复制算法,在占用空间不大的场景上非常适用。 Scavange算法将新生代堆分为两部分,分别叫 from-space 和 to-space,工作方式也很简单,就是将 from-space 中存活的活动对象复制到 to-space 中,并将这些对象的内存有序的排列起来,然后将 from-space 中的非活动对象的内存进行释放,完成之后,将 from space 和 to space 进行互换,这样可以使得新生代中的这两块区域可以重复利用。
具体步骤为以下4步:
那么,垃圾回收器是怎么知道哪些对象是活动对象,哪些是非活动对象呢?
这就要不得不提一个东西了——可达性。什么是可达性呢?就是从初始的根对象(window或者global)的指针开始,向下搜索子节点,子节点被搜索到了,说明该子节点的引用对象可达,并为其进行标记,然后接着递归搜索,直到所有子节点被遍历结束。那么没有被遍历到节点,也就没有被标记,也就会被当成没有被任何地方引用,就可以证明这是一个需要被释放内存的对象,可以被垃圾回收器回收。
新生代中的对象什么时候变成老生代的对象?
在新生代中,还进一步进行了细分。分为nursery子代和intermediate子代两个区域,一个对象第一次分配内存时会被分配到新生代中的nursery子代,如果经过下一次垃圾回收这个对象还存在新生代中,这时候我们将此对象移动到intermediate子代,在经过下一次垃圾回收,如果这个对象还在新生代中,副垃圾回收器会将该对象移动到老生代中,这个移动的过程被称为晋升
新生代空间的对象,身经百战之后,留下来的老对象,成功晋升到了老生代空间里,由于这些对象都是经过多次回收过程但是没有被回收走的,都是一群生命力顽强,存活率高的对象,所以老生代里,回收算法不宜使用Scavenge算法,为啥呢,有以下原因:
所以老生代里使用了Mark-Sweep算法(标记清理)和Mark-Compact算法(标记整理)
Mark-Sweep(标记清理)
Mark-Sweep分为两个阶段,标记和清理阶段,之前的Scavenge算法也有标记和清理,但是Mark-Sweep算法跟Scavenge算法的区别是,后者需要复制后再清理,前者不需要,Mark-Sweep直接标记活动对象和非活动对象之后,就直接执行清理了。
由上图,我想大家也发现了,有一个问题:清除非活动对象之后,留下了很多零零散散的空位。
Mark-Compact(标记整理)
Mark-Sweep算法执行垃圾回收之后,留下了很多零零散散的空位,这有什么坏处呢?如果此时进来了一个大对象,需要对此对象分配一个大内存,先从零零散散的空位中找位置,找了一圈,发现没有适合自己大小的空位,只好拼在了最后,这个寻找空位的过程是耗性能的,这也是Mark-Sweep算法的一个缺点 这个时候Mark-Compact算法出现了,他是Mark-Sweep算法的加强版,在Mark-Sweep算法的基础上,加上了整理阶段,每次清理完非活动对象,就会把剩下的活动对象,整理到内存的一侧,整理完成后,直接回收掉边界上的内存