Home > Archives > How JS Works学习笔记(2)

How JS Works学习笔记(2)

Publish:

今天记录How JavaScript Works第三部分的学习笔记,主要是以下内容:

内存管理 + 如何处理4个常见的内存泄漏

下面开始讲解。

内存管理 + 如何处理4个常见的内存泄漏

概念

JS不像C这样的语言拥有原生内存管理原语(如malloc和free)。

JS在创建变量时自动分配内存,在变量不被需要时自动释放内存(垃圾回收,GC),这让一些开发者认为在使用JS的过程中我们不必关心内存管理,但这个概念实际上是错误的。

因为自动的内存管理可能存在一些问题,比如GC的实现可能存在缺陷和不足,因此我们必须对内存管理有一定的理解。

内存生命周期

内存的声明周期在不同的语言中几乎都是一样的:

1、 分配内存,并允许程序使用它,在JS中自动进行。
2、 使用内存,实际使用之前分配的内存,通过代码对内存进行读写。
3、 释放内存,在内存不再被需要的时候释放它,以便于重新分配,在JS中也是自动进行的。

什么是内存及怎样分配内存

我们可以想象,内存中包含有大量的触发电路,每一个触发电路都包含一些可以存储1位数据的晶体管。

触发器可以通过唯一标识符来寻址,然后进行读取和覆盖。

因此我们可以认为内存是一个巨大的可读写阵列。

在内存中存储的位信息被人们以更加容易被理解的方式组织。

如,8位是一个字节,2或4个字节是一个字等。

所有的变量和程序中用到的数据以及程序和操作系统的代码都被存放在内存中

而在分配内存时有两种方式:

1、 静态分配
2、 动态分配
1. 静态分配

假设有下面一段代码:

int n;
int x[4];
double m;

编译器在编译过程中马上就知道内存需要4 + 4 * 4 + 8 = 28字节,于是在编译时进行内存分配如下:

静态内存分配图

注意,静态分配的内存是被分配在栈空间上的。因为函数在调用时被添加到栈上成为一个栈帧,这个栈帧保存函数的内部变量。

2. 动态分配

事情总是这么简单吗?未必。

请看:

int n = readInput();

// 创建一个有n个元素的数组

编译器在编译这段代码时并不知道这个数组需要多少内存,因为数组大小取决于用户提供的值。

因此,程序必须在运行时想操作系统请求足够的空间,此时的内存从堆空间被分配。

3. 静态分配和动态分配的区别

这张图总结的很好:

静态分配和动态分配的区别

JS中的内存管理

JS中内存分配、内存使用和内存释放都是由引擎自动完成的。

在声明变量时引擎自动分配内存,在操作变量时引擎自动读/写内存,在内存不被需要时自动释放内存。

难点就在于,如何知道这些被分配的内存何时不再被需要。

JS中,通过垃圾回收算法来跟踪内存的分配和使用,以便于发现一些内存不再被需要的情况,并且自动的释放内存。

目前的垃圾回收算法主要有两个:

1、 内存引用计数法
2、 标记清除法

垃圾回收算法-内存引用计数

内存引用计数算法是通过查询内存引用来确定内存是否可以被释放的算法。

让我们先来明确一个概念,什么是引用?

简单的说,一个对象访问(指向)了另一个对象,就说另一个对象被引用了。

引用又分为隐式引用或者显式引用。

比如说一个对象,隐式的指向它的原型[[prototype]],则它的原型被隐式引用。

function Person() {}
var person1 = new Person() // Person.prototype被隐式引用

比如说一个对象,显式的访问了它原型[[prototype]]的一个方法,那么它的原型被显式引用。

function Person() {}
Person.prototype.sayHi = function () {}
var person1 = new Person()
person1.sayHi() // Person.prototype被显式引用

内存引用计数算法的思想很简单:当一个对象不再被引用,则回收它。

但这个算法有一个致命的缺陷在于,它无法解决循环引用的问题,请看:

function f() {
  var o1 = {}
  var o2 = {}
  o1.p = o2 // o1 引用 o2
  o2.p = o1 // o2 引用 o1. 形成循环引用
}

f()

这个例子中两个对象被创建并相互引用,这样就创建了一个循环引用,在函数执行结束后,按照道理来讲,应该o1和o2都可以被释放,但考虑到两个对象都至少被引用了一次,所以不可以被回收。

垃圾回收算法-标记清除算法

标记清除算法原文讲的很详细,也比较容易理解,所以引用原文的讲法:

1、垃圾回收器生成一个根列表。根通常是其引用被保存在代码中的全局变量。在JavaScript中,window对象是一个可以作为根的全局变量。
2、所有的根都被检查和标记成活跃的(不是垃圾),所有的子变量也被递归检查。所有可能从根元素到达的都不被认为是垃圾。
3、所有没有被标记成活跃的内存都被认为是垃圾。垃圾回收器就可以释放内存并且把内存还给操作系统

标记清除算法可以解决循环引用的问题,因为在函数执行完后,这两个对象没有被任何的全局对象可达的引用,因此被标记为不可达。

注,大多数GC实现会在分配内存时启动回收例程,在其他时间保持空闲。

内存泄漏

内存泄漏指的是不再被应用需要的内存,由于某种原因,没有被归还给系统或进入可用内存池。

在JS中,常见的内存泄漏主要有以下几种形式:

1. 全局变量

我们有时候会意外的创建一些全局变量:

function foo(arg) {
    bar = 'some text'
    this.par = 'whaaaat'
}

foo()

在函数foo()执行完毕后,bar和par可不会被回收!

除了意外的创建全局变量,明确创建全部变量也是不可以被回收的,如果只是用来暂存大量数据的全局变量,应该确保在使用后将它赋值为null。

2. 被遗忘的定时器或回调
var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); //每5秒执行一次

即使DOM中的renderer被移除了,让interval处理器内部的整个块都变得没有用。但由于interval仍然起作用,处理程序并不能被回收(除非interval停止)。如果interval不能被回收,它的依赖也不可能被回收。这就意味着serverData(大概保存了大量的数据)也不可能被回收。

事件侦听器也会造成这种类型的内存泄漏,要注意明确的移除不需要的事件侦听。

3. 闭包

无需多说…

4. DOM外引用

我们有时候会在数据结构中存储DOM节点,这时候一个DOM节点有两个引用,一个在DOM树中,另一个在JS代码结构中,我们需要保证两个引用都不可达。

声明: 本文采用 BY-NC-SA 授权。转载请注明转自: How JS Works学习笔记(2) - 无火的余灰