首页 > 其他分享 >从 GC 到 WeakMap、WeakSet

从 GC 到 WeakMap、WeakSet

时间:2024-10-30 09:46:50浏览次数:3  
标签:arr 对象 WeakMap heapUsed GC 内存 WeakSet 引用

一、内存泄漏

1.1 简介

内存泄漏: 指计算机科学中的一种资源泄漏, 主要是因为计算机程序 内存 管理疏忽或错误造成程序 未能释放 已经 不再使用 的内存, 因而失去对一段 已分配内存 空间的控制, 程序将继续占用 已不再使用 的内存空间, 或是存储器所存储的对象, 无法通过执行代码被访问到, 令内存资源空耗, 简单讲就是: 已不再使用的内存没有得到释放

内存泄漏会因为减少 可用内存 的数量从而降低计算机的性能, 在最糟糕的情况下, 过多的 不可用内存被分配掉可能会导致设备停止正常工作、应用程序崩溃等严重后果

1.2 内存生命周期

不管什么程序语言, 内存生命周期基本是一致的:

  1. 向系统申请所需要的内存
  2. 使用分配到的内存进行读、写操作
  3. 不需要时将内存进行释放

对于上述提到的内存生命周期, 所有语言中第二点都是明确的, 但是对于第一、第三点就不一定了:

  1. C 语言这样的底层语言中因为是手动管理内存的, 所以对于第一、第三点是明确的, 它们有一套底层的内存管理接口, 比如 malloc()free(), malloc() 方法用来申请内存而 free() 方法释放内存
char * buffer;
buffer = (char*) malloc(42);
free(buffer);
  1. 但是呢, 手动管理内存这事本身是很麻烦, 所以大多数语言都提供了 自动内存管理 功能, 从而减轻程序员的负担, JS 也是如此, 它在创建变量 (对象、字符串、函数…) 时是自动进行了内存的分配, 并且在不使用它们时 "自动" 释放, 所以对于 JS 来说第一、第三并不是明确的, 是不可控的

1.3 JS 中内存分配

  1. 值的初始化: 对于开发者来说, JS 的内存管理是自动的、无形的, 为了不让程序员费心分配内存, JS 在定义变量时就自动完成了内存申请、分配
const n = 123;  // 给数值变量分配内存
const s = "azerty"; // 给字符串分配内存

const o = { a: 1, b: null }; // 给对象及其包含的值分配内存

const a = [1, null, "abra"]; // 给数组及其包含的值分配内存(就像对象一样)

function f(a) { return a + 2; } // 给函数(可调用的对象)分配内存

process.on("exit", (code) => {}); // 函数表达式也能分配一个对象

const d = new Date(); // 分配一个 Date 对象

const e = document.createElement('div'); // 分配一个 DOM 元素
  1. 使用值: 使用值的过程实际上是对所 分配内存 进行 读取写入 的操作; 读取写入 可能是写入一个变量或者一个对象的属性值, 甚至传递函数的参数

  2. 内存释放:

  • 大多数内存管理的问题都在这个阶段, 在这里最艰难的任务是找到 哪些被分配的内存确实已经不再需要了。在底层语言中它往往要求开发人员来确定, 在程序中哪一块内存不再需要并且需要手动释放它;
  • 高级语言解释器嵌入了 垃圾回收 简称 GC, 它的主要工作是跟踪内存的分配和使用, 以便当分配的内存不再使用时 自动释放

二、垃圾回收机制(GC)

JS 中通过 垃圾回收 机制来 检测释放 不再使用的内存, 来避免内存泄漏和资源浪费, 同时 垃圾回收时机 通常由 垃圾回收器 自动决定, 而不是由开发人员手动控制, 而 垃圾回收 又简称为 GCGarbage Collection

现代 JS 引擎通常会使用复杂的算法和策略来优化 GC 的性能和效率, 尽量减少对应用程序性能的影响; 因此, 在编写 JS 代码时, 通常不需要过多关注 GC 的时机, 而应该专注于编写高效的代码和合理地管理对象的生命周期, 这里 GC 主要有两种策略: 引用计数标记清除

2.1 引用计数

这是一种简单且古老的 GC 策略, 首先它会 跟踪 每个对象被 引用的次数, 当对象的 引用计数 时, 表示该对象不再被使用, 是个可被清除的垃圾, 那么在下一次进行 GC 时将自动释放这部分内存

如下代码: 创建了两个对象, 一个赋值给了 obj 另一个赋值给了 obj.user, 代码中变量 obj 虽然没有被使用到, 但是这两个对象的引用次数依然都为 1 所以 GC 时无法释放这部分内存, 从而将会导致内存泄漏

const obj = {
  user: {
    age: 18,
    name: 'lh',
  }
};

上面代码中, 如果我们将 obj 设置为 null, 这时变量指向的对象引用次数为 0
就会在下一轮 GC 时被销毁, 那么它的属性 user 也会被销毁, 也就是说 obj.user 所指向的对象引用次数也变为 0, 那么该对象后面自然也会被销毁

let obj = {
  user: {
    age: 18,
    name: 'lh',
  }
};
obj = null

引用计数 策略确实简单, 但是很快就遇到一个很严重的问题 —— 循环引用, 即对象 A 有一个指针指向对象 B, 而对象 B 也引用了对象 A, 如下代码: obj1obj2 两个对象之间相互引用了, 所以 obj1obj2 两个所指的对象引用次数都为 2, 这时我们即便将 obj1obj2 都设置为 null 这两个对象的引用次数依然不为 0, 但这两个对象确确实实是没有用的, 因为我们已经没有途径可以访问到它们了

let obj1 = {};
let obj2 = {};

obj1.a = obj2; // obj1 引用 obj2
obj12.a = obj1; // obj2 引用 obj1

obj1 = null
obj2 = null

下面再讲一个循环引用的实际例子: 在 IE8 以及更早版本的 IE 中, 对于 DOM 对象是采用 引用计数 策略来回收对象的, 如下代码 myDivElement 这个 DOM 元素(对象)中 circularReferenc 属性引用了 myDivElement 自身, 其实就是一个对象有个属性指向了自己, 这时候如果我们没有将 circularReferenc 移除或者设为 null, 那么 myDivElement 这个 DOM 将会永远无法销毁, 从而造成内容泄漏, 特别是如果 lotsOfData 数据量比较大的情况下, 这个内存泄漏的情况将会更加严重, 同时, 这里其实还有一个错误操作就是在全局声明了变量 div, 在没有手动设置为 null 情况下同样会引起内存的泄漏

var div;
window.onload = function(){
  div = document.getElementById("myDivElement");
  div.circularReference = div;
  div.lotsOfData = new Array(10000).join("*");
};

优点: 简单清晰

缺点:

  • 需要一个庞大(占大内存)的计数器用于记录每个对象的引用次数
  • 无法解决循环引用问题

2.2 标记清除

说到 标记清除 就不得不先提下 可达性 了, JS可达性 简单来说就是指, 程序可以通过某种方式能够访问到该值, 那么称这个值是 可达 的, 是可访问的; 同时所有不可达的值, 将被认为是无效的、无用的值

标记清除 策略就是将 对象是否不再需要 简化定义为 对象是否可达, 其实也好理解如果一个对象我们 无法访问 那么它必然就是无效的 垃圾

标记清除 中会设定一个 root(根) 对象, 在 JSroot 对象是 全局对象, GC 将定期从 root 开始, 一层层往下查找对象, GC 将找到所有 可达对象 和收集所有 不可达对象, 然后对于 不可达 的对象将会进行销毁, 释放其所占用的内存

如下图所示, 虚线框出部分则是从根节点出发, 无法被访问到的对象, 那么这几个对象将被视为垃圾被处理掉

image

标记清除 中就可以解决上文提到的 循环引用 问题, 因为从根对象出发他们是不可访问到的

优势:

  • 能有效解决循环引用问题: 该算法相对于 引用计数 就更加合理, 因为 零引用的对象 总是不可访问的, 但是相反却不一定, 比如 循环引用

  • 实现可以说是非常简单的, 就是打标记、清除, 现在的各类 GC 算法也都是它思想的延续、优化

缺点: 在多次回收操作后, 会产生大量的内存碎片, 因为在清除内存后并没有对内存进行整理, 所以会导致剩余的内存不连续

image

三 标记清除优化

2012 年起, 所有现代浏览器都使用了 标记清除 策略, 之后所有 GC 算法的改进都是基于 标记清除 来进行改进的, 并没有改变策略本身和它对 对象是否不再需要 的判断逻辑(从根对象出发不可达、不可访问), 下列几种是比较常见的几种优化算法:

3.1 空间复制: Scavenge 算法

算法实现思路:

  1. 将整个空间平均分成 from(使用区)to(空闲区) 两部分
  2. 先在 from(使用区) 空间进行内存分配, 当空间快被占满时, 对该空间内所有对象进行 标记清除
  3. from(使用区) 中剩余的 可达对象 拷贝到 to(空闲区)
  4. 复制完成后, 将 from(使用区)to(空闲区) 角色互换, 进行下一轮循环

image

优点: 不会发生碎片化, 每次都是对其中的一块进行内存回收, 内存分配时也就不用考虑内存碎片等复杂情况

缺点:

  • 内存使用效率低, 把内存分为对等的两份, 通常情况下只能利用到其中的一半来存储,另一半堆一直都空闲
  • 效率低: 需要同时对存活的对象进行操作, 复制操作次数多, 效率降低

3.2 标记压缩(整理)算法

算法实现思路:

  1. 标记: 对所以存活的对象进行标记
  2. 压缩(整理): 将所有 可达对象 移动到内存的其中一端, 这时必然会划分出一个分界线
  3. 清除: 直接释放分界线另一段的内存

image

优点: 不会产生空间碎片化

缺点: 整理内存空间需要花费一定的时间

3.3 分代回收

分代回收的依据「对象的生存时间呈现两极化」

  1. 大部分对象的生命周期都非常短暂, 存活时间较短
  2. 而另一部分对象生命周期又是很长, 甚至有些对象是一直存在的

算法实现思路:

  1. 把内存划分为 新生代老生代, 这样就可以根据不同生命周期的对象, 采用不同的算法进行 GC
  2. 新生代 中存储新增的对象, 数量相对比较少所以这里一般选用 空间复制 来处理, 同时在 新生代GC 的周期一般比较短, 当一个对象经过多次复制后依然存活, 它将会被认为是生命周期较长的对象, 随后会被移动到老生代中, 采用老生代的 GC 算法进行管理
  3. 老生代 一般都是存活周期较长的对象, 所以 GC 的周期一般比较长, 同时一般采用 标记压缩 算法来处理

image

3.4 标记增量

标记清理 中需要遍历对象, 对所有 可达不可达 对象进行标记, 但这里有个问题: 如果一个对象特别庞大那么这个遍历时间就会被拉长, 从而阻碍到 JS 正常逻辑的执行

标记增量就是解决上面问题, 该算法将整个 标记 的过程划分为多个步骤, 每执行完一小步就执行一会 JS 逻辑, 直到完成所有的 标记 工作

image

但这里其实还有一个问题, 每开始新的步骤时, GC 又是如何确定上一次标记到哪里了? 这里就得借助 三色标记法 了, 该算法作为工具可辅助推导, 它精髓在于将遍历过的对象, 按照 是否访问过 这个条件标记成以下三种颜色:

  • 白色: 表示对象尚未被垃圾收集器访问过; 在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段, 仍然是白色的对象即代表该对象不可达(可被清理)
  • 灰色: 表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过(只标记了一半)
  • 黑色: 表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过(该对象可达)

image

到此还有可能存在一个问题: 当一次标记结束后, 在执行程序过程中, 代码将 已标记完 对象的引用改为 新的对象, 这时就有可能出现 漏标 的情况, 如下图所示: 在标记阶段 A B C 都已被标记为黑色, 但是在进行程序执行过程中, B 对象由指向 C 改为指向 D

image

这个问题其实可以使用 写屏障 (Write-barrier) 机制 来规避, 即一旦有黑色对象 引用 白色对象, 该机制会强制将引用的 白色对象 改为 灰色, 从而保证下一次增量标记阶段可以正确标记, 这个机制也被称作 强三色不变性

image

优点: 使得主线程的停顿时间大大减少了, 让用户与浏览器交互的过程变得更加流畅

缺点: 并没有减少主线程的总暂停的时间, 甚至会略微增加; 同时由于对象指针可能发生了变化, 需要使用 写屏障技术 来记录这些引用关系的变化, 所以可能会降低应用程序的吞吐量

3.5 小结

本节所介绍的几个算法, 都是对 标记清除 策略的一个优化, 当然实际情况可能更为复杂, 需要更多的算法来同时进行优化, 比如: 懒性清理、并发回收等等

最后补充下 V8 引擎中常用的几个算法: 分代回收、空间复制、标记清除、标记整理、标记增量……

四、WeakMap & WeakSet

在此姑且认为大家对 MapSet 类型数据都是清楚的, 那么 WeakMapWeakSetMap Set 又有啥区别呢? 在我看来它们的主要区别如下:

  1. WeakMapKey 只能是一个对象, value 是任意值, 但是在 MapkeyValue 都可以是任意值
  2. WeakSetvalue 只能是对象, 但是 Set 中可以是任意值
  3. WeakMapKey 值的引用是 弱引用, Map 中则不是
  4. WeakSetvalue 值的引用是 弱引用, Set 中则不是
  5. 由于 弱引用 特性(猜测)在 WeakMapWeakSet 中不存在 keys values 方法
  6. 由于 弱引用 特性(猜测)在 WeakMapWeakSet 中不能在初始化时同 Map Set 一样设置初始值

4.1 弱引用

WeakMapWeakSetMapSet 之间的区别看似很简单也就两句话的事, 但是呢实际上问起 弱引用 大部分人可能还都是一知半解的, 那么什么是 弱引用 呢? 在我看来主要还是和 GC 有关

在上文我们提到目前 JSGC 一般采用 标记清除, 会遍历对象所有属性, 找到 不可达对象 进行清除, 但是在遍历过程 GC 将会忽略 弱引用, 也就是 弱引用 指向的那个对象在当前遍历路径中是不可达的, 如果该对象在其他路径也是不可达的那么该对象将被会被回收掉

image

标签:arr,对象,WeakMap,heapUsed,GC,内存,WeakSet,引用
From: https://blog.csdn.net/qianyin925/article/details/143320586

相关文章

  • T-GCN解读(论文+代码)
    一、引言     提出交通预测是一个具有挑战性的任务,原因在于其复杂的时空依赖性。    首先,交通流量随着时间动态变化,主要体现在周期性和趋势性上。左图是交通流量一周内的周期变化,右图是交通流量在一天内随着时间推移发生的变化。      除了随......
  • 【langchain4j接入springboot项目】想学AI平台接入?langchain4j,是不二的选择
    一、项目结构二、示例代码1.Calulator.javapackageorg.ivy.aiservice.func;importdev.langchain4j.agent.tool.Tool;importorg.springframework.stereotype.Component;@ComponentpublicclassCalculator{@Tool("Calculatesthelengthofastring")......
  • AIGC 推动短剧出海市场蝶变:降本、提效、增质
    短剧出海的新一轮风浪,由AI掀起。  自2022年以来,短剧一步步迈过了验证需求、出海拓荒的蛮荒阶段,到2024年,仅过了短短两年多时间,在ReelShort、DramaBox、FlexTV、ShortMax等头部玩家的“带领”下,短剧出海已进入了史无前例的“亿”级爆发阶段,伺机而动的上下游企业纷纷入局,根......
  • 深入了解 ArkTS 的高性能垃圾回收(HPP GC)
    本文旨在深入探讨华为鸿蒙HarmonyOSNext系统(截止目前API12)的技术细节,基于实际开发实践进行总结。主要作为技术分享与交流载体,难免错漏,欢迎各位同仁提出宝贵意见和问题,以便共同进步。本文为原创内容,任何形式的转载必须注明出处及原作者。引言垃圾回收(GC)是现代编程语言中重要......
  • 多线程应用在鸿蒙 HarmonyOS Next 中的内存管理与 GC 实战
    本文旨在深入探讨华为鸿蒙HarmonyOSNext系统(截止目前API12)的技术细节,基于实际开发实践进行总结。主要作为技术分享与交流载体,难免错漏,欢迎各位同仁提出宝贵意见和问题,以便共同进步。本文为原创内容,任何形式的转载必须注明出处及原作者。在构建高性能应用时,尤其是需要处理大......
  • LightningChart部署到Windows11某些电脑,无法启动问题
       问题经过注册表排查、SDK排查,均没有解决问题。   在可以运行的电脑上,全盘搜索LightningChat、Arction(厂家名称)比对,终于发现一个temp目录下的Arction.DirectX_32目录以及下边俩个dll:D3DCompiler_43.dll、d3dx11_43.dll,删除了就启动不了。   解决方案就是增......
  • EnhancerByCGLIB和EnhancerBySpringCGLIB代理
    $$EnhancerByCGLIB 和 $$EnhancerBySpringCGLIB 是由两个不同的库生成的Java字节码代理类的命名后缀。尽管两者都与CGLIB(CodeGenerationLibrary)有关,但它们有一些重要的区别。CGLIB代理简介CGLIB是一个开源的字节码生成库,允许在运行时动态创建类和对象。它最著名的用途......
  • ArkTS 编程语言中的垃圾回收模型:分代式 GC 详解
    本文旨在深入探讨华为鸿蒙HarmonyOSNext系统(截止目前API12)的技术细节,基于实际开发实践进行总结。主要作为技术分享与交流载体,难免错漏,欢迎各位同仁提出宝贵意见和问题,以便共同进步。本文为原创内容,任何形式的转载必须注明出处及原作者。引言垃圾回收(GC)是现代编程语言中重要......
  • HarmonyOS Next:内存管理与 GC 基础
    本文旨在深入探讨华为鸿蒙HarmonyOSNext系统(截止目前API12)的技术细节,基于实际开发实践进行总结。主要作为技术分享与交流载体,难免错漏,欢迎各位同仁提出宝贵意见和问题,以便共同进步。本文为原创内容,任何形式的转载必须注明出处及原作者。引言HarmonyOSNext作为华为自研的......
  • goframe gconv.structs示例代码
    以下是一些使用 gconv.structs  的示例代码,展示了如何在GoFrame框架中进行结构体转换:示例1:基本使用packagemainimport(  "fmt"  "github.com/gogf/gf/frame/g"  "github.com/gogf/gf/util/gconv")typeUserstruct{  Uid   int  ......