|
JavaScript 基本数据类型的值是存放于栈内存,引用类型分别存储在栈和堆中——栈里面存的就是对象在堆中的地址,而堆里面存的是对象里的数据。
通常我们会说:“name等于Joe”,但从技术上讲,name
等于一个内存地址,在这个地址空间保存着 Joe 这个值。
解释和执行 |
JS运行分为:解释和执行两个阶段。
解释阶段 |
- 词法分析、语法分析
- 可执行代码生成
- 作用域规则确定
在词法分析的阶段,JS引擎(如V8)会将变量名写入符号表中,其中每个变量标识符都是唯一的。变量标识符都指向栈中特定的地址,如果是基本数据类型,直接读取栈中该地址存储的值;如果是引用类型,栈中存放的则是该对象所在堆中的起始地址。
执行阶段 |
- 创建执行上下文
- 执行函数代码
- 垃圾回收
进一步了解内存的数据结构 |
以下内容是以阮一峰老师<JavaScript 的 this 原理>为导结合自己理解展开讲解的。
对象中的属性 |
var obj = { foo: 5 };
JS引擎在堆内存开辟一块空间存放{ foo: 5 }
,然后将对象的内存地址赋值给变量obj
。
原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。举例来说,上面例子的foo
属性,实际上是以下面的形式保存的。
对象中的方法 |
var obj = { foo: function () {} };
引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo
属性的value
属性。
正因为函数单独保存在内存中,那么它就可以被不同的环境(上下文)执行。
这些[[xx]]
是什么?
{
foo: {
[[value]]: 5
[[writable]]: true
[[enumerable]]: true
[[configurable]]: true
}
}
属性描述符 |
在 JavaScript 中可以通过Object.defineProperty()来为对象设置属性描述符。
从ES5开始,添加了对对象属性描述符的支持。现在JavaScript中支持6种属性描述符:
configurable
:设为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。enumerable
: 设置为true的时候该属性才会出现在对象的枚举属性中。value
: 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等),默认值undefined
。writable
: 设置为true的时候,value才能被赋值运算符改变。get
: 属性的 getter 函数。当访问该属性时,才会调用此函数,执行时不传入任何参数,但是会传入 this 对象。该函数的返回值会被用作属性的值。默认值undefined
。set
:属性的 setter 函数。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认值undefined
。
Object.defineProperty(obj, prop, descriptor)
使用Object.defineProperty()定义属性
-
通过Object.defineProperty()定义的属性,enumerable,configurable,writable这几个值默认为false
-
直接为对象动态添加属性,enumerable,configurable,writable这几个值默认为true
let cat ={name:'猫'}
Object.defineProperty(cat, "sound", {
value: "喵喵"
});
等价
let cat ={name:'猫'}
Object.defineProperty(cat, "sound", {
enumerable: false,
configurable: false,
writable: false,
value: "喵喵"
});
函数内定义的对象是在栈中还是在堆中 |
堆中!所有对象数据都保存在堆中。在哪定义只能说明变量的作用域,而不能确定变量值所在的内存区。
比如只是在函数中定义 let a=1;
那这是个临时变量是在栈中的,使用完自动销毁;
比如定义是let b={name:'Joe'}
那这个是分配在堆中的,使用完需要手动释放,防止内存泄漏。
突发奇想:对象动态添加属性时的内存 |
然后我突发又想到了一个问题?声明了对象之后再给对象添加新的属性,新属性存放在哪里?存放在对象里的话,对象是不是需要扩容以存放该值?或是建立新的对象,然后改变栈中存放的地址?
var person = {};
person.name='Joe';
查阅百般资料,未查到相关信息。也未找到具体文档证据支撑。以下是我个人的推测:
首先,新建对象会在堆中开辟了一块内存,哪怕初始化的是无值对象,它占据的内存大小也不会是0,而是一块有一定容量的内存空间。在为该对象新增属性时候,直接写入对象未使用的内存空间中。
对象的内存空间是做了算法控制的,然后通过加载因子识别到空间不足的时候,会将对象扩容。扩容就是在堆中开辟一块新的内存空间,将原来的数据拷贝到新的内存空间,与此同时,将这个新空间的首地址覆盖掉原首地址存放的对象数据(不是存对象了,改成对象所在的地址)。然后对于变量标识符而言,所指向的堆地址(栈中的存放的那个对象首地址)是没有变的。你懂得,二重指针。
|
不要误将内存泄露当成了内存溢出!没那么严重。
不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。
垃圾回收机制 |
垃圾回收就是找出不用的值,释放其占用的内存。
JS引擎设置都有特定的垃圾回收机制,使得垃圾收集器会每隔固定的时间段就执行一次释放操作。
垃圾发现算法(标记垃圾阶段) |
引用计数法(最简单) |
给每个对象添加一个计数器,当有地方引用该对象时计数器加1,当引用失效时计数器减1。用对象引用计数器是否为0来判断对象是否可被回收。
缺点:无法解决循环引用的问题。
如何使引用失效?变量赋值null
即可!let obj={}; obj=null;
//循环引用
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2
o2.a = o; // o2 引用 o
可达性分析法 |
该算法的基本思路就是通过一些被称为GC Root的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。
垃圾回收算法 |
标记清除法 |
从2012年起,大部分现代浏览器都使用了标记-清除垃圾回收算法。
算法分为“标记”和“清除”两个阶段。首先标记出所有需要回收的对象(可达性分析法),在标记完成后统一回收所有被标记的对象。
存在的不足:
- 标记和清除两个过程的效率都不高
- 标记清除之后会产生大量不连续的内存碎片,这种碎片太多会导致程序在后需的运行中无法找到足够大的连续空间而必须提前触发另一次垃圾回收机制
分代收集算法(V8在用) |
就是将内存空间分为新生代和老生代两种,然后采用不同的回收算法进行回收。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清除算法或者标记-整理算法来进行回收。