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

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

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


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


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


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


引用:内存的起始地址


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

相关文章
|
2月前
|
存储 监控 算法
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程 ?
尼恩提示: G1垃圾回收 原理非常重要, 是面试的重点, 大家一定要好好掌握
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程  ?
|
2月前
|
算法 Java 开发者
Java中的垃圾回收机制:从原理到实践
Java的垃圾回收机制(Garbage Collection, GC)是其语言设计中的一大亮点,它为开发者提供了自动内存管理的功能,大大减少了内存泄漏和指针错误等问题。本文将深入探讨Java GC的工作原理、不同垃圾收集器的种类及它们各自的优缺点,并结合实际案例展示如何调优Java应用的垃圾回收性能,旨在帮助读者更好地理解和有效利用Java的这一特性。
|
2月前
|
算法 JavaScript 前端开发
垃圾回收算法的原理
【10月更文挑战第13天】垃圾回收算法的原理
24 0
|
3月前
|
监控 算法 Java
深入理解Java中的垃圾回收机制在Java编程中,垃圾回收(Garbage Collection, GC)是一个核心概念,它自动管理内存,帮助开发者避免内存泄漏和溢出问题。本文将探讨Java中的垃圾回收机制,包括其基本原理、不同类型的垃圾收集器以及如何调优垃圾回收性能。通过深入浅出的方式,让读者对Java的垃圾回收有一个全面的认识。
本文详细介绍了Java中的垃圾回收机制,从基本原理到不同类型垃圾收集器的工作原理,再到实际调优策略。通过通俗易懂的语言和条理清晰的解释,帮助读者更好地理解和应用Java的垃圾回收技术,从而编写出更高效、稳定的Java应用程序。
|
3月前
|
监控 算法 Java
掌握Java的垃圾回收机制:从原理到实践
在Java的世界中,垃圾回收(Garbage Collection,简称GC)是一块神秘的领域,它如同一位默默无闻的清洁工,确保内存中不再使用的对象得到妥善处理。本文将带你走进垃圾回收的大门,探索它的工作原理、常见算法及其在实际应用中的调优策略。无论你是初学者还是有一定经验的开发者,这篇文章都将为你揭开垃圾回收的神秘面纱,让你的Java程序运行得更加高效和稳定。
68 5
|
5月前
|
缓存 监控 算法
Java面试题:描述Java垃圾回收的基本原理,以及如何通过代码优化来协助垃圾回收器的工作
Java面试题:描述Java垃圾回收的基本原理,以及如何通过代码优化来协助垃圾回收器的工作
90 8
|
5月前
|
算法 Java
Java面试题:解释垃圾回收中的标记-清除、复制、标记-压缩算法的工作原理
Java面试题:解释垃圾回收中的标记-清除、复制、标记-压缩算法的工作原理
66 1
|
5月前
|
设计模式 安全 Java
Java面试题:请解释Java中的四种访问控制符及其作用范围,请解释Java中的垃圾回收机制及其工作原理,请解释Java中的并发工具包及其主要用途
Java面试题:请解释Java中的四种访问控制符及其作用范围,请解释Java中的垃圾回收机制及其工作原理,请解释Java中的并发工具包及其主要用途
36 0
|
5月前
|
存储 算法 Java
Java面试题:解释JVM的内存结构,并描述堆、栈、方法区在内存结构中的角色和作用,Java中的多线程是如何实现的,Java垃圾回收机制的基本原理,并讨论常见的垃圾回收算法
Java面试题:解释JVM的内存结构,并描述堆、栈、方法区在内存结构中的角色和作用,Java中的多线程是如何实现的,Java垃圾回收机制的基本原理,并讨论常见的垃圾回收算法
73 0
|
5月前
|
存储 算法 Java
Java面试题:详细描述Java堆内存的垃圾回收过程,解释Java中的线程池(ThreadPool)的工作原理,解释Java中的FutureTask的工作原理
Java面试题:详细描述Java堆内存的垃圾回收过程,解释Java中的线程池(ThreadPool)的工作原理,解释Java中的FutureTask的工作原理
34 0