JavaScript 内存机制

每种编程语言都有它的内存管理机制,比如C语言这样的底层语言,有原生内存管理接口,像malloc()动态的分配内存空间free()释放动态分配的内存空间。开发人员使用这些接口可以显式分配和释放操作系统的内存。

JS作为一门高级语言,JS并不像底层语言C那样拥有对内存操作的完全掌控。相对地,JavaScript会在创建变量(对象、字符串)时自动分配内存,并在这些变量不被使用时自动释放内存,这个过程被称为垃圾回收

内存生命周期

不管什么程序语言,内存生命周期基本是一致的:

  • 分配你所需要的内存
  • 使用分配到的内存(进行读、写)
  • 不需要时将内存进行释放

JS 内存模型

JavaScript中的内存分配是由js引擎完成的,内存空间分为两种:栈内存(stack) 与 堆内存(heap), 而JavaScript的数据类型也分为两大类, 分别是基本数据类型和引用数据类型,与两种内存空间相对应。

基础数据类型与栈内存

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

基础数据类型:

Number、String、Null、Boolean、Undefiend、Symbol(ES6新增)

简单理解栈的存取方式,我们可以通过类比乒乓球盒子来分析。


这种乒乓球的存放方式与栈中存取数据的方式如出一辙。处于盒子中最顶层的乒乓球5,它一定是最后被放进去,但可以最先被拿出来。而我们想要拿出底层的乒乓球1,就必须将上面的4个乒乓球取出来,让乒乓球1处于盒子顶层。这就是栈空间先进后出,后进先出的特点。

引用数据类型与堆内存

JS的引用数据类型,比如数组Array,它们值的大小是不固定的。引用数据类型的值是保存在堆内存中的对象。JavaScript不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。因此,引用类型的值都是按引用访问的。这里的引用,我们可以粗浅地理解为保存在变量对象中的一个地址,该地址与堆内存的实际值相关联。

特点:不连续的内存区域,容量较大,读取速度慢(因为引用地址在堆中,多了一次中转,所以读取速度自然会比栈要慢)。随意读取,类似于图书馆书架上的书,喜欢哪本拿哪本。

熟知的引用数据类型:

Object、Array、Date、RegExp、Function 等。

var a1 = 0;   // 变量对象
var a2 = 'this is string'; // 变量对象
var a3 = null; // 变量对象

var b = { m: 20 }; // 变量b存在于变量对象中,{m: 20} 作为对象存在于堆内存中
var c = [1, 2, 3]; // 变量c存在于变量对象中,[1, 2, 3] 作为对象存在于堆内存中
image.png

当我们要访问堆内存中的引用数据类型时,实际上我们首先是从变量对象中获取了该对象的地址引用(或者地址指针),然后再从堆内存中取得我们需要的数据。

接下来,我们通过下面的例子来加深对JS内存的理解

var a = 20;
var b = a;
b = 30;

var m = { a: 10, b: 20 };
var n = m;
n.a = 15; 
image.png

在变量对象中的数据发生复制行为时,系统会自动为新的变量分配一个新值。var b = a执行之后,a与b虽然值都等于20,但是他们其实已经是相互独立互不影响的值了。具体如图。所以我们修改了b的值以后,a的值并不会发生变化。


image.png

通过var n = m执行一次复制引用类型的操作。引用类型的复制同样也会为新的变量自动分配一个新的值保存在变量对象中,但不同的是,这个新的值,仅仅只是引用类型的一个地址指针。当地址指针相同时,尽管他们相互独立,但是在变量对象中访问到的具体对象实际上是同一个。

内存回收

垃圾回收是一种内存管理机制,就是将不再用到的内存及时释放,以防内存占用越来越高,防止卡顿甚至进程崩溃。在JavaScript中有自动化的垃圾回收机制,自动回收过期无效的变量。

在JavaScript中内存垃圾回收是由js引擎自动完成的。实现垃圾回收的关键在于如何确定内存不再使用,也就是确定对象是否无用。主要有两种方式:*引用计数标记清除

引用计数算法

引用就是指向某一地址的指针。我们可简单将引用视为一个对象访问另一个对象的路径。(这里的对象是一个宽泛的概念,泛指JS环境中的实体)。

引用计数算法定义就是以内存不再使用为标准,就是看一个对象是否有指向它的引用。如果没有其他地址指向它了,说明该对象已经不再需要了,可以进行回收。

下面来看个例子:

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

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

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

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

由上面例子可以看出,引用计数算法是个简单有效的算法。但它却存在一个致命的问题:循环引用。如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露。

function cycle() {
    var o1 = {};
    var o2 = {};
    o1.a = o2;
    o2.a = o1; 
    
    return "Cycle reference!"
}

cycle();

上面我们申明了一个cycle方程,其中包含两个相互引用的对象。在调用函数结束后,对象o1和o2实际上已离开函数范围,因此不再需要了。但根据引用计数的原则,他们之间的相互引用依然存在,因此这部分内存不会被回收,内存泄露不可避免了。

正是因为有这个严重的缺点,这个算法在现代浏览器中已经被下面要介绍的标记清除算法所取代了。但绝不可认为该问题已经不再存在了,因为还占有大量市场的IE6、IE7使用的正是这一算法。在需要照顾兼容性的时候,某些看起来非常普通的写法也可能造成意想不到的问题:

var div = document.createElement("div");
div.onclick = function() {
    console.log("click");
};

现在虽然有各种框架,很少直接操作dom了 ,但上面这种JS写法很简单却存在问题。创建一个DOM元素并绑定一个点击事件,这里有什么问题呢?请注意,变量div有事件处理函数的引用,同时事件处理函数也有div的引用!(div变量可在函数内被访问)。一个循序引用出现了,按上面所讲的算法,该部分内存无可避免地泄露了。

标记清除算法

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

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

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

根据这个概念,上面的例子可以正确被垃圾回收处理了。当div与其时间处理函数不能再从全局对象出发触及的时候,垃圾回收器就会标记并回收这两个对象。

image.png

内存管理友好的JS代码

如果还需要兼容老旧浏览器,那么就需要注意代码中的循环引用问题。或者直接采用保证兼容性的库来帮助优化代码。
对现代浏览器来说,唯一要注意的就是明确切断需要回收的对象与根部的联系。有时候这种联系并不明显,且因为标记清除算法的强壮性,这个问题较少出现。最常见的内存泄露一般都与DOM元素绑定有关:

email.message = document.createElement(“div”);
displayList.appendChild(email.message);

// 稍后从displayList中清除DOM元素
displayList.removeAllChildren();

div元素已经从DOM树中清除,也就是说从DOM树的根部无法触及该div元素了。但是请注意,div元素同时也绑定了email对象。所以只要email对象还存在,该div元素将一直保存在内存中。

内存泄露

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

常见的内存泄露

1.意外的全局变量
function foo() {
      bar = '全局变量'; // 没有声明变量 实际上是全局变量=>window.bar
    }
    foo();


function foo() {
     const bar = 'foo变量'; // 没有声明变量 实际上是全局变量=>window.bar
    }
    foo();
2.定时器和回调函数

当不需要setInterval或者setTimeout时,定时器没有被清除,定时器的回调函数以及内部依赖的变量都不能被回收,造成内存泄漏。

setInterval(function() {
    // 执行什么
}, 1000);

页面卸载或者执行完定时器需要主动清除

3.滥用闭包
function fn2(){
  let test = new Array(1000).fill('test')
  return function(){
    console.log(test)
    return test
  }
}
let fn2Child = fn2()
fn2Child()
//fn2Child = null 解决方法 函数调用后,把外部的引用关系置空

return 的函数中存在函数 fn2 中的 test 变量引用,所以 test 并不会被回收,也就造成了内存泄漏。fn2Child = null 解决方法 函数调用后,把外部的引用关系置空

4.没有清理DOM元素引用
var refA = document.getElementById("test");
document.body.removeChild(refA); // dom删除了
console.log(refA, "refA");  // 但是还存在引用 能console出整个div 没有被回收
refA = null;
console.log(refA, "refA");  // 解除引用
5.console保存大量数据在内存中

过多的console,比如定时器的console会导致浏览器卡死。
合理利用console,线上项目尽量少的使用console。

内存查看

  • 浏览器方法
    1.打开Chrome浏览器开发者工具的Performance面板
    2.选项栏中勾选Memory选项
    3.点击左上角录制按钮(实心圆状按钮)
    4.在页面上进行正常操作
    5.一段时间后,点击Stop,观察面板上的数据


    image.png

如果内存占用基本平稳,接近水平,就说明不存在内存泄漏。

image

反之,就是内存泄漏了。

image
  • 命令行方法
    命令行可以使用 Node 提供的 process.memoryUsage 方法。
console.log(process.memoryUsage());
//{
  //rss: 101568512,
  //heapTotal: 72605696,
  //heapUsed: 51070584,
  //external: 5819790,
  //arrayBuffers: 4286309
//}

process.memoryUsage返回一个对象,包含了 Node 进程的内存占用信息。该对象包含四个字段,单位是字节,含义如下:

rss(resident set size):所有内存占用,包括指令区和堆栈。
heapTotal:"堆"占用的内存,包括用到的和没用到的。
heapUsed:用到的堆的部分。
external: V8 引擎内部的 C++ 对象占用的内存。
arrayBuffers: ArrayBufferSharedArrayBuffer 分配的内存,包括所有 Node.js Buffer

判断内存泄漏,以heapUsed字段为准。

参考:
https://juejin.cn/post/6844903801191661575#heading-13
http://www.ruanyifeng.com/blog/2017/04/memory-leak.html
https://juejin.cn/post/6844903801191661575#heading-9

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354

推荐阅读更多精彩内容

  • 简介每种编程语言都有它的内存管理机制,比如简单的C有低级的内存管理基元,像malloc(),free()。同样我们...
    曲昶光阅读 199评论 0 1
  • 前言 每种编程语言都有它的内存管理机制,比如简单的C有低级的内存管理基元,像malloc(),free()。而对于...
    青城墨阕阅读 973评论 0 0
  • 为什么要关注内存 任何程序的运行都要分配运行空间。 如果不在使用的内容得不到释放,不会返回到操作系统或空闲内存池,...
    夏末远歌阅读 314评论 0 0
  • 对于前端攻城师来说,JS的内存机制不容忽视。如果想成为行业专家,或者打造高性能前端应用,那就必须要弄清楚JavaS...
    IT沐华阅读 438评论 0 0
  • JavaScript不同于其他语言,在JavaScript中的内存都是自动分配和回收。如同请人打扫卫生。其实在大多...
    Pamcore阅读 169评论 0 0