当前位置: 首页 > 科技观察

深入理解谷歌最强V8垃圾回收机制

时间:2023-03-21 00:29:04 科技观察

V8引擎是前端开发者想要升值加薪必须跨过的一道坎,因为说到性能,很多人觉得没必要,但现在性能是程序中最重要的事情。作为当下使用最广泛的JavaScript引擎,V8拥有庞大的生态系统,这与其革命性的设计密不可分。阅读本文后,您可以了解:JavaScript内存是如何管理的?Chrome垃圾是如何收集的?Chrome对垃圾回收做了哪些优化?JavaScript的内存管理无论是哪种编程语言,内存生命周期基本相同:分配你需要的内存,使用分配的内存(读、写),不需要时释放,不需要时归还不需要它。在JavaScript中,当我们创建一个变量(对象、字符串等)时,系统会自动为该对象分配相应的内存。varn=123;//为数值变量分配内存vars="azerty";//为字符串分配内存varo={a:1,b:null};//为对象及其包含的值分配内存//为数组及其包含的值分配内存(就像对象一样)vara=[1,null,"abra"];functionf(a){returna+2;}//为函数分配内存(可调用对象)//函数表达式也可以分配一个对象someElement.addEventListener('click',function(){someElement.style.backgroundColor='blue';},false);当系统发现这些变量不再被使用时,会自动释放(垃圾回收)这些变量的内存,开发者无需过多关心内存问题。尽管如此,我们在开发过程中也需要了解JavaScript的内存管理机制,避免出现一些不必要的问题,比如下面的代码:{}=={}//false[]==[]//false''==''//true在JavaScript中,数据类型分为两类,简单类型和引用类型。对于简单类型,内存存储在栈(stack)空间中,而对于复杂数据类型,内存存储在堆(heap)空间中。基本类型:这些类型在内存中占据固定大小的空间,它们的值存储在栈空间中。我们按值访问的引用类型:引用类型,值大小不固定,存放在栈内存的地址指向堆内存Object。通过引用访问。至于栈的内存空间,只保存简单数据类型的内存,由操作系统自动分配和释放。至于堆空间中的内存,由于大小不固定,系统无法自动释放。这时候需要JS引擎手动释放内存。为什么需要垃圾回收在Chrome中,v8对内存使用有限制(64位约为1.4G/1464MB,32位约为0.7G/732MB),为什么要限制?表面上的原因是,V8本来就是为浏览器设计的,不太可能遇到大内存的场景。深层次的原因是V8的垃圾回收机制是有限的(如果清理大量内存垃圾耗时,会导致JavaScript线程暂停执行时间,那么性能和应用会急剧下降)上面提到了栈中的内存,操作系统会自动分配和释放内存,而堆中的内存是由JS引擎手动释放的(比如Chrome的V8),当我们的代码写得正确时,JS引擎的垃圾回收机制将无法正确释放内存(memoryleak),从而导致浏览器占用的内存不断增加,从而导致JavaScript、应用程序、操作系统性能下降。Chrome垃圾收集算法在JavaScript中,绝大多数对象的生命周期都很短。大部分内存会在一次垃圾回收后释放,而少数对象的生命周期会很长。是活动对象,不需要回收。为了提高回收效率,V8将堆分为新生代和老年代两种。新生代存放存活时间短的对象,老年代存放存活时间长的对象。新生区通常只支持1-8M的容量,而老区支持的容量要大得多。针对这两个方面,V8使用了两种不同的垃圾收集器来更高效地实现垃圾收集。DeputyGarbageCollector-Scavenge:主要负责新生代的垃圾收集。MainGarbageCollector-Mark-Sweep&Mark-Compact:主要负责老年代的垃圾回收。新生代垃圾收集器——Scavenge在JavaScript中,任何对象声明分配的内存都将首先放在新生代中,由于大多数对象在内存中的生存时间很短,因此需要一种非常高效的算法。在新生代中,Scavenge算法主要用于垃圾回收。Scavenge算法是典型的牺牲空间换取时间的复制算法,非常适合占用空间小的场景。Scavange算法将新生代堆分为两部分,称为from-space和to-space。将它们有序排列,然后释放from-space中非活动对象的内存。完成后,交换from空间和to空间,这样新生代中的这两个区域就可以复用了。简单的描述就是:标记活动对象和非活动对象,将活动对象从空间复制到空间,并对其进行排序释放非活动对象在from空间中的内存交换from空间和to空间的角色然后,垃圾收集器如何知道哪些对象是活动的和不活跃?有一个概念叫对象可达性,意思是从初始根对象(window、global)的指针开始,这个根指针对象称为根集(rootset),从中向下查找子节点,搜索到的子节点表示该节点的引用对象是可达的,并为其留下标记,然后递归搜索过程,直到遍历完所有子节点,则未标记的对象节点表示该对象没有任何地方可以被引用证明这是一个需要被释放并且可以被垃圾收集器回收的对象。新生代中的对象什么时候成为老年代中的对象?在新生代中,进一步细分为两个区域:nurserychildren和intermediatechildren。当一个对象第一次分配内存时,它会被分配给新生代中的nurserychildren。垃圾回收的对象在新生代中依然存在。这时候我们移动到中间child,然后在下一次垃圾回收之后,如果这个对象还在新生代,secondarygarbagecollector就会把这个对象移动到oldgeneration。移动的过程称为提升。OldGenerationGarbageCollection-Mark-Sweep&Mark-Compact新生代空间中的对象满足一定条件,被提升到老年代空间。老年代空间中的对象至少经历过一次或多次回收,所以如果此时使用scavenge算法,会有两个问题:scavenge是一种复制算法,对活动对象进行重复复制会导致效率低下.Scavenge是一种牺牲空间换取时间效率的算法,oldgeneration支持的容量过大会造成空间资源的浪费。因此在老年代空间使用了Mark-Sweep(标记去除)和Mark-Compact(标记压缩)算法。Mark-SweepMark-Sweep处理分为两个阶段,标记阶段和清理阶段,看起来和Scavenge类似,不同的是Scavenge算法复制活动对象,而由于活动对象在老年代占多数,Mark-Sweep标记活动对象和非活动对象后,直接清除非活动对象。标记阶段:第一次扫描老年代,标记活跃对象清理阶段:对老年代进行二次扫描,清除未标记对象,即清理不活跃对象,看似完美,但还是有问题left,清除的对象遍布内存地址,导致内存碎片很多。Mark-CompactMark-Sweep完成后,在老年代的内存中会产生大量的内存碎片。如果不清理这些内存碎片,如果需要分配一个大对象,根本无法分配所有碎片空间。垃圾回收会提前触发,这个回收其实不是必须的。为了解决内存碎片问题,提出了Mark-Compact,它是在Mark-Sweep的基础上演变而来的。与Mark-Sweep相比,Mark-Compact增加了一个活动对象排序阶段,将所有活动对象向一端移动,移动完成后,直接清理边界外的内存。全暂停Stop-The-World由于垃圾回收是在JS引擎中进行的,而Mark-Compact算法在执行过程中需要移动对象,当活动对象较多时,其执行速度不可能很快。为了避免JavaScript应用逻辑和垃圾收集器的内存资源竞争造成的不一致,垃圾收集器会暂停JavaScript应用。这个过程称为停止世界。在新生代中,由于空间小,存活对象少,Scavenge算法执行效率更快,全停顿的影响并不大。老一代就不一样了。如果老年代活跃对象较多,垃圾回收器会长时间挂起主线程,导致页面卡住。OptimizationOrinocoorinoco是V8垃圾收集器的项目代号。为提升用户体验,解决全卡顿问题,采用增量标记、惰性清洗、并发、并行减少主线程挂起时间。Incrementalmarking-增量标记为了减少全堆垃圾回收的暂停时间,增量标记将整个堆对象的原始标记一个一个地拆分成任务,让它们在JavaScript应用逻辑之间执行,这使得堆的标记5~10ms的停顿。当堆的大小达到某个阈值时,启用增量标记。启用后,每当分配一定数量的内存时,脚本的执行就会暂停并进行增量标记。Lazycleaning-Lazysweep增量标记只是标记活动对象和非活动对象,而lazycleaning用于实际清理和释放内存。增量标记完成后,如果当前可用内存足够我们快速执行代码,其实我们并不需要立即清理内存。我们可以延迟清理过程,让JavaScript逻辑代码先执行,没必要一下子清理完所有非活动对象内存耗尽后,垃圾回收器会根据需要一个一个清理,直到所有页面都清理完向上。增量标记和惰性清理的出现减少了主线程最大停顿时间80%,使得用户和浏览器的交互更加顺畅。JavaScript代码写完后,堆中的对象指针可能会发生变化,需要使用writebarrier技术记录这些引用关系的变化,所以增量标记的缺点也暴露出来:没有减少总停顿时间主线程,甚至由于写屏障(Write-barrier)机制的成本略有增加,增量标记可能会降低应用程序的吞吐量只是偶尔需要短时间停止让垃圾收集器做一些特殊的操作。但是这种方式也面临着增量回收的问题,即在垃圾回收过程中,由于JavaScript代码的执行,堆中对象的引用关系可能随时发生变化,因此也需要写屏障操作必需的。Parallel-Parallel并行GC允许主线程和辅助线程同时执行相同的GC工作,这样辅助线程就可以分担主线程的GC工作,这样垃圾回收所花费的时间就等于总时间除以参与线程的数量(加上一些同步开销)。V8目前的垃圾回收机制2011年,V8应用了增量标记机制。直到2018年,Chrome64和Node.jsV10开启了并发标记(Concurrent),并在并发的基础上加入了并行(Parallel)技术,大大缩短了垃圾回收时间。二级垃圾收集器V8在新生代垃圾收集中使用了并行机制。在整理整理阶段,即从from-to到space-to复制活动对象时,启用多个辅助线程并行进行。整齐的。由于多个线程争夺新生代堆的内存资源,可能会出现一个活动对象被多个线程复制的问题。为了解决这个问题,V8在第一个线程中复制活动对象,复制完成。之后,需要维护复制活动对象后的指针转发地址,以便其他辅助线程找到活动对象,判断活动对象是否被复制。如果在老年代垃圾收集期间堆中的内存大小超过某个阈值,主垃圾收集器V8将启用并发标记任务。每个辅助线程都会跟踪每个标记对象的指针和这个对象的引用。执行JavaScript代码时,后台辅助进程也会进行并发标记。当堆中的一个对象指针被JavaScript访问到代码修改时,写屏障(writebarriers)技术会跟踪工作线程何时在做并发标记。当并发标记完成或动态分配的内存达到限制时,主线程将执行最后的快速标记步骤。这时主线程会挂掉,主线程会再次扫描根集,确保所有对象都完成标记,由于辅助线程已经标记了活动对象,所以主线程的扫描只是检查操作。确认完成后,一些辅助线程会进行内存清理操作,一些辅助进程会进行内存清理操作,因为都是并发的,不会影响JavaScript代码在主线程上的执行。其实大部分JavaScript开发者不需要考虑垃圾回收,但是了解垃圾回收的一些内部原理可以帮助你了解内存的使用情况,并根据内存使用情况观察是否存在内存泄漏。防止内存泄漏是提高应用程序性能的重要衡量标准。