美文网首页前端在路上
Javascript内存机制

Javascript内存机制

作者: 夏末远歌 | 来源:发表于2020-03-06 03:40 被阅读0次

    为什么要关注内存

    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%。

    ![1583435310890.png](https://img.haomeiwen.com/i6366468/78c5ffa23e1d0520.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

    老生代

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

    • 一是存活对象较多,复制存活对象的效率将会很低;

    • 另一个问题则是由于老生代空间较大,空闲一半空间的做法对内存是极大的浪费

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

    Mark-Sweep

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

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

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

    Mark-Compact

    分为标记和合并阶段:

    • 标记阶段会遍历老生代空间的所有对象,将其中非存活的对象标记出来;

    • 合并阶段会将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存

    1583435431583.png

    算法对比

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

    相关文章

      网友评论

        本文标题:Javascript内存机制

        本文链接:https://www.haomeiwen.com/subject/nkjurhtx.html