内容是由一个很大的字节数组组成的,每个字节都有自己的地址;CPU只能从内存中加载指令来运行应用程序。
理想状态下,应用程序都应该永久存放于内存中,但由于内存可存储的数据太少,而且属于易失性存储(例如停电就会造成数据丢失),因此不可能实现这种情况。
不同的编程语言在执行代码的过程中都需要给他分配内存,不同的是有些编程语言需要开发人员手动实现,有些编程语言可以自动实现。
例如javascript在创建变量时自动分配内存,而变量不再引用时进行自动释放,释放的过程就是js的垃圾回收。
在js中,内存的生命周期主要有三个阶段:
1,分配内存:当我们创建变量、函数、对象等时,系统会自动为他们分配内存
2,内存使用:在调用变量或函数的时候,为内存的使用阶段
3,内存回收:使用完毕后由垃圾回收机制自动回收不再使用的内存
实例:
// 分配内存
var man = {
name: '张三',
age: 18
};
// 使用内存
console.log(max.age);
// 回收内存
man = null;
在js中,根据不同的数据类型分配不同的内存,例如基本数据类型存储到栈内存中,引用类型存储到堆内存中(详情可见博客数据类型与堆栈内存篇)
栈内存特点:
- 每次分配内存和内存回收都比较简单,即压栈和退栈速度较快
- 每次压栈里面的空间大小是固定的,因此里面的变量数目和数据结构大小也是固定的。
- 退栈时要立即释放空间,里面的变量将无法保存(如果需要保存,就要将数据存入堆内存中。例如闭包就是将变量存在堆内存中)
堆内存特点:
- 分配和释放空间需要大量的工作,例如寻找合适大小的内存,对不同的空间做垃圾扫描和回收等
- 访问速度较慢,运行代价较高,影响性能
V8引擎是谷歌开源的高性能引擎,使用c++编写,目前主要应用于Chrome和nodejs中。可以把V8引擎当做虚拟机,他的核心功能就是将javascript编译成CPU可以理解的二进制指令。
V8执行js代码是通过内部的解释器、编译器、抽象语法书生成等一系列复杂操作完成。
可以独立运行、也可以嵌入任何C++应用程序中,可以实现跨平台与操作系统
V8引擎在操作系统中有内存的限制,例如在64位系统下最多只能使用1.4GB左右的内存,在32位系统下最多只能使用0.7GB左右的内存。
执行上下文是一种规范,用来跟踪ECMAScript实现对代码运行时的评估。默认的上下文是全局执行上下文,他在编译阶段会创建一个window对象(node中是Global对象),且此时this指向全局对象。而
执行上下文的创建阶段,V8只扫描代码,并不执行。V8扫描代码并创建作用域、为作用域内变量与函数分配内存、初始化this的值。
创建阶段同时会创建一个词法环境和变量环境的组件。(这时候他们俩的值是一样的;执行上下文中的词法环境和变量环境始终都是词法环境)
词法环境:
词法环境就是一个规范类型,可以理解为ECMAScript的作用域Scope。他与执行上下文不同,他是静态的。他包含了一个环境记录(用来存储变量和函数声明的链表结构)和外部环境的引用(指向此法环境父级作用域的引用,用来实现作用域链)。
由上可知,因为V8引擎的内存空间限制,需要GC垃圾回收将不再使用后的内存或不可达的对象进行释放,回收空间。
引用计数:
他的原理是对每个值都记录他的引用次数,在声明一个变量时给他的引用计数赋值为1;如果这个值被赋给另一个变量,则引用数加1;同理,如果保存这个值引用的变量被其他值覆盖了,引用计数减1。当一个值的引用计数为0时,就会被立即回收。
对于引用计数方式,有个弊端就是两个对象通过自己的属性进行了互相引用,则这两个对象的引用计数就永远是2,永远不会被回收。
标记清理:
标记清理的原理是在变量进入上下文时(比如函数内部声明一个变量),将该[活动]变量增加一个存在于上下文中的标记。在上下文中的变量都是活动变量,不会被释放内存,而当变量离开当前上下文时,会被追加一个离开上下文的标记。
引擎在执行标记算法时,是从根对象开始深度遍历内存中的所有对象,标记不是垃圾的变量并清除有垃圾标记的变量。
标记清理虽然解决了循环引用的问题,但他的缺点是内存碎片化,容易造成资源浪费;不会立即回收。
V8的垃圾回收策略主要是基于分代式垃圾回收策略,根据对象的存活时间,将内存的垃圾回收进行不同的分代,采用不用的算法。
V8引擎的内存结构将堆内存分成了一个新生代内存和老生代内存。
新生代内存又细分为:
code代码区(唯一拥有执行权限的内存)
map区(存放Cell和Map,结构简单)
large Object大对象区(用于存放超出其他区域大小的对象。垃圾回收不会移动大对象区)
inactive new space(未激活的内存区域)
说明:
新生代的inactive new space未激活的区域称为To空间,其他的区域统称为新生代的激活区域From空间。这两个空间始终有一个处于使用状态,一个处于闲置状态。
新生代中主要存放存活时间较短的对象。他主要采用Scavenge算法。
原理:
在声明对象时,首先存入From空间;在进行垃圾回收时,会将From空间的存活对象复制到To空间中,非存活的对象进行回收(相当于清空了From空间),然后将To空间和From空间角色转换,To空间变成了新的From空间,From空间变成了新的To空间。
而在将From空间的活动对象复制到To空间之前需要进行检查,需要将存活时间长的对象移动到老生代内存中,完成对象晋升。否则才复制到To空间。
对象晋升的条件:
1,该对象经历过一次Scavenge回收
2,To空间的内存使用占比超出25%的限制
说明:
老生代因为存储着大量的存活对象,因此需要采用另一种算法Mark-Sweep(标记清除)和Mark-Compact(标记整理)来管理。
原理:
标记清除分为标记和清除两个阶段。标记阶段会遍历所有对象(构建一个根节点开始进行遍历),对活着的对象进行标记,清除阶段会将未标记的对象(即根节点不能达到的地方)进行清除回收。
而对于这种标记清除方法可以会导致清除后造成内存不连续,碎片化,因此标记整理算法主要用来解决这个问题。主要过程是整理阶段先将活动的对象移动到堆内存的一端,然后清理掉边界外的全部内存。
说明:
因为js是单线程的,因此垃圾回收时会阻碍主线程的同步执行,直到垃圾回收执行完毕再继续执行主线程的任务,这种行为被称为全停顿。
而Orinoco是V8垃圾回收器项目的代号,主要提供最新的回收技术来降低主线程的挂起时间,解决全停顿的问题。(例如增量垃圾回收、并行垃圾回收)。
增量垃圾回收:
他属于主线程间歇式执行。执行垃圾回收时,先标记一部分,然后将执行权交给主线程,主线程任务执行完之后再将执行权交给辅助线程继续进行标记垃圾回收。不过他也有缺点,例如主线程执行期间修改了标记过的对象。
延迟清理:
对于增量标记的回收方法,是每次只标记一部分,然后js执行一段。这样可能会出现刚标记一个活动对象,js执行时就将该对象的状态改变,变成非活动对象,反而造成了标记错误。因此如果将增量标记方法只用来做标记,而采用延迟清理的方式,可以在增量标记完成后,判断如果当前剩余内存足以用来执行完代码,就没必要立即清理内存,或者只清理一部分内存。
并发垃圾回收:
他属于主线程一直执行,而辅助线程在后头执行垃圾回收。并发线程的优势是完全不阻碍主线程的执行,但可能会造成他与主线程同一时间修改同一个对象,造成读写竞争;也可能造成修改时,堆内存中部分数据已经被主线程修改过,造成无效操作。