垃圾回收与内存泄漏
经典真题
- 请介绍一下 JavaScript 中的垃圾回收站机制
什么是内存泄露
程序的运行需要内存。只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存。
对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。
也就是说,不再用到的内存,如果没有及时释放,就叫做内存泄漏(memory leak)。
JavaScript 中的垃圾回收
浏览器的 Javascript 具有自动垃圾回收机制(GC:Garbage 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();
在上面的代码中,我们首先声明了两个函数,分别叫做 fn1 和 fn2。
当 fn1 被调用时,进入 fn1 的环境,会开辟一块内存存放对象 {name: 'zhangsan', age: 10} 。而当调用结束后,出了 fn1 的环境,那么该块内存会被 JavaScript 引擎中的垃圾回收器自动释放;
在 fn2 被调用的过程中,返回的对象被全局变量 b 所指向,所以该块内存并不会被释放。
这里问题就出现了:到底哪个变量是没有用的?
所以垃圾收集器必须跟踪到底哪个变量没用,对于不再有用的变量打上标记,以备将来收回其内存。
用于标记的无用变量的策略可能因实现而有所区别,通常情况下有两种实现方式:标记清除和引用计数。
引用计数不太常用,标记清除较为常用。
标记清除
标记:从根对象(全局作用域、window、栈变量)出发,遍历所有可达对象,标记为「存活」;
清除:没有被标记的对象,判定为垃圾,直接回收。
优点: 完美解决循环引用**
缺点:
- GC 执行时阻塞 JS 主线程,暂停所有代码执行,回收完才恢复。
- 零散回收后,空闲内存四分五裂,大块连续内存分配不出来。
- 堆越大、对象越多,标记和清除越慢。
优化点:
- 分代回收
新生代用复制算法;老年代主要用 标记清除 + 标记整理。
- 增量标记
把标记工作拆分成小块,穿插到主线程空闲时执行,减少长时间卡顿。
- 标记整理
清除后把存活对象往一端平移,规整内存,解决内存碎片。
引用计数
原理:每个对象记录被引用次数,次数为 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();以上代码 a 和 b 的引用次数都是 2,fn 执行完毕后,两个对象都已经离开环境,在标记清除方式下是没有问题的,但是在引用计数策略下,因为 a 和 b 的引用次数不为 0,所以不会被垃圾回收器回收内存,如果 fn 函数被大量调用,就会造成内存泄露。在 IE7 与 IE8 上,内存直线上升。
JS垃圾回收机制现状
现代 JS(浏览器 / Node.js V8 引擎)
主流采用:标记清除 + 分代回收
V8 把堆内存分成 新生代、老年代,不同区域用不同算法:
1. 新生代(新生区)
-
存放:短期存活、生命周期短的对象
-
空间小、回收频率高
-
算法:复制算法 Scavenge
把内存分成 From / To 两个等大空间,存活对象复制到 To,直接清空 From。
-
规则:熬过几轮回收的对象,晋升到老年代。
2. 老年代(老生区)
- 存放:长期存活、全局、闭包对象
- 空间大、回收频率低
- 算法:标记清除 + 标记整理
真题解答
- 请介绍一下 JavaScript 中的垃圾回收站机制
参考答案:
JavaScript 具有自动垃圾回收机制。垃圾收集器会按照固定的时间间隔周期性的执行。
常见的垃圾回收方式:标记清除、引用计数方式。JS 用的的标记清除,引用计数有循环引用的问题。