GC
GC是什么
GC是Garbage Collection的缩写,中文意为垃圾回收。它是程序语言内存管理的一种自动机制。
GC主要的作用
- 自动释放不再使用的内存空间,避免内存泄漏。
- 整理内存碎片,提高内存利用率。
GC的实现一般遵循的步骤
- 根节点扫描:扫描根节点(全局变量),找到所有存活的对象,将其标记。
- 存活对象标记:从根节点开始递归扫描,将找到的所有存活对象及其关联对象标记。
- 回收未标记对象:扫描内存空间,回收所有未标记的对象占用的内存空间。
- 内存整理:将存活对象移至一端,紧凑内存布局,释放未使用内存。
常见的GC算法
- 引用计数(Reference Counting):给每个对象维护一个引用计数,当计数为0时回收内存。简单但无法解决循环引用问题。
- 标记-清除(Mark and Sweep):遍历内存标记存活对象,然后清除未标记对象。可以解决循环引用问题但会产生内存碎片。
- 复制收集(Copying):将内存一分为二,每次只使用其中一半。将存活对象复制到未使用内存,然后清除源内存。避免内存碎片但浪费空间。
- 分代收集(Generational Collection):根据对象存活周期将内存分为新生代和老生代,各自使用不同的收集算法。提高回收效率。
- 标记-整理(Mark and Compact):标记存活对象后,将对象移至一端,然后清除边界外内存。既解决碎片又不浪费空间。
垃圾回收策略
可达性:
JavaScript 内存管理中当对象不再被任何引用指向(访问、调用、可用)时,该对象就可以被垃圾回收,反之则存储在内存中。
JavaScript 垃圾回收机制:
主要基于标记-清除算法实现
标记-清除算法:
标记阶段:垃圾回收器会从根节点(全局变量)开始遍历对象图,查找并标记所有可达对象(活动对象)。
清除阶段:垃圾回收器会清扫内存,释放所有未标记对象(垃圾对象)占用的空间。
内存整理:将存活对象移至更紧凑的内存布局,避免内存碎片。
实现过程:
当JavaScript程序运行时,会在内存中创建各种对象。
部分对象会被存储在其他对象或全局变量中,成为可达对象。而其他对象失去引用变成垃圾对象。
当垃圾回收被触发时(一般由JS引擎触发),会暂停其他线程的执行。
垃圾回收器会从根节点开始遍历所有可达对象,并标记为活动对象。然后递归遍历活动对象的子对象标记。
所有未标记的对象会被视为垃圾对象,准备被清除。
垃圾回收器会清除垃圾对象占用的内存空间,并整理内存布局。
垃圾回收结束后,JS引擎继续执行其他任务。
此外,JavaScript还采用分代收集法对内存进行分区管理。新创建的对象多存活短暂,存放于新生代。可达对象逐渐变老,搬迁到老生代。分代回收能更高效的回收对象,减少内存碎片。当然,JS引擎也提供其他的优化手段,诸如增量并发标记、空闲列表等,不断提高内存管理效率,为最佳用户体验做支撑。
优点:
- 可以有效地解决循环引用问题。标记-清除算法会从根节点开始迭代标记所有可达对象,而不仅仅依赖引用计数。
- 实现简单,耗时短,GC停顿时间较短。标记和清除过程的时间复杂度都为O(n),所以总体来说标记-清除算法实现简单高效。
缺点:
- 会产生内存碎片。由于清除后会留下未使用内存空间,长期来看会使内存布局越来越碎片化,降低内存利用率。
- 标记和清除过程会造成较长GC停顿时间。对于一些实时性要求较高的应用来说,这个停顿时间可能无法接受。
- 标记过程需要递归遍历对象图,对象规模较大时,标记时间过长。标记算法时间复杂度O(n),n代表对象数量,对象过多时标记过程会十分耗时。
- 全堆扫描效率低下。标记-清除算法需要扫描整个内存堆区,对于大内存应用,全堆扫描的代价太大。
注意:为了规避标记-清除法的缺点,目前的垃圾回收器一般会采用分代收集法,仅对新生代采用标记-清除算法进行快速回收。而对老生代则采用标记-整理或者增量标记算法实现更高效的回收。
引用计数算法
基本思想:
为每个对象维持一个引用计数,表示当前有多少个对象或变量引用该对象。当一个对象的引用计数变为0时,表示该对象成为不可达对象,这个对象的内存空间可以被回收。
优点:
- 实现简单,时间复杂度O(1)。仅需要在创建和销毁引用时递增和递减计数,性能很高。
- 回收速度快。一旦引用计数为0,对象内存可以立即回收,不需要像标记-清除那样需要等到下次GC周期。
缺点:
- 无法解决循环引用问题。两个对象相互引用,导致引用计数无法为0,发生内存泄漏。
- 需要额外空间存储引用计数。每次有新的引用创建时,需要找到对象并递增引用计数,这个额外操作会消耗一定性能。
- 回收时需要重置大量对象的引用计数。当一个包含大量子对象的父对象被销毁时,需要找到所有子对象并递减其引用计数,这也是比较耗时的操作。
引用计数算法适用的场景:
- 对象之间没有循环引用,或者循环引用较少的情况。
- 对实时性要求较高的应用,需要快速回收内存。
- 内存回收频率较高,但每次回收规模较小的场景。
- 作为其他算法(如标记-清除算法)的补充,在老生代中部分使用。
注意: 现代浏览器会选用标记-清除算法或其优化算法(如增量标记)进行内存管理,而非纯引用计数算法。
引用计数算法+标记-清除算法:
- 分代收集:根据对象存活周期不同,将内存分为新生代和老生代。新生代采用引用计数法,老生代采用标记-清除法。这可以解决引用计数法无法回收循环引用对象的问题,同时发挥其在频繁回收小对象方面的优势。
- 定期循环检测:每隔一定时间对内存进行一次完整扫描,发现引用计数未为0的循环引用对象并进行标记。然后将这些对象的引用计数设为0,等到下次GC时进行回收。这种策略可以有效减少循环引用导致的内存泄漏。
- 由引用计数触发标记-清除:当引用计数法无法回收更多内存时(回收速度下降),触发一次标记-清除算法进行内存回收。这可以快速释放大块内存,避免由于引用计数法导致的频繁GC造成的性能问题。
- 引用计数递增时检测循环:在对象引用计数递增时,检测其是否已被其他对象引用。如果是,则说明存在循环引用,需要标记这两个对象以防止内存泄漏。这种策略可以及早发现和解决循环引用问题。
- 定期执行标记-清除:虽然引用计数法可以快速回收内存,但是会产生一定内存碎片。定期执行标记-清除算法可以整理内存,提高内存利用率。
总结:引用计数法用于快速回收短期存活对象和新生代对象,发挥其快速回收的优势。而标记-清除算法用于解决循环引用问题,回收老生代对象,实现内存整理,发挥其解决碎片和循环引用的优势。