大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。
Jetbrains全系列IDE稳定放心使用
文章目录
前置知识
堆栈
栈
什么是栈
栈其实是一种数据结构,有着先进后出,后进先出
的特性,用生活中的事物来理解最形象的就是汉诺塔了。我们在栈中存储的数据就像汉诺塔的盘子一样,最先放进去在最下面,最后放入的盘子在最上面。我们想拿数据的时候,也需要从塔顶开始拿,也就是最后放入的开始,上面的拿完才能拿下面的。
下图可以看做有三个栈
简而言之,我们可以将栈理解为一个具有先进后出,后进先出
特点的存储空间,对于JavaScript来说,它会把基本数据类型放入栈内存储。
堆
什么是堆
当我们创建一个对象的时候,实际上会在堆空间开辟一个空间,我们声明的变量保存的其实是堆空间的地址
简而言之,堆空间是存放复杂数据类型的存储空间,我们通过变量存储的其实是这些数据在堆空间内的地址。
当我们将一个本来存有地址的变量设置为null时,本质上是将该变量与堆空间的联系斩断,但堆空间内仍存有之前的复杂数据类型。只有当垃圾回收机制执行时,才会将这些没有人引用的复杂数据类型销毁,释放出堆空间。
执行上下文与作用域链
执行上下文
在JavaScript中有三种上下文
-
全局执行上下文:Global Code
JavaScript代码开始运行的默认环境 -
函数执行上下文:Function Code
存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文。 -
Eval 函数执行上下文: 使用eval()执行代码,因为很少用所以本次不做讨论。
在JavaScript代码执行的过程中,默认进入的总是全局执行上下文,JavaScript会把其存入上下文栈中去,每当遇到定义的函数被执行,便会创建一个新的执行上下文,并将其存入上下文栈中去。
当一个函数执行完毕后,上下文栈会将其弹出,将上下文环境交还给上个函数。
作用域链
上下文栈会根据栈内的顺序形成一条作用域链,用来控制变量的访问。处于上层作用域链的函数内部无法访问下层作用域链的变量。
下层作用域链中的函数可以访问上层作用域链的对象,若上层也没有,则再向上查找,直到全局作用域也没有,则返回null。
一、JavaScript中怎么被定义为垃圾
使用局部变量
function makeTrash(){
var a = 1;
}
// 在makeTrash执行时,会创建a这个变量。
// 此时栈空间会为其分配一块区域供其存储。
makeTrash();
// 函数执行完后,由于不再需要变量a,所以此时的a便成为了垃圾
// 栈空间将会释放之前为a分配的空间。
// 至此,垃圾回收完毕
使用对象
// 此时的obj指向堆内存中创建的一块空间
var obj = {
name:'ZhangSan',
age:'18'
}
obj = null
// 当我们对其赋值为null的时候,obj与堆内存中的空间的关系被斩断。
// 由于堆内存中的空间没有人引用,所以这块空间就成了JavaScript中所谓的的垃圾
概括
凡是未被引用的变量或对象,都会被视为垃圾。
可能成为垃圾的特例
全局变量
由于全局对象window的销毁一般发生在页面卸载时,所以对于全局变量是否为垃圾很难进行判断,所以要尽量少用全局变量,或在用完设置为null。
闭包
在闭包中,由于返回的函数对于变量持有引用,垃圾回收机制也无法对外层函数中被引用的变量进行回收,所以需要手动把接收闭包返回值的对象设置为null。
二、两种回收策略
JavaScript中垃圾回收机制的策略分为两种
- 标记清理
- 引用计数
标记清理
当变量进入上下文时,会对其添加上 存在于上下文 的标记。当变量退出上下文时,对退出上下文的变量添加上退出上下文的标记
例如在一个函数中声明一个变量,该变量就会被标记为存在于上下文中。当函数执行完毕,上下文栈弹出该函数的上下文,其内变量添加 退出上下文的标记。
此种策略的垃圾回收机制在运行的时候,会对所有已存在于内存的变量进行标记。
之后垃圾回收机制会清除上下文中所有变量的标记,包括其引用的变量的标记也会在此被清除。
最后仍然被标记的变量,即为要回收的垃圾。因为没有地方引用他们。
引用计数
该种策略会对每个值记录它被引用的次数。声明变量并给他赋一个引用值时,这个值的引用数为1。如果同一个值又被赋给另一个变量,则引用次数+1。
类似的,如果保存对该值引用的变量被其他值给覆盖了,那么引用数-1。
当一个值的引用数为0时,就说明没有办法在访问到这个值了。此时被判断为垃圾,在下次垃圾回收机制执行时会释放引用值为0的值所占用的内存。
概括
本质上都是找到未被引用的值,从而在垃圾回收执行时释放其空间。
三、什么时候执行垃圾回收
不同浏览器的引擎执行垃圾回收的时机也不一样。
根据网上查阅的资料来看,对于大部分浏览器的引擎来说,我们无法人为的去控制什么时候进行垃圾回收,因为js并没有暴露出相关的接口供我们调用。
我们在MDN中可以看到一些相关的说明
不再需要内存时释放
大多数内存管理问题发生在这个阶段。此阶段最困难的方面是确定何时不再需要分配的内存。 低级语言要求开发人员手动确定程序中哪个点不再需要分配的内存并释放它。 一些高级语言,例如 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,应用程序可以继续运行。
具体细节可参考
引擎V8推出“并发标记”,可节省60%-70%的GC时间
四、内存问题
内存泄漏
什么是内存泄漏?
内存泄漏指的是在执行垃圾回收的时候, 由于一些原因导致本应释放掉的空间没有被释放掉。
常见的内存泄漏
循环引用
在浏览器早起采用引用计数法的时候,如果两个变量相互引用,则其引用数始终为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。
内存溢出
内存溢出是一种程序运行的错误。指的是当程序运行需要的内存超过了剩余内存的时候,就会抛出内存溢出的错误。
内存泄漏积累过多时,就会导致内存溢出。
频繁的垃圾回收
通过上文我们可以知道,早期的垃圾回收是单线程的,执行时会引起主线程的停顿。过于频繁的垃圾回收会造成程序的卡顿。
后来加入的并行标记和并发标记都是为了解决这个主线程卡顿的问题,但是否被现在的主流浏览器采用还是不太清楚。
五、Es6 WeakMap
为了解决内存泄漏这个问题,ES6添加了WeakMap
和WeakSet
两个数据结构。他们对于值的引用都是不计入垃圾回收机制的,所以名字里才会有一个Weak,表示弱引用。
基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。
参考文章
- 【译】V8 之旅: 垃圾回收器(想了解细节方面的可以看看)
- 原文(翻译的看起来可能会有些不易理解,建议结合起来看)
- javascript垃圾回收机制
- JavaScript执行机制之垃圾回收
- 引擎V8推出“并发标记”,可节省60%-70%的GC时间
- JavaScript 内存管理以及垃圾回收机制(引用计数、标记清除)
- 图解 JavaScript 垃圾回收 — 现代 JavaScript 教程
- MDN
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/184064.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...