JS内存管理与垃圾回收机制

1.JS内存管理


1.1 内存生命周期

分配内存 => 内存读写 => 释放内存

在JS语言中,内存分配是由JS引擎自动完成的,释放内存也是如此,由JS引擎根据垃圾回收机制进行回收,开发者能参与的是内存读写这一环节

1.2 两种内存存储类型

基本类型:内存是固定大小的,其值存储在栈(stack)空间中
引用类型:内存是非固定的,其值存储在堆(heap)空间中,指向堆空间中的对象的指针存储在栈空间中

不多bb,来看代码:

let n = 7  // 在栈中给数值分配内存
let s = 'NO x ONE' // 在栈中给字符串分配内存
let b = true // ...
let a = null // ...

let obj = { // 在堆中存储新的对象,在栈中存储指向该对象的指针并返回给obj,构成引用关系
	id: 1
}
let obj2 = obj // 共享同一个指针
obj2 = 2 // 在下一轮垃圾回收中,将堆中不再被引用的对象从内存中释放,原先栈空间的指针变为了2
// 由于obj2与obj共享同一个指针,故obj=obj2= 1
let arr = [1,2] // ...
let fn = function(a){ alert(a)} // ...

2.垃圾回收机制


2.1 核心原理

GC即Garbage Collection,垃圾回收机制。JS引擎定期找出不再用到的内存,然后释放,至于如何实现有不同的策略,常见的有标记清除算法引用计数算法

2.2 标记清除算法

标记清除(Mark-Sweep),大多数浏览器的JS引擎都在采用这种算法,只是在此基础之上又进行各自的优化加工
该策略分为MarkSweep两个阶段

过程如下:
Mark阶段

  • 运行时在内存中所有变量标记0(即垃圾)
  • 从各个根对象遍历,将非垃圾变量标记变为1

Sweep阶段

  • 将所有标记0的变量内存释放,GC

来看看代码:

function fn(a){ // 开始执行此函数时,将其作用域中a、B以及匿名函数标记为0
	alert(a) // 0
	let B = new Object() // 0
	return function (){ // 由于这里return出去会被其他变量引用,故标记变为1
		altert(B) // 由于这里的闭包,B的标记变为1
	}
	... // 执行函数完毕,销毁作用域,在某个GC回收循环时会清理标记为0的变量a,B和匿名函数被保留了下来即非垃圾变量
}

let fn2 = fn(new Object()) 
// 补充一下:fn和fn2作为window.fn和window.fn2,标记一直为1,仅仅当手动设置fn=null和fn2=null才会标记为0

但是这种策略存在内存碎片化缺陷,即释放的内存空间往往是不连续的,如下图所示:
在这里插入图片描述
这样不利于内存的回收利用,即空闲内存与非空闲内存是相交错的,不利于存储
故考虑优化在MarkSweep阶段之间再补充上整理Compact阶段

  • Mark阶段结束后,将标记1的变量内存往内存一段移动,标记0的变量内存往另一端移动
  • 开启Sweep阶段

优化后的策略即标记整理清除算法(Mark-Compact-Sweep),入下图所示:
在这里插入图片描述

2.3 引用计数算法

JS引擎很早之前使用过这种策略回收内存,其核心思想为:将不再被引用的对象(零引用)作为垃圾回收,需要提醒的是,这种策略由于存在很多问题,目前逐渐被弃用了

过程如下:

  • 当声明一个引用类型并赋值给变量时,这个值的引用次数初始为1
  • 若该值又被赋值给另一个变量,引用次数+1
  • 若该变量的被其他值覆盖了,引用次数-1
  • 当这个值引用次数变为0时,说明该值不再被引用,垃圾回收器会在运行时清理释放其内存

代码如下:

let a = new Object() // 引用次数初始化为1
let b = a // 引用次数2,即obj被a和b引用
a=null // 引用次数1
b=null // 引用次数0,
... // GC回收此引用类型在堆空间中所占的内存

但是存在一些问题,例如最常见的是循环引用现象

function fn(){ // fn引用次数为1,因为window.fn = fn,会在window=null即浏览器关闭时回收
	let A = new Object() // A: 1
	let B = new Object() // B: 1
	A.b = B // B: 2
	B.a = a // A: 2
	... // 执行完fn函数,作用域销毁时,A和B引用次数-1,但是还不为0,在某个GC回收循环执行时不会释放其内存,存在了内存泄漏
}
fn() // 若执行无限多次fn,那么内存将会被占满,程序宕机

若是采用标记清除策略则会在fn执行完毕后,作用域销毁,将域中的AB变量标记为0以便GC回收内存,不会存在这种问题。

3.V8对GC的优化


前面提过,现在大部分浏览器JS引擎都采用标记清理策略来实现垃圾回收机制,但是又各自基于此策略又进行了不同的优化,这里主要来看Chrome的JS引擎V8对此进行的优化

3.1 分代式回收

标记清理策略在每次垃圾回收前都要检测内存中所有的对象标记是否为0来作为是否回收的依据,若一些大、老、存活长的对象(老生代)与小、新、存活时间短的对象(新生代)采用一个频率检查的话将会消耗很大的性能,故要区别对待,所以V8采用分代的方式进行垃圾回收,对前者使用老生代GC(清理频率低),后者采用新生代GC(清理频率高)

3.1.1 内存存储分代

由于V8的GC策略主要是基于分代,故V8存储变量的方式也是分代的,将堆空间开辟为新生代和老生代

新生代:堆空间内存空间小(1~8MB),对应GC算法Scavenge效率高
老生代:堆空间内存较大,对应GC算法Mark-Compact-Sweep效率低些
在这里插入图片描述
这种分配是非常合理的,对于频繁回收操作,牺牲空间换取时间;对于低频回收操作,牺牲时间换取空间。此思想可以类比计算机存储分层结构,将需要读取频率高的存储在小容量、高处理的cache中,将读取频率低的存储在大容量、低处理的内存中

3.1.2 新生代GC

对于新生代采用Scavenge算法进行垃圾回收,将新生代存储区一分为二:使用区空闲区

程序运行时,作用域中所有变量都会存入使用区,当该区域内存写满之后就会进行一次GC。在GC开始前,先将使用区里的活动对象移动到空闲区非活动对象则保留,随后进行GC,清除保留在使用区里的对象,最后将空闲区使用区进行角色互换,如下图所示
在这里插入图片描述

3.1.3老生代GC

首先,老生代都是从新生代转变来的,但要经过考核,满足以下任意一种条件即可:

  • 当一个新生代对象经过多次Scavenge GC仍然是存活,那么就判定它是生命周期长的对象(老油条),会将其移动到老生代内存中,作为老生代对象
  • 当一个变量刚进入使用区时就已经占了25%,那么为了性能考虑,直接将其移动到老生代内存中

随后采用前面所说的Mark-Compact-Sweep即标记整理清除策略来进行垃圾回收即可

3.2 并行回收

V8主要是采用分代式回收,但是对于老生代使用Mark-Compact-Sweep性能还是有提升空间,故又对老生代垃圾回收机制采用并行回收进行优化

首先为什么要并行回收呢?这是由于JS是单线程的,运行在主线程上,在GC回收也是运行在主线程中,这会造成JS脚本暂时堵塞,在GC回收完毕才会恢复脚本运行,这种现象叫作全停顿(Stop-To-World),所以为了加快GC回收,V8引擎引入了并行回收,即并行开启多个辅助线程,协同完成GC回收工作(人多好干活,社会主义好啊~)
在这里插入图片描述

3.3并发回收

采用并行回收还是存在一个问题,那就是它还是多多少少造成JS脚本堵塞,并未从根本上解决问题,所以又提出了并发回收机制,如下图所示:
在这里插入图片描述
GC回收完全在辅助线程中进行,不占用主线程,丝毫不会导致JS脚本挂起,这就是并发的好处
但是要实现很难,因为主线程在执行 JavaScript 时,堆中的对象引用关系随时可能变化,这时辅助线程之前做的一些标记或者正在进行的标记就会改变,所以需要额外实现一些读写锁机制来控制,具体怎么搞又是一个深入的话题

3.4增量标记、惰性清理

这也是对并行回收存在的全停顿现象进行的优化,在Mark阶段采用增量标记代替全停顿标记,在Sweep阶段采用惰性清理来代替,尽最大的可能减少全停顿时间,具体怎么搞也是一个深入的话题

参考一:深入理解Chrome V8垃圾回收机制
参考二:内存管理
参考三:「硬核JS」你真的了解垃圾回收机制吗

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值