JavaScript 内存机制&垃圾回收

前言


        每种编程语言都有它的内存管理机制,比如简单的C有低级的内存管理基元,像malloc(),free()。而对于JavaScript来说,会在创建变量(对象,字符串等)时分配内存,并且在不再使用它们时“自动”释放内存,这个自动释放内存的过程称为垃圾回收。
因为自动垃圾回收机制的存在,让大多Javascript开发者感觉他们可以不关心内存管理,所以会在一些情况下导致内存泄漏。

内存生命周期


JS 环境中分配的内存有如下声明周期:

  1. 内存分配:当我们申明变量、函数、对象的时候,系统会自动为他们分配内存
  2. 内存使用:即读写内存,也就是使用变量、函数等
  3. 内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存

JavaScript 的内存分配


一般来说JS的内存空间分为栈(stack)堆(heap)(一般也会归类为栈中)。
其中栈存放变量,堆存放复杂对象,池存放常量,所以也叫常量池。
通俗点说就是:栈是存放基本数据类型及对象变量的指针的,堆是存放引用数据类型的。

堆内存与栈内存是有区别的:
        栈内存运行效率比堆内存高,空间相对推内存来说较小,反之则是堆内存的特点。所以将构造简单的原始类型值放在栈内存中,将构造复杂的引用类型值放在堆中而不影响栈的效率。

数据类型:
基本数据类型:StringNumberBooleanNullUndefined
引用类型:Object

1. 基本数据类型与栈

        基本数据类型保存在栈内存中,因为基本数据类型占用空间小、大小固定,通过按值来访问,属于被频繁使用的数据。(需要注意的是闭包中的基本数据类型变量不保存在栈内存中,而是保存在堆内存中

举个栗子:
        为了不让程序员费心分配内存,JavaScript 在定义变量时就完成了内存分配。(在变量对象中执行数据复制的时候,其实系统会自动为新的变量分配一个新的值,所以a与b其实已经是完全独立的两个变量,只是值一样而已。)

let a = 20; // 给数值变量分配内存
let b = a; // 给数值变量分配内存
b = 30
console.log(a,b)  // a = 20 b = 30
变量在栈中的内存
2. 引用数据类型与堆

        引用数据类型存储在堆内存中,因为引用数据类型占据空间大、大小不固定。 如果存储在栈中,将会影响程序运行的性能;
        引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。 当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

举个栗子:

var m = { 
    a:10,
    b:20
};
var n = m;
n.a = 15;
console.log(m.a);  //15
复制后两个引用类型的地址指向一致


垃圾回收


为什么要回收?程序的运行需要内存,只要程序提出要求,操作系统或者运行时就必须提供内存,那么对于持续运行的服务进程,必须要及时释放内存,否则,内存占用越来越高,轻则影响系统性能,重则就会导致进程崩溃。

什么是内存泄漏?不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。
有些语言(比如 C 语言)必须手动释放内存,程序员负责内存管理。

垃圾回收算法

最常见的两种垃圾回收算法:引用计数标记清除

1. 引用计数

这种方式常常会引起内存泄漏,低版本的IE使用这种方式。
原理:跟踪记录每个值被引用的次数。(引用计数算法定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用。 如果没有其他对象指向它了,说明该对象已经不再需了。)

工作流程:
  1. 当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值的引用次数就为 1。
  2. 如果同一个值又被赋给另一个变量,那么引用数加 1。
  3. 如果该变量的值被其他的值覆盖了,则引用次数减 1。
  4. 当引用次数变成0时,说明没办法访问这个值了。
  5. 当垃圾收集器下一次运行时,它就会释放引用次数是0的值所占的内存。
let a = new Object()    // 此对象的引用计数为 1(a引用)
let b = a       // 此对象的引用计数是 2(a,b引用)
a = null        // 此对象的引用计数为 1(b引用)
b = null        // 此对象的引用计数为 0(无引用)
...         // GC 回收此对象

目前看起来此方式很简单明了,但是很快我们就会遇到一个问题:循环引用,即对象 A 有一个指针指向对象 B,而对象 B 也引用了对象 A ,如下:

function test(){
  let A = new Object();
  let B = new Object();
  
  A.b = B;
  B.a = A;
}
// 对象 A 和 B 通过各自的属性相互引用着,按照上文的引用计数策略,它们的引用数量都是 2,
// 按理来说在函数 test 执行完成之后,对象 A 和 B 是要被清理的
// 但由于它们的引用数量不会变成 0,故使用引用计数则不会被清理。
// 假如此函数在程序中被多次调用,那么就会造成大量的内存不会被释放

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


2. 标记清除

原理: 是当变量进入环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。

工作流程:
  1. 垃圾收集器会在运行的时候会给存储在内存中的所有变量都加上标记。
  2. 清除「进入环境中的变量的标记」或者「被进入环境中的变量所引用的变量的标记」。
  3. 那些还存在标记的变量被视为准备删除的变量。
  4. 最后垃圾收集器会执行最后一步内存清除的工作,销毁那些带标记的值并回收它们所占用的内存空间。

标记清除算法也有一个很大的缺点,就是在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了 内存碎片(如下图),并且由于剩余空闲内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配的问题


假设我们新建对象分配内存时需要大小为 size,由于空闲内存是间断的、不连续的,则需要对空闲内存列表进行一次单向遍历找出大于等于 size 的块才能为其分配(如下图)


那如何找到合适的块呢?我们可以采取下面三种内存分配策略

  • First-fit,找到大于等于 size 的块立即返回
  • Best-fit,遍历整个空闲列表,返回大于等于 size 的最小分块
  • Worst-fit,遍历整个空闲列表,找到最大的分块,然后切成两部分,一部分 size 大小,并将该部分返回
分类 First-fit Best-fit Worst-fit
优点 分配的速度和效率更高 -- 空间利用率看起来是最合理
缺点 -- -- 切分之后会造成更多的小块,
形成内存碎片,
所以不推荐使用


3. 引用计数PK标记清除
分类 引用计数 标记清除
优点 回收时机:
    引用计数算法:引用计数在引用值为 0 时,也就是在变成垃圾的那一刻就会被回收,所以它可以立即回收垃圾
    标记清除算法:需要每隔一段时间进行一次,那在应用程序(JS脚本)运行过程中线程就必须要暂停去执行一段时间的 GC,另外,标记清除算法需要遍历堆里的活动以及非活动对象来清除,而引用计数则只需要在引用时计数就可以了。
算法:
    比较简单,打标记也无非打与不打两种情况,这使得一位二进制位(0和1)就可以为其标记,非常简单
缺点 1. 它需要一个计数器,而此计数器需要占很大的位置,因为我们也不知道被引用数量的上限

2. 无法解决循环引用无法回收的问题
1. 内存碎片化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块

2. 分配速度慢,因为即便是使用 First-fit 策略,其操作仍是一个 O(n) 的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢

补充:

常见内存泄漏的原因:
(1)全局变量引起的内存泄露
(2)闭包引起的内存泄露:慎用闭包
(3)dom清空或删除时,事件未清除导致的内存泄漏
(4)循环引用带来的内存泄露
(5)未被清空的定时器






参考文献:
https://www.jianshu.com/p/3d6b82f5242c
https://juejin.cn/post/6981588276356317214
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_Management
https://juejin.cn/post/6844903615300108302

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

推荐阅读更多精彩内容