ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

调用堆栈(四)-内存机制

2020-11-24 14:01:13  阅读:212  来源: 互联网

标签:调用 对象 新生代 回收 V8 内存 堆栈 垃圾


垃圾回收的必要性

一句话就是为防止内存泄漏。JS有自己的自动垃圾回收机制。

内存回收

JavaScript有自动垃圾收集机制,垃圾收集器会每隔一段时间就执行一次释放操作,找出那些不再继续使用的值,然后释放其占用的内存。

  • 局部变量和全局变量的销毁

    • 局部变量:局部作用域中,当函数执行完毕,局部变量也就没有存在的必要了,因此垃圾收集器很容易做出判断回收。
    • 全局变量: 全局变量什么时候需要自动释放内存空间则很难判断,所以在开发中尽量避免使用全局变量。
  • 以Google的V8引擎(一种JS引擎的实现)为例,V8引擎中的所有JS对象都是通过堆来进行内存分配的。

    • 初始分配:当声明变量并赋值时,V8引擎就会在堆内存中分配给这个变量。
    • 继续申请: 当申请的内存不足以存储这个变量时,V8引擎就会继续申请内存,直到堆的大小达到了V8引擎的内存上限为止。
  • V8引擎对堆内存中的JS对象进行分代管理

    • 新生代:存活周期较短的JS对象,如临时变量、字符串。
    • 老生代:经过多次垃圾回收仍然存活,存活周期较长的对象,如主控制器、服务器对象等。

V8引擎的内存机制

V8引擎的内存使用限制:
在64位系统下能使用约1.4GB;在32位系统下能使用约0.7GB。

  • V8引擎对内存进行限制的原因是什么呢?
    1、JS的单线程执行限制
    2、JS垃圾回收机制的限制

在JS中,由于是单线程运行,也就是一次只能做一件事,那当它进入了垃圾回收阶段,其他的运行逻辑都得暂停了,得等它过了这个阶段才继续执行。但是,垃圾回收是一件非常耗时的事情,如果时长过久就会造成应用卡顿,所以V8引擎干脆限制了堆内存的大小,这样就算你到顶了也不会说太卡,而且大部分情况下也不会说有操作几个G的情况,因此这也是V8的一种权衡。而且这个大小限制也是可以手动修改的。

  • V8的内存分代
    V8将堆中的对象分为两类:
    1、新生代:年轻的新对象,未经历垃圾回收或经历过一次。
    在V8引擎的内存结构中,新生代主要用于存放存活时间较短的对象。新生代内存是有两个semispace(半空间)构成的,内存最大值在64位系统和32位系统上分别为32MB和16MB,在新生代的垃圾回收过程中主要采用了Scavenge算法。
    Scavenge算法是一种典型的牺牲空间换取时间的算法,对于老生代内存来说,可能会存储大量的对象,如果在老生代中使用这种算法,势必会造成内存资源的浪费,但是在新生代内存中,大部分对象的生命周期较短,在时间效率上表现客观,所以还是比较合适这种算法。

2、老生代:存活时间长的对象,经历过一次或更多次垃圾回收的对象。

新对象都会被分配到新生代中,当新生代空间不足以分配新对象时,将触发新生代的垃圾回收。

  • 新生代的垃圾回收
    新生代中的对象主要通过Scavenge算法进行垃圾回收,这是一种采用复制的方式实现内存回收的算法。
    Scavenge算法将新生代的总空间一分为二,只使用其中一个,另一个处于闲置,等待垃圾回收时使用。使用中的那块空间称为From,闲置的空间称为To。

    当新生代触发垃圾回收时,V8将From空间中所有应该存活下来的对象依次复制到To空间。
    有两种情况不会将对象复制到To空间,而是晋升到老生代:
    1、对象此前已经经历过一次新生代垃圾回收,这次依旧应该存活,则晋升至老生代。
    2、To空间已经使用了25%,则将此对象直接晋升至老生代。

From空间所有应该存活的对象都复制完成后,原本的From空间将被释放,成为闲置空间,原本To空间则成为使用中空间,两个空间进行角色翻转。

为何To空间使用超过25%时,就需要直接将对象复制到老年代呢?因为To空间完成垃圾回收后将会翻转为From空间,新的对象分配都在此处进行,如果没有足够的空闲空间,将会影响新对象的分配。

因为Scavenge只复制活着的对象,而新生代中大多数对象寿命都不长,长期存活对象少,则需要复制的对象相对来说很少,因此总体来说,新生代使用Scavenge算法的效率非常高。且由于Scavenge是依次连续复制,所以To空间永远不会存在内存碎片。
不过由于Scavenge会将空间对半划分,所以此算法的空间利用率较低。

  • 老生代的垃圾回收

在老生代中的对象,至少都已经经历过一次甚至更多次垃圾回收,相对于新生代中的对象,它们有更大大概率继续存活,只有相对少数的对象面临死亡,且由于老年代的堆内存是新生代的几十倍,其中存活着大量对象,因此如果使用Scavenge算法回收老生代,将会面临大量的存货对象需要复制的情况,将老生代空间对半划分也会浪费相当大的空间,效率低下。因此老生代垃圾回收主要采用标记-清除(Mark-Sweep)和标记-整理(Mark-Compact)。
这两种方式并非相互替代关系,而是配合关系,在不同情况下,选择不同方式,交替配合以提高回收效率。

新生代中死亡对象占多数,因此Scavenge算法只处理存活对象,提高效率。老生代中存活对象占多数,于是采用标记清除算法只处理死亡对象,提高效率。

当老生代的垃圾回收被触发时,V8会将需要存活的对象打上标记,然后将没有标记的对象,也就是需要死亡的对象,全部擦除,一次标记清除式回收就完成了。

灰色为存货对象,白色为清除后的闲置空间

一切看起来都完美了,可是随着程序的继续运行,却会出现一个问题:被清除的对象遍布各个内存地址,空间有大有小,其闲置空间不连续,产生了很多内存碎片。当需要将一个足够大的对象晋升至老生代时,无法找到一个足够大的连续空间安置这个对象。
为了解决这种空间碎片的问题,就出现了标记整理算法。它是在标记清除的基础上演变而来,当清理了死亡对象后,它会将所有的存货对象往一端移动,使其内存空间紧挨,另一端就成为了连续内存。

虽然标记整理算法可以避免空间碎片,但是却需要依次移动对象,效率比标记清除算法更低,因此大多数情况下V8会使用标记清理算法,当空间碎片不足以安放新晋升对象时,才会触发标记整理算法。

增量标记
早期V8在垃圾回收阶段,采用全停顿(stop the world),也就是垃圾回收时程序运行会被暂停。这在JavaScript还仅被用于浏览器端开发时,并没有什么明显的缺点,前端开发使用的内存少,大多数时候仅触发新生代垃圾回收,速度快,卡顿几乎感觉不到。但是对于Node程序,使用内存更多,在老年代垃圾回收时,全停顿很容易带来明显的程序迟滞,标记阶段很容易就会超过100ms,因此V8引入了增量标记,将标记阶段分为若干小步骤,每个步骤控制在5ms内,每运行一段时间标记动作,就让JavaScript程序执行一会儿,如此交替,明显地提高了程序流畅性,一定程度上避免了长时间卡顿。

参考:
https://blog.csdn.net/biao0309/article/details/107170980/
https://muyiy.cn/blog/1/1.4.html#内存回收
https://github.com/LinDaiDai/niubility-coding-js/blob/master/JavaScript/调用堆栈/JavaScript进阶-内存机制.md

标签:调用,对象,新生代,回收,V8,内存,堆栈,垃圾
来源: https://www.cnblogs.com/pureshee/p/14029932.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有