在上文中我们讲到作用域,当申明一个变量时,变量在指定的作用域内能被正常使用,这时js引擎就需要给声明的变量分配一块独立的内存空间。当变量使用完之后,后续不需要再用到这个变量时,引擎又会帮我们清理掉这些无用的变量,来释放内存。在了解js内存前,我们首先来了解一下js的两种数据结构(栈与堆)和两种数据的基本类型(原始类型和引用类型)。
1、栈与堆
栈:栈是只能在一端插入或者删除的一种特殊的线性表,遵循后进先出的原则。先进入的数据压入栈底,后进入的数据在栈顶,读取数据时由栈顶往下读。能够操作的一端称为栈顶,另一端为栈底,栈底为固定,栈顶浮动。js中数组 push和pop实现栈的数据结构。栈的效率比较高(后文数据结构具体分析)。栈的简单实现如下图:
![52cece0df7a160e815e856b2f947058a.png](https://i-blog.csdnimg.cn/blog_migrate/b503f827d0f1b265dd43a09a96d44925.png)
堆:堆有两个特性:动态分配和释放程序所使用的对象。在不知道对象的大小和数量的情况下调用堆操作。堆的内存存储空间相比栈内存要大的多,但是相对的查询效率较低。
2、原始数据类型与引用类型
js中原始数据类型包括:Undefined、Null、Number、String、Boolean、Symbol(es6)、Bigint(es6)。
js 中引用类型包括:Object (Array)、Function
js中可通过typeof 查看变量的数据类型。根据上述的特点,我们很容易发现,在js引擎中,原始类型值适用存放在栈中,拥有高效的查询效率,不用占据太多的存储空间。引用类型适用于存放在堆中。在我们声明一个对象时,在栈中存储一个指针,指针指向堆中存储的对象。结构存储类似如图:
![61be556c3e4ec860a28779cca16bbc78.png](https://i-blog.csdnimg.cn/blog_migrate/b48b42af2f25c641ce56d8825338033f.jpeg)
因此当重新定义对象内部值时,同一指针指向的对象值跟着改变如:
![acf828a47c24872fd42ae5fa1d8da24f.png](https://i-blog.csdnimg.cn/blog_migrate/1bbd836d0cd909cb9459091c5c50b41f.jpeg)
3、js引擎内存清理
在js为变量分配了内存空间后,在我们使用完该变量时,接下来就是找出不再使用的变量,然后释放其占用的空间。在js引擎中内存管理是自动执行,且不可见的。一般js引擎内存清理有两种策略:标记清除法和引用计数法(已废弃,下文做简单介绍)
3.1、标记清除法:顾名思义当每个对象进入环境中便会被做一个标记,当对象不再使用时记录标记,垃圾回收器根据标记消除存储值,并回收内存。因此可分为标记阶段和清除阶段。
标记阶段:以某种方式可访问或可用的对象,都会存储在内存中,并做标记(又称作对象的可达性)。有一部分固有的可达值是没参与内存清理如:全局变量、嵌套在调用链上的其他函数的变量或参数等
清除阶段:当垃圾对象把内存占满时,开始执行垃圾回收,垃圾回收时应用程序执行暂停,垃圾回收器根据对堆内存进行线性遍历,查找内存中的被标记的对象,根据对象的可达性,判断是否清理该对象占用的内存,如果是清理其内存,并清理对象的可达性标记,方便进行下次清除。
3.2、引用计数法(老版IE浏览器的内存清理):跟踪每个值引用的次数,当变量被声明时,这个值的引用次数就是1,如果同一个值又被赋给另一个变量,则这个值得引用次数+1.相反,如果包含对这个值得引用的变量又取到另一个值,则这个值得引用次数-1,当该变量值为0时,标记为可清理对象。引用计数法缺点是无法清除 循环引用。
4、V8引擎中的内存清理
V8使用分代式垃圾回收机制,在v8引擎中,堆结构组成可分为:新生代、老生代、大对象区、代码区(存放代码,拥有执行权限)、Map区(官方解释存放大小相同的元素,俺也不懂)。
新生代:主要存放存活时间较短的对象,它有两个半空间构成(semisapce)构成内存最大值由操作系统决定(64位最大32M,32位最大16M)。由于新生带的处理频率较高,所以采用空间换时间的复制算法(Scavenge)来清理内存,大致原理如图:
![c8275d89a6a309e557c74a93fcca2cc4.png](https://i-blog.csdnimg.cn/blog_migrate/f11a19f063de773e619e8ab2dd513fcb.jpeg)
老生代:由上图当对象在新生代中多次复制,依旧存活时,在下一次进行垃圾回收时,将直接该对象移至老生代,这个过程称之为对象晋升。对象晋升需满足对象是否经历过一次新生代算法,且TO空间是否内存占比已超过25%。所以在老生代存储着大量的存活对象,因此需要更大的内存空间,管理也不能牺牲空间来换取时间,因此在老生代中采用标记清除及标记整理来管理内存。上述我们介绍了关于标记清除的原理,但是标记清除存在一个很大的弊端就是碎片化问题(进行一次标记清除后,内存空间存在不连续状态,出现碎片化问题),标记整理便是为了处理这个问题。具体流程如图:
![1dca4043e193fd22e3862c6591c7be22.png](https://i-blog.csdnimg.cn/blog_migrate/4aee4766106db3882fd9ff898f1040ca.jpeg)
当然在V8的标记清除法中,我们需要通过遍历所有的对象添加标记,在单线程机制的js中务必会影响js的主进程运行造成严重的卡顿,因此V8又引入了增量标记法(先标记堆中一部分对象,然后执行劝归还主线程,主线程执行完任务后,再从原来的标记位置,继续标记,原理类似react fiber架构)概念,之后又引入延迟清理和增量整理,提升了更多性能,V8引擎已经帮我们自动进行了内存的分配和管理,好让我们有更多的精力去专注于业务层面的复杂逻辑,但是随之带来的问题也是显而易见的,那就是由于不用去手动管理内存,导致写代码的过程中不够严谨从而容易引发内存泄漏 。
5、内存泄露:
一块内存不再被应用程序使用,但是未归还给操作系统或者内存池的现象。在js中通常可以通过少创建全局变量、少用闭包、及时清除定时器、弱应用等,可使用weakMap、weakSet等数据结构来解决内存泄露的问题。
总结:
本文介绍了关于内存分配和清理及V8引擎相关的知识,希望在开发的同时,能给同学们带来更多的帮助。
下一篇js灵魂整理(四):es6 相关新特性