灵魂三问:什么是垃圾回收,回收的是什么?为什么要有这货色?本文会介绍并尝试答复这三个问题
什么是垃圾回收?
在说这个货色之前,先要解释什么是内存透露,因为内存透露了,所以引擎才会去回收这些没有用的变量,这一过程就叫垃圾回收
什么是内存透露?
程序的运行须要占用内存,当这些程序没有用到时,还不开释内存,就会引起内存透露。举个艰深的例子,就好比占着茅坑不拉屎,坑位(内存量)就这么多,你还不进来(开释内存),就会引起想拉的人不能拉(零碎变卡,重大点的会引起过程解体)
也就是说不再用到的内存,没有及时开释,就被称为内存透露。而内存透露,会让零碎占用极高的内存,让零碎变卡甚至奔溃。所以会有垃圾回收机制来帮忙咱们回收用不到的内存
当咱们遇到遇到内存透露时,咱们须要做什么呢?
不须要做任何事,因为 JavaScript 中的垃圾回收是主动的
如果你看过《JoJo 的微妙冒险:不灭钻石》,就晓得替身中有主动型的替身
如吉良吉影的替身「杀手皇后」的第二状态:枯败穿心攻打
在 JavaScript 的世界里,JavaScript 引擎会主动执行命令,帮咱们清理用不到的变量(即缩小内存开销)
当然,不同的语言采纳不同的内存治理形式,大多数语言采纳的是主动内存治理
主动内存治理(垃圾回收)营垒:
JavaScript、Java、Go、Python、PHP、Ruby、C#
手动内存治理营垒:
C、C++、Rust
回收的是什么
当初咱们能够答复第二个问题:回收什么?
回收内存。清理变量,开释内存空间
为什么要有这货色
为什么要有垃圾回收呢?在前文的形容中,咱们讲到过,如果任由内存透露,会让零碎变卡甚至解体。导致这问题的起因是 JavaScript 的引擎 V8 只能应用一部分内存,具体来说,在 64 位零碎下,V8 最多只能调配 1.4G;在 32 位零碎中,最多只能调配 0.7G
因为应用内存大小下限,所以当有用不到的变量时,引擎会帮咱们清理掉
这里咱们不禁会想,这货色是怎么运行的?怎么晓得我的变量哪些是用不到的?把正在用的变量革除掉会怎么样呢?
带着这个问题咱们理解下垃圾回收的运行机制
垃圾回收运行机制
在说这个话题前,咱们先回顾下,在 JavaScript 由什么组成 中已经介绍过,JavaScript 的数据类型可分为根本类型和援用类型。根本类型存在栈内存,援用类型存在堆内存
然而咱们那时没有解释为什么根本类型要存在栈中,援用类型要存在堆中。只是介绍,因为根本类型所花销的内存小,而援用类型所花销的内存大,而这恰好是分两个空间寄存不同数据的起因
在 JavaScript 中,引擎须要用栈来维护程序执行时的上下文状态(即执行上下文),如果栈空间大了的话,所有数据寄存在栈空间中,会影响到上下文切换的效率,从而影响整个程序的执行效率,所以占内存大的数据会放在堆空间中,援用它的地址来示意这个变量
堆内存的分类
一个 V8 过程的内存通常由以下局部组成
- 新生代内存区(new space)
- 老生代内存区(old space)
- 大对象区(large object space)
- 代码区(code space)
- map 区(map space)
其余几个不重要,要害是新生代(内存)和老生代(内存)。针对新生代和老生代,引擎采纳了两种不同的垃圾回收机制
新生代与老生代的垃圾回收
在介绍两种垃圾回收机制前,要先晓得两个知识点:代际假说和分代收集
代际假说有以下两个特点:
- 大部分对象在内存中存活的工夫很短,简略说,就是很多对象一经分配内存,很快就变得不可拜访
- 不死的对象,会活得更久
因为有代际假说的认知,所以咱们在垃圾回收时,会依据对象不同的生存周期采纳不同的算法,其中 V8 把堆内存分为新生代和老生代两个区域(其余几个区域用途不大)
新生代中寄存生存工夫短的对象,老生代寄存生存工夫久的对象
为此,新生代区通常只反对 1~8M 的容量,而老生代区会反对更大的容量,而针对这两块区域,V8 别离应用两个不同的垃圾回收器
- 主垃圾回收器,负责老生代的垃圾回收
- 副垃圾回收器,负责新生代的垃圾回收
咱们先说说副垃圾回收器时如何解决垃圾回收的
新生代内存回收
新生代采纳的是 Scavenge 算法,所谓 Scavenge 算法,是把新生代空间对半分为两个区域,一半是对象区域(from),一半是闲暇区域(to)。如下图所示:
新的对象会首先被调配到对象(from)空间,当对象区域快写满时,就须要执行一次垃圾清理操作。当进行垃圾发出时,先将 from 空间中存活的对象复制到闲暇(to)空间进行保留,对未存活的空间进行回收。复制实现后,对象空间和闲暇空间进行角色调换,闲暇空间变成新的对象空间,原来的对象空间则变成闲暇空间。这样就实现了垃圾对象的回收操作,同时这种角色调换的操作能让新生代中的这两块区域有限重复使用上来
而当一个对象在两次变换中还存在时,就会从新生代区”降职“到”老生代区“。这一过程被称为对象降职策略
老生代内存回收
主垃圾回收器负责老生代区的垃圾回收。其中的对象包含新生代区”降职“的对象和一些大的对象。因而老生代区中的对象有两个特点,对象占用空间大,对象存活工夫长
它不会像新生代区那样应用 Scavenge 算法,因为复制大对象所破费的工夫长,执行效率并不高。所以它采纳标记 – 革除(Mark – Sweep)进行垃圾回收
简略来说,先标记,而后革除,然而内存空间里的对象还是不间断,所以引入整顿。这就是老生代区的垃圾回收过程 标记 – 革除 – 整顿。先标记哪些是要回收的变量,再进行回收(革除),而后将内存空间整顿(到一边),这样空间就大了
因为老生代区的对象绝对大,尽管采纳”标记 - 革除“算法会比 Scavenge 更快,但架不住卡顿问题。为什么会卡顿?因为 JavaScript 是单线程。为此,V8 将标记过程分为一个个子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段实现,这一算法被称为增量标记算法
而这一行为,与 React Fiber 的设计思路相似,将小人物宰割成小工作,因为小,所以执行快,让人觉察不到卡顿
新生代 VS 老生代
- 新生代垃圾回收是长期调配的内存,存活工夫短;老生代垃圾回收是常驻内存,存活工夫长
- 新生代垃圾回收由副垃圾回收器负责;老生代垃圾回收由主垃圾回收器负责
-
新生代采纳 Scavenge 算法;老生代采纳「标记 - 革除」算法
- Scavenge 算法:将空间分为两半,一半是 from 空间,一半是 to 空间。新退出的对象会放在 from 空间,当空间快满时,执行垃圾清理;再角色调换,再当调换完后的 from 空间快蛮时,再执行垃圾清理,如此重复
-
标记 - 清理 - 整顿:此为两个算法,「标记 - 清理」算法和「标记 - 整顿」算法
- 标记 - 清理:标记用不到的变量,清理掉
- 标记 - 整顿:清理完内存后,会产生不间断的内存空间,为节俭空间,整顿算法会将内存排序到一处空间,空间就变大了
援用计数(reference counting)
在《JavaScript 高级程序设计》中介绍了另一种垃圾回收的机制——援用计数
简略来说:引擎会有张”援用表“,保留了内存外面的资源的援用次数。如果一个值的援用次数是 0,就示意这个值不再用到了,因而能够将这块内存开释
但起初这个机制被放弃了,因为它会遇到一个重大的问题:循环援用,从而导致内存透露,所以被放弃了
编年体垃圾回收历史
1960 年,John McCarthy 发表了一篇论文,提出了 标记 - 革除算法。可是标记 - 革除算法由两个要命的毛病:调配速度慢,容易产生碎片
为了解决这个问题,1963 年,Marvin L. Minsky 提出了 复制算法。而 JavaScript 中的 Scavenge 算法就是以它为根底的改进版本。它的毛病是空间利用率不大,每次只能应用一次
1960 年,George E. Collins 提出了一个新的 GC 算法:援用计数,毛病是不能回收“循环援用”,目前 JavaScript 的引擎是没有采纳这种回收机制
如此,垃圾回收大厦地基曾经建好,前人只是在此基础上修修补补
总结
咱们介绍了什么是垃圾回收机制,为什么会有垃圾回收机制,以及介绍了垃圾回收的运行机制,它的两种内存采纳的不同的垃圾回收算法等等。理解垃圾回收机制,是为了让咱们更清晰地明确其运行原理,尽管咱们没必要去理解「标记 - 清理」、「标记 - 整顿」、「Scavenge」等等算法,但如果明确它们为什么要采纳这样的算法有肯定的必要性
不然,小白问起网站为什么会卡时,你就能够“无心”走漏是不是内存透露了啊,而后引出 JavaScript 的垃圾回收机制等,装一次老前辈的经验之谈
参考资料
- V8 内存治理及垃圾回收机制
- V8 引擎垃圾内存回收原理解析
- 高性能 JavaScript 引擎 V8 – 垃圾回收
- 漫画的模式解释垃圾回收
- JavaScript 内存透露教程
- 垃圾回收:垃圾数据是如何主动回收的?
系列文章
- 深刻了解 JavaScript——开篇
- 深刻了解 JavaScript——JavaScript 是什么
- 深刻了解 JavaScript——JavaScript 由什么组成
- 深刻了解 JavaScript——所有皆对象
- 深刻了解 JavaScript——Object(对象)
- 深刻了解 JavaScript——new 做了什么
- 深刻了解 JavaScript——Object.create
- 深刻了解 JavaScript——拷贝的机密
- 深刻了解 JavaScript——原型
- 深刻了解 JavaScript——继承
- 深刻了解 JavaScript——JavaScript 中的始皇
- 深刻了解 JavaScript——instanceof——找祖籍
- 深刻了解 JavaScript——Function
- 深刻了解 JavaScript——作用域
- 深刻了解 JavaScript——this 关键字
- 深刻了解 JavaScript——call、apply、bind 三大将
- 深刻了解 JavaScript——立刻执行函数(IIFE)
- 深刻了解 JavaScript——词法环境
- 深刻了解 JavaScript——执行上下文与调用栈
- 深刻了解 JavaScript——作用域 VS 执行上下文
- 深刻了解 JavaScript——闭包
- 深刻了解 JavaScript——防抖与节流
- 深刻了解 JavaScript——函数式编程