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

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

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


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


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


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


引用:内存的起始地址


弱引用: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

相关文章
|
4月前
|
消息中间件 算法 Java
jvm性能调优 - 14JVM的老年代垃圾回收器CMS原理
jvm性能调优 - 14JVM的老年代垃圾回收器CMS原理
49 0
|
2月前
|
存储 算法 Java
精华推荐 | 【JVM深层系列】「GC底层调优专题」一文带你彻底加强夯实底层原理之GC垃圾回收技术的分析指南(GC原理透析)
精华推荐 | 【JVM深层系列】「GC底层调优专题」一文带你彻底加强夯实底层原理之GC垃圾回收技术的分析指南(GC原理透析)
55 0
|
2月前
|
开发框架 算法 Java
C垃圾回收:原理与代码实践揭秘
C垃圾回收:原理与代码实践揭秘
9 0
|
2月前
|
存储 算法 Java
Go语言GC(垃圾回收)的工作原理
【2月更文挑战第23天】
32 0
|
3月前
|
算法 Java Go
Go语言中的垃圾回收机制:原理、优化与影响
【2月更文挑战第5天】本文深入探讨了Go语言中的垃圾回收机制,包括其工作原理、性能优化方法以及对程序运行的影响。通过了解这些内容,读者可以更好地理解Go语言的内存管理特点,并在实际开发中更好地应对垃圾回收带来的挑战。
|
4月前
|
Web App开发 算法 前端开发
备战大厂,彻底搞懂垃圾回收机制底层原理(下)
备战大厂,彻底搞懂垃圾回收机制底层原理(下)
|
4月前
|
Java
jvm性能调优 - 18白话G1垃圾回收器的工作原理
jvm性能调优 - 18白话G1垃圾回收器的工作原理
45 0
|
4月前
|
算法 Java 应用服务中间件
jvm性能调优 - 13JVM的年轻代垃圾回收器ParNew原理
jvm性能调优 - 13JVM的年轻代垃圾回收器ParNew原理
51 1
|
5月前
|
存储 JavaScript 算法
JS——垃圾回收的原理
JS——垃圾回收的原理
|
2月前
|
Java 程序员
探讨JVM垃圾回收机制与内存泄漏
探讨JVM垃圾回收机制与内存泄漏