js中的垃圾回收机制

js中的垃圾回收机制

1.什么是堆、栈

一种数据结构,栈有先进后出的特性, 堆是用来存放复杂数据类型的地址(栈用来存放它的引用)

2、执行上下文与作用域链

执行上下文:全局执行上下文、函数执行上下文、Eval 函数执行上下文

凡是未被引用的对象或变量,都会被视为垃圾

3、js垃圾回收的两种机制

  • 标记清除
  • 引用计数

标记清除

当变量进入上下文时,会对其添加上 存在于上下文 的标记。当变量退出上下文时,对退出上下文的变量添加上退出上下文的标记

例如在一个函数中声明一个变量,该变量就会被标记为存在于上下文中。当函数执行完毕,上下文栈
弹出该函数的上下文,其内变量添加 退出上下文的标记。 

此种策略的垃圾回收机制在运行的时候,会对所有已存在于内存的变量进行标记。

之后垃圾回收机制会清除上下文中所有变量的标记,包括其引用的变量的标记也会在此被清除。

最后仍然被标记的变量,即为要回收的垃圾。因为没有地方引用他们。

引用计数

该种策略会对每个值记录它被引用的次数。声明变量并给他赋一个引用值时,这个值的引用数为1。如果同一个值又被赋给另一个变量,则引用次数+1。

类似的,如果保存对该值引用的变量被其他值给覆盖了,那么引用数-1。

当一个值的引用数为0时,就说明没有办法在访问到这个值了。此时被判断为垃圾,在下次垃圾回收机制执行时会释放引用值为0的值所占用的内存。

本质上都是找到未被引用的值,从而在垃圾回收的时候释放空间

4、什么时候执行垃圾回收
不同浏览器执行垃圾回收的时机不一样,对于大部分浏览器来说我们不能人为的控制什么时候进行垃圾回收,因为js并没有暴露相关的接口供我们调用

不再需要内存时释放 
大多数内存管理问题发生在这个阶段。此阶段最困难的方面是确定何时不再需要分配的内存。 低级语
言要求开发人员手动确定程序中哪个点不再需要分配的内存并释放它。 一些高级语言,例如 
JavaScript,使用一种称为垃圾收集 (GC) 的自动内存管理形式。垃圾收集器的目的是监控内存分配并
确定何时不再需要分配的内存块并回收它。这个自动过程是一个近似值,因为确定是否仍然需要特定
内存的一般问题是不可判定的。

在拥有了两种垃圾回收策略后,执行的周期性不再是问题,因为我们能够将垃圾明确出来,只需要等下次回收即可。

周期不再是问题

function f() {
var x = {};
var y = {};
x.a = y;        // x references y
y.a = x;        // y references x

return 'azerty';
}
f();

在上面的示例中,函数调用返回后,这两个对象不再被可从全局对象访问的任何资源引用。因此,垃圾收集器将发现它们无法访问并回收分配的内存。

关于Chrome V8引擎的GC

分代回收

大多数对象的生存周期很短,小部分对象的生存周期比较长,利用这一特性,V8对它们进行了分区管理

  • 新生区:大多数对象被分配在这里。新生区是一个很小的区域,垃圾回收在这个区域非常频繁,与其他区域相独立。
  • 老生指针区:这里包含大多数可能存在指向其他对象的指针的对象。大多数在新生区存活一段时间之后的对象都会被挪到这里。
  • 老生数据区:这里存放只包含原始数据的对象(这些对象没有指向其他对象的指针)。字符串、封箱的数字以及未封箱的双精度数字数组,在新生区存活一段时间后会被移动到这里。
  • 大对象区:这里存放体积超越其他区大小的对象。每个对象有自己mmap产生的内存。垃圾回收器从不移动大对象。
  • 代码区:代码对象,也就是包含JIT之后指令的对象,会被分配到这里。这是唯一拥有执行权限的内存区(不过如果代码对象因过大而放在大对象区,则该大对象所对应的内存也是可执行的。译注:但是大对象内存区本身不是可执行的内存区)。
  • Cell区、属性Cell区、Map区:这些区域存放Cell、属性Cell和Map,每个区域因为都是存放相同大小的元素,因此内存结构很简单。

回收的执行周期

对象起初会被分配在新生区(通常很小,只有1-8 MB,具体根据行为来进行启发)。在新生区的内存分配非常容易:我们只需保有一个指向内存区的指针,不断根据新对象的大小对其进行递增即可。当该指针达到了新生区的末尾,就会有一次清理(小周期),清理掉新生区中不活跃的死对象。

对于活跃超过2个小周期的对象,则需将其移动至老生区。老生区在标记-清除或标记-紧缩(大周期)的过程中进行回收。大周期进行的并不频繁。一次大周期通常是在移动足够多的对象至老生区后才会发生。至于足够多到底是多少,则根据老生区自身的大小和程序的动向来定。

Scavenge算法

V8采用了Scavenge算法,是按照Cheney的算法实现的。

算法的大致流程为:将新生区划分为入区(from-space)和出区(to-space)。绝大多是内存分配是在出区进行,而当出区被填满时,我们会交换出区和入区,然后将入区中活跃的对象复制至出区或老生区当中。在这时我们会对活跃对象进行紧缩,以便提升Cache的内存局部性,保持内存分配的简洁快速。

在这里插入图片描述
上图描述了在新生区中,如何回收的垃圾b。

而当一个变量在两次从入区(from-space) 移动到 出区(to-space) 时。他就会被提升到老生区的内存空间中

注意,在上面的回收过程中,为了避免有老生区的变量指向新生区,但在新生区的清理周期中被引用的变量被错误回收,V8引擎做了额外的处理:写屏障

写屏障

肯定不可能通过遍历老生区去查找到底哪个变量引用了新生区的变量,耗时太大。所以通过在写缓冲区中创建一个列表去记录所有老生区对象指向新生区的情况,这样就可以避免上述错误回收。该记录行为总是发生在写操作的时候,每个写操作都会经历这么一关。

老生区

在老生区中,用到的是上文我们说过的标记清理法结合标记紧缩法去回收。

标记清理法是如何标记的
V8 使用每个对象的两个 mark-bits 和一个标记工作表来实现标记。两个 mark-bits 编码三种颜色:白色(00),灰色(10)和黑色(11)。

如果一个对象的状态为白,那么它尚未被垃圾回收器发现,同时最开始所有对象都是白色
如果一个对象的状态为灰,那么它已被垃圾回收器发现,但它的邻接对象仍未全部处理完毕
如果一个对象的状态为黑,则它不仅被垃圾回收器发现,而且其所有邻接对象也都处理完毕

算法的核心实际是深度优先搜索,从根(Root)可达的对象会被染为灰色,并放入标记用的一个单独分配的双端队列。标记阶段的每次循环,GC会将一个对象从双端队列中取出,染为黑色,然后将它的邻居对象染为灰色,并把邻居对象放入双端队列。这一过程在双端队列为空且所有对象都变黑时结束。

特别大的对象,如长数组,可能会在处理时分片,以防溢出双端队列。如果双端队列溢出了,则对象仍然会被染为灰色,但不会再被放入队列(这样他们的邻接对象就没有机会再染色了)。因此当双端队列为空时,GC仍然需要扫描一次,确保所有的灰对象都成为了黑对象。对于未被染黑的灰对象,GC会将其再次放入队列,再度处理。

标记算法结束时,所有的活跃对象都被染为了黑色,而所有的死对象则仍是白的。

标记紧缩法

在使用完标记清理法后,确实能够将垃圾清理掉,但是清理后的空间是不连续的。而一些数据的存储要求的是连续的空间,所以这时候就需要用标记紧缩法去整理碎片空间。

达到这种效果
在这里插入图片描述

增量标记法

当一个堆很大而且有很多活跃对象时,标记-清除和标记-紧缩算法会执行的很慢,又因为垃圾回收机制在执行时会阻塞js代码(JS是单线程的),所以在2012年年中,谷歌引入了增量标记和惰性清理两项技术。

增量标记允许堆的标记发生在几次5-10毫秒(移动设备)的小停顿中。增量标记在堆的大小达到一定的阈值时启用。启用后每当一定量的内存分配后,脚本就会停顿一次用来执行标记,同样是黑白灰三色,也同样是深度优先搜索。

写屏障

和上文提到过的写屏障类似,为了避免出现黑色指向白色这种情况出现,我们通过写屏障记录黑色指向白色的指针,一旦发现这种指针,就会将黑色对象重新染色为灰色对象,重新放回到双端队列中。当算法将该对象取出时,其包含的指针会被重新扫描,这样活跃的白对象就不会漏掉。

惰性清理

因为所有对象已被处理,因此非死即活。谁是垃圾已经很明确了,所以不用着急释放空间,延迟一下清理也可以。

效果类似下图所示

上面的是完整的GC执行,下方的是增量标记法与惰性清理的执行。当清理完后,即可随时开始再一次的标记。

并发标记与并行标记

并发标记支持在主线程进行GC的时候启动多个worker thread一起执行GC。应用程序在整个并发标记阶段暂停,它是 stop-the-world 标记的多线程版本。

并行标记则是在主线程还在运行时即可启动多个worker thread执行GC,应用程序可以继续运行。

4、内存问题

内存泄漏
内存泄漏指的是在执行垃圾回收的时候, 由于一些原因导致本应释放掉的空间没有被释放掉。

常见的引起内存的方法

循环引用

在浏览器早起采用引用计数法的时候,如果两个变量相互引用,则其引用数始终为1,而垃圾回收只会对引用数为0的变量进行回收,这时就导致了内存泄漏。这也是为什么现在大都采用的标记清理法

没有被销毁的全局变量和计时器

function fn(){
	bar = 'bar'; // 声明了全局变量
}
fn();

var timer = getStart();
getStart(function() {
    var temp = document.getElementById('temp');
    if(temp) {
        temp.innerHTML = JSON.stringify(temp);
    }
}, 5000); // 每5秒调用一次

此时若不手动置为null/调用clearInterval,则该变量和计时器将会一直存在,造成内存泄漏。直到window对象被销毁。

闭包

var closure = function(){
	var count = 0;
	return function(){
		return count++
	}
}
const fn = closure(); 

由于被返回的函数一直持有其外层函数closure的变量count导致count无法被回收,造成内存泄漏。所以能少用闭包就少用,或者用完及时置为null。

内存溢出

内存溢出是一种程序运行的错误。指的是当程序运行需要的内存超过了剩余内存的时候,就会抛出内存溢出的错误。

内存泄漏积累过多时,就会导致内存溢出

参考链接:https://blog.csdn.net/qq_45670042/article/details/121181328

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值