问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

JavaScript执行机制三-深入之垃圾回收器

发布网友 发布时间:2024-09-06 13:50

我来回答

1个回答

热心网友 时间:2024-10-07 23:44

垃圾回收器

在讲垃圾收回器之前,首先需要明确一些概念。JavaScript是弱类型的动态语言。

弱类型表示不需要告诉JavaScript声明的语言是什么类型,JavaScript便能自动计算出类型;

动态表示JavaScript可以用一个变量存储不同类型的语言;

JavaScript的数据类型:

基础类型:Undefined、Null、String、Boolean、Number、BigInt、Symbol

引用类型:Object

了解了上面的概率后,先来做一道题:

//题1functionshow1(){vara=1;varb=a;a=100;console.log(a,b);}show1();//题2functionshow2(){vara={name:"zhangsan"};varb=a;a.name="lisi";console.log(a.name,b.name);}show2();

猜猜上面的两个方法执行后,分别打印的是什么?执行方法show1,很明显打印的值,a为100,b为1;执行方法show2,打印的值a、b的name值都为lisi。这是为什么呢?方法show1中,声明的a、b变量都是基础类型,方法show2中的声明的a、b变量是object,即为引用类型,所以区别应该就在此。要完全弄懂这点,需要先知道JavaScript中,变量在内存空间中是如何存储的。

内存空间

要理解JavaScript代码运行过程中,是如何存储的,首先要弄清楚存储空间的种类:

代码空间:存储可执行代码的空间;

栈空间:存储基础类型的空间;

堆空间:存储引用类型的空间;

如下图所示:

代码空间会在后续的文章中进行讲解,本文不会涉及。

栈空间和堆空间

栈空间就是前面文章中反复提到过的调用栈,用来存储执行上下文。先复习一下栈空间中是如何存储执行上下文,看下面的代码:

functionshow(){vara=1;varb={name:"zhangsan"};varc=a;vard=b;}show();

当代码编译后,遇到函数调用,就会在栈空间中创建一个执行上下文,然后在按照顺序执行下去。调用show方法时,会在栈空间生成一段执行上下文,存储show方法中声明的变量,如下图所示:

从图中可以看到,代码执行到变量b时,判断到b声明的变量是一个引用类型,JavaScript声明引用类型的变量并不是存在栈空间中,而是存在堆空间中,存储后堆空间中会生成一个地址,然后将地址写进变量b的值,同样在执行d=b时,d的变量值存储的是堆中的地址,如图:

由图可知,b和d在栈空间存储的是堆空间的引用地址,b和d指向堆中同一个地址空间,因此,这儿就解释了,在开头例子中,改变了a.name的值,b.name的值也会跟随改变。

到这里,我们知道栈空间存储的基础类型,堆空间存储的是引用类型。声明引用类型的变量,会在堆中开辟一片空间进行存储,并在堆中生成一个地址,栈空间对应值存的只是堆的地址。相当于做了一次中转。

了解了栈空间和堆空间存储过程,再来看上一篇文章JavaScript执行机制二(深入之闭包),相信对闭包会有一个深入理解。

GC如何自动回收

上面通篇在说变量是如何存的,基础类型存储在栈空间,引用类型存储在堆空间,如果空间分配后,变量就不再使用,那么这些声明变量就成为垃圾数据。内存中存的垃圾数据不回收,随着程序的运行,内存中会存在大量的垃圾数据,占用内存越大,必然会造成卡顿问题。所以,JavaScript会对这些垃圾数据进行自动回收,自动释放空间。JavaScript对于栈空间和堆空间的垃圾数据回收策略是不一样的,接下来会一步一步分析,它们是如何回收的。

调用栈的回收策略

还是先看一段代码,如下:

functionshow(){vara=1;varb={name:"张三"};functionshowName(){varc=2;vard={name:"李四"};}showName();}show();

上面代码,调用show方法,会产生一个show的执行上下文,进入调用栈,该执行上下文的变量环境中有两个变量,a是基础类型的变量,存放值为1,b是一个引用类型的变量,存放值为堆空间中的地址。同样调用showName方法时,创建一个showName的执行上下文,变量环境中c存放2,d存放对应在堆空间中的地址。如下图所示:

在执行showName函数,会创建showName的执行上下文,同时还会有一个记录当前执行状态的指针(ESP),指向当前正在执行的执行上下文showName,当showName执行完成后,ESP指针下移,这时showName执行上下文就应该销毁了,如下图所示:

所以,当执行上下文执行完成后,JavaScript会通过向下移动ESP的方式来销毁该函数在内存中的执行上下文。

堆空间的回收策略

堆空间中的回收策略相对于栈空间回收策略要复杂很多,在上面的例子中,当指针下移指向全局执行上下文后,showName和show的执行上下被销毁,并回收对应内存空间,b和d的真实数据存放在堆空间中,并没有被回收,如下:

上图中,当ESP指向全局执行上下文时,栈空间的其他执行上下文所占用的空间已经被回收,但是堆空间中的变量1003和1005仍然存储在内存中。对于堆空间中存储的内存回收,就要涉及到垃圾回收器。

在V8中,堆分为新生代和老生代两个区域:

新生代:存放占用内存小、生存时间短的对象,空间内存大约是1-8M的容量,使用副垃圾回收器回收;

老生代:存放内存占用大、存放周期久的对象,空间内存相对于新生代大的多,使用主垃圾回收器回收;

无论主垃圾回收器还是副垃圾回收器的回收机制,他们都有一套相同的流程:

标记活跃对象和非活跃对象,活跃对象表示正在运行的对象,非活跃对象表示可以进行垃圾回收的对象;

在标记完成后,统一回收所有被标记可以回收的对象;

非活跃对象回收后,可能会造成内存碎片,所以需要进行内存整理,将不连续的空间整理为连续的内存空间,如果垃圾回收器没有造成内存碎片,那这一步就不会执行;

副垃圾回收器

副垃圾收回器主要负责新生代中的垃圾收回,通常针对一些小的、存活时间短的对象,会被分配到新生代中,虽然新生代中的内存空间不大,但回收频繁。

副垃圾回收器使用的算法是Scavenge算法,把对象分为两个空间,一个是对象区域(又称分配空间,AllocationSpace),另一个是空闲区域(又称幸存者空间,SurvivorSapce),把空间分成大小相同的两个空间,如下:

当创建新对象时,会在对象空间中分配内存,当空间满后,会进行一次垃圾回收,使用的是就是简单的copy算法,首先标记活跃对象和非活跃对象,将活跃对象复制到空闲区域,并将这些对象有序的排列起来,相当于进行一次内存整理,所以复制到空闲空间后就没有内存碎片了。

复制结束后,将对象空间中的非活跃对象全部清空,此时将对象空间和空闲空间对调,原来的对象空间设置为空闲空间,原来的空闲空间设置为对象空间,这样就完成了垃圾回收的操作,同时角色翻转在新生代的两个区域中可以无限重复使用下去。

前面介绍过,新生代中的空间只有1-8M左右,是因为使用的copy操作,如果对象占用的内存较大,复制需要很多时间,所以才将新生代的内存设置的比较小。但是因为内存设置的比较少,所以很容易在角色翻转几次后,空间就装满了。JavaScript采用了晋升策略:

经过两次垃圾回收依然存活的对象,会被移动到老生代空间中;

进行一次垃圾回收,复制到空闲区的对象占用25%以上,那么这个对象会晋升到老生代中。设置25%的原因,是因为角色翻转后,如果占用的内存较大,对于新对象存储和分配都会有影响;

主垃圾回收器

主垃圾回收器负责老生代的垃圾回收,对于大多数占用空间大、存活时间长的对象会被分配到老生代空间中,如果在老生代中使用scavenge算法进行回收,数据大复制所花费的时间较长,存活时间长,来回复制所需时间也会比较长,运行效率不高。因此,主垃圾回收器使用标记-清除(Mark-Sweep)算法进行垃圾回收。简单来讲,Mark-Sweep算法由Mark和Sweep两个阶段组成。在Mark阶段,垃圾回收器遍历活跃对象,将他们标记为存活。在Sweep阶段,回收器遍历整个堆,然后将未被标记的区域回收。

空闲链表

深入讲,Mark-Sweep用一个链表将所有的空闲空间维护起来,这个链表叫做空闲链表(freelist)。当内存管理器需要申请内存时,便向这个链表查询,找到适合的空闲块,然后将空闲块分配出去,同时将它从空闲链表中移除。如果空间块较大,就需要将空闲块进行分割,一部分用于分割,剩余的部分重新加到空闲链表中。如图所示:

图中A、C、D、F是空间中的活跃对象,B、E、G是三块空闲区域,假设B的大小为20,E的大小为36,G的大小为60,他们为空闲链表统一管理。现有两个分配请求,都需要分配30的空间,那么需要将空间进行分割,才会满足,如下图:

E空间的大小为36,满足分配要求,分配后E所剩余的空间很小了,分配器不再将其加回空闲链表,因此E和F间就产生了一块内存碎片。第二个请求在G区域进行分配,分配完成后G所剩余的空间还比较大,因此分配器会把分割后的空闲区域,也就是H挂回空闲链表。上图中显示了分配完成后堆中的最终结果。

标记-清除(Mark-Sweep)标记阶段(Mark)

讲完空闲链表,接着说Mark-Sweep,从第一阶段开始,即Mark阶段。Mark阶段核心任务是从根引用出发,根据引用关系进行遍历,所有可遍历的对象就是活跃对象。我们需要一边遍历一边将活跃对象记录下来,遍历方式采用BFS和DFS两种策略。当遍历完成后,就找到了所有的活跃对象。比如最开始的那段代码,当showName退出执行后,调用栈和堆空间如下:

从图中可以看到,当showName执行完后,ESP指针向下移动,指向show的执行上下文,这时候遍历调用栈,找不到1003的引用地址,便会将这块地址标记为垃圾数据。1005这块数据被b引用,标记为活跃对象。那么活跃对象是怎么被标记的呢?常见的方法有两种:

一种是在每个对象前面增加一个机器字,采用其中的某一位作为"标记位",如果该位置位,就表示这个对象是活跃对象;如果该位未置位,那么表示这个对象是要回收的对象;

另一种方法采用标记位图,将每一个机器字映射成为图中的一个比特。在真正实现时,一般会采用两个位图,一个标记活跃对象的起始位置,另一个标记活跃对象的结束位置;

清除阶段(Sweep)

Sweep阶段将非活跃对象清除,也就是将垃圾对象占用的空间回收起来,重新将他们放回空闲链表中。具体做法是,按照空间顺序,从头到尾扫描整个空间的所有对象,将非活跃对象的空间回收到空闲链表。下面图示简单展示了回收过程,如图:

从图中可以看到,标记清除后,堆空间中会产生大量不连续内存碎片,而这些内存碎片会导致大对象无法分配到连续的存储空间。因此又出现了标记-整理(Mark-Compact).

标记-整理(Mark-Compact)

标记整理中的标记算法和标记清除算法中的标记算法一样,整理阶段对活跃对象移动到另外一端,然后清理调边界外的内存。如下:

这里只简单讲了如何标记整理的过程,实际上,底层算法是很复杂的,感兴趣的可以了解下,G1GC的分区回收算法,大概意思是新生代和老生代不再是一块连续的空间,整个堆被划分成若干个大小相同的Region。Region的类型有Eden、Survivor、Old、Humongous四种,而且每个Region都可以单独进行管理。具体挺复杂的,就不在这儿细讲了。

全停顿(StopTheWorld,SWT)

早期Mark-Sweep算法在标记活跃对象时,会将业务线程都停下来,以便垃圾回收器标记活跃对象,假如页面正在运行动画,标记所需时间为1s,那么就会出现屏幕卡顿现象,把这种情况叫做全停顿或世界停止(StopTheWorld)。为了解决垃圾回收造成的卡顿问题,后使用了增量标记算法(IncrementalMarking)。v8将标记过程分为一个个子标记过程,同时让JavaScript代码和垃圾回收标记交替进行,直到标记完成。如下图所示:

使用增量标记算法,可以把一个完整的垃圾回收任务拆分成若干个小的任务,每个任务耗时很短,将这些任务穿插在JavaScript代码中间执行,这样就避免了全停顿问题。

总结

本文开始讲了声明变量在内存中是如何存储的,基础类型存在栈空间中,引用类型存在堆空间中。

栈空间的垃圾回收器,JavaScript会通过向下移动ESP的方式来销毁该函数在内存中的执行上下文。如果被回收的执行上下文中,存在堆空间地址引用,对应存储在堆空间中的对象会被标记为垃圾数据。

堆空间回收机制分为两部分,新生代和老生代:

新生代的空间大小只有1-8M,用于存放内存占用小,存放时间短的对象,使用scavenge算法进行垃圾回收,将新生代的空间等分为两个部分,一个是对象空间,一个是空闲空间,当新对象进来时,会进入对象空间存储,对象空间存满后会进行一次垃圾回收,将对象空间中的对象进行标记,活跃对象和非活跃对象,活跃对象复制到空闲空间,有序排列,非活跃对象回收,然后将对象空间和空闲空间交换,这样就能无限进行下去;

新生代中进行两次复制的对象和空间占用率大于25%的对象会根据晋升策略晋升到老生代中;

老生代开始讲了空闲链表,用一个空闲链表来管理空闲空间,有空间请求,都需要向空闲链表申请,根据申请空间的大小进行分割、分配;

然后讲了标记-清除(Mark-Sweep)算法,标记清除后会造成大量不连续的内存碎片,因此使用了标记-整理(Mark-Compact)算法,将内存整理为连续的空间;

最后全停顿(Stop-The-World),使用增量标记算法(IncrementalMarking)解决全停顿问题。

对于其他语言的垃圾回收器,如Java,可能会有异同,Java中也使用了MarkSweep,新生代老生代等概率,但垃圾回收涉及分代垃圾回收算法,跨代引用,并发标记算法等和v8的垃圾回收器有一定区别,感兴趣的可以自行去了解。

原文:https://juejin.cn/post/7099370331025965093
声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
易企秀如何分享链接给微信好友?-易企秀分享链接给微信好友的方法 华为荣耀3c系统辅助功能服务老是自动关闭,求解决方案。 我用一键清理忽... 华为荣耀3c手机双击唤醒屏幕不好用了怎么办? 荣耀手机刷机包从哪下 木地板为啥走过会响 瓷砖块要斜成几度角人踩上去不会烂 刚出生的婴儿肚子胀怎么办 新生儿涨肚子按摩图 刚生小宝宝胀气怎么办 白果怎么炒好吃 白果可以制作哪些美食 csweep是什么意思? fancji是什么金 金子中f是什么意思? 莲子一次吃多少才符合健康饮食? 新鲜莲子一次吃多少合适 新鲜莲子每次吃多少合适 米白色的V 领针织马甲适合搭配哪些款式的上衣? 狗狗身上臭味很重怎么办 ...本人今年刚毕业,电子信息工程专业,现在从事低压配电柜设计,我想一 ... 狗身上怎么有股腥味? 关于电子信息工程、电子商务类的问题 电容型验电器测试单位 hm²什么意思? 如何查看自家wifi连接了多少人 空调多久要加雪种,加几次? 空调制冷雪种加多少 用vs 2005(VB.net)编写软件如何做成安装文件,给用户安装,用户没有装Vs... vs2005 c#,更新数据库 二手货车不过户可以吗? 二手货车不过户有什么影响 vs2005可以写C语言么?不会用这个软件啊!谁教程发给我 wang626yahoo.com... 转让大货车注意事项 转让大货车要注意什么 终止合同的理由怎么写 引起合同终止的原因有哪些 开一家会计师事务所需要哪些条件 注册会计师事务所需要什么条件 出血性脑卒中的预防 缺血性脑卒中一级预防 找中介卖房子注意事项 脑卒中如何进行预防? 脑卒中如何预防? 脑卒中的二级预防 脑卒中的危险因素及预防 脑卒中应该如何预防? 怎么做百吃不腻的反沙芋头? 九九云围棋是什么意思? 刘阔工笔花鸟画技法·花卉篇内容简介 中国工笔花鸟画画法图解内容简介 数字ic学习资料 1946年第一台计算机是谁发明的 交通法酒驾怎么进行处罚的? 关于转发通知范文