JS 垃圾回收机制

4 阅读6分钟

垃圾回收与内存泄漏

经典真题

  • 请介绍一下 JavaScript 中的垃圾回收站机制

什么是内存泄露

程序的运行需要内存。只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存。

对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。

也就是说,不再用到的内存,如果没有及时释放,就叫做内存泄漏(memory leak)。

JavaScript 中的垃圾回收

浏览器的 Javascript 具有自动垃圾回收机制(GCGarbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存。其原理是:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存

但是这个过程不是实时的,因为其开销比较大并且 GC 时停止响应其他操作,所以垃圾回收器会按照固定的时间间隔周期性的执行。

特点:JS 是自动垃圾回收机制,无需手动释放内存。

JS 可以手动释放内存吗?

JS 无法直接哦手动释放堆内存,但可以强制触发 gc,但生产环境禁用,因为会阻塞主线程造成卡顿。

 window.gc()

某些语言是需要手动释放开辟的内存空间的,比如 C 语言 C++语言

举个例子解释内存占用

不再使用的变量也就是生命周期结束的变量,当然只可能是局部变量,全局变量的生命周期直至浏览器卸载页面才会结束。局部变量只在函数的执行过程中存在,而在这个过程中会为局部变量在栈或堆上分配相应的空间,以存储它们的值,然后在函数中使用这些变量,直至函数结束,而闭包中由于内部函数的原因,外部函数并不能算是结束。

下面是一段示例代码:

 function fn1() {
     var obj = {name: 'zhangsan', age: 10};
 }
 function fn2() {
     var obj = {name:'zhangsan', age: 10};
     return obj;
 }
 ​
 var a = fn1();
 var b = fn2();

在上面的代码中,我们首先声明了两个函数,分别叫做 fn1fn2

fn1 被调用时,进入 fn1 的环境,会开辟一块内存存放对象 {name: 'zhangsan', age: 10} 。而当调用结束后,出了 fn1 的环境,那么该块内存会被 JavaScript 引擎中的垃圾回收器自动释放;

fn2 被调用的过程中,返回的对象被全局变量 b 所指向,所以该块内存并不会被释放。

这里问题就出现了:到底哪个变量是没有用的?

所以垃圾收集器必须跟踪到底哪个变量没用,对于不再有用的变量打上标记,以备将来收回其内存。

用于标记的无用变量的策略可能因实现而有所区别,通常情况下有两种实现方式:标记清除引用计数

引用计数不太常用,标记清除较为常用。

标记清除

标记:从根对象(全局作用域、window、栈变量)出发,遍历所有可达对象,标记为「存活」;

清除:没有被标记的对象,判定为垃圾,直接回收。

优点: 完美解决循环引用**

缺点:

  1. GC 执行时阻塞 JS 主线程,暂停所有代码执行,回收完才恢复。
  2. 零散回收后,空闲内存四分五裂,大块连续内存分配不出来
  3. 堆越大、对象越多,标记和清除越慢。

优化点:

  1. 分代回收

新生代用复制算法;老年代主要用 标记清除 + 标记整理

  1. 增量标记

把标记工作拆分成小块,穿插到主线程空闲时执行,减少长时间卡顿

  1. 标记整理

清除后把存活对象往一端平移,规整内存,解决内存碎片

引用计数

原理:每个对象记录被引用次数,次数为 0 就回收。

致命缺点:无法处理循环引用

Tips: 详细解释

引用计数的含义是跟踪记录每个值被引用的次数。

当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 1。如果同一个值又被赋给另一个变量,则该值的引用次数加 1

相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。

这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为 0 的值所占用的内存。

 function test() {
  var a = {};  // a 指向对象的引用次数为 1
  var b = a; // a 指向对象的引用次数加 1,为 2
  var c = a; // a 指向对象的引用次数再加 1,为 3
  var b = {};  // a 指向对象的引用次数减 1,为 2
 }

Netscape Navigator3 是最早使用引用计数策略的浏览器,但很快它就遇到一个严重的问题:循环引用

循环引用指的是对象 A 中包含一个指向对象B的指针,而对象 B 中也包含一个指向对象 A 的引用。

 function fn() {
  var a = {};
  var b = {};
  a.pro = b;
  b.pro = a;
 }
 fn();

以上代码 ab 的引用次数都是 2fn 执行完毕后,两个对象都已经离开环境,在标记清除方式下是没有问题的,但是在引用计数策略下,因为 ab 的引用次数不为 0,所以不会被垃圾回收器回收内存,如果 fn 函数被大量调用,就会造成内存泄露。在 IE7IE8 上,内存直线上升。

JS垃圾回收机制现状

现代 JS(浏览器 / Node.js V8 引擎)

主流采用:标记清除 + 分代回收

V8 把堆内存分成 新生代、老年代,不同区域用不同算法:

1. 新生代(新生区)

  • 存放:短期存活、生命周期短的对象

  • 空间小、回收频率高

  • 算法:复制算法 Scavenge

    把内存分成 From / To 两个等大空间,存活对象复制到 To,直接清空 From。

  • 规则:熬过几轮回收的对象,晋升到老年代

2. 老年代(老生区)

  • 存放:长期存活、全局、闭包对象
  • 空间大、回收频率低
  • 算法:标记清除 + 标记整理

真题解答

  • 请介绍一下 JavaScript 中的垃圾回收站机制

参考答案:

JavaScript 具有自动垃圾回收机制。垃圾收集器会按照固定的时间间隔周期性的执行。

常见的垃圾回收方式:标记清除引用计数方式。JS 用的的标记清除,引用计数有循环引用的问题。