摘要:大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!
1.垃圾收集的可达性(Reachability)
JavaScript 中的内存管理是自动执行的,对开发者来说并不可见。当创建变量、对象、函数等时会自动分配内存。而当程序不再需要这些变量时候,则会自动清理。那么 JavaScript 引擎如何发现并执行清理操作?
其实,JavaScript 内存管理的核心概念是可达性。简而言之,“可达” 是指可以以某种方式访问或使用的值,其保证存储在内存中。比如:
当前执行的函数、其局部变量和参数当前嵌套调用链上的其他函数、其局部变量和参数全局变量一些内部变量,称为根 root任何其他值如果可以通过引用或引用链从根到达,则被认为是可达的。
例如,如果全局变量中有一个对象,并且该对象具有引用另一个对象的属性,则该对象被认为是可访问的,其引用的变量也是可以访问的。
let user = {name: "John"// user 可达user = null;// 此时 user 不可达JavaScript 引擎中有一个后台进程,称为垃圾收集器,其会监视所有对象并删除那些无法访问的对象。
2.标记清除(mark-and-sweep)算法
目前各大浏览器通常采用的垃圾回收有两种方法:
引用计数标记清除引用计数是最早最简单的垃圾回收机制,当有其它对象引用这个对象时,当前对象的引用计数加一,反之减一,当该对象引用计数为 0 时就会被回收。但是,该方式很简单,可能会引起内存泄漏。
// 循环引用的问题,只有低版本 IE 还用这种方式function temp{var a={};var b={};a.o = b;b.o = a;}标记清除 mark-and-sweep 算法会定期执行以下 “垃圾收集” 步骤:
gc 收集器从 root 开始并 “标记”访问并 “标记” 来自它们的所有引用访问标记的对象并进一步标记引用, 所有访问过的对象都会被记住,以免将来两次访问同一个对象依此类推,直到访问每个可到达的(从根)引用除标记对象外的所有对象都将被删除如下图表示了 mark-and-sweep 阶段的不同工作:
可以将 mark 过程想象为从根部溢出一大桶油漆,流经所有引用并标记所有可到达的对象,然后将未标记的删除。同时,JavaScript 引擎对应用的 gc(Garbage Collection) 做了许多优化使其运行速度相当快。
分代收集:对象被分为 “新” 和 “旧”,生命周期短的对象内存会被很快清除,而存活时间足够长的对象会变得 “老”,并且很少接受检查。增量收集:通过将需要遍历和标记的对象进行分组来依次清除,使用多个小的垃圾收集器来替代大的垃圾收集器,最终延迟更小。但,需要额外的簿记来跟踪变化。空闲时收集:垃圾收集器尝试仅在 CPU 空闲时运行,以减少对执行的影响。3.对象作为另一个对象的键、值、子元素不会被 gc按照上面的介绍,此时会有一个疑问?如果一个对象被多次引用时,例如:作为另一对象的键、值或子元素时,将该对象引用设置为 null 时,该对象是不会被回收的!
var a = {"name":"高级前端进阶"};var arr = [a];a = null;console.log(arr)// [{"name":"高级前端进阶"}]如果设置为 Map 的 key 则依然如此:
var a = {"name":"高级前端进阶"};var map = new Map;map.set(a, '一起学前端')a = null;// 将 Map 的 key 设置为 nullconsole.log(map.keys)// MapIterator {{key: {name: '高级前端进阶'},value: "一起学前端"}}console.log(map.values)// MapIterator {'一起学前端'}那么如果想让 a 置为 null 时,该对象被回收,该怎么做喃?
4.对象作为另一个对象的键、值、子元素如何正常 gc
ES6 推出了 WeakMap ,WeakMap 对于值的引用都是不计入垃圾回收机制的,所以名字里面才会有一个 "Weak",表示这是弱引用。
对对象的弱引用是指当该对象应该被 GC 回收时不会阻止 GC 的回收行为,Map 相对于 WeakMap 有以下不同:
Map 键可以是任意类型,WeakMap 只接受对象(非 null)和非全局 Symbol(Symbol.for 和 Symbol.keyFor 从全局 symbol 注册表设置和取得的 symbol 为全局 Symbol) 作为键Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键WeakMap 键是弱引用,键所指向的对象可以被垃圾回收,此时键无效Map 可以被遍历, WeakMap 不能被遍历下面使用 WeakMap 进行 gc 测试:
var a = {"name":"高级前端进阶"};var map = new WeakMap;map.set(a, '一起学前端')map.get(a)a = null;// 此时会被 gc 掉,只是时机不确定,可以在 console 中测试假如是 Node.js 代码,可以手动调用 gc 进行垃圾回收,比如:执行 node --expose-gc weakmap.js 命令来运行下面代码:
// weakmap.jsfunction usedSize {const used = process.memoryUsage.heapUsed;return Math.round((used / 1024 / 1024) * 100) / 100 + "M";global.gc;console.log(usedSize);// 初始状态,执行 gc 和 memoryUsage 以后,heapUsed 值为 1.64Mvar map = new WeakMap;var b = new Array(5 * 1024 * 1024);map.set(b, 1);global.gc;console.log(usedSize);// Map 中加入元素 b,为一个 5*1024*1024 的数组后,heapUsed 为 41.82Mb = null;global.gc;console.log(usedSize);// b 置为空以后,heapUsed 变成了 1.82M 左右,说明 WeakMap 长度为 5*1024*1024 的数组被销毁总之,WeakMap 中,每个键对所引用对象的引用都是弱引用,在没有其他引用和该键引用同一对象的时候,这个对象将会被垃圾回收(相应的 key 则变成无效的),所以,WeakMap 的 key 是不可枚举的。
5.垃圾回收器运行时机和主线程繁忙相关么?
以下面长时间运行的代码为例:
// Let's say this loop takes 10 seconds to executefor(let i = 0; i其实,GC 可以随时运行,并且会在需要时自动运行。
GC 是一个相当复杂的系统,它会执行多个不同的任务,并且以增量步骤和主线程同时执行其中大部分任务,每次都会触发一些增量 GC 工作。
回到上面的问题,垃圾收集器是否可以在循环期间运行,还是只能在应用程序空闲时运行?答案是:可以在循环期间运行!!!
虽然 Node.js 有 --nouse-idle-notification ,但该标志仅仅禁用了触发 GC 活动的一种特定机制,但这依然意味着 GC 可以由其他机制触发。
https://javascript.info/garbage-collection#reachability
https://v8.dev/blog/concurrent-marking#parallel-marking
来源:度半科技圈