4

为什么要关注内存

  1. 任何程序的运行都要分配运行空间。
  2. 如果不在使用的内容得不到释放,不会返回到操作系统或空闲内存池,会导致内存泄露。
  3. 程序运行所需的内存空间大于当前的可用内存空间会引发内存溢出。

JS数据类型与JS内存机制

数据类型

原始数据类型:

  • 字符串 string
  • 数字 number
  • 布尔 boolean
  • 空对象 null
  • 未定义 undefined

引用数据类型:

  • object
  • function
  • array

内置对象(实际上是内置函数,可以当做构造器使用)

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

内存空间:

  • 栈 stack 存放原始数据类型
  • 堆 heap 存放引用数据类型( Array、Object、Function)

栈,一种数据结构,限定在表尾进行插入和删除操作的线性表。

特点:后进先出(Last In First Out)–LIFO

特别的是,允许插入和删除的一端称为栈顶,另一端称为栈底。

栈的插入操作,叫进栈、入栈或压栈。

栈的删除操作,叫出栈、或弹栈。

可以想象成弹夹压子弹,1-2-3 入弹夹,3-2-1 出弹夹。

var a = 10;
var b;
b=a;

Javascript编译原理:

  1. var a ,编译器判断当前作用域中是否已存在该变量,如果有,则忽略;否则在当前作用域中新声明一个变量,命名为 a
  2. a = 2,引擎运行时,先判断作用域中是否存在 变量 a。如果存在变量 a,进行赋值操作,将2赋值给a;否则抛出异常。

当声明变量a并初始化值为10时

  1. 为变量a创建为标识符
  2. 在栈中分配地址,指向标识符
  3. 将值10存储在标识符对应的地址

    也就是值传递。

1583419546970.png

声明变量b,然后赋值时:

  1. 为变量b创建标识符
  2. 将变量a在栈中的地址,指向b。

1583420078960.png

a==b,结果是什么?true

因为a,b均为原始数据引用,在比较值的时候,比较的是值的本身。

如果此时我们执行a=true,栈中会发生什么变化呢

因为栈中存在的是原始数据类型,其不可变,当我们将赋值true时,将在栈中新分配地址,并指向a,同时b的值指向不变,仍为10.

1583420312259.png

再次操作b=null后,新分配内存空间值为null,由于,地址为0,值为10的内存未关联任何变量,会被垃圾回收释放此空间。

1583420997669.png

基本数据类型存在堆的情况
闭包:将内部函数传递到所在的词法作用域以外,都会持有对原始定义作用域的引用。

当一个基本类型被闭包引用之后,就可以长期存在于内存中,这个时候即使他是基本类型,也是会被存放在堆中的。

function foo(){
    var name='bob';
    return function (){
        console.log(name)
    }
}
var bar=foo();
bar();//bob

正常情况下,foo在执行完成后,会被垃圾回收器掉,但是因为闭包的存在,内部函数仍保留着局部变量name的引用,导致内存无法释放,所以不能滥用闭包。需要及时将退出函数前,将闭包内的变量引用删除。

是存储引用类型的地方。跟调用堆栈主要的区别在于,堆可以存储无序的数据,这些数据可以动态地增长,非常适合数组和对象。在Javascript中我们无法直接操作堆,我们在操作对象时,实际是在操作对象的引用

当如下声明时:

var a={
    name:'Bob',
    age:18
}
  1. 为变量创建标识符a
  2. 在栈中分配地址,指向标识符
  3. 在堆内存中分配空间
  4. 在栈中存储堆内存的存储地址

1583422758145.png

那如果我将一个对象赋值给另一个变量呢?var b=a ,栈中会配一个新的值,来存放新的变量,但是这两个变量的地址是一样的,相当于指向的对象是一样的

1583424712387.png

a.name=‘Tom’这里只是修改了堆内存地址0x1021中的数据,并未修改变量a的指向的内存地址。又因为变量a和b指向了内存空间的同一个地址,所有b.name也等于Tom

对象属性的内存模型

不同于原始数据内存模型,一个对象可以包含多个属性,而对象的属性又可以分为原始数据和引用数据。

var obj = {
    name:'Bob',
    age:'18',
    behaviour:{
        fly:function(){
            console.log("can fly")
        },
        eat:{
            noodles:'大碗宽面'
        }
    }
}

并不是说obj变量为引用类型,在堆内存中直接存放了。

obj来说,变量obj指向了堆内存中分配给引用数据对象的地址。从obj的属性来看,属性只是指向了属性值的内存地址,并不指向实际的对象。也就是说对象的属性指向的也是引用,指向这些值真正存放的地方。

垃圾回收

Javascript在创建变量时(对象、字符串等)时会自动分配内存, 并且在不使用他们时释放 。

优势:由引擎跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它,减少内存空间不足带来的内存泄露。

劣势:未提供相应的api,无法人为进行内存操作。

垃圾回收算法主要依赖 引用在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象

引用计数法

记录每个值被引用的次数,当引用数为0时,表示这个值不再使用了,判定可以进行释放。

var o = { 
  a: {
    b:2
  }
}; 
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o
// 很显然,没有一个可以被垃圾收集


var o2 = o; // o2变量是第二个对“这个对象”的引用

o = 1;      // 现在,“这个对象”只有一个o2变量的引用了,“这个对象”的原始引用o已经没有

var oa = o2.a; // 引用“这个对象”的a属性
               // 现在,“这个对象”有两个引用了,一个是o2,一个是oa

o2 = "yo"; // 虽然最初的对象现在已经是零引用了,可以被垃圾回收了
           // 但是它的属性a的对象还在被oa引用,所以还不能回收

oa = null; // a属性的那个对象现在也是零引用了
           // 它可以被垃圾回收了

弊端(IE8及以下)

我们来看个例子

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o
}
f();

这里创建了两个对象 oo2并且相互引用,形成了一个循环。当函数f执行完成后,内部作用域销毁,我们期待垃圾回收机制帮助我们销毁这两个对象并回收对应的空间,但是两个对象之间都保留有一次引用。

如果出现循环引用,那么值所占的空间将用永远得不到释放,运行时间越长,越容易引擎内存泄露。

小tip:可以使用JSON.stringfy(o)来检测对象是否存在循环引用

标记清除法(2012年起,所有浏览器均使用了此机制)

主要依赖与计算环境

执行环境:定义了变量或函数有权访问的其他数据,决定了他们各自的行为。每个执行环境都有一个与之相关联的变量对象(全局对象/局部对象),环境中定义的所有变量和函数都保存在这个对象中。

当变量进入执行环境时,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”。

垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。

简单理解为:当每个变量或函数在作用域链中无法访问,那么就该收集了。

目前主流浏览器都是使用标记清除式的垃圾回收策略,只不过收集的间隔有所不同

V8内存管理

弊病

  1. 为浏览器设计,不太可能遇到大量内存的场景,64位下 新生代默认的最大内存空间为32MB,老生代默认的最大内存空间为1400MB。
  2. 垃圾回收会导致线程短暂停止线程从而引起性能问题。

回收策略:分代式垃圾回收机制

  • 新生代:大多数对象被分配在这里。新生区是一个很小的区域,垃圾回收在这个区域非常频繁,与其他区域相独立
  • 老生代:这里包含大多数可能存在指向其他对象的指针的对象。大多数在新生区存活一段时间之后的对象都会被挪到这里

回收算法

新生代

新生代中的对象主要通过Scavenge算法进行垃圾回收。

1583434994243.png

内存分配空间时,分为两个区域:From空间和To空间。

  • 当分配新的对象时,总是往From空间中分配。
  • 在回收时,先扫描From空间,将From空间中存活的对象复制到To空间中,然后将From空间的内存全部释放,最后将From和To的角色交换

特点:

  • 只能使用一半的内存,但由于只需要复制存活对象,因此该算法非常适合应用在新生代垃圾回收中,因为新生代中对象的生命周期较短,垃圾回收时多为未存活对象。
  • 不会在内存中留下碎片

对象晋升

在执行Scavenge的存活对象复制操作时进行对象是否晋升的判断(新生代迁移至老生代)

晋升标准:

  1. 该对象已经进行过一次Scavenge回收;
  2. To空间已使用了25%。

1583435096879.png

老生代

对于老生代中的对象,由于存活对象占较大比重,再采用Scavenge的方式会有两个问题:

  • 一是存活对象较多,复制存活对象的效率将会很低;
  • 另一个问题则是由于老生代空间较大,空闲一半空间的做法对内存是极大的浪费

主要采用了Mark-Sweep和Mark-Compact两种算法相结合的方式进行垃圾回收。

Mark-Sweep

分为标记阶段和清除阶段:

  • 标记阶段会遍历老生代空间的所有对象,将其中非存活的对象标记出来;
  • 清除阶段则会将标记的死亡对象一一清除,释放内存空间。

1583435310890.png

缺点:回收后会在内存中留下一些碎片,如果这时候需要分配大对象,不连续的内存可能无法满足需求

Mark-Compact

分为标记和合并阶段:

  • 标记阶段会遍历老生代空间的所有对象,将其中非存活的对象标记出来;
  • 合并阶段会将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存

1583435431583.png

算法对比

回收算法 Mark-Sweep Mark-Compact Scavenge
速度 中等 最慢 最快
空间开销 少(有碎片) 少(无碎片) 双倍空间(无碎片)
是否移动对象
主动启动时机 进程空闲时 进程空闲时 进程空闲时(频率低)
被动启动时机 1.老生代空间中被分配了一定数量的对象的时候;<br/>2.老生代空间里没有新生代空间大小相同的空间的时候 老生代空间的碎片到达一定数量的时候 From空间没有足够的空间分配对象

王大山
28 声望2 粉丝