zoukankan      html  css  js  c++  java
  • 浏览器的垃圾回收机制

    JS垃圾回收机制----浏览器

    为什么要有垃圾回收机制?

      在C语言和C++语言中,我们如果想要开辟一块堆内存的话,需要先计算需要内存的大小,然后自己通过malloc函数去手动分配,在用完之后,还要时刻记得用free函数去清理释放,否则这块内存就会被永久占用,造成内存泄露。
      但是我们在写JavaScript的时候,却没有这个过程,因为浏览器已经替我们封装好了,V8引擎会根据当前定义对象的大小去自动申请分配内存。

      不需要我们去手动管理内存了,所以自然要有垃圾回收,否则的话只分配不回收,岂不是没多长时间内存就被占满了吗,导致应用崩溃。

      垃圾回收的好处是不需要我们去管理内存,把更多的精力放在实现应用逻辑上,但坏处也来自于此,不用管理了,就有可能在写代码的时候不注意,造成循环引用等情况,导致内存泄露。
    内存泄漏:不再用到的内存,没有及时释放,内存被占用,这就叫做内存泄漏

    如何判断是否可以回收

    标记清除法(mark and sweep): 

      大部分浏览器以此方式进行垃圾回收,当变量进入执行环境(函数中声明变量)的时候,垃圾回收器将其标记为'进入环境', 从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们;当变量离开环境时(函数执行结束)将其标记为'离开环境', 在离开环境之后还有的变量则是需要被删除的变量。标记方式不定,可以是某个特殊位的反转或维护一个列表

      垃圾收集器给内存中的所有变量都加上标记,然后除了环境中的变量以及被环境中的变量引用的变量。在此之后再被加上标记的变量即为需要回收的变量,因为环境中的变量已经无法访问到这些变量。

    1. 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)
    2. 然后,它会去掉运行环境中的变量以及被环境中变量所引用的变量的标记
    3. 此后,依然有标记的变量就被视为准备删除的变量,原因是在运行环境中已经无法访问到这些变量了。
    4. 最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间

    目前,IE、Firefox、Opera、Chrome和Safari的JavaScript实现使用的都是标记清除式的垃圾回收策略(或类似的策略),只不过垃圾收集的时间间隔互有不同

    引用计数

      低版本的IE使用这种方式。机制就是跟踪一个值的引用次数,当声明一个变量并将一个引用类型赋值给该变量时该值引用次数加1,当这个变量指向其他一个值,该值的引用次数便减一。当该值引用次数为0时就会被回收。这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占用的内存。

      引用计数最大的问题(循环引用):当两个对象互相引用时,在采用标记清除策略的实现中,由于函数执行后,这两个对象都离开了作用域,因此这种相互引用不是问题。但在采用引用次数策略的实现中,当函数执行完毕后,objA和objB还将继续存在,因为它们的引用次数永远不会是0。

      该值的引用次数为0,还不能被回收,叫做内存泄漏,即使IE的JS引擎是用标记清除来实现的,但是JS访问DOM,BOM等对象还是基于引用计数。

      我们大部分人时刻都在写着循环引用的代码

    var el = document.getElementById('#el');
    el.onclick = function (event) {
        console.log('element was clicked');
    }

      我们为一个元素的点击事件绑定了一个匿名函数,我们通过event参数是可以拿到相应元素el的信息的。

      大家想想,这是不是就是一个循环引用呢?el有一个属性onclick引用了一个函数(其实也是个对象),函数里面的参数又引用了el,这样el的引用次数一直是2,即使当前这个页面关闭了,也无法进行垃圾回收。

      如果这样的写法很多很多,就会造成内存泄露。我们可以通过在页面卸载时清除事件引用,这样就可以被回收了

    V8垃圾回收策略

      V8 实现了准确式 GC(Garbage Collection),GC 算法采用了分代式垃圾回收机制。因此,V8 将内存(堆)分为新生代(new generation)老生代(old generation)两部分。

      新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象分别对新老生代采用不同的垃圾回收算法来提高效率,对象最开始都会先被分配到新生代(如果新生代内存空间不够,直接分配到老生代),新生代中的对象会在满足某些条件后,被移动到老生代,这个过程也叫晋升,后面会详细说明。

    分代内存

      默认情况下,32位系统新生代内存大小为16MB,老生代内存大小为700MB,64位系统下,新生代内存大小为32MB,老生代内存大小为1.4GB。

      新生代平均分成两块相等的内存空间,叫做semispave, 每块内存大小大小8MB(32位)或16MB(64位)。

    新生代

    分配方式

      新生代存的都是生存周期短的对象,分配内存也很容易,只保存一个指向内存空间的指针,根据分配对象的大小递增指针就可以了,当存储空间快要满时,就进行一次垃圾回收。

    算法

      新生代采用Scavenge垃圾回收算法,在算法实现时主要采用Cheney算法。

      Cheney算法将内存一分为二,叫做semispace,一块处于使用状态,一块处于闲置状态。

      处于使用状态的semispace称为From空间。处于闲置状态的semispace称为To空间

    接下来我们看看Cheney算法是怎么工作的

    step1: 在From空间中分配3个对象A、B、C

    step2:GC进来判断对象B没有其他引用,可以回收,对象A和C依然为活跃对象

     step3: 将活跃对象A、C从From空间复制到To空间

     step4: 清空全部From空间的内存

     step5: 交换From空间和To空间

    step6: 在From空间中又新增了2个对象D、E

    step7:下一轮GC进来发现对象D没有引用了,做标记

     step8:将活跃对象A、C、E从From空间复制到To空间

    step9: 清空From空间全部内存

    step10:继续交换From空间和To空间,开始下一轮

    通过上面的流程,可以很清楚的看到,进行From和To交换,就是为了让活跃对象始终保持在一块semispace, 另一块semispace始终保持空闲的状态

    Scavenge由于只复制存活的对象,并且对于声明周期短的场景存活对象只占少部分,所以它在时间效率上有优异的体现。Scavenge的缺点是只能使用堆内存的一半,这是由划分空间和复制机制所决定的。

    由于Scavenge是典型的牺牲空间换取时间的算法,所以无法大规模的应用到所有的垃圾回收中。但是可以看到,Scavenge非常适合应用到新生代中,因为新生代对象的生命周期较短,恰恰适合这个算法。

    晋升

      当一个对象经过多次复制仍然存活时,它就会被认为是生命周期较长的对象。这种较长生命周期的对象随后会被移动到老生代中,采用新的算法进行管理。

    对象从新生代移动到老生代的过程叫做晋升

    对象晋升的条件主要有两个:

    对象从From空间复制到To空间时,会检查它的内存地址来判断是否已经经历过一次Scavenge回收。如果已经经历过了,会将该对象从From空间移动到老生代空间中,如果没有,则复制到To空间。总的来说就是如果一个对象是第二次经历从From空间到To空间,那么这个对象会被移动到老生代中

    当要从From空间复制一个对象到To空间时,如果To空间已经使用了超过25%,那这个对象直接晋升到老生代中。设置25%这个阈值的原因是当这次Scavenge回收完成后,这个To空间会变成From空间,而接下来的内存分配将在这个空间中进行。如果占比过高,会影响后续的内存分配。

    老生代

      在老生代中,存活对象占比较大,如果继续采用Scavenge算法进行管理,就会存在两个问题:

    由于存活对象较多,复制存活对象的效率会很低。

    采用Scavenge算法会浪费一半的内存,由于老生代所占堆内存远大于新生代,所以浪费会很严重。所以,V8在老生代中主要采用了Mark-SweepMack-Compact相结合的方式进行垃圾回收。

    老生代中的空间很复杂,有如下几个空间:

    enum AllocationSpace {
      // TODO(v8:7464): Actually map this space's memory as read-only.
      RO_SPACE,    // 不变的对象空间
      NEW_SPACE,   // 新生代用于 GC 复制算法的空间
      OLD_SPACE,   // 老生代常驻对象空间
      CODE_SPACE,  // 老生代代码对象空间
      MAP_SPACE,   // 老生代 map 对象
      LO_SPACE,    // 老生代大空间对象
      NEW_LO_SPACE,  // 新生代大空间对象
    
      FIRST_SPACE = RO_SPACE,
      LAST_SPACE = NEW_LO_SPACE,
      FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
      LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
    };

    Mark-Sweep

    Mark-Sweep是标记清除的意思,它分为标记和清除两个阶段。

    与Scavenge不同,Mark-Sweep并不会将内存分为两份,所以不存在浪费一半空间的行为。Mark-Sweep在标记阶段遍历堆内存中的所有对象,并标记活着的对象,在随后的清除阶段,只清除没有被标记的对象。

    Scavenge只复制活着的对象,而Mark-Sweep只清除死了的对象。活对象在新生代中只占较少部分,死对象在老生代中只占较少部分,这就是两种回收方式都能高效处理的原因。

    现在用过流程图来看看具体实现:

    step1. 老生代中有对象A、B、C、D、E、F

     step2. GC进入标记阶段,将A、C、E标记为存活对象

     step3. GC进入清除阶段,回收掉死亡的B、D、F对象所占用的内存空间

    可以看到,Mark-Sweep最大的问题是,在进行一次清除回收后,内存空间会出现不连续的状态。这种内存碎片会对后续的内存分配造成很大的影响。

    如果出现需要分配一个大内存的情况,由于剩余的碎片空间不足以完成此次分配,就会提前触发垃圾回收,而这次回收是不必要的。

    在标记大型对像内存时,可能需要几百毫秒才能完成一次标记。这就会导致一些性能上的问题。为了解决这个问题,2011 年,V8 从 stop-the-world 标记切换到增量标志。在增量标记期间,GC 将标记工作分解为更小的模块,可以让 JS 应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿情况。但在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术可以让 GC 扫描和标记对象时,同时允许 JS 运行

    Mark-Compact

    为了解决Mark-Sweep的内存碎片问题,Mark-Compact就被提出来了。

    Mark-Compact是标记整合的意思,是在Mark-Sweep的基础上演变而来的。Mark-Compact在标记完存活对象以后,会将活着的对象向内存空间的一端移动,移动完成后,直接清理掉边界外的所有内存。

    两者结合

      在V8的策略中,Mark-Sweep和Mark-Compact两者是结合使用的。由于Mark-Compact需要移动对象,所以它的执行速度不可能很快,在取舍上,V8主要使用Mark Sweep,在空间不足以对新生代中晋升过来的对象进行分配时,才使用Mark-Compact。

    总结

      V8的垃圾回收机制会分为新生代和老生代。

    新生代主要使用Scavenge进行管理,主要实现是Cheney算法,将内存平均分成两块,使用空间叫From, 闲置空间叫To, 新对象都先分配到From空间中,在空间将要被占满时将存活对象复制到To空间中,然后清空From的内存空间,此时,调换From空间和To空间,继续进行内存分配,当满足那两个条件时对象会从新生代晋升到老生代。

    老生代主要采用Mark-Sweep和Mack-Compact算法,一个是标记清除,一个是标记整理。两者不同的地方是,Mark-Sweep在垃圾回收后会产生碎片内存,而Mark-Compact在清除前会进行整理,将存活对象向一侧移动,然后清空边界的另一侧内存,这样空闲的内存都是连续的,但是带来的问题就是速度会慢一些。在V8中,老生代是Mark-Sweep和Mark-Compact两者共同进行管理的。

    参考:https://www.jianshu.com/p/b1fe1ca7b1f8

          https://www.cnblogs.com/tangshiwei/p/12026915.html

  • 相关阅读:
    Hadoop生态圈-Hive快速入门篇之HQL的基础语法
    Hadoop生态圈-Hive快速入门篇之Hive环境搭建
    Hadoop生态圈-zookeeper的API用法详解
    Hadoop生态圈-zookeeper完全分布式部署
    Hadoop基础-MapReduce的工作原理第一弹
    Hadoop基础-HDFS的读取与写入过程
    java基础-回调函数(callback)
    Hadoop基础-网络拓扑机架感知及其实现
    Hadoop基础-HDFS数据清理过程之校验过程代码分析
    Hadoop基础-Protocol Buffers串行化与反串行化
  • 原文地址:https://www.cnblogs.com/jett-woo/p/12696485.html
Copyright © 2011-2022 走看看