备战大厂,彻底搞懂垃圾回收机制底层原理(上)

简介: 备战大厂,彻底搞懂垃圾回收机制底层原理(上)

通过前面的学习我们知道,当一个网页运行时,浏览器会给网页分配一段连续的内存空间以供网页使用。


并且通过使用方式的不同,内存空间会被分为栈内存与堆内存。栈内存只用于管理函数的执行顺序,堆内存用于存储其他所有对象。


我们还知道,程序的运行过程中,会使用内存。而内存空间是有限的,因此,内存空间的重复利用就变得非常重要。垃圾回收的概念也因此而生。


在学习垃圾回收机制之前,我们明确几个概念。


引用:内存的起始地址


弱引用:WeakMap WeakSet


垃圾:无任何引用的对象


回收:清空被垃圾占用的内存


垃圾回收区域:堆内存


发生时间:程序空闲时间时


第一个问题:如何识别垃圾


ECMAScript 规范中并没有明确指出 JS 引擎必须使用哪种算法来识别垃圾,因此我们这里介绍几种常用的方式。


引用计数法


堆中的每个对象都有一个引用计数器。当一个对象被创造初始化赋值之后,该变量计数就设置为1

var a = new Object() // 计数变量 = 1

每当有一个地方引用它时,计数器的值就加1

var a = new Object() // 计数变量 = 1
var b = a   // 计数变量 + 1 = 2

当引用失效时,计数器的值就减1

var a = new Object() // 计数变量 = 1
var b = a   // 计数变量 + 1 = 2
var c = a   // 计数变量 + 1 = 3
a = null    // 引用失效,计数变量 -1 = 2
b = {}      // 引用失效,计数变量 -1 = 1

当该对象的计数值为0时,就表示失去了所有的引用,该对象就成为了垃圾。

var a = new Object() // 计数变量 = 1
var b = a   // 计数变量 + 1 = 2
var c = a   // 计数变量 + 1 = 3
a = null    // 引用失效,计数变量 -1 = 2
b = {}      // 引用失效,计数变量 -1 = 1
c = null    // 引用失效,计数变量 -1 = 0

知识体系关联:这样的管理方式,类似于数组的 length 字段。


优点:引用计数收集器执行简单,实现简单,判定效率高,无延迟,对程序不被长时间打断的实时环境比较有利。


缺点:赋值时需要更新计数器,增加了微量时间开销,影响不大。最严重的问题是引用计数器无法处理循环引用的问题。

var p = 
{ 
  n: 1, 
  next: {
   n: 2,
   next: p
  }
}
p = null

image.png

对象不可访问,计数也不为0,无法被回收,导致内存泄漏。


引用计数法虽然有这样致命的缺陷,但是由于其性能的优越性,依然有开发语言采用该算法,例如早期的 Java,以及现在的 Python。并通过手动解除、或者在循环引用的环节使用弱引用的方式。


根搜索算法 Tracing Collector


首先了解一个概念:GC Roots Set(根集),他是可访问的引用集合。Roots Set 中的引用变量可以用于访问对象的属性以及调用对象的方法。


这种算法的基本思路就是:先通过一系列 GC Roots 的对象作为起点,遍历寻找对应的引用节点。找到这些节点之后,继续向下递归寻找节点。


搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,就证明该对象是不可用的。


如果不考虑循环引用,Roots Set 会表现出一棵棵树状结构,如果考虑循环引用,则会呈现出图结构。

image.png

哪些对象可以作为根节点:


  1. 所有正在运行的栈上的引用变量
  2. 所有的全局对象全局变量
  3. 所有的内置对象


在内存中对整个堆进行遍历,先从 GC 根对象开始,然后找到根对象引用的其它对象,能访问到的所有对象都标记为存活。


关于标记阶段有几个关键点是值得注意的:


  1. 开始进行标记前,需要先暂停应用线程,否则如果对象图一直在变化的话是无法真正去遍历它的。这就是后面我们会提到的 stop-the-world
  2. 暂停时间的长短并不取决于堆内对象的多少也不是堆的大小,而是存活对象的多少。因此,调高堆的大小并不会影响到标记阶段的时间长短。
  3. 在 Blink 引擎的垃圾回收器 Oilpan 中,则某个对象在被回收之前,可能会执行一个回收之前需要做什么的生命周期函数 finalize。


如果该对象被判定为有必要执行 finalize() 方法,那么这个对象将会被放置在一个名为 finalization-queue 队列中,并在稍后由一条低优先级的 Finalizer 线程去执行这些任务。finalize 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 finalization-queue 中的对象进行第二次小规模的标记,如果要在 finalize() 中成功拯救自己,只要让该对象重新引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。

image.png

而在 V8 引擎的实现中,由于我们无法访问垃圾回收器,因此就没有提供这样的生命周期函数让 JavaScript 开发者有所作为。


  1. GC 判断对象是否可达看的是强引用,而非弱引用


V8 的垃圾回收器


V8 的垃圾回收器名为 Orinoco。上面我们也提到,垃圾回收器无论在进行标记或者回收行为时,我们都会暂停 JS 主线程的执行。因此早期的 Orinoco 采用了这种 stop-the-world 的方式。


任何垃圾收集器都有一些必须定期执行的基本任务:


  1. 识别活/死对象
  2. 回收/重用死对象占用的内存
  3. 压缩/碎片整理内存(可选)


这些任务可以按顺序执行,也可以任意交错执行。stop-the-world 的方式暂停 JavaScript 执行并在主线程上按顺序执行这些任务。当然这种方式的副作用就是会导致主线程出现卡顿和延迟,用户感知明显。

image.png

那么这种方式会做什么事呢?


首先,标记存活对象。


GC 通过根搜索算法验证活跃对象的可达性,在这个过程中,GC 可以收集任何无法访问的对象。


收集到了所有无法访问的对象之后,就会清空对应的内存空间。与此同时,会在一个 free-list 的列表记录这些清理出来的内存位置与大小,当有新的对象需要分配内存空间时,就会在 free-list 中查找。


如果不做任何特殊的处理,新的对象所需要的内存空间不可能完整的跟 free-list 的空闲内存大小一致,因此最后就会存在许多难以利用的内存缝隙。为了解决这个问题,我们还需要在回收过程中,对内存进行碎片整理。以确保我们总能够得到连续的空闲内存分配给新的对象。


在过去几年中,Orinoco 有了很大的转变。我们接着往下继续了解。


V8 中的堆内存区域划分


V8 主要将堆内存划分为两个区域,新生代 Young Generation 与 老生代 Old Generation。从概念上来说,新生代主要用于存储生命短暂的对象,例如执行上下文,老生代用于存储生命漫长的对象例如函数声明。


新生代又被进一步划分为两个区域,如下图,在后面的分析中,我们用 From、To 来称呼他们

image.png

在 GC 中有一个重要的术语:The Generational Hypothesis。也就是说,我们大胆的预测大多数对象都会在新生代中死亡,实际上也是这样,这是 Orinoco 具体实现的大前提。V8 的内存区域分布则利用了这一假设,只有少数对象能在新生代中存活下来,然后移动到老生代中。所以大多数对象都是隐式垃圾,用完即走。


所以,GC 复制算法得以在 V8 中被使用,因为被复制的对象一定是少数。后面我们分析复制算法。


Major GC (Full Mark-Compact)


在 Orinoco 中,存在两个不同的 GC。Major GC:用于回收老生代的垃圾, 与 Minor GC:用于回收新生代的垃圾。


Major GC 管理整个堆内存,主要是对老生代区域的内存进行回收。Major GC 采用了 Mark-compact 算法「标记-整理」来管理内存。


他是为了解决 Mark-Sweep 算法所带来的内存缝隙而提出来的优化方案。标记方式依然通过根搜索算法进行标记,compact 整理算法我们用图例来讲解一下。


在这之前,我们要明确 compact 要做的两件事情


  1. 把存活的对象移动到该去的位置
  2. 修改引用,让他们指向新的地址

image.png

通过这样的方式之后,我们就得到一个整理之后的新布局。不过这样的方式也存在一些问题,因为要对堆内存遍历很多遍,因此内存越大,性能消耗就越大。不过得益于老生代中的内存对象比较少,并且变动比较小,因此 V8 依然选中该方法来管理老生代对象。

image.png

相关文章
|
11月前
|
Prometheus 监控 算法
CMS圣经:CMS垃圾回收器的原理、调优,多标+漏标+浮动垃圾 分析与 研究
本文介绍了CMS(Concurrent Mark-Sweep)垃圾回收器的工作原理、优缺点及常见问题,并通过具体案例分析了其优化策略。重点探讨了CMS的各个阶段,包括标记、并发清理和重标记
CMS圣经:CMS垃圾回收器的原理、调优,多标+漏标+浮动垃圾 分析与 研究
|
11月前
|
存储 算法 Java
G1原理—5.G1垃圾回收过程之Mixed GC
本文介绍了G1的Mixed GC垃圾回收过程,包括并发标记算法详解、三色标记法如何解决错标漏标问题、SATB如何解决错标漏标问题、Mixed GC的过程、选择CollectSet的算法
G1原理—5.G1垃圾回收过程之Mixed GC
|
9月前
|
缓存 算法 Java
JVM深入原理(八)(一):垃圾回收
弱引用-作用:JVM中使用WeakReference对象来实现软引用,一般在ThreadLocal中,当进行垃圾回收时,被弱引用对象引用的对象就直接被回收.软引用-作用:JVM中使用SoftReference对象来实现软引用,一般在缓存中使用,当程序内存不足时,被引用的对象就会被回收.强引用-作用:可达性算法描述的根对象引用普通对象的引用,指的就是强引用,只要有这层关系存在,被引用的对象就会不被垃圾回收。引用计数法-缺点:如果两个对象循环引用,而又没有其他的对象来引用它们,这样就造成垃圾堆积。
236 0
|
9月前
|
算法 Java 对象存储
JVM深入原理(八)(二):垃圾回收
Java垃圾回收过程会通过单独的GC线程来完成,但是不管使用哪一种GC算法,都会有部分阶段需要停止所有的用户线程。这个过程被称之为StopTheWorld简称STW,如果STW时间过长则会影响用户的使用。一般来说,堆内存越大,最大STW就越长,想减少最大STW,就会减少吞吐量,不同的GC算法适用于不同的场景。分代回收算法将整个堆中的区域划分为新生代和老年代。--超过新生代大小的大对象会直接晋升到老年代。
232 0
|
11月前
|
存储 监控 架构师
ZGC圣经:ZGC垃圾回收器的原理、调优,ZGC 漏标的 分析与 研究
ZGC圣经:ZGC垃圾回收器的原理、调优,ZGC 漏标的 分析与 研究
|
11月前
|
存储 缓存 算法
G1原理—3.G1是如何提升垃圾回收效率
本文深入探讨了G1垃圾回收器提升GC效率的核心机制,包括记忆集(RSet)、位图(BitMap)和卡表(CardTable)的设计与作用。记忆集通过记录跨代引用避免了不必要的老年代遍历,位图用于高效描述内存使用状态以优化标记过程,而卡表则在节约记忆集内存的同时提供更详细的引用信息。此外,文章还解析了DCQ(Dirty Card Queue)和DCQS(Dirty Card Queue Set)机制如何异步更新RSet,确保在高并发场景下的性能与准确性。这些设计共同提升了G1在标记、清理及整理内存时的效率。
591 10
|
11月前
|
存储 算法 Java
G1原理—6.G1垃圾回收过程之Full GC
本文详细探讨了G1垃圾回收器对Full GC(FGC)的优化处理,涵盖FGC的前置处理、整体流程及并行化改进。重点分析了传统FGC串行化的局限性以及G1通过Region分区和RSet机制实现并行标记的优势,包括任务窃取提升效率、跨分区压缩以生成空闲Region等技术细节。此外,文章还介绍了G1的新特性——字符串去重优化,通过判断char数组一致性减少重复字符串占用内存,从而提升内存使用效率。总结部分全面回顾了G1在FGC中的各项优化措施及其带来的性能改善。
G1原理—6.G1垃圾回收过程之Full GC
|
11月前
|
存储 算法 Java
G1原理—4.G1垃圾回收的过程之Young GC
本文详细解析了G1垃圾回收器中YGC(Young Generation Collection)的完整流程,包括并行与串行处理阶段。内容涵盖YGC相关参数设置、YGC与Mixed GC及FGC的关系、新生代垃圾回收的具体步骤(如标记存活对象、复制到Survivor区、动态调整Region数量等),以及并行阶段的多线程操作和串行阶段的关键任务(如处理软引用、整理卡表、重构RSet)。
G1原理—4.G1垃圾回收的过程之Young GC
|
存储 监控 算法
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程 ?
尼恩提示: G1垃圾回收 原理非常重要, 是面试的重点, 大家一定要好好掌握
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程  ?
|
算法 JavaScript 前端开发
垃圾回收算法的原理
【10月更文挑战第13天】垃圾回收算法的原理
254 0