JavaScript性能优化1——内存管理(JS垃圾回收机制引用计数标记清除标记整理)
Posted JIZQAQ
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript性能优化1——内存管理(JS垃圾回收机制引用计数标记清除标记整理)相关的知识,希望对你有一定的参考价值。
目录
一、导入
随着我们javascript代码需要实现的功能越来越复杂,性能优化变得重要了起来。那么哪些内容可以被看做是性能优化呢?本质上来说,任何一种提高运行效率,降低运行开销的行为都可以看做是优化操作。
前端优化无处不在,例如请求资源时候用到的网络、数据的传输方式、开发过程中使用的框架等。本阶段讨论的核心是JavaScript语言本身的优化,也就是从认知内存空间的实用到垃圾回收的方式介绍。从而让我们编写出高效的JavaScript代码。
在这篇文章里,主要讨论内存管理相关的内容。
随着这些年来的硬件不断发展,同时高级编程语言当中都自带GC机制,这些变化都让我们可以在不需要过多注意内存空间的使用下,也能正常完成相应的功能开发。
二、内存管理
- 内存:由可读写单元组成,表示一篇可操控空间
- 管理:人为的去操作一片空间的申请、使用和释放
- 内存管理:开发者主动申请空间、使用空间、释放空间
1.JavaScript中的内存管理
分为下面三个步骤
- 申请内存空间
- 使用内存空间
- 释放内存空间
//DEMO2
// Memory management
// 申请
let obj = {}
// 使用
obj.name = 'lg'
// 释放 js中没有释放api,我们这里就把它设置为null
obj = null
2.JavaScript中的垃圾回收
概念
对我们前端而言JS的内存管理是自动的,每次我们创建一个对象、数组、函数的时候会自动分配一个内存空间。
那么什么是垃圾呢?
- 对象不再被引用的时候就是垃圾
- 对象不能从根上访问到的时候也是垃圾。
知道什么是垃圾之后,JS引擎就会出来工作,把它们占据的对象空间进行回收,这就叫做JS的垃圾回收。
下面我们引入一个概念,叫可达对象
可达对象
- 可以访问到的对象就是可达对象(引用、作用域链)
- 可达的标准就是从根出发是否能够被找到
- JavaScript中的根就可以理解为是全局变量对象
接下来我们在代码中看一下JS的引用和可达是什么样的
引用样例
// reference
let obj = {name: 'xm'}// 这里就发生了引用,obj也是可达的,xm空间也是可达的
let ali = obj // xm空间又多了一次引用
obj = null // obj引用xm的这条路断了,但是xm空间还是可达的,因为ali还在引用xm空间
console.log(ali)//打印{ name: 'xm' }
可达样例
先看一下下面这段代码
//DEMO4
//可达对象
//我们定义了一个函数去接收两个变量obj1,obj2,互相指引
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)
说明:
首先从全局的根出发,我们能找到一个可达的对象obj,它是通过一个函数调用之后,指向了一个内存空间(里面就是o1和o2),又通过相应的属性指向obj1和obj2的空间,这两个空间之间通过next和prev互相指向。
所以在这个例子里面,我们能从根访问到任何一个内存空间。
然后我们删除掉两行代码之后
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)
这之后,我们所有能够找到obj1的线条都被删除了,于是obj1空间就会被认为是垃圾,js引擎就会找到它把它删除。
3.GC算法介绍
GC定义与作用
- GC就是垃圾回收机制的简写
- GC可以找到内存中的垃圾、并释放和回收空间。
GC里的垃圾是什么
- 程序中不再需要使用的对象(下面例子里的name)
- 程序中不能再访问到的对象(下面例子里的name)
GC算法是什么
- GC是一种机制,垃圾回收器完成具体的工作
- 工作的内容就是查找垃圾释放空间、回收空间
- 算法就是工作时查找和挥手所遵循的规则
常见GC算法
- 引用计数:通过一个数字判断当前的是不是垃圾
- 标记清除:进行工作的时候给到活动对象添加一个标记,判断是否垃圾
- 标记整理:和标记清除类似,回收过程中做的事情不太一样
- 分代回收:不同生命周期的对象可以采取不同的收集方式,以便提高回收效率
3.1引用计数
实现原理
核心思想:设置应用数,判断当前引用数是否为0,从而判断是否垃圾对象。数字为0,GC开始工作,将其所在的对象空间回收再释放使用。
引用关系发生改变的时候,引用计数器会主动去修改引用数值。引用数字为0的时候立刻回收。
//DEMO4
//GC 引用计数
const user1 = {age:11}
const user2 = {age:22}
const user3 = {age:33}
const nameList = [user1.age,user2.age,user3.age]
function fn() {
// 当加上const之后,num1作用域变了,一旦fn()结束之后,我们就再也找不到num1了,引用计数为0,gc就会吧num1的内存空间回收
const num1 = 1
num2 = 2
}
fn()
总结:
靠着我们当前对象身上引用计数器的数值是否为0,从而决定它是不是垃圾对象。
引用计数的优点
- 发现垃圾时立即回收
- 最大限度减少程序暂停(当内存快满的时候就立刻去找引用计数为0的删掉)
引用计数的缺点
- 无法回收循环引用的对象
- 时间开销大(因为要时刻监控数值的修改)
什么是循环引用的对象
//DEMO5
//对象之间的循环引用
//虽然obj1和obj2全局作用域下找不到了,但是引用还存在的,obj1和obj2互相在他们的作用域内引用
//用引用计数算法无法释放这部分空间。
function fn() {
const obj1 = {}
const obj2 = {}
obj1.name = obj2
obj2.name = obj1
return 'hello world'
}
fn()
3.2标记清除
实现原理
这个原理实现要比引用计数算法更加简单,还能解决一些问题。在后续学习的v8当中会被大量用到。
核心思想:将整个垃圾回收操作分成标记和清楚两个阶段完成。第一个阶段会遍历所有对象,找到活动对象标记。第二个阶段仍然遍历所有对象,把那些身上没有标记的对象进行清除。还会把第一个阶段的标记抹掉,便于GC下次正常工作。通过两次遍历行为,把我们当前的垃圾空间进行回收,最终交给相应的空闲列表去维护。
通过下面图片举例说明一下:
在全局的地方,我们可以找到A、B、C三个可达对象,如果发现他们的下边有child,或者child还有child,它会使用递归的方式去寻找可达对象。所以D、E也会被做上可达标记。从global的链条下是找不到a1,b1的,所以GC工作的时候会认为a1、b1是垃圾,把它清除掉,并且把可达标记都清除掉。要注意最终还会把回收的空间放在一个空闲列表上面,方便后面的程序直接申请空间使用。
标记清除优点
可以解决对象循环引用的回收操作
标记清除缺点
如图显示,左右两侧有从根无法被查找的区域,这种情况下在第二轮进行清除操作的时候,就会直接吧两侧对应的空间回收,然后把释放的空间添加到空闲链表之上。紧接着后续的程序进来,在空闲链表上申请空间。
但是我们从图片上看得到,即时左右两边的空间被释放,但是他们这两块空间被中间的内容给分割着,空出来的内存地址不连续,是分散的。假如如图的例子,左侧空出2个空间,右侧空出1个空间,而我们想要申请一个1.5的空间。我们申请左侧呢,就会空余0.5个空间,而右边空间又不足。
这就是标记清除最大的问题,会造成空间碎片化,不能让我们空间最大化使用。
标记清除不会立即回收垃圾对象。
3.3标记整理
实现原理
标记整理可以看作是标记清除的增强操作,他在标记阶段的操作和标记清除一致,但是清楚阶段会先执行整理,移动对象位置。
下面用图片来更好的 解释一下标记整理回收阶段的过程。
回收之前我们内存摆放位置如下,有着很多活动对象、非活动对象和空闲的空间,当他去执行当前标记操作的时候,会把所有的活动对象进行标记,紧接着会去进行一个整理的操作。
整理会把我们的活动对象进行移动,在地址上变成连续的位置,紧接着将当前活动对象右侧的范围进行回收。
回收之后大概长下面的样子,现在回收到的空间基本上都是连续的,后续可以最大化利用内存释放出来的空间。它会配合着标记清除,在我们的V8引擎当中配合实现频繁的GC操作。
标记整理优点
减少碎片化空间
标记整理缺点
不会立即回收垃圾对象
以上是关于JavaScript性能优化1——内存管理(JS垃圾回收机制引用计数标记清除标记整理)的主要内容,如果未能解决你的问题,请参考以下文章