对于值类型b来说,就直接释放了其占用的内存,对于引用类型obj来说,销毁的只是变量obj对堆内存地址 1001 的引用,obj的值 { c: 3 } 依然存在于堆内存中。那么堆内存中的变量如何进行回收呢?
- V8的垃圾回收策略主要是基于分代式垃圾回收机制,其根据对象的存活时间将内存的垃圾回收进行不同的分代,然后对不同的分代采用不同的垃圾回收算法。
- 在新生代的垃圾回收过程中主要采用了Scavenge算法;在老生代采用标记清除和标记整理算法。
============================
全停顿
垃圾回收算法在执行前,需要将应用逻辑暂停,执行完垃圾回收后再执行应用逻辑,这种行为称为 「全停顿」。例如,如果一次GC需要50ms,应用逻辑就会暂停50ms。
全停顿的目的,是为了解决应用逻辑与垃圾回收器看到的情况不一致的问题。
JavaScript中会被判定为垃圾的情形如下:
- 对象不再被引用;
- 对象不能从根上访问到;
GC
算法
常见的GC
算法如下:
- 引用计数
- 标记清除
- 标记整理
- 分代回收
浏览器的垃圾回收机制:
浏览器垃圾回收机制根据数据的存储方式分为栈垃圾回收和堆垃圾回收。
栈垃圾回收,当一个函数执行结束之后,JavaScript 引擎会通过向下移动 ESP 来销毁该函数保存在栈中的执行上下文,遵循先进后出的原则。
堆垃圾回收,当函数直接结束,栈空间处理完成了,但是堆空间的数据虽然没有被引用,但是还是存储在堆空间中,需要垃圾回收器将堆空间中的垃圾数据回收。
在V8中,将内存一分为二,分为了新生代和老生代。它们特点如下:
- 新生代:对象的存活时间较短。新生的对象或只经过一次垃圾回收的对象。
- 老生代:对象存活时间较长。经历过一次或多次垃圾回收的对象。
我们可以通过 --max-old-space-size
命令设置老生代空间的最大值,--max-new-space-size
命令设置新生代空间的最大值。老生代与新生代的空间大小在程序初始化时设置,一旦生效则不能动态改变。
新生区中使用Scavenge(清除)算法,老生区中使用标记-清除算法和标记-整理算法。
新生代垃圾回收(副垃圾回收器)
新生代的特点:
- 通常把小的对象分配到新生代
- 新生代的垃圾回收比较频繁
- 通常存储容量在1~8M
新生代中垃圾回收算法:
Scavenge算法:标记->复制->角色反转
回收新生代对象主要采用复制算法(Scavenge 算法
)加标记整理算法。而Scavenge 算法
的具体实现,主要采用了Cheney算法
。
Cheney算法
将内存分为两个等大空间,使用空间为From
,空闲空间为To
。
过程如下:
- 从 From 空间分配对象,若 semispace 被分配满,则执行 Scavenge 算法进行垃圾回收。
- 对对象区域中的垃圾进行标记,检查 From 空间的存活对象,若对象存活,则检查对象是否符合晋升条件,若符合条件则晋升到老生代,否则将对象从 From 空间复制到 To 空间。并且有序的排列起来,复制后空闲区域就没有内存碎片了
- 若对象不存活,则释放不存活对象的空间。
- 完成复制后,将 From空间(对象区域)与 To 空间(空闲区域)进行角色翻转(flip)。这样就完成了垃圾对象的回收操作,同时这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去
一轮GC
还存活的新生代需要晋升。
当对象从From
空间复制到 To
空间时,若 To
空间使用超过 25%,则对象直接晋升到老生代中。
缺点:由于只能使用堆内存的一半,所以不适用大规模的垃圾回收机制中
在讲解老生代Mark-Sweep(标记清除)和Mark-Compact(标记整理)算法之前,先来回顾一下引用计数法:对于对象A,任何一个对象引用了A的值,计数器+1,引用失效时计数器-1,当计数器为0时责备回收,但是会存在循环引用的情况,可能会导致内存泄漏,自2012年起,所有的现代浏览器均放弃了这种算法。
引用计数
早期的浏览器最常使用的垃圾回收方法叫做"引用计数":语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。
引用计数有一个问题就是:循环引用
引用计数算法优点:
- 引用计数为零时,发现垃圾立即回收;
- 最大限度减少程序暂停;
引用计数算法缺点:
- 无法回收循环引用的对象;
- 空间开销比较大;
老生代垃圾回收(主垃圾收集器)
因为新生代存储容量小,很容易写满,所以经过两次垃圾回收之后依然活动的对象,就会被移动到老生代中,这个策略被称为对象晋升策略。
晋升条件:
对象晋升的条件主要有两个。
1、对象在新生代期间是否经历过Scavenge回收;
2、是To空间的内存占用比超过限制(To空间内存消耗是否超过25%,如果超过对象直接晋升)(设置为25%的比例的原因是,当完成 Scavenge
回收后,To
空间将翻转成From
空间,继续进行对象内存的分配。若占比过大,将影响后续内存分配,因此超过这个限制之后对象会被直接转移到老生代来进行管理)
————————————————
回收老生代对象主要采用标记清除、标记整理、增量标记算法,主要使用标记清除算法,只有在内存分配不足时,采用标记整理算法。
- 首先使用标记清除完成垃圾空间的回收;
- 采用标记整理进行空间优化;
- 采用增量标记进行效率优化;
标记清除
核心思想:分标记和清除两个阶段完成。
标记:标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。(标记存活的对象)
清除:将垃圾数据进行清除。
对比引用计数算法,标记清除算法最大的优点是能够回收循环引用的对象,它也是v8引擎使用最多的算法。
缺点:对一块内存多次执行标记 - 清除算法后,会产生大量不连续的内存碎片。而碎片过多会导致大对象无法分配到足够的连续内存。此时需要对内存碎片进行整理。
这种内存碎片会对后续的内存分配造成问题,因为很可能出现需要分配一个大对象的情况,这时所有的碎片空间都无法完成此次分配,就会提前触发垃圾回收,而这次回收是不必要的。
标记整理
与标记清除呈现一个策略递进关系,当空间不足以对从新生代中晋升过来的对象进行分配时才使用,在整理的过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。这也是两者最大的区别。标记整理对待未存活对象不是⽴即回收,⽽是将存活对象移动到⼀边,然后直接清掉端边界以外的内存。
1. 标记:和标记 - 清除的标记过程一样,从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素标记为活动对象。
2. 整理:让所有存活的对象都向内存的一端移动
3. 清除:清理掉边界以外的内存
V8 是使用副垃圾回收器和主垃圾回收器处理垃圾回收的,不过由于 JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JS 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。这种行为叫做全停顿。 为了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JS 应用逻辑交替进行,直到标记阶段完成,这个算法称为增量标记
这里为了便于理解,引用两个流程图。
增量标记
新生代和老生代回收对比
新生代由于占用空间比较少,采用空间换时间机制。
老生代区域空间比较大,不太适合大量的复制算法和标记整理,所以最常用的是标记清除算法,为了就是让全停顿的时间尽量减少。
=======================================================
全停顿
V8 是使用副和主垃圾回收器处理垃圾回收的,不过由于 js是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的js 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)。
将原本需要一次性遍历堆内存的操作改为增量标记的方式,先标记堆内存中的一部分对象,然后暂停,将执行权重新交给JS主线程,待主线程任务执行完毕后再从原来暂停标记的地方继续标记,直到标记完整个堆内存。
即:把垃圾回收这个⼤的任务分成⼀个个⼩任务,穿插在 JavaScript任务中间执⾏
这个理念其实有点像React框架中的Fiber架构,只有在浏览器的空闲时间才会去遍历Fiber Tree执行对应的任务,否则延迟执行,尽可能少地影响主线程的任务,避免应用卡顿,提升应用性能。
得益于增量标记的好处,V8引擎后续继续引入了延迟清理(lazy sweeping)和增量式整理(incremental compaction),让清理和整理的过程也变成增量式的。同时为了充分利用多核CPU的性能,也将引入并行标记和并行清理,进一步地减少垃圾回收对主线程的影响,为应用提升更多的性能。
识别内存泄漏的方法 :performance
点击 检查的performance 然后点击录制 执行我们觉得比较消耗内存的操作 然后stop录制。
可以看到内存在短时间消耗的比较快,下降的小凹槽,就是浏览器在进行垃圾回收
垃圾回收优化策略
1.延迟回收:
因为垃圾hi收会有一个运行的阻塞,所以可以选择在cpu空闲时候时候进行垃圾回收,从而尽可能减少对应用程序运行的影响
2.增量标记:
由于全堆垃圾回收会导致JS应用暂停执行,为了减少全堆垃圾回收带来的卡顿,V8采用增量标记的策略。也就是将一次完整的垃圾回收分解为多个小的步骤,同时让垃圾回收和应用逻辑交替执行,以达到流畅的用户体验。
3.对象晋升:
在新生代中存活下来的对象会被移动到老生代中,这就是对象晋升策略。在V8中通常采用两次垃圾回收后仍然存活的对象会被晋升到老生代。
当进行大规模的垃圾回收时,V8引擎使用增量标记来减少对应用程序的阻塞。
增量标记是一种垃圾回收的优化策略,它将一次完整的垃圾回收过程分解为多个小的步骤,使得垃圾回收和应用程序的逻辑可以交替执行。这样可以减少垃圾回收造成的长时间阻塞,提高应用程序的响应性和用户体验。
V8引擎的增量标记策略主要包括以下步骤:
初始标记(Initial Marking):在这个阶段,V8会标记出根对象和直接从根对象可达的对象,确定它们为活动对象。这个阶段需要阻塞应用程序的执行,但是尽量保持时间短暂。
并发标记(Concurrent Marking):在初始标记之后,V8引擎会启动增量标记线程,与应用程序的执行并发进行。增量标记线程会遍历剩余的对象图,标记出所有的活动对象。同时,应用程序的逻辑也在继续执行。
再标记(Remark):在并发标记过程中,应用程序可能会继续修改对象的引用关系,因此需要进行再标记。再标记阶段会对并发标记过程中发生变化的对象进行重新标记,以确保准确性。
清除阶段(Sweeping):在增量标记完成后,V8引擎会进行清除阶段,回收非活动对象所占用的内存。这个阶段通常会阻塞应用程序的执行,因为它需要遍历堆中的所有对象。
通过增量标记的方式,V8引擎可以在垃圾回收过程中与应用程序的逻辑交替执行,减少长时间的阻塞。这种方式可以有效降低垃圾回收对应用程序性能的影响,提高应用程序的响应速度和用户体验。
————————————————
性能优化即如何避免内存泄漏:
1.尽可能减少全局变量的使用
1.避免使用全局变量
- 全局变量会挂载在window下;
- 全局变量至少有一个引用计数;
- 全局变量存活更久,持续占用内存;
- 在明确数据作用域的情况下,尽量使用局部变量;如果确实需要使用全局变量,确保在使用完毕后将其设置为 null,以便垃圾回收机制可以及时释放内存。
2.手动清除定时器以及不用闭包
在使用定时器时,一定要记得在适当的时机手动清除定时器。如果忘记清除定时器,定时器的回调函数将持续执行,可能导致内存泄漏。确保在不需要定时器时,使用 clearTimeout 或 clearInterval 主动清除定时器。
3.清除 DOM 引用
当操作 DOM 元素时,确保在不再需要使用它们时清除对 DOM 元素的引用。如果仍然保留对已移除或隐藏的 DOM 元素的引用,这些元素将无法被垃圾回收。
4.使用弱引用weakMap
5.减少判断层级
function doSomething(part, chapter) {const parts = ['ES2016', '工程化', 'Vue', 'React', 'Node']if (part) {if (parts.includes(part)) {console.log('属于当前课程')if (chapter > 5) {console.log('您需要提供 VIP 身份')}}} else {console.log('请确认模块信息')}
}doSomething('Vue', 6)// 减少判断层级
function doSomething(part, chapter) {const parts = ['ES2016', '工程化', 'Vue', 'React', 'Node']if (!part) {console.log('请确认模块信息')return}if (!parts.includes(part)) returnconsole.log('属于当前课程')if (chapter > 5) {console.log('您需要提供 VIP 身份')}
}doSomething('Vue', 6)
6.减少数据读取次数
对于频繁使用的数据,我们要对数据进行缓存。
<div id="skip" class="skip"></div><script>var oBox = document.getElementById('skip')// function hasEle (ele, cls) {// return ele.className === cls// }function hasEle (ele, cls) {const className = ele.classNamereturn className === cls}console.log(hasEle(oBox, 'skip'))
</script>
7.事件绑定优化
<ul class="ul"><li>Hello World!</li><li>25</li><li>岂曰无衣,与子同袍</li>
</ul><script>var list = document.querySelectorAll('li')function showTxt(ev) {console.log(ev.target.innerHTML)}for (item of list) {item.onclick = showTxt}// 优化后function showTxt(ev) {var target = ev.targetif (target.nodeName.toLowerCase() === 'li') {console.log(ev.target.innerHTML)}}var ul = document.querySelector('.ul')ul.addEventListener('click', showTxt)
</script>
8.避开闭包陷阱
<button class="btn">点击</button><script>function foo() {let el = document.querySelector('.btn')el.onclick = function() {console.log(el.className)}}foo()// 优化后function foo1() {let el = document.querySelector('.btn')el.onclick = function() {console.log(el.className)}el = null // 将el置为 null 防止闭包中的引用使得不能被回收}foo1()
</script>
与weakMap的关联
通过【垃圾回收机制】的角度认识【Map与WeakMap】的区别 - 知乎 (zhihu.com)
V8 垃圾回收机制与 WeakMap 之间有一些联系,主要涉及到垃圾回收对于弱引用的处理。以下是它们之间的关系:
1.弱引用和垃圾回收: WeakMap 中的键是弱引用的。这意味着,如果没有其他引用指向 WeakMap 中的键对象,这些键对象可以被垃圾回收。垃圾回收器在执行时会检测并处理弱引用,当检测到某个对象的引用计数为零时,可以安全地回收该对象。
2.避免内存泄漏: 由于 WeakMap 的键是弱引用,当键对象不再被其他部分引用时,它们可以被垃圾回收,相应的键值对也会从 WeakMap 中自动删除。这有助于防止一些潜在的内存泄漏问题,因为对象在 WeakMap 中的存在不会阻止它们被垃圾回收。
3.私有数据存储: WeakMap 通常被用于存储对象的私有数据,因为这样的数据不会影响对象的垃圾回收。这使得在不破坏封装性的情况下关联额外信息成为可能。
let weakMap = new WeakMap();let obj = {};
weakMap.set(obj, "some private data");// 当 obj 不再被引用时,垃圾回收可以回收 obj,并清理 weakMap 中对应的项。
obj = null;
总体而言,WeakMap 的设计与垃圾回收机制的协同工作有助于更有效地管理对象的生命周期,避免潜在的内存泄漏问题,同时提供一种安全地存储私有数据的机制。
Map和WeakMap都是JavaScript的内置数据结构,用于存储键值对。它们之间的主要区别在于以下几点:
- 键类型的限制:在Map中,键可以是任意类型的值(包括基本类型和对象引用),而在WeakMap中,键只能是对象引用。这是因为WeakMap的键是弱引用,不会阻止对象被垃圾回收,使得WeakMap更适合于存储对象之间的关联信息。
- 垃圾回收机制:在Map中,如果某个键不再被引用,它仍然会被Map引用,并且不会被垃圾回收。而在WeakMap中,如果某个键不再被引用,它会被自动从WeakMap中删除,这也是WeakMap的一个特性,可以避免内存泄漏。
- 迭代:在Map中,可以使用
Map.prototype.keys()
、Map.prototype.values()
和Map.prototype.entries()
等方法来迭代Map中的键、值或键值对,而在WeakMap中,由于键是对象引用,无法直接迭代键或值。 - 功能:Map相比WeakMap提供了更多的功能,比如可以获取Map的大小(使用
Map.prototype.size
属性),可以通过键获取值(使用Map.prototype.get()
方法),可以遍历Map中的键值对等。而WeakMap相对简单,只提供了WeakMap.prototype.get()
、WeakMap.prototype.set()
、WeakMap.prototype.has()
和WeakMap.prototype.delete()
等基本操作。