先简单说说在js中数据是怎么储存的?
原始数据类型是存储在栈空间中的,引用类型的数据是存储在堆空间中的,通过这种分配方式来解决内存分配的问题
不过有些数据被使用之后,可能就不再需要了,我们把这种数据称为垃圾数据。如果这些垃圾数据一直保存在内存中,那么内存会越用越多,所以我们需要对这些垃圾数据进行回收,以释放有限的内存空间。
不同语言的垃圾回收机制
手动回收
代表语言:C/C++
//在堆中分配内存
char* p = (char*)malloc(2048);;
//在堆空间中分配2048字节的空间,并将分配后的引用地址保存到p中
//使用p指向的内存
{
//....
}
//使用结束后,销毁这段内存
free(p);
p = NULL;
没有主动调用 free 函数来销毁情况就被称为内存泄漏。
自动回收
代表语言:JavaScript、Java、Python
产生的垃圾数据由垃圾回收器来释放,而不需要手动通过代码释放
调用栈中的数据是如何回收的
function foo(){
var a = 1
var b = {name:"极客邦"}
function showName(){
var c = 2
var d = {name:"极客时间"}
}
showName()
}
foo()
当执行到第 6 行代码时,其调用栈和堆空间状态图如下所示:
从图中可以看出,原始类型的数据被分配到栈中,引用类型的数据会被分配到堆中。当 foo 函数执行结束之后,foo 函数的执行上下文会从堆中被销毁掉,那么它是怎么被销毁的呢?
执行到showName函数时,那么 JavaScript 引擎会创建showName 函数的执行上下文,与此同时。还有一个记录当前执行状态的指针(称为ESP) ,指向当前调用栈中的执行上下文
,对应上面这个代码块摧毁 showName函数执行上下文,指针下移到foo函数执行上下文。
如下图:
在了解垃圾回收器之前先了解两个概念
代际假说和分代收集
代际假说(The Generational Hypothesis) ,这是垃圾回收领域中一个重要的术语,后续垃圾回收的策略都是建立在该假说的基础之上的,所以很是重要。
两大特点
- 第一个是大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问;
- 第二个是不死的对象,会活得更久。
分代收集
在 V8 中会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。
新生区通常只支持 1~8M 的容量,而老生区支持的容量就大很多了。
- 副垃圾回收器,主要负责新生代的垃圾回收。
- 主垃圾回收器,主要负责老生代的垃圾回收。
堆中的数据是如何回收的
通过上面的讲解,我想现在你应该已经知道,当上面那段代码的 foo 函数执行结束之后,ESP 应该是指向全局执行上下文的,那这样的话,showName 函数和foo 函数的执行上下文就处于无效状态了,不过保存在堆中的两个对象依然占用着空间,如下图所示:
从图中可以看出,1003 和 1050 这两块内存依然被占用。要回收堆中的垃圾数据,就需要用到 JavaScript 中的垃圾回收器了。
垃圾回收器执行流程
-
标记空间中的活动对象和非活动对象
-
回收非活动对象所占据的内存
-
内存整理,频繁回收对象后,内存中就会存在大量不连续空间,我们把这些不连续的内存空间称为内存碎片。
这步其实是可选的,因为有的垃圾回收器不会产生内存碎片,比如接下来我们要介绍的副垃圾回收器。
副垃圾回收器
副垃圾回收器主要负责新生区的垃圾回收。
新生代中用 Scavenge 算法来处理。所谓 Scavenge 算法,是把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域,如下图所示:
新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。
在垃圾回收过程中,首先要对对象区域中的垃圾做标记;标记完成之后,就进入垃圾清理阶段,副垃圾回收器会把这些存活的对象复制到空闲区域中,同时它还会把这些对象有序地排列起来,所以这个复制过程,也就相当于完成了内存整理操作,复制后空闲区域就没有内存碎片了。
完成复制后,对象区域与空闲区域进行角色翻转,也就是原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域。这样就完成了垃圾对象的回收操作,同时这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去。
由于新生代中采用的 Scavenge 算法,所以每次执行清理操作时,都需要将存活的对象从对象区域复制到空闲区域。但复制操作需要时间成本,如果新生区空间设置得太大了,那么每次清理的时间就会过久,所以为了执行效率,一般新生区的空间会被设置得比较小。
也正是因为新生区的空间不大,所以很容易被存活的对象装满整个区域。为了解决这个问题,JavaScript 引擎采用了对象晋升策略,也就是经过两次垃圾回收依然还存活的对象,会被移动到老生区中。
主垃圾回收器
主垃圾回收器主要负责老生区中的垃圾回收。
老生区对象特点:
- 占用空间大
- 存活时间长
垃圾回收方法
-
标记 - 清除(Mark-Sweep) 算法
对一块内存多次执行**标记 - 清除算法**后,会产生大量不连续的内存碎片。而碎片过多会导致大对象无法分配到足够的连续内,于是又产生了下面这种算法
- 标记 - 整理(Mark-Compact) 算法
全停顿
V8执行垃圾回收算法需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)。
为了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,我们把这个算法称为增量标记(Incremental Marking)算法。
图示如下:
思考Tips:如何判断 JavaScript 中内存泄漏的?
chrome的performance观察一段时间,要是内存一直呈现上升趋势,就有可能是内存泄漏了。 最近工作中就遇到同事,对象自引用造成了栈溢出报错。
题外话:
- chatgpt输入: 描述一个电脑引擎在垃圾回收的画面
- 生成上下语境英文版:This is a futuristic image portraying the intricate mode of a computer engine while recycling. The camera focuses on the core of the engine, where parts are constantly moving and radiating green energy light and faint electronic noises. The recycling algorithm is running continuously, seemingly separating and allocating waste into different folders and directories. In this blue and black world, the engine gradually controls, guides, and erases the useless data.
- 复制到midjourney中:生成了本文章的封面