- 内存管理
- 垃圾回收与常见GC算法
- V8引擎的垃圾回收
- Performance工具
- 代码优化实例
内存管理
为什么要内存管理
function fn() {
let = arrList = []
arrList[100000] = 'js'
}
fn()
内存持续升高没有回落,就代表着内存泄漏,至于如何泄漏,这里先不纠结,这里想说的是,如果不了解内存问的话,烂代码写的多了,程序总会有意想不到的bug
- 内存:由可读写单元组成,表示一片可操作空间
- 管理:人为的去操作一片空间的申请,操作和释放
- 内存管理:开发者主动申请空间,使用空间,释放空间
- 管理流程:申请 - 使用 - 释放
// 申请
let a = {};
// 使用
a.name = 'js'
// 释放
a = null
JS垃圾回收
- JS中内存管理是自动的
- 对象不再被引用时就是垃圾
- 对象不能从根上访问到时是垃圾
JS引擎知道什么是垃圾了,就会定期回收,这就是垃圾回收机制,后续阅读还需以下几个概念
- 可达对象:可以访问到的对象(可以是引用,可以是作用域链),只要能够访问到就是可达对象
- 可达的标准就是从根出发,是否能够被找到
- js中的根可以理解为是全局变量对象(全局执行上下文)
let obj = {name: 'xm'}; // 当前的xm对象,就是可达的,obj指向xm这个对象的内存地址
let ali = obj; // xm对象又多一了一个引用
obj = null; // 此时将obj1的引用断掉了
// 我们的xm对象是否还是可达呢,当然是的,因为ali的引用还在
function objGroup(obj1, obj2) {
obj1.next = obj2;
obj2.prev = obj1;
return {
o1: obj1,
o2: obj2
}
}
let obj = objGroup({name: 'obj1'}, {name: 'obj2'});
console.log(obj)
// => 输出
{
o1: { name: 'obj1', next: { name: 'obj2', prev: [Circular] } },
o2: { name: 'obj2', prev: { name: 'obj1', next: [Circular] } }
}
// Circular代表循环的又各自指向彼此
看看目前这几个对象,obj,obj1,obj2,都是可以从根上查找到的,无论找起来多么麻烦
如果删掉obj的o1属性和obj2的prev属性,相当于红色线条断掉,那么obj1就没有引用了,它此时就会变成垃圾,JS引擎就会回收掉它。
GC算法
垃圾回收机制的简写就是GC,GC可以找到内存中的垃圾,并释放和回收空间
GC眼里的垃圾是什么?
// 程序中不再需要使用的对象
function func() {
name = 'zh'
return `${name} is a coder`
}
func()
// 程序中不能再访问到的对象
function func() {
const name = 'zh' // 变量再函数作用域中
return `${name} is a coder`
}
func() // 执行后,就再也访问不到了
- GC是一种机制,垃圾回收器完成具体的工作
- 工作的内容就是查找垃圾,释放空间,回收空间
- 算法就是工作时查找和回收所遵循的规则
常见GC算法
- 引用计数
- 标记清除
- 标记整理
- 分代回收
引用计数算法实现原理
核心思想:设置引用数,判断引用数是否为0。
引用关系改变时,引用计数器修改引用数值,引用数字为0时立即回收。
const u1 = {age : 11}
const u2 = {age : 22}
const list = [u1.age, u2.age] // ?????
function fn1() {
n1 = 100; // 由于没有设置关键字,他现在是挂载在window下的。window.n1 =100
n2 = 200;
}
fn1();
// 所以以上的变量的引用计数肯定都不是0, list中还保持对u1,u2的引用
function fn2() {
const n3 = 100; // 这里的n3,n4只能在当前的函数作用域内访问得到
const n4 = 200;
}
fn2();
// 一旦fn2()执行结束后,在外部全局的角度去出发,就不能再找到n3n4了,n3n4身上的引用计数就=0,此时GC将他们回收。
引用计数算法优缺点
优点
- 发现垃圾时立即回收
- 最大限度减少程序暂停
缺点
- 无法回收循环引用的对象
- 时间开销大(因为它需要去维护一个数值的变化,需要时刻监控对象的引用数值,数值需要频繁的修改)(相对其他算法来说)
// 无法回收循环引用的对象
function objGroup() {
let obj1 = {};
let obj2= {};
obj1.next = obj2;
obj2.prev = obj1;
return 'xixi'
}
objGroup()
// 虽然obj1在函数作用域中,objGroup()执行后在全局以找不到它,
// 但是obj1还被obj2引用着,所以obj1的计数不等于0,就不会被清除
// obj2同理
标记清除算法实现原理
核心原理:将垃圾回收清除阶段,分标记和清除两个阶段完成
- 遍历所有对象找标记活动对象(可达对象)
- 遍历所有对象清除没有标记对象
-
回收相应空间
从全局开始,递归的找到所有对象,并进行标记,abced都将被标记,由于a1a2在一个函数作用域中,函数执行后就无法被找到了,与全局失去了联系(例如上面的objGroup例子),a1a2的地址就会进入空闲链表。这就完成了一次垃圾回收。
标记清除算法优缺点
优点
- 可以解决循环引用的对象的回收
缺点
-
容易产生碎片化空间,浪费空间
从根上我们找得到红色对象,找不到蓝色蓝色对象(头是指一个对象的元信息,比如说对象的尺寸地址,域是存放数值的地方)。第一个对象有2个空间,第三个对象有1个空间。虽然释放后看起来是三个空间,但是他们中间被红色对象隔开着,所以是分散的,也就是地址不连续。这就造成了空间碎片化。
这时候来了一个需要1.5空间的对象,如果放在一位置就会多,放在3位置还不够。
标记整理算法实现原理
标记整理可以看作是标记清除的增强
- 标记阶段的操作和标记清除一致
-
清除阶段会先执行整理,移动对象位置
优点
- 可以解决空间碎片化
认识V8
- V8是一款主流的JS执行引擎(目前的Chrome浏览器和Node都采用这个)
- V8采用即时编译(可以直接将源码转成机器执行的机器码,省掉了中间的字节码)机器码与字节码
- V8内存设限(比如64位电脑,数值不超过1.5G)
V8垃圾回收策略
我们都知道在程序的使用过程中,那么我们会用到很多的数据,而这些数据呢,我们又可以分为原始的数据和对象类型的数据,那么对于这些基础的原始数据来说,它都是有程序的语言自身来进行控制的,所以在这里我所提到的回收主要还是指的是当前存活在我们堆区里的对象数据。
- 采用分代回收思想
- 内存分为新生代和老生代
-
针对不同对象采用不同算法
V8常用GC算法
分代回收,空间复制,标记清除,标记整理,标记增量
V8如何回收新生代对象
- 左侧的白色部分就是新生代存储区,包含From和To。
- 小空间用于存储新生代对象
- 新生代指的是存活时间较短的对象
什么是存活时间较短的对象?比如说我们有一个局部的作用域,那么这个局部作用域中的变量,在执行完成之后就肯定要去回收,而我们在其他地方,比如全局的地方,也可能会有个变量,而全局下方的这个变量呢,他肯定要等到我们的程序退出之后才会被回收,所以相对来说,我们的新生代就指的是那些存活时间比较短的那些对象。
那么如何完成回收
- 回收过程采用复制算法+标记整理
- 新生代内存区分为两个等大小空间
- 使用空间称为From,空闲空间称为To
- 首先,活动对象存储于From空间,此时To为空闲
- 一旦From空间存储到一定程度,就会触发GC操作
- 就会通过标记整理,对From空间进行活动对象标记
- 标记后整理,防止空间碎片化
- 整理后将活动对象拷贝至To
- 最后将From空间完全释放,相当于From和To交换空间(每一轮GC,From和To都会互换)
拷贝过程中可能出现晋升,晋升就是将新生代对象移动至老生代,那怎样触发晋升呢,一轮GC还存活的新生代需要晋升,或To空间的使用率超过25%
V8如何回收老生代对象
老生代对象存放在右侧老生代区
老生代区在64位操作系统中位1.4G
老生代对象就是指存活时间较长的对象
比如全局对象下的变量,闭包中的变量
回收过程
- 主要采用标记清除,标记整理,增量标记算法
- 首先使用标记清除完成垃圾空间的回收(主要回收方式,虽然会有空间碎片,但是速度提升明显)
- 当新生代对像移动至老生代中(也就是上面说的晋升),且老生代空间不足以容纳新生代对象时,采用标记整理进行空间优化
- 采用增量标记进行效率优化
细节对比
新生代区域垃圾回收采用空间换时间
老生代区垃圾回收不适合复制算法
增量标记如何优化垃圾回收
当进行垃圾回收时,是阻塞当前JS的执行的,图中上半部分是js程序执行,下半部分是垃圾回收,增量标记就是指把标记清除机制,分段进行,然后让js执行和垃圾回收能交替进行,提高效率
为什么使用Performance工具
- GC的目的是为了实现内存空间的良性循环
- 良性循环的基础是代码的合理使用
- 由于JS没有操作内存的API,都是靠V8的GC
- 所以我们需要时刻关注,才能确定是否合理
-
Performance提供了更多的监控方式
上图是百度搜索的,蓝色线就是JS堆内存,看起来有升有降,是没问题的
内存问题的外在表现
- 页面出现延迟加载或经常性暂停(应该是出现了频繁GC,程序中出现了瞬间让我们的内存升高爆掉的代码)
- 页面出现持续性糟糕性能(应该是出现了内存膨胀)
- 页面的性能随时间延长越来越差(内存泄漏)
监控内存的几种方式
界定内存问题的标准
- 内存泄漏:内存使用持续升高。
- 内存膨胀:为了达到当前应用程序最好的效果,他就需要很大的内存空间,也许是当前设备的硬件就不支持,所以他的标准是多数设备上存在性能问题。
- 频繁GC:通过内存变化图分析
监控方式
浏览器任务管理器
Timeline时序图记录
堆快照查找分离DOM
TimeLine记录内存
堆快照查找分离DOM
什么是分离DOM
- 界面元素存活在DOM树上
- 垃圾对象时的DOM节点
- 分离状态的DOM节点
已经脱离了文档的DOM,在界面也没有呈现,但是代码里依然有对其的引用,就是分离DOM
<body>
<button id = "btn">add</button>
<script>
let ele;
function fn() {
var ul = document.createElement('ul')
for (let i = 0; i < 10; i++){
var ul = document.createElement('li')
ul.appendChild(li)
}
ele = ul
}
document.getElementById('btn').onclick = fn
</script>
</body>
看以上代码,我们创建了ul但是并没有把它插进文档,但是却使它有引用,这就是分离DOM,分离DOM是内存泄漏的常见原因
判断是否频繁GC
通过Time中是否频繁的上升下降判断