JavaScript之浅谈内存空间
JavaScipt 内存自动回收机制
在JavaScript中,最独特的一个特点就是拥有自动的垃圾回收机制(周期性执行),这也就意味者,前端开发人员能够专注于业余,从而减少在内存的管理,提高开发的效率。
用户自定义的对象、函数,但这些都是我们肉眼不可见的,而是依靠在外部的媒介“内存条”中,自动垃圾回收的本质也就是找出已不再使用的变量、函数,释放其占用的内存空间
当不再需要某样东西时会发生什么? JavaScript 引擎是如何发现并清理它?
可达性
JavaScript 中内存管理的主要概念是可达性。
简单地说,“可达性” 值就是那些以某种方式可访问或可用的值,它们被保证存储在内存中。
固有的可达值(根)
-
本地函数的局部变量和参数
-
当前嵌套执行上下文其他函数的变量和函数
-
全局变量
如果引用或者引用链可以通过根访问到任何其他值,则认为该值是可访问的
实际上网页的元素就是由一个个对象构建成了一个dom树(特殊的图结构)(树结构是单向,图结构是双向的,)
通过JavaScipt提供的api我们可以找到页面上指定元素的对象,并对其进行操作
每一个DOM元素对象都可以看作是一个根,我们可以还可以访问自身元素的亲戚
-
父元素
-
兄弟元素
-
祖先元素
-
后代元素
单项引用
var user = {
name : 'EYS',
}
这里的箭头表示一个对象引用,全局变量 "user" 引用对象{name : 'EYS'} user对象中的name属性存储了一个基本类型的数据
但如果user的值被覆盖,则引用丢失
user = null;
现在user变成不可达的状态,没有办法访问之前的值,他们之间没有联系,就被JavaScript引擎发现他了!!!然后就把他丢到小黑屋去了,自动释放了它所占用的内存空间
// user具有对象的引用
var user = {
name: "John"
};
var admin = user; //引用传递
该对象仍然可以通过 admin 全局变量访问,所以它在内存中。如果我们也覆盖admin,那么它可以被释放。
相互关联的对象
function marry (man, woman) {
woman.husban = man;
man.wife = woman;
return {
father: man,
mother: woman
}
}
let family = marry({
name: "John"
}, {
name: "Ann"
})
产生的内存结构:
内存中的图片变成:
现在让我们删除两个引用:
delete family.father;
delete family.mother.husband;
仅仅删除这两个引用中的一个是不够的,因为所有对象仍然是可访问的。
输出引用无关紧要。只有传入的对象才能使对象可访问,因此,John 现在是不可访问的,并将从内存中删除所有不可访问的数据。
垃圾回收之后:
无法访问的数据块
family = null;
但是如果我们把这两个都删除,那么我们可以看到 John 不再有传入的引用:
“family”对象已经从根上断开了链接,不再有对它的引用,因此下面的整个块变得不可到达,并将被删除。
回收过程
标记清除 (推荐)
分为『进入环境』和『离开环境』
进入环境 : 指变量进入的执行环境
离开环境 : 指变量完成任务,离开了执行的环境
垃圾收集器会在脚本运行的时候给存储在内存中的所有变量都加上标记
它会去掉环境中的变量以及被环境中的变量引用的变量的标记
而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了
最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间
引用计数 (不推荐)
含义 : 跟踪记录每个值被引用的次数
-
当用户声明了一个变量并将一个引用类型值赋给该变量时,则这个值的应用次数就为1 【声明变量并赋值】
-
如果同一个值又被赋给另一个变量,则该值的引用次数加 1 【变量的值传递】
-
如果包含这个值引用的变量被覆盖了,则之前的值的应用次数减1 【覆盖变量之前的值】
-
当这个值的引用次数变成 0 时,则说明没有办法再访问这 个值了,因而就可以将其占用的内存空间回收回来。 【变量回收】
这种机制其实在js中并不常用,因为这种机制会产生循环引用的问题,『循环引用』指的是对象 A 中包含一个指向对象 B 的指针,而对象 B 中也包含一个指向对象 A 的引用。对于像js类的自动回收机制的语言来说,需要额外手动的去释放内存,其实并不友好。
在学习内存空间之前,我们需要对三种数据结构有一个清晰的理解。他们分别是堆(heap),栈(stack)与队列(queue)。
三种数据结构
一、栈(Stack)数据结构
JavaScript中并没有严格意义上区分栈内存与堆内存
如JavaScript的执行上下文(关于执行上下文我会在下一篇文章中总结)。执行上下文的执行顺序借用了栈数据结构的存取方式(也就是后面我们会经常提到的函数调用栈)。因此理解栈数据结构的原理与特点十分重要。
JavaScript的数据类型分为两种 : 基本类型,引用类型
我们可以简单粗暴的理解 基本类型数据是存储在栈,引用类型的数据是存储在堆中,等待变量建立引用关系
要简单理解栈的存取方式,我们可以通过类比乒乓球盒子来分析。如下图左侧。
基本特征为 : 先进后出,后进先出
二、堆(Heap)数据结构
堆数据结构是一种树状结构。它的存取数据的方式,则与书架与书非常相似。
书虽然也整齐的存放在书架上,但是我们只要知道书的名字,就可以很方便的取出我们想要的书,而不用像从乒乓球盒子里取乒乓一样,非得将上面的所有乒乓球拿出来才能取到中间的某一个乒乓球。好比在JSON格式的数据中,我们存储的key-value是可以无序的,因为顺序的不同并不影响我们的使用,我们只需要关心书的名字。
三、队列
队列是一种先进先出(FIFO)的数据结构。正如排队过安检一样,排在队伍前面的人一定是最先过检的人。用以下的图示可以清楚的理解队列的原理。