最近一直热衷于计算机组成原理,会抽空看下<<计算机组成原理>>和B站的视频,遇到了很多自己不明白的地方,然后在去查资料搜索相关的知识.
本文仅是对自己看到的想到的一些知识做一些总结和梳理,并不是一个全方位的系统的 内存管理, 垃圾回收机制,作用域 的讲解.
- javascript中的 基础数据类型 存储在内存中的 栈区,引用数据类型 存储在内存中的 堆区
- 现代浏览器 的垃圾回收机制是采用 标记清除法,ie8 以下是 引用计数法
- 全局变量,闭包,计时器 容易造成内存泄漏
如果能把上面的问题解释清楚,其实就对这三个知识点理解的差不多了
内存管理
内存中存储变量数据的地方有 栈 和 堆 两种
栈区 :编译器自动分配释放,存放局部变量,函数参数等
堆区: 存放引用类型数据,程序员手动分配释放,js中释放是由引擎的垃圾回收机制来做,程序员一般无需关心
TODO:配图
function fn(){
var a = '123'
// do something
}
fn()
程序执行fn函数,
先将变量a压入栈中,
fn函数执行结束后,会将指针下移,下次会把栈尾的值覆盖掉
所以,函数调用结束后,内存就会释放,
function fn(){
var a = {name: '123'} // 引用类型,存储在堆中
}
fn()
程序执行fn函数,
先将变量a(对象的地址)压入栈中,{name: ‘123’} 会存放在堆中,
fn函数执行结束后,将指针下移,下次会把栈尾的值覆盖掉
函数结束后释放a中存放的对象地址
注意:这时对象{name:‘123’}就会继续存放在堆中,等待回收
下面这句话是我自己理解的:
栈中内存空间的释放与垃圾回收无关,因为是即时释放的,无需定时回收
垃圾回收一般指的是堆中的垃圾回收
垃圾回收机制
TODO:垃圾回收的技术博客地址展示
MDN的概念
大多数内存管理的问题都在这个阶段。在这里最艰难的任务是找到“哪些被分配的内存确实已经不再需要了”。它往往要求开发人员来确定在程序中哪一块内存不再需要并且释放它。
自动寻找是否一些内存“不再需要”的问题是无法判定的。因此,垃圾回收实现只能有限制的解决一般问题。
哪些被分配的内存确实已经不再需要了,这个判断条件该如果定义
现在有两种解决方案:
- 引用计数
- 标记计数
引用计数
此算法把"哪些被分配的内存确实已经不再需要了"定义为 对象有没有其他对象引用到它
如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
var o = {
a: {
b:2
}
};
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o
// 很显然,没有一个可以被垃圾收集
var o2 = o; // o2变量是第二个对“这个对象”的引用
o = 1; // 现在,“这个对象”只有一个o2变量的引用了,“这个对象”的原始引用o已经没有
var oa = o2.a; // 引用“这个对象”的a属性
// 现在,“这个对象”有两个引用了,一个是o2,一个是oa
o2 = "yo"; // 虽然最初的对象现在已经是零引用了,可以被垃圾回收了
// 但是它的属性a的对象还在被oa引用,所以还不能回收
oa = null; // a属性的那个对象现在也是零引用了
// 它可以被垃圾回收了
限制:循环引用
该算法有个限制:无法处理循环引用的事例
function f(){
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2
o2.a = o; // o2 引用 o
return '123'
}
f();
标记计数
这个算法把“对象是否不再需要”简化定义为“对象是否可以从根部获得”
垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。
作用域链
问题:变量到底是怎么访问的,沿着作用域链的话,那就是存储在堆中了,那局部变量存储在栈中又是怎么得来的
执行环境
执行环境(也叫环境)是js中最为重要的一个概念,执行环境定义了 变量或函数 有权访问的其他数据,决定了他们各自的行为。
每个执行环境都有一个与之对应的 变量对象(VO),环境中定义的所有的 变量和函数 都保存在这个对象中。
我们编写的代码不行访问这个对象,但是解析器在处理数据是可以在后台使用它。
执行环境分为
- 全局执行环境
- 局部执行环境
全局执行环境 是最外围的一个执行环境,每个函数都有自己的执行环境,
当 执行流进入一个函数 时,函数的环境就会被推入一个环境栈中。
函数 执行结束 之后,栈将其环境弹出,把控制权交给之前的执行环境。
注意:某个执行环境中的代码执行结束之后,保存在其中所有的 变量 和 函数定义 也会随之销毁,全局执行环境 直到应用程序退出才会被销毁。
变量对象(VO)
如果变量与执行上下文相关,那变量应该知道自己的数据存储在哪里,并且知道如何访问,这种机制成为 变量对象。
变量对象是一个与执行环境相关的特殊对象,它存储这执行环境相关的以下内容:
1. 变量(var变量声明)
2. 函数声明
3. 函数的形参
活动对象(AO)
在函数的执行环境中,VO不能直接访问,此时由活动对象扮演VO的角色
活动对象 是进入函数的执行环境时创建的,它通过函数的 arguments 属性初始化的
[[scope]]
当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain),
作用域链的用途,是保证 执行环境 有权访问的所有变量和函数的 有序访问。
作用域链的前端,始终是当前执行的代码所在环境的变量对象。
闭包是如何形成的?
那么就会有一个问题:既然函数执行结束之后,局部变量就会出栈销毁,那么闭包是如何做到的呢?
这个跟作用域有关系
functin fn(){
var a = 1
var b = 2
return function (){
console.log(a)
}
}
var log = fn()
log()
这里的log的作用域链的上一级包含了fn的作用域链,因为fn的作用域链有被引用(可以访问到),所以是不会被垃圾回收机制回收掉,所以就形成了函数fn执行结束后,其作用域链仍然存在的效果。
闭包为什么会造成内存泄漏
其实说闭包会造成内存泄漏是不正确的,因为我们就是需要将函数外部的环境保留至未来某一时间使用,这个就是闭包的特性。
应该说:滥用闭包才会造成内存泄漏。
遗留未明白的问题:
- 函数的变量存在作用域链上,那么不是应该是在堆中么,这个跟基本数据类型存储在栈区有矛盾。应该怎么解释?
总结
这一块是js中比较复杂切庞大的内容,任何一个环节没有搞明白,都不会彻底理解它,搞明白之后就可以解释很多js的东西
- 变量声明提升,函数声明提升
- 值复制,引用复制
- 闭包相关的问题
- 作用域相关的问题
- 内存泄漏
- const,let的实质
- 一些奇奇怪怪的函数执行结果,都与作用域有关系