前言
js的内存模型相对比较简单,可以简单的分为堆内存和栈内存。本节主要讨论,js的内存模型,以及js如何做垃圾回收的。因为垃圾回收,其实对闭包有一定的思考意义。当然,我相信并不是所有人都能认识到这点。
内存模型
内存模型是代码中对硬件运行环境对一个抽象,其可以表示为执行过程中,变量和数据在实时内存中的一个表现。
在历史长河中,语言对内存对分配大体可分为三种,静态分配,堆分配,栈分配。
静态分配
静态分配是最早出现的内存分配模式。我们可以简单理解为,代码需要在最开始时候就确定所使用的内存空间,并且所占用的空间是固定的。这样既不需要内存执行时创建,也不需要摧毁,效率更高。当然,灵活性方面也很差,对于我们现在高级的语言来说。这种方式显得笨拙。
栈分配
栈分配是后来演变出来的一种比较灵活的内存分配方式。如果你记得前面讲到的动态作用域语言,那么你可以理解到,对于一开始只有栈分配类型的语言,他的作用域是跟着其内存分配走的,意思是:这种类型但分配语言,每次运行时,都是以一种活动记录方式压入系统栈。活动记录在你可以看成类似js执行上下文的东西。也就是说,这种分配方式特点是: 1. 程序所涉及到的内存需要提前预知,在运行时固定大小的内存作用类似栈结构的数据结构。 2. 由于其作用域是跟着活动记录存活走,固栈分配的内存摧毁会根据活动记录的销毁而销毁
堆分配
堆分配相对栈分配则更加灵活,堆分配克服了栈分配内存无法动态分配的缺陷。同时,堆分配使得作用域灵活性更高,比如js的闭包,在栈结构的设计中是无法达成的,但是用堆内存分配之后,闭包具有更灵活的生命周期,而非跟着执行上下文周期去走。
这种动态内存分配的特性,必然导致了很多其他问题的出现,比如,计算机不能像栈内存那样去确定该内存的存活周期。你可以理解为:堆内存的变量我们无法知道其该在什么时候摧毁。栈内存之所以简单在于其变量是跟着活动记录走。所以堆内存的出现,在c++这种语言中,需要人工分配,人工去释放。这样的意思是:程序对于自己分配的内存具有自己把握的权利,但是对于程序员自己分配的内存,只有程序员知道什么时候可以摧毁。
后来,类似java这种,借助算法的实现,从而可以固定时间去确定某些变量已经无法继续使用。从而用算法方式去回收它们。这正是我们后面讨论的垃圾回收算法。
js内存模型
在c++中,c++的内存模型会相对更复杂,会有全局静态存储区,堆栈等等。对于js来说,其模型会相对简单很多。可以简单分为堆内存和栈内存。
我们可以简单看看这张图片:
栈空间(Stack)
在v8中,每一个js进程会有自己管辖的栈空间,内存中的栈空间可以抽象成类似数据结构的后进先出的结构。这样的内存空间,在物理上表现为连续固定的区域,在运行时表现为后进先出。比如我们之前提到执行上下文的概念,可以认为是对整个块环境的一个完整处理,那执行上下文中的数据以栈分配形式存储。
而在实际v8的运行过程中,栈空间的处理是类似于游标的指针决定。类似于下图的游标,很多时候我们对内存对处理不会像现实中那样,真正意义上对清理,游标下移,表示游标以上对空内存为空闲,这时候可以认为其已经出栈。在程序中,我们可以在用到时候覆盖这块内存,所以不需要显式处理
堆空间(Heap)
在v8中,对堆空间堆划分会比较详细很多,这也难怪,堆是具有灵活多变堆一种内存分配方式。在物理上表现为不一定连续不固定大小区域,在运行时,它可以根据代码上需要的空间,动态分配需要的内存。同时,最大的特点和问题,也是我们经常关注的内存回收问题。我们知道,在c++中,部分人为堆内存的分配是由人自己去释放。这是因为堆的灵活性,导致其回收也具有一定灵活性,无法像栈结构那样有确定的生命周期。
而在js中,语言的灵活性更高。好在js有灵活的算法去确定其是否需要回收,并且定时进行回收。
按v8的堆内存做划分: - 新生代(new space):新生代和老生代是我们的js代码所能分配的空间,是垃圾回收主要关注的区域,新生代主要存生命周期比较短的变量和内容。所以空间较小,但是变更频繁。 - 老生代(old space):老生代是生命周期较长的分配区域,空间大,不过相对变化比较不频繁。新生代和老生代在后面垃圾回收会主要讲解,他们垃圾回收关注的主要区域。 - 大对象空间(Large object space):超过内存限制的大对象会在这里开辟空间。 - 代码空间(Code-space):在我们前面提到,执行上下文中,会有些编译后的二进制可执行代码,函数的过程代码,大部分会存储在这个区域。有一部分也会存储在大对象空间。 - 细胞空间,属性细胞空间,map 空间(Cell space, property cell space, and map space):这些空间分别包含:Cells, PropertyCells, 和 Maps的数据结构。这个空间的大小结构是固定的。
js数据类型和内存模型的关系
通过上面的讨论,我们对内存对模型有个基本的认识。但是所有的知识,不能脱离js本身。那么在js中,我们对数据对使用,在内存中又是如何具体体现对呢?
首先,我们得知道js的数据类型有哪些,在js中,有八种基本的数据类型:
Boolean: true或者false
Number: 数字类型,固定64位空间。
String: 字符串类型
Undifined:一个没有值的空间,默认会是undifined。前面提到的变量提升之后,由于变量没有具体执行的内容,表现即为undifined。
Null: 就是null
Symbol: es6中新引入的一种类型
BigInt: 具有大空间的整型
Object: 引用类型基类,我们称它对象,函数,数组等特殊结构都是以它实现的。
在这八种类型中,Object我们叫引用类型,其他七种叫基本数据类型。这么区分的原因在于他们在内存分配上的区别导致的。
我们先简单的说,基本类型是存储在栈结构中,而引用类型存储在堆结构中。为什么要这么去划分,这是一个值得思考的问题。 这是因为,基本结构具有可提前预知,固定的数据大小,所以可以满足用栈存储的前提条件。而引用类型,他们的大小是会动态变化的,我们知道,堆的优点就是动态分配,所以引用类型需要借助堆动态分配的方式,灵活的变化空间。
这里我们用具体代码来看看:
var a = 100
a = a + 100
var b = []
b.push(1)
上面代码中,我们知道,对于number类型来说,64位固定大小空间,无论是100,还是200,都是用64位表示。所以当我们如果数字较大,就会溢出。这其实是固定位数大小的体现。而对于array类型来说,其存储的空间会不断真正意义上的变化。比如十个长度的数组和20个长度的数组其占用空间就不一样。
如此,array只能用堆动态分配才能满足其要求。
接下来,我们在看看两种不同类型如何用体现:
对于栈空间,变量a是基本数据类型,所以具有唯一内存标识,对应在栈里面的一个值。而对于变量b,执行上下文也会为其分配一定的栈内存,不过存储的是指向堆的一个内存区域,用于数组的存储,该数组是动态分配的。这样很好的看出,基本类型和引用类型在存储上的区别了。
包装类型
看过高级程序设计的书的同学应该知道,基本类型具有如引用类型一样特殊的方法,比如toString方法。相信大部分人,不会去过多思考这个东西,也许你们只是简单了解到它的特性,比如包装的周期很短,仅仅在于用到的语句间存活。但是其实包装类型体现了更多有趣的东西,正好验证了我们上面的说法。
这里先回顾下包装类型的特点
我们知道,基本类型都具有自己的构造函数,这个构造函数的原型是Object。所以他们的实例具有Object的方法,也具有一些自身的特殊方法。
但是我们前面又说到,基本类型是存储在栈内存,它的大小是固定的,它不应该有类型Object这种面向对象方式的灵活方法。这似乎是矛盾的,一方面它表现像实例,一方面它又是固定结构。
只有在这个层面上思考,你才真正能理解到包装类型的本质,包装类型的意思,真是如它名字表示的,包装。也就是说,在运行的时候,如果基本类型用了如对象一样的函数调用,那么会把基本类型做一个包装,让他表现的像一个对象。而与我们自己使用的对象类型不同,由于这个包装是编译器自己产生的,所以编译器会在其使用完之后,立即摧毁掉对应的对象。
如何理解这个行为,我们知道,垃圾回收是比较复杂的,对于这种编译器自己具有控制权,并且知道生命周期的堆内存空间,这种用完立即摧毁的方式是理所当然的。
我们可以用代码简单的去抽象这个行为:
var a = 1
console.log(a.toString())
a = a + 1
// 以上代码会转化称
var a = 1
var tem = a // 包装
a = new Number(tem) // 包装
console.log(a.toString()) // 包装
a = tem // 包装
a = a + 1
正如上面代码,a在用到toString的时候会进行包装,但是用完也会立即回收Number类型的包装。这样有利于更好利用堆空间。这里希望你在细细体会。
另外,在ts中,会对包装类型和基本类型做区别,体现为number和Number的类型。
最后,由于篇幅的原因,垃圾回收就放后面讲了,这里就先提出一个内存模型概念就好了。end
相关题目: 1. 1 为什么没有toString方法 2. 1 为什么能调用toString方法 3. 1 和 new Number(1)在内存占用上有什么区别 4. Number(1) 和 new Number(1) 的区别 5. ts中Number和number类型有什么区别
如需转载,请注明文章出处和来源网址:http://www.divcss5.com/html/h63277.shtml