前言大家好,我是林三鑫。前两天无意中在B站看到一个关于V8垃圾回收机制的视频,饶有兴致地看了看,觉得有点难懂,于是就在想,大家对V8垃圾回收有没有同样的理解机制和我一样吗?这方面的知识比较孤陋寡闻,或者看过这方面的知识,但是看不懂。于是,我想了三天,想着怎么用最通俗的词来讲解最难的知识点。共同理解相信大部分同学在面试中经常被问到:“说说V8的垃圾回收机制”。这时候大部分同学肯定会回答:“垃圾回收机制有两种方式,一种是引用方式,一种是记号方式”引用方式就是判断一个对象被引用的次数,如果次数ofreferences为0,会被回收,如果references的个数大于0,则不会被回收。请看下面的代码下面的代码执行完后,按理说obj1和obj2会被回收,但是因为它们互相引用,每个引用数都是1,所以不会被回收,导致内存泄漏函数fn(){constobj1={}constobj2={}obj1.a=obj2obj2。a=obj1}fn()表示法表示法是标记可达对象,不可达对象作为垃圾回收处理。那么问题来了,是否可达,判断的依据是什么?(这里的reachability不是reachabilityduck。)言归正传,要判断是否可达,就不得不说reachability了。什么是可达性?它从初始根对象(窗口或全局)的指针开始,向下搜索子节点。如果找到子节点,则说明该子节点的引用对象是可达的,并进行标记,然后递归查找,直到遍历完所有的子节点。那么如果这个节点没有被遍历过,就不会被标记,就被认为没有被任何地方引用过,就可以证明这是一个需要释放的对象,可以被垃圾回收器回收。//可达varname='林三鑫'varobj={arr:[1,2,3]}console.log(window.name)//林三鑫console.log(window.obj)//{arr:[1,2,3]}console.log(window.obj.arr)//[1,2,3]console.log(window.obj.arr[1])//2functionfn(){varage=22}//unreachableconsole.log(window.age)//对undefined的普通理解是不够的,因为垃圾回收机制(GC)其实不止这两个算法,想对V8垃圾有更深入的理解收集机制,请继续阅读!!!JavaScript内存管理其实JavaScript内存的过程很简单,分为3步:1.分配用户需要的内存2.用户获取内存并使用3.用户不需要内存,释放它和回到系统那么这些用户是谁?比如:varnum=''varstr='林三鑫'varobj={name:'林三鑫'}obj={name:'林胖子'}上面的num,str,obj就是用户,我们都是知道,JavaScript数据类型分为基本数据类型和引用数据类型:基本数据类型:有固定大小,值存储在栈内存中,可以直接通过值引用数据类型:大小不固定(可以加属性),栈内存中有指针,指向堆内存中的对象空间,通过引用访问。由于栈内存中存放的基本数据类型的大小是固定的,所以栈内存中的内存是由操作系统自动分配和释放的。堆内存大小不固定,系统无法自动释放和回收,需要JS引擎手动释放内存。0.7G/732MB),为什么要限制呢?表面原因:V8本来就是为浏览器设计的,不太可能遇到大内存的场景。更深层次的原因:V8垃圾回收机制的局限性(清理大量内存垃圾需要很长时间,导致JavaScript线程暂停执行时间,然后性能和应用直线下降)如前所述,栈中的内存,操作系统会自动分配和释放内存,堆中的内存由JS引擎决定(如Chrome的V8)要手动释放,当我们的代码写得不正确时,JS引擎的垃圾回收机制将无法正确释放内存(memoryleak),从而增加浏览器占用的内存,然后V8的垃圾回收算法1.分代回收在JavaScript中,一个对象的生命周期分为两种情况。生命周期很短:一次垃圾回收后,释放并回收。生命周期非常短。Long:经过多次垃圾回收,还是存在的。你不走,问??题就来了。生命周期短的,回收还好,生命周期长的,就回收不了多少次了。如果丢不掉,还继续做无用功,那岂不是很耗性能?针对这个问题,V8实现了分代回收的优化方法。通俗地说:V8把堆分成两个空间,一个叫新生代,一个叫老年代。新生代是存放长生命周期对象的地方。新生代通常只有1-8M的容量,而老年代的容量要大得多。针对这两个方面,V8使用了不同的垃圾收集器和不同的回收算法来更高效地实现垃圾收集。二级垃圾收集器+Scavenge算法:主要负责新生代的垃圾收集。Maingarbagecollector+Mark-Sweep&&Mark-Compactalgorithm:主要负责老年代的垃圾回收1.1新生代在JavaScript中,任何对象声明分配的内存都会先放在新生代中,因为大多数对象在内存中存活的周期时间很短,因此需要一个非常高效的算法。在新生代中,Scavenge算法主要用于垃圾回收。Scavenge算法是典型的牺牲空间换取时间的复制算法,非常适合占用空间小的场景。Scavange算法将新生代堆分为两部分,称为from-space和to-space。将它们有序排列,然后释放from-space中非活动对象的内存。完成后,交换from空间和to空间,这样新生代中的这两个区域就可以复用了。具体步骤为以下4步:1.标记活动对象和非活动对象2.将from-space中的活动对象复制到to-space并排序3.清除from-space中的非活动对象4.更改from-space与to-space互换角色,进行下一步的Scavenge算法垃圾回收那么,垃圾回收器如何知道哪些对象是活动对象,哪些是非活动对象呢?这就不得不提一件事——可达性。什么是可访问性?它从初始根对象(窗口或全局)的指针开始,向下搜索子节点。如果找到子节点,则说明该子节点的引用对象是可达的,并进行标记,然后递归查找,直到遍历完所有的子节点。那么如果这个节点没有被遍历过,就不会被标记,就被认为没有被任何地方引用过,就可以证明这是一个需要释放的对象,可以被垃圾回收器回收。什么时候新生代的对象变成老年代的对象?在新一代中,进行了进一步的细分。分为幼儿期儿童和中级儿童。当一个对象第一次分配内存时,它会被分配给新生代中的nurserychildren。如果下一次垃圾回收后该对象仍然存在于新生代中,此时我们将这个对象移动到中间child。下一次垃圾回收后,如果对象还在新生代,二级垃圾回收器会将对象移动到老年代。这个移动过程称为提升1.2oldgeneration新生代空间中的对象,经过多次争斗,已经成功将旧对象提升到老年代空间。由于这些对象被多次回收但没有被回收,是一群生命力顽强、存活率高的Object,所以在老年代,回收算法不应该使用Scavenge算法。原因如下:Scavenge算法是一种复制算法。重复复制这些存活率很高的对象是没有意义的,效率极低。Scavenge算法是基于空间换时间的算法,老年代内存空间大。如果使用Scavenge算法,空间资源非常浪费,得不偿失。.因此老年代采用Mark-Sweep算法(标记清理)和Mark-Compact算法(标记整理)。和清理,但是Mark-Sweep算法和Scavenge算法的区别在于后者需要复制然后清理,而前者不需要。Mark-Sweep直接标记活动对象和非活动对象后,直接进行清理。标记阶段:第一次扫描老年代对象,标记清理活跃对象阶段:对老年代对象进行二次扫描,清除未标记对象,即非活跃对象。从上图中,我想大家也发现了,嗯,有一个问题:清除不活跃的对象后,留下了很多零散的空槽。Mark-Compact(标记整理)Mark-Sweep算法进行垃圾回收后,留下了很多零散的空间。有什么害处?如果这时候进来一个大对象,就需要为这个对象分配一块大内存。首先,从零散的空位中找地方。找了一圈,发现没有适合自己体型的空位,只好拼到最后。这个找空位的过程很消耗性能,这也是Mark-Sweep算法的一个缺点。这时,Mark-Compact算法出现了。它是Mark-Sweep算法的增强版本。在Mark-Sweep算法的基础上,增加了排序阶段,每次清理不活跃的对象,将剩余的活跃对象排序到内存的一侧。排序完成后,会直接回收边界上的内存。2.Stop-The-World完结V8的分代回收,说一个问题。JS引擎是用来跑JS代码的,JS引擎也是用来做垃圾回收的,那如果两者同时进行,发生冲突了怎么办?答案是垃圾回收优先于代码执行,它会先停止代码执行,垃圾回收完成后再执行JS代码。这个过程称为fullpause,因为新生代空间小,存活对象少,结合Scavenge算法,暂停时间短。但是老一辈就不一样了。在某些情况下,当活动对象较多时,停顿时间会较长,导致页面卡顿。3、Orinoco优化Orinoco是V8垃圾收集器的项目代号。为了提升用户体验,解决全停顿问题,提出了增量标记、惰性清洗、并发、并行优化方法。3.1增量标记(Incrementalmarking)我们一直在强调先标记后清除,增量标记是在标记这个阶段进行优化的。举个形象的例子:马路上垃圾很多,路人走不动路,需要清洁工清理干净后才能行走。前几天,路上的垃圾比较少,路人都是等清洁工清理干净后才过去,但接下来的几天,垃圾越来越多。说:“你打扫一段,我走一段,效率更高。”在上面的例子中,你可以理解清洁工清理垃圾标记进程的进程与路人JS代码之间的一一对应关系。有少量垃圾时不会做增量标记优化,但是当垃圾达到一定数量时,会开启增量标记:标记一个点,JS代码会运行一段时间,从而提高效率。3.2惰性清扫(Lazysweeping)如前所述,incrementalVolumemarking只针对marking阶段,而lazycleaning针对的是clearing阶段。增量标记后,在清理非活动对象时,垃圾回收器发现即使不清理,剩余的空间也足够JS代码运行,所以延迟清理,让JS代码先执行,或者只清除一些垃圾,而不是全部。这种优化称为惰性清理标记和惰性清理的出现,大大改善了全停顿现象。但是问题来了:增量标记是标记一个点,JS运行一段时间。如果只是在前脚将一个对象标记为活动对象,后脚的JS代码会将此对象设置为非活动对象,或者反过来,前脚不标记对象。该对象为活动对象,后脚JS代码将此对象设置为活动对象。总结一下:穿插标记和代码执行可能会导致对象引用的变化和标记错误。这就需要使用写屏障技术来记录这些引用关系的变化。3.3并发(Concurrent)并发GC允许主线程在垃圾回收期间挂起。让垃圾收集器做一些特殊的操作。但是这种方式也面临着增量回收的问题,即在垃圾回收过程中,由于JavaScript代码的执行,堆中对象的引用关系可能随时发生变化,因此也需要写屏障操作必需的。3.4ParallelParallelGC让主线程和辅助线程同时进行相同的GC工作,让辅助线程分担主线程的GC工作,这样垃圾回收所花费的时间就等于总时间除以参与线程的数量(加上一些同步开销)。V8目前的垃圾回收机制2011年,V8应用了增量标记机制。直到2018年,Chrome64和Node.jsV10开启了并发标记(Concurrent),并在并发的基础上加入了并行(Parallel)技术,大大缩短了垃圾回收时间。二级垃圾收集器V8在新生代垃圾收集中使用了并行机制。在整理整理阶段,即从from-to到space-to复制活动对象时,启用多个辅助线程并行进行。整齐的。由于多个线程争夺新生代堆的内存资源,可能会出现一个活动对象被多个线程复制的问题。为了解决这个问题,V8在第一个线程中复制活动对象,复制完成。之后,需要维护复制活动对象后的指针转发地址,以便其他辅助线程找到活动对象,判断活动对象是否被复制。如果在老年代垃圾收集期间堆中的内存大小超过某个阈值,主垃圾收集器V8将启用并发标记任务。每个辅助线程都会跟踪每个标记对象的指针和这个对象的引用。执行JavaScript代码时,后台辅助进程也会进行并发标记。当堆中的一个对象指针被JavaScript访问到代码修改时,写屏障(writebarriers)技术会跟踪工作线程何时在做并发标记。当并发标记完成或动态分配的内存达到限制时,主线程将执行最后的快速标记步骤。这时主线程会挂掉,主线程会再次扫描根集,确保所有对象都完成标记,由于辅助线程已经标记了活动对象,所以主线程的扫描只是检查操作。确认完成后,一些辅助线程会进行内存清理操作,一些辅助进程会进行内存清理操作,因为都是并发的,不会影响JavaScript代码在主线程上的执行。结语看完这篇文章,下次面试官问你的时候,你不用傻傻的说:“quotationandnotation”。相反,你可以更全面、更细致地征服面试官。后面会有一篇关于项目内存泄露的文章,敬请期待!!!我是林三鑫,一个狂热的前端菜鸟程序员。如果你有上进心,喜欢前端,想学前端,那我们可以交个朋友,一起钓鱼哈哈,摸摸鱼群,加我,请注意[思想]
