2024-08-27 10:14:31 +08:00

184 lines
7.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 垃圾回收与内存泄漏
## 经典真题
- 请介绍一下 *JavaScript* 中的垃圾回收站机制
## 什么是内存泄露
程序的运行需要内存。只要程序提出要求,操作系统或者运行时(*runtime*)就必须供给内存。
对于持续运行的服务进程(*daemon*),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。
也就是说,不再用到的内存,如果没有及时释放,就叫做内存泄漏(*memory leak*)。
## *JavaScript* 中的垃圾回收
浏览器的 *Javascript* 具有自动垃圾回收机制(*GC**Garbage Collecation*),也就是说,执行环境会负责管理代码执行过程中使用的内存。其原理是:**垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存**。
但是这个过程不是实时的,因为其开销比较大并且 *GC* 时停止响应其他操作,所以垃圾回收器会按照固定的时间间隔周期性的执行。
不再使用的变量也就是生命周期结束的变量,当然只可能是局部变量,全局变量的生命周期直至浏览器卸载页面才会结束。局部变量只在函数的执行过程中存在,而在这个过程中会为局部变量在栈或堆上分配相应的空间,以存储它们的值,然后在函数中使用这些变量,直至函数结束,而闭包中由于内部函数的原因,外部函数并不能算是结束。
下面是一段示例代码:
```js
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* 所指向,所以该块内存并不会被释放。
这里问题就出现了:到底哪个变量是没有用的?
所以垃圾收集器必须跟踪到底哪个变量没用,对于不再有用的变量打上标记,以备将来收回其内存。
用于标记的无用变量的策略可能因实现而有所区别,通常情况下有两种实现方式:**标记清除**和**引用计数**。
引用计数不太常用,标记清除较为常用。
## 标记清除
*JavaScript* 中最常用的垃圾回收方式就是标记清除。
当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为“进入环境”。
从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。
而当变量离开环境时,则将其标记为“离开环境”。
```js
function test(){
var a = 10 ; // 被标记 ,进入环境
var b = 20 ; // 被标记 ,进入环境
}
test(); // 执行完毕 之后 a、b 又被标离开环境,被回收。
```
垃圾回收器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。
然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包)。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。
最后,垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
到目前为止,*IE9+、Firefox、Opera、Chrome、Safari* 的 *JS* 实现使用的都是标记清除的垃圾回收策略或类似的策略,只不过垃圾收集的时间间隔互不相同。
## 引用计数
引用计数的含义是跟踪记录每个值被引用的次数。
当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 *1*。如果同一个值又被赋给另一个变量,则该值的引用次数加 *1*
相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 *1*。当这个值的引用次数变成 *0* 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。
这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为 *0* 的值所占用的内存。
```js
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* 的引用。
```js
function fn() {
var a = {};
var b = {};
a.pro = b;
b.pro = a;
}
fn();
```
以上代码 *a**b* 的引用次数都是 *2**fn* 执行完毕后,两个对象都已经离开环境,在标记清除方式下是没有问题的,但是在引用计数策略下,因为 *a**b* 的引用次数不为 *0*,所以不会被垃圾回收器回收内存,如果 *fn* 函数被大量调用,就会造成内存泄露。在 *IE7**IE8* 上,内存直线上升。
## 真题解答
- 请介绍一下 *JavaScript* 中的垃圾回收站机制
> 参考答案:
>
> *JavaScript* 具有自动垃圾回收机制。垃圾收集器会按照固定的时间间隔周期性的执行。
>
> *JavaScript* 常见的垃圾回收方式:**标记清除**、**引用计数**方式。
>
> 1、标记清除方式
>
> - 工作原理:当变量进入环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。
>
> - 工作流程:
>
> - 垃圾回收器,在运行的时候会给存储在内存中的所有变量都加上标记;
>
> - 去掉环境中的变量以及被环境中的变量引用的变量的标记;
>
> - 被加上标记的会被视为准备删除的变量;
>
> - 垃圾回收器完成内存清理工作,销毁那些带标记的值并回收他们所占用的内存空间。
>
> 2、引用计数方式
>
> - 工作原理:跟踪记录每个值被引用的次数。
>
> - 工作流程:
>
> - 声明了一个变量并将一个引用类型的值赋值给这个变量,这个引用类型值的引用次数就是 *1*
>
> - 同一个值又被赋值给另一个变量,这个引用类型值的引用次数加 *1*
>
> - 当包含这个引用类型值的变量又被赋值成另一个值了,那么这个引用类型值的引用次数减 *1*
>
> - 当引用次数变成 *0* 时,说明没办法访问这个值了;
>
> - 当垃圾收集器下一次运行时,它就会释放引用次数是 *0* 的值所占的内存。
-*EOF*-