内存管理
为什么要管理内存
减少浏览器负担
内存过大会让浏览器压力过大,导致浏览器卡顿
Node 端 - 做服务
内存如果不够,服务就会中断
内存的数据存储
栈内存
线性的,后进先出
堆内存
非线性,不连续的
存储普通类型的变量 - 栈内存
let a = 1
let b = 2
- 先入栈 a-123
- 再入栈 b-10
变量直接指向值,变量和值放在一起
存储引用类型的变量 - 堆栈
let a = { a1: 123 }
let b = a
let c = [1, 2, 3]
let d = function() { console.log(123) }
{ a2: 666 }
- 声明引用类型变量 a 的时候,先在堆内存中开辟一个地址存放变量(变量本体),再把这个地址赋值给变量,即入栈 a-0x00000000
- a b 指向同一地址,所以入栈 b-0x00000000
- 同声明 a, 入栈 c-0x00000005
- 同声明 a, 入栈 d-0x00000008
- 直接定义一个引用类型,在堆内存中开辟一个地址存放变量(变量本体)
V8 内存的管理
V8到底有多大
64 位
下是1.4G(标准)
32 位
下是700MB (标准)
- 根据浏览器不同会有些许扩容
- 新版本 Node 也会有自动调用 C++ 内存进行内扩容(动态扩容)
新生代和老生代
比如我们 64 位
1.4G
的内存,分为两块
- 新生代:短时间存活的新变量会存在新生代中,新生代的内存量极小,
64 位
下大概是32MB
,32 位
则减半。新生代平均分成两块相等的内存空间,叫做semispace
,每块内存大小8MB(32位)
或16MB(64位)
- 老生代:存活时间比较长的变量会转存到老生代,老生代占据了几乎所有内存(常驻),
64 位
下大概是1400MB
新生代
新生代
使用 Scavenge GC 垃圾回收算法
,该算法实现时主要采用 Cheney 算法
。主要处理存活周期短
的对象中的可访问对象。
Cheney 算法
使用了 semi-space 半空间
的设计,将内存一分为二,始终只使用一半的空间,一块 From-Space
是使用空间,另一块 To-Space
是空闲空间
初始的时候 to 空间是空的,新定义的变量都在 from 空间中
|<- 新生代 ->|<- 老生代 ->|
|-----|-----|--------------------------------|
From To
- 内存先进入
From
(新生代在From
中分配对象) - 等到
From
空间使用到一定程度之后,新生代的GC
会启动(在垃圾回收阶段检查并按需复制From
中可访问对象到To
或老生代) From
释放不可访问对象占用的内存空间,达到清空效果,回收完成- 再将
From
和To
的进行互换,即From => To
,To => From
如果一个对象已经经历过一次回收,就需要进入老生代
新生代发现本次复制后,会占用超过 25% 的 To 空间,更替时就直接进入老生代
总结就是重复 复制-清空 的过程
老生代
Mark-Sweep - 标记清除
标记清除,分为标记和清除两个阶段
- 标记阶段:遍历所有可访问对象,
GC
会从一组已知的对象指针(称为根集,包括执行堆栈和全局对象等)中,进行递归标记可访问存活的对象 - 清除阶段:清除没有被标记的对象,将不可访问的对象留下的内存空间,添加到空闲链表的过程。未来为新对象分配内存时,可以从空闲链表中进行再分配
Mark-Compact - 整理
主要处理存活周期长
的对象中的不可访问对象, 主要使用 Cheney 复制算法
清除完对象后,内存内部的对象就不连续了,这样无法很好的利用内存地址(需要连续的内存),比如大的对象无法塞入刚释放的小内存中,提前触发垃圾回收
Mark-Compact
移动这些对象,让他们变紧凑,即标记清除对象后内存空间会出现内存碎片,当碎片超过一定限制后会启动压缩算法,将存活的可访问对象向内存一端移动,直到所有对象都移动完成,然后清理不需要的内存
因为新生代中占少数的是可访问对象,老生代中占少数的是不可访问对象,所以 Scavenge GC 垃圾回收算法 和 Mark-Compact 算法配合十分高效
Incremental Marking - 增量标记
上面提到的标记阶段需要遍历所有可访问对象,对于大型的堆内存来说,可能需要几百毫秒才能完成一次标记。在这期间会造成停顿,阻碍主线程的执行,一般来说老生代存在大量存活的对象,标记阶段的堆内存遍历会造成一定的卡顿,影响用户体验。所以 v8 引入了增量标记。
增量标记中,先标记堆内存中的一部分对象,然后暂停,将执行权重新交给 JS 主线程,等待主线程任务执行完毕后再从原来暂停标记的地方继续标记,知道标记完整个堆内存,即把垃圾回收分段,穿插在 JS任务中间执行,尽可能少的影响 JS 主线程任务,避免应用卡顿,提升应用性能
得益于增量标记的好处,v8 又引入了延迟清理(lazy sweeping)和增量式整理(incremental compaction)
为了充分利用多核 CPU 性能,也引入了平行标记、并发标记和并行清理,进一步减少垃圾回收对主线程的影响,提升性能
平行标记发生在主线程和工作线程上,应用程序在整个平行标记阶段暂停,是全停顿遍历标记的多线程版本
并发标记主要发生在工作线程上,并发标记进行时,主线程的 JS 可以继续运行
什么时候触发回收
1.执行完一次代码
let a = 1
let b = 2
console.log(a)
setTimeout(() => {
b++
console.log(b)
// 回收一次
}, 2000)
// 回收一次
2.内存不足
let size = 30 * 1024 * 1024
let arr1 = new Array(size)
testMemory()
let arr2 = new Array(size)
testMemory()
let arr3 = new Array(size)
testMemory()
let arr4 = new Array(size)
testMemory()
let arr5 = new Array(size)
testMemory()
let arr6 = new Array(size)
testMemory()
判断一个变量可以回收的标准
- 全局变量直到程序执行完毕,才会回收
- 普通变量失去引用时会被回收
function testMemory() {
let memory = process.memoryUsage().heapUsed
console.log(memory / 1024 / 1024 + 'mb')
}
let size = 30 * 1024 * 1024
let arr1 = new Array(size)
testMemory()
(function() {
let arr2 = new Array(size)
testMemory()
let arr3 = new Array(size)
testMemory()
let arr4 = new Array(size)
testMemory()
})()
let arr5 = new Array(size)
testMemory()
let arr6 = new Array(size)
testMemory()
// node --max-old-space-size=1000 xx.js 指定最大老生代空间
如何检测内存
浏览器
window.performance.memory
MemoryInfo
{
totalJSHeapSize: 95959081,
usedJSHeapSize: 90730505,
jsHeapSizeLimit: 4294705152
}
Node
process.memoryUsage()
> process.memoryUsage()
{
rss: 35344384, // node 总占用内存,不仅有 v8引擎占用,还有 C++ 程序占用
heapTotal: 4784128, // v8 引擎总内存
heapUsed: 3360920, // v8 引擎使用内存
external: 1582927, // C++ 分配给 v8 的额外内存
arrayBuffers: 9405 // node 14 版本后的新特性,arrayBuffers 的占用内存
}
- 手动触发垃圾回收:global.gc
- 设置内存:
- --max-old-space-size 老生代空间最大值
- --max-new-space-space 新生代空间最大值
如何优化内存
- 尽量不要定义全局变量,定义后需要及时手动释放
- 注意闭包
为什么 v8 要设计为 1.4g
- 1.4g 对于浏览器够用
- 回收是阻塞式的,即垃圾回收时会中断代码执行