JavaScript 内存机制

看了几篇关于js内存机制的文章,感觉大家讲内容的都是大差不差,下面是我整理了多篇文章后得出的最全最核心内容。

1.简介

学过c++的同学都知道,内存是要主动申请和释放的。每种编程语言都有它的内存管理机制,比如简单的C有低级的内存管理基元,像malloc(),free()。同样我们在学习JavaScript的时候,很有必要了解JavaScript的内存管理机制。

JavaScript的内存管理机制是:内存基元在变量(对象,字符串等等)创建时分配,然后在他们不再被使用时“自动”释放。

对于前端开发来说,内存空间并不是一个经常被提及的概念,很容易被大家忽视。当然也包括我自己。在很长一段时间里认为内存空间的概念在JS的学习中并不是那么重要。可是后我当我回过头来重新整理JS基础时,发现由于对它们的模糊认知,导致了很多东西我都理解得并不明白。比如最基本的引用数据类型和引用传递到底是怎么回事儿?比如浅复制与深复制有什么不同?还有闭包,原型等等。

在使用JavaScript进行开发的过程中,了解JavaScript内存机制有助于开发人员能够清晰的认识到自己写的代码在执行的过程中发生过什么,也能够提高项目的代码质量。


2.内存模型

Js的内存模型分为栈(stack)、堆(heap)、池(池一般包括在栈中);其中栈存放变量,堆存放复杂对象,池存放常量。

js中数据类型可以分为基础类型和复杂类型;

  • 基础类型 string, boolean,number,null,undefined
  • 引用类型 function ,array ,object

下面我们讲基础类型和栈,引用类型和堆;

JS中的基础数据类型,这些值都有固定的大小,往往都保存在栈内存中(闭包除外),由系统自动分配存储空间。我们可以直接操作保存在栈内存空间的值,因此基础数据类型都是按值访问
数据在栈内存中的存储与使用方式类似于数据结构中的堆栈数据结构,遵循后进先出的原则。

堆和栈的区别:

  • 栈内存空间的存储方式:先入后出,后入先出。
  • 堆内存空间的存储方式类似json的key-value存储,我们只关心key(指向堆内存的引用或者说指针)就行。
  • 栈(Stack)会自动分配内存空间,会自动释放,大小固定。
  • 堆(Heap)动态分配内存,大小不确定,也不会主动释放。

除此之外,我们还可以以此为基础,一步一步的理解JavaScript的执行上下文,作用域链,闭包,原型链等重要概念。


3.内存的生命周期

JS环境中分配的内存一般有如下生命周期:

  • 内存分配:当我们申明变量、函数、对象的时候,系统会自动为他 们分配内存
  • 内存使用:即读写内存,也就是使用变量、函数等
  • 内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存
    为了便于理解,我们使用一个简单的例子来解释这个周期。
var a = 20;  // 在内存中给数值变量分配空间
alert(a + 100);  // 使用内存
var a = null; // 使用完毕之后,释放内存空间

第一步和第二步我们都很好理解,JavaScript在定义变量时就完成了内存分配。第三步释放内存空间则是我们需要重点理解的一个点。

现在想想,从内存来看 null 和 undefined 本质的区别是什么?

var a = undefined;
var b = null;

ab变量再定义的时候都在栈内存开辟了空间,只不undefined是表示变量定义后未赋值,没有赋值变量的默认值。而b变量在栈中保存的是一个指针,指向一个空的对象。
这就是为什么null可以释放一个对象的内存;下面我们会说内存回收机制

内存回收

JavaScript有自动垃圾收集机制,那么这个自动垃圾收集机制的原理是什么呢?其实很简单,就是找出那些不再继续使用的值,然后释放其占用的内存。垃圾收集器会每隔固定的时间段就执行一次释放操作。

在JavaScript中,最常用的是通过标记清除的算法来找到哪些对象是不再继续使用的,因此 a = null 其实仅仅只是做了一个释放引用的操作,让 a 原本对应的值失去引用,脱离执行环境,这个值会在下一次垃圾收集器执行操作时被找到并释放。而在适当的时候解除引用,是为页面获得更好性能的一个重要方式。

在局部作用域中,当函数执行完毕,局部变量也就没有存在的必要了,因此垃圾收集器很容易做出判断并回收。但是全局变量什么时候需要自动释放内存空间则很难判断,因此在我们的开发中,需要尽量避免使用全局变量,以确保性能问题。

以Google的V8引擎为例,在V8引擎中所有的JavaScript对象都是通过堆来进行内存分配的。当我们在代码中声明变量并赋值时,V8引擎就会在堆内存中分配一部分给这个变量。如果已申请的内存不足以存储这个变量时,V8引擎就会继续申请内存,直到堆的大小达到了V8引擎的内存上限为止(默认情况下,V8引擎的堆内存的大小上限在64位系统中为1464MB,在32位系统中则为732MB)。

另外,V8引擎对堆内存中的JavaScript对象进行分代管理。
新生代:新生代即存活周期较短的JavaScript对象,如临时变量、字符串等;
老生代:老生代则为经过多次垃圾回收仍然存活,存活周期较长的对象,如主控制器、服务器对象等。
这个跟java的对象 内存模型分代很像 在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:EdenFrom SurvivorTo Survivor。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。

垃圾回收算法

1.引用计数算法
熟悉或者用C语言搞过事的同学的都明白,引用无非就是指向某一物体的指针。对不熟悉这个语言的同学来说,可简单将引用视为一个对象访问另一个对象的路径。(这里的对象是一个宽泛的概念,泛指JS环境中的实体)。

引用计数算法定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用。如果没有其他对象指向它了,说明该对象已经不再需了。

// 创建一个对象person,他有两个指向属性age和name的引用
var person = {
    age: 12,
    name: 'aaaa'
};

person.name = null; // 虽然设置为null,但因为person对象还有指向name的引用,因此name不会回收

var p = person; 
person = 1;         //原来的person对象被赋值为1,但因为有新引用p指向原person对象,因此它不会被回收

p = null;           //原person对象已经没有引用,很快会被回收

从上面例子能看出,当定义p对象引用person对象时,重新赋值person对象并不能改变 person对象在堆内存中的被引用的事实。只要这个应用还存在,person对象指向的堆内存中的对象就不会被GC(garbage colleciton)回收。
由上面可以看出,引用计数算法是个简单有效的算法。但它却存在一个致命的问题:循环引用。如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露

function cycle() {
    var o1 = {};
    var o2 = {};
    o1.a = o2;
    o2.a = o1; 

    return "Cycle reference!"
}

cycle();

像上面这个函数内部的变量,当函数执行完后,由于变量01和02之间的互相应用,导致引用计数器无法回收这两个变量的内存,内存泄露不可避免了。
正是因为有这个严重的缺点,这个算法在现代浏览器中已经被下面要介绍的标记清除算法所取代了。但绝不可认为该问题已经不再存在了,因为还占有大量市场的IE老祖宗们使用的正是这一算法。在需要照顾兼容性的时候,某些看起来非常普通的写法也可能造成意想不到的问题!


2.标记清除算法
上面说过,现代的浏览器已经不再使用引用计数算法了。现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。

标记清除算法将“不再使用的对象”定义为“无法达到的对象”。简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。

从这个概念可以看出,无法触及的对象包含了没有引用的对象这个概念(没有任何引用的对象也是无法触及的对象)。但反之未必成立。

根据这个概念,上面的例子可以正确被垃圾回收处理了(亲,想想为什么?)。当初想函数内部变量互相引用时,从根部无法触及到这两个变量,所以这两个变量会被清除。

4.内存泄露

对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。
不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。
有些语言(比如 C 语言)必须手动释放内存,程序员负责内存管理。

char * buffer;
buffer = (char*) malloc(42);

// Do something with buffer

free(buffer);
内存泄漏的识别方法

1.浏览器开发工具

  • 打开开发者工具,选择 Timeline 面板
  • 在顶部的Capture字段里面勾选 Memory
  • 点击左上角的录制按钮。
  • 在页面上进行各种操作,模拟用户的使用情况。
  • 一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况。

2.通过命令行方法
命令行可以使用 Node 提供的 process.memoryUsage 方法。

WeakMap、WeakSet

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。

  1. WeakSet中的成员只能 是对象,不是对象会报错;
const ws = new WeakSet();
ws.add(1)
// TypeError: Invalid value used in weak set
ws.add(Symbol())
// TypeError: invalid value used in weak set
  1. WeakSet中的对象成员都是弱引用,如果这些对象在其他地方的引用被清除,那么垃圾回收机制会回收这些对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

WeakMap结构与 Map 类似,也是可以接受一个数组做参数,用于生成键值对的集合。但是,它与 Map 有两个区别。

  1. WeakMap 的键值只接受对象类型。
const map = new WeakMap();
map.set(1, 2)
// TypeError: 1 is not an object!
map.set(Symbol(), 2)
// TypeError: Invalid value used as weak map key
map.set(null, 2)
// TypeError: Invalid value used as weak map key
  1. WeakMap 的键是弱引用,键名所指向的对象,不计入垃圾回收机制。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值