为什么要管理内存
减少浏览器负担: 内存占用过大会让浏览器压力过大,导致浏览器卡顿
node端: 内存如果不够,服务就会中断,而nodejs开启的服务如果不管理内存,就会中断
内存如何存放数据
栈内存(stack) 主要用于存储各种基本类型的变量,包括Boolean、Number、String、Undefined、Null,以及对象变量的指针,基本数据类型占用空间小、大小固定,通过按值来访问,属于被频繁使用的数据。 栈内存也是用来存储执行上下文的调用栈
堆内存(heap) :动态分配的内存,大小不定也不会自动释放,存放引用类型,但是堆内存存储变量时没有什么规律可言,堆内存存储的对象类型数据对于大小这方面,一般都是未知的,它只会用一块足够大的空间来存储变量
为什么数据不能全部放在栈内存中?
因为 JavaScript 引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率;
通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据。而引用类型的数据占用的空间都比较大,所以这一类数据会被存放到堆中,堆空间很大,能存放很多大的数据,不过缺点是分配内存和回收内存都会占用一定的时间。
V8如何回收内存
V8有多大
64位下是1.4G
32位下700MB
但是根据浏览器不同,有些许扩容。node情况下会有一些C++内存扩容
栈内存如何回收
function foo() {
var a = 1;
var b = { name: "CR7" };
function bar() {
var c = "ball";
var d = { name: "Messi" };
}
bar();
}
foo();
上面代码先编译,并创建执行上下文,然后再按照顺序执行代码
foo函数执行上下文中:保存a变量和c变量在堆内存中的引用地址,bar函数执行上下文中:保存c变量和d变量在堆内存中的引用地址,所以a、c保存在栈内存中,b、d保存在堆内存中
当函数执行完,函数执行上下文就按照后进先出(LIFO)原则从调用栈中弹出,函数执行上下文中保存的基本数类型,如a、c就等待被回收,同时指向堆内存中数据的引用也没有了,堆内存中的引用类型数据也会被堆内存回收
堆内存如何回收
在 V8 中会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象 对于这两块区域,V8 分别使用两个不同的垃圾回收器,以便更高效地实施垃圾回收:
- 副垃圾回收器,主要负责新生代的垃圾回收。
- 主垃圾回收器,主要负责老生代的垃圾回收。
垃圾回收器的工作流程
V8 把堆分成两个区域——新生代和老生代,并分别使用两个不同的垃圾回收器。其实不论什么类型的垃圾回收器,它们都有一套共同的执行流程。
第一步是标记空间中活动对象和非活动对象。所谓活动对象就是还在使用的对象,非活动对象就是可以进行垃圾回收的对象。
第二步是回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。
第三步是做内存整理。一般来说,频繁回收对象后,内存中就会存在大量不连续空间,我们把这些不连续的内存空间称为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大连续内存的时候,就有可能出现内存不足的情况。所以最后一步需要整理这些内存碎片,但这步其实是可选的,因为有的垃圾回收器不会产生内存碎片,比如副垃圾回收器回收新生代内存。
新生代内存:存放短时间存活的新变量,内存量极小,64位下大概32MB
新生代回收算法:
把新生代空间对半划分为两个区域,一半是对象区域(就叫from吧),一半是空闲区域(就叫to吧)
如果a变量不用了,下次回收把b、c变量复制到to空间,然后清空from空间,然后对调from和to空间,由于复制需要时间成本,所以新生代内存不能太大,否则影响执行效率。典型的牺牲空间换时间
老生代内存:生存时间比较长的变量,几乎占据所有内存,64位下大概是1400MB
老生代回收分三步:标记已死变量,清除已死变量,整理磁盘
图中灰色代表已死变量,清除后,那两块灰色的内存不连续了,像数组需要连续的内存来存储, 所以需要整理磁盘,会花费比较多时间
新生代和老生代变量转化
新生代内存不大,容易被存活的对象装满整个区域。为了解决这个问题,JavaScript 引擎采用了对象晋升策略,也就是经过两次垃圾回收依然还存活的对象,会被移动到老生区中。
什么时候触发内存回收呢
执行完一次主线程代码
const a = "CR7"
const b = "Messi"
console.log(a)
setTimeout(()=>{
a = "kaka"
console.log(b)
// 把回调从宏任务队列取出来,放到主线程执行完后,回收一次
},2000)
// 主线程执行完,回收一次
内存不够的时候,怎么回收变量
全局变量直到程序执行完毕,才会回收
const size = 30 * 1024 * 1024
function testMemory() {
//node方法,获取目前使用的堆内存
const memory = process.memoryUsage().heapUsed
console.log(memory / 1024 / 1024 + 'MB');
}
const arr1 = new Array(size)
testMemory()
const arr2 = new Array(size)
testMemory()
const arr3 = new Array(size)
testMemory()
const arr4 = new Array(size)
testMemory()
const arr5 = new Array(size)
testMemory()
const arr6 = new Array(size)
用node执行上面代码,指定最大内存为1000,发现定义第五个变量内存不够了,但是程序没有执行完,又没有可以回收的变量,就报错了
普通变量,就是当他们失去引用
修改一下代码,定义局部变量
const size = 30 * 1024 * 1024
function testMemory() {
//node方法,获取目前使用的堆内存
const memory = process.memoryUsage().heapUsed
console.log(memory / 1024 / 1024 + 'MB');
}
const arr1 = new Array(size)
testMemory();
(function () {
const arr2 = new Array(size)
testMemory()
const arr3 = new Array(size)
testMemory()
})()
const arr4 = new Array(size)
testMemory()
const arr5 = new Array(size)
testMemory()
const arr6 = new Array(size)
在定义arr5时,内存不够了,arr3和arr2没有外部引用,就被回收了,arr5和arr6可以继续定义
那么如何优化内存呢
尽量不要定义全局变量,定义了及时手动释放
注意闭包
function foo() {
var myName = "CR7";
let test1 = 1;
const test2 = 2;
var innerBar = {
setName: function (newName) {
myName = newName;
},
getName: function () {
console.log(test1);
return myName;
},
};
return innerBar;
}
debugger;
var bar = foo();
bar.setName("Messi");
bar.getName();
console.log(bar.getName());
站在内存的角度来分析一下上面的代码:
当 JavaScript 引擎执行到 foo 函数时,首先会编译,并创建一个空执行上下文,
在编译过程中,遇到内部函数 setName,JavaScript 引擎还要对内部函数做一次快速的词法扫描,发现该内部函数引用了 foo - 函数中的 myName 变量,由于是内部函数引用了外部函数的变量,所以 JavaScript 引擎判断这是一个闭包,于是在堆空间创建换一个“closure(foo)”的对象(这是一个内部对象,JavaScript 是无法访问的),用来保存 myName 变量。
接着继续扫描到 getName 方法时,发现该函数内部还引用变量 test1,于是 JavaScript 引擎又将 test1 添加到“closure(foo)”对象中。这时候堆中的“closure(foo)”对象中就包含了 myName 和 test1 两个变量了。
由于 test2 并没有被内部函数引用,所以 test2 依然保存在调用栈中
当执行到 foo 函数时,闭包就产生了;当 foo 函数执行结束之后,返回的 getName 和 setName 方法都引用“clourse(foo)”对象,所以即使 foo 函数退出了,“clourse(foo)”依然被其内部的 getName 和 setName 方法引用。所以在下次调用bar.setName或者bar.getName时,创建的执行上下文中就包含了“clourse(foo)”。
产生闭包的核心有两步:第一步是需要预扫描内部函数;第二步是把内部函数引用的外部变量保存到堆中。
补充:node在内存管理中的特殊点
node可以手动触发垃圾回收:global.gc();
node端可以设置内存:node --max-old-space-size=1000 文件名 ,限制 V8 使用的内存量;
Node 提供的process.memoryUsage
方法来查看内存: process.memoryUsage()返回一个对象
- rss(resident set size):所有内存占用,包括指令区和堆栈。
- heapTotal:"堆"占用的内存,包括用到的和没用到的。
- heapUsed:用到的堆的部分。
- external: V8 引擎内部的 C++ 对象占用的内存。
补充:为什么V8要设计为1.4G
1.4G 对浏览器脚本已经够用
回收的时候是阻塞式的,垃圾回收需要时间,垃圾回收的时候会中断代码执行,所以为了减少阻断时间,设置为1.4G
参考: