JavaScript中的垃圾回收

内存管理

为什么需要内存管理

在我们写代码过程中,如果不够了解内存管理机制,就会无意中写出不易察觉的内存问题性代码,这种代码多了以后,对我们的程序可能就会带来意想不到的bug。所以掌握内存管理还是很有必要的。

内存
由可读写单元组成,表示一片可操作空间。

管理
人为的去操作一片空间的申请、使用和释放。

内存管理:
开发者主动申请空间、使用空间、释放空间。

在JavaScript中,内存管理比较特殊,是无法像别的语言一样有自己的api调用内存空间。但是依然可以通过脚本演示JS中内存管理的整个生命周期:

// 申请
let obj = {}
// 使用
obj.name = 'xm'
// 释放
obj = null

垃圾回收

JavaScript中的内存管理是自动的,当对象不再被引用时,这个对象就会被视为垃圾;或者对象不能从根上访问到时,也被视为垃圾。过程其实就是找到垃圾,然后让JavaScript执行引擎进行垃圾回收和释放。

可达对象:
可以访问到的对象就是可达对象。无论是通过引用,还是从作用域链查找,只要能访问到,都被认为是可达对象。可达的标准就是从根出发是否能够被找到。

从根上:
在JavaScript中的根就可以理解为全局变量对象。

function(obj1, obj2) {
	obj1.next = obj2
	obj2.prev = obj1
	return {
		o1: obj1,
		o2: obj2
	}
}
let obj = objGroup({name: 'obj1'}, {name: 'obj2'})
console.log(obj)

把上面的代码互相引用关系用下图表示:

在这里插入图片描述
如图,在全局中obj1和obj2存在互相引用,其中把可以从根上查找到obj1的方式全部删掉(o1和Prev),此时obj1就不再是可达对象,此时就会被视为垃圾,被JavaScript执行引擎回收释放。

GC算法

GC实际就是垃圾回收机制的简写,GC可以找到内存中的垃圾,并释放和回收空间。

从需求上讲,比如在函数内定义了一个全局变量,当函数执行完后不再使用这个变量了,这个变量就属于垃圾(是否回收暂且不管)。从程序运行中,声明一个变量,这个变量是否是可达对象,也就是还有没有被引用到,如果没有,就可以视为垃圾。

  • GC是一种机制,垃圾回收器完成具体的工作。
  • 工作的内容就是查找垃圾释放空间、回收空间。
  • 算法就是工作时查找和回收所遵循的规则。

常见GC算法

  • 引用计数
  • 标记清除
  • 标记整理
  • 分代回收
引用计数

核心思想:
设置引用数,判断当前的引用数是否为0。当引用关系改变时引用计数器修改引用数字,当数字为0时立即回收。

优点:
发现垃圾时立即回收,最大限度减少程序暂停(内存有上限,当在极限情况下,内存即将占满时,有垃圾会立即回收,更大程度上避免了内存占满,从而减少程序暂停)

缺点:
无法回收循环引用的对象,时间开销更大一些。

function() {
	const obj1 = {}
	const obj2 = {}
	obj1.name = obj2
	obj2.name = obj1
}
fn()
// 当fn执行完毕后,此时obj1和obj2属于垃圾,应该被释放回收,但是因为两者在函数内部循环引用问题,导致引用计数不可能为0,导致无法进行垃圾回收。
标记清除

核心思想:
分为标记和清除两个阶段完成。第一步会遍历所有对象,对活动的对象(可达对象)进行标记,第二步还是遍历所有对象,对未标记的进行释放回收,同时对已标记的进行清除标记,之后继续执行第一步。

优点:
解决了对象循环引用不能回收的问题(相对于引用技术)。

缺点:
回收的内存存在不连续的情况,再重新分配使用时,会有存储碎片存在。比如有ABC三个对象存在一片连续的内存中,B是可达对象,AC不可达,会被当做垃圾回收掉,此时A占用2个字节内存,C占用1一个字节内容,回收后AC的内存也是不连续的,此时如果有个对象需要申请占用1.5个字节,假如找到C释放的1个字节空间,会因为内存不够无法使用。然后找到A释放的2个字节空间,那么就会剩余0.5个,形成碎片浪费。

标记整理

标记整理是标记清除的增强,标记阶段的操作和标记清除一致,但是在清除阶段,会先执行整理工作,移动对象的位置,让可达对象在一起,垃圾对象在一起,然后再执行清除工作,这样回收的内存就是连续的了。
在这里插入图片描述
在这里插入图片描述

V8

  • v8是一款主流的JavaScript执行引擎
  • 采用即时编译,速度很快
  • 设置了内存上限(64位操作系统一般不超过1.5G,32位不超过800MB)
V8垃圾回收策略
  • 采用分代回收的思想

  • 内存分为新生代(存活时间较短的对象)和老生代(存活时间较长的对象)

  • 针对不同对象采用不同算法

在这里插入图片描述
在这里插入图片描述
V8内存空间被一分为二,分别使用不同的GC算法提高效率。较小的空间(64位系统是32MB,32位是16MB大小)用于存储新生代对象。较大的空间(64位系统是1.4G,32位是700MB大小)用于存储老生代对象。

V8中常用的GC算法
  • 分代回收
  • 空间复制
  • 标记清除
  • 标记整理
  • 标记增量
回收新生代对象

新生代回收过程主要采用复制算法+标记整理算法

新生代内存被一分为二,等大的两个空间,使用 的为from,空闲的为to。开始垃圾回收时,会检查from区域中的存活对象,活着的拷贝到to空间,当所有存活对象都拷贝完成后,清空from空间,from和to空间互换。

拷贝过程中可能存在晋升,晋升就是将新生代对象移动到老生代。晋升有一般两个标准,一轮GC后还存活的新生代对象需要晋升,to空间的使用率超过25%也需要晋升。因为将来在回收时,from和to空间会互换,如果to的使用率过高,互换后from的剩余空间就会很小,新的对象可能就存不进去。

回收老生代对象

老生代回收过程主要采用标记清除、标记整理、增量标记算法

老生代回收时,首先还是使用标记清除算法,虽然会产生内存碎片,但是效率较高。当新生代对象产生晋升,移动到老生代区域,而且老生代空间不足时,此时会采用标记整理算法进行空间优化。

在这里插入图片描述

增量标记算就是标记清除的一种优化,之前在执行标记清除时,在程序执行出触发垃圾回收后,此时会暂停程序执行,开始遍历对象进行标记,然后完成清除后再继续执行。增量标记把这个停顿较长的标记时间进行了拆解,先标记根上的直接可达对象,然后程序执行一会,再标记子项的可达对象,按照这样逻辑把较长的标记时间拆解成一段很短时间,用户体验较好。

细节对比

新生代采用复制算法,使用空间换时间。

老生代不适合复制算法,因为老生代所占空间很大,复制算法会浪费一半使用空间。而且存储对象较多,复制的效率不能保证。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值