当前位置: 首页 > Web前端 > JavaScript

V8引擎的JavaScript内存机制

时间:2023-03-27 10:45:53 JavaScript

对于前端攻城高手来说,JS的内存机制不容忽视。如果你想成为行业专家,或者创建一个高性能的前端应用,你必须搞清楚JavaScript的内存机制先看栗子函数foo(){leta=1letb=aa=2console.log(a)//2console.log(b)//1letc={name:'掘金'}letd=cc.name='慕华'console.log(c)//{name:'Muhua'}console.log(d)//{name:'Muhua'}}foo()可以看出,我们修改不同数据类型的值后,得到的结果有点不同。这是因为不同的数据类型存储在内存中的不同位置。JS执行过程中,内存空间主要有3个:代码空间、栈、堆。我的另一篇文章有??详细介绍。我们来看看栈、栈、堆。在JS中,每条数据都需要一块内存空间。而不同的内存空间有什么区别特征呢?,如图所示,调用栈也叫执行栈。它的执行原则是先进后出,最后执行的会先出栈,如图栈:存储基本类型:Number、String、Boolean、null、undefined、Symbol、BigInt存储使用方法后进先出(像瓶子一样,后放的先取出)自动分配内存空间,自动释放,占用固定大小的空间存放引用类型变量,但实际上保存的不是变量本身,而是指向对象的指针(存储在堆内存中的地址)和所有方法中定义的变量都存储在堆栈中。方法执行后,该方法的内存栈自动销毁。可以递归调用方法,这样随着栈深度的增加,JVW会保持很长的方法调用轨迹,内存分配不足,会导致栈溢出堆:存储引用类型:对象(Function/Array/Date/RegExp)动态分配内存空间,大小是可变的,不会自动释放堆内存中的对象会因为方法执行结束而销毁,因为可能被另一个变量引用(传参等)为什么有栈和堆是通常与垃圾收集机制有关。每个方法在执行时都会创建自己的内存栈,然后将方法中的变量一一放入这个内存栈中。随着方法执行结束,该方法的内存栈也会自动销毁,以尽量减少程序运行时占用的内存,栈空间不会设置太大,堆空间要大。每次创建一个对象时,该对象都会被保存在堆中以供重复使用。即使方法执行结束,对象也不会被销毁,因为有可能被另一个变量引用(传参等),直到对象没有引用时才会被系统的垃圾回收机制销毁。而JS引擎在程序执行过程中需要使用栈来维护上下文的状态。如果所有的数据都在栈中,如果栈空间很大,会影响上下文切换的效率,进而影响整个程序的执行效率。内存在不再使用时“自动”释放,这种自动释放内存的过程称为垃圾回收。正是因为垃圾回收机制的存在,导致很多开发者在开发的时候不太关心内存管理,导致在某些情况下会出现内存泄漏。内存生命周期:内存分配:当我们声明变量、函数、对象时,系统会自动为它们分配内存。内存使用:即读写内存,即使用变量、函数、参数等。函数执行完后,没有其他引用(闭包),变量会被回收。直到浏览器卸载页面,全局变量的生命周期才会结束。也就是说,全局变量不会被垃圾回收。内存泄漏程序需要内存才能运行。对于一个持续运行的服务进程,必须及时释放不再使用的内存,否则内存占用会越来越大,影响系统性能,严重的会导致进程崩溃。不再使用的内存造成内存浪费来判断内存泄漏。在Chrome浏览器中,可以通过以下方式查看内存使用情况:开发者工具=>性能=>检查内存=>点击左上角的记录=>页面运行后点击停止则显示这段时间的内存使用情况。检查一次内存使用情况后,再看一下当前的内存使用趋势图,趋势是在上升的。可以认为存在内存泄漏。多次查看内存使用情况后,对比截图,每次对比。如果内存使用率呈上升趋势,也可以认为存在内存泄漏。在Node中使用process.memoryUsage方法查看内存情况console.log(process.memoryUsage());heapUsed:已使用的堆部分。rss(residentsetsize):所有内存使用,包括指令区和堆栈。heapTotal:“堆”占用的内存,包括已用和未用。external:V8引擎内部C++对象占用的内存,根据heapUsed字段判断内存泄漏。什么情况下会导致内存泄漏?全局变量在没有声明的情况下意外创建。引用会一直保留在内存中关闭DOM操作引用(比如引用td但删除整张表,内存会保留整张表)。如何避免内存泄露所以记住一个原则:没有使用的东西要及时归还。借归,减少不必要的全局变量并不难,比如使用严格模式避免创建意外的全局变量,减少生命周期长的对象,避免过多的对象在使用完数据后及时解引用(闭包变量)、DOM引用、定时器清零)来组织好逻辑,避免死循环导致浏览器卡顿,垃圾回收崩溃。JS有自动垃圾回收机制,那么这个自动垃圾回收机制是如何工作的呢?回收执行栈中的数据看栗子functionfoo(){leta=1letb={name:'Muhua'}functionshowName(){letc=2letd={name:'Muhua'}}showName()}foo()执行过程:JS引擎首先为foo函数创建一个执行上下文,并将执行上下文压入执行栈。因此在堆栈中,showName被压在foo之上,然后showName函数的执行上下文首先被执行。JS引擎中有一个记录当前执行状态的指针(ESP),会指向正在执行的上下文,也就是showName执行完后,执行流程进入下一个执行上下文,也就是foo函数,此时需要销毁showName执行上下文。主要原因是JS引擎将ESP指针下移,指向了showName下的执行上下文,也就是foo。这个down操作就是破坏showName函数执行上下文的过程。如图所示,回收堆中的数据其实就是把那些不再使用的值找出来,然后释放它占用的内存。比如刚才的栗子,当foo函数和showName函数的执行上下文都被执行时,会被清理掉,但是里面的两个对象还是占空间的,因为对象的数据存放在heap,而且只有清理过的栈中的对象引用地址,没有对象数据,这就需要垃圾回收器了。垃圾回收阶段最困难的任务就是找到不需要的变量,所以垃圾回收算法有很多,但没有一种适合所有场景。需要根据场景。权衡选择引用计数引用计数是以前的垃圾回收算法。算法定义“不再使用内存”的标准很简单,就是看一个对象是否有对它的引用,如果没有其他对象指向它,就说明这个对象不再需要了,但是它有一个致命的问题:循环引用是指如果两个对象相互引用,虽然不再使用,垃圾回收也不会回收,从而导致内存泄漏。为了解决循环引用带来的问题,现代浏览器不使用引用计数。在V8中,堆分为两个区域:新生代和老年代。分为新生代(二级垃圾收集器)和老年代(主垃圾收集器)两部分。新生代通常只支持1~8M的容量,所以主要存放生存时间短的对象。新生代中使用ScavengeGC算法将新生代空间划分为两个区域:对象区和空闲区。如图所示:顾名思义,这两个空间只有一个被使用,另一个是免费的。工作流程如下:新分配的对象存放在对象区。当对象区域已满时,会启动GC算法,标记对象区域中的垃圾。标记完成后,对象区中存活的对象将被复制到空闲区中,不再使用的对象被销毁。这个过程复制完成后不会留下内存碎片,然后用free交换object区。不仅垃圾被回收利用,新生代中的两个区域也可以无限次重复使用。因为新生代中的空间不大,很容易被填满,所以经过两次垃圾回收还活着的对象,如果空闲空间中的对象占到该对象的25%以上被移动到老年代空间,为了不影响内存分配,对象会被转移到老年代空间。在老年代对象中,使用了标记清除算法和标记压缩算法。因为如果同时使用ScavengeGC算法的话,复制大对象会花费更多的时间。标记清除在以下情况下会优先启动标记清除算法:当空间中对象过多,超过一定的空间容量限制时,该空间将无法使用。当新生代中的对象转移到老年代时,标记清除过程如下:从根(js的全局对象)开始,遍历堆中的所有对象,然后对存活的对象进行标记。标记完成后,销毁未标记的由于垃圾回收阶段,对象会暂停JS脚本的执行,等垃圾回收完成后再恢复JS执行。这种行为称为stop-the-world。例如,如果堆中的数据超过1G,那么完成垃圾回收可能需要1秒以上的时间。在此期间,JS线程的执行会被挂起,从而导致页面性能和响应速度下降。增量标记因此在2011年,V8从停止世界标记切换到增量标记。使用增量标记算法,GC可以将回收任务分解成很多小任务,穿插在JS任务中执行,从而避免出现应用卡顿和并发标记的情况。然后在2018年,GC技术又有了一个重大突破,就是并发标记。在允许GC扫描并标记对象时,允许JS同时运行标记压缩会导致堆内存出现内存碎片。当碎片超过一定限度时,将启动标记压缩算法,将幸存的对象移动到堆的一端。移动完所有对象后,清理不需要的内存