Part2 · 前端工程化实战
JavaScript性能优化
文章说明:本专栏内容为本人参加【拉钩大前端高新训练营】的学习笔记以及思考总结,学徒之心,仅为分享。如若有误,请在评论区支出,如果您觉得专栏内容还不错,请点赞、关注、评论。共同进步!
本篇主要内容时JavaScript的性能优化内容,包括:内存管理、GC算法介绍、V8引擎等
一、性能优化介绍
-
性能优化时不可避免的
-
哪些内容可以看做是性能优化
任何一种可以提升程序运行效率,降低程序开销的行为,我们都可以看做是一种优化操作。这就意味着在软件开发的过程中,必然存在着很多值得优化的地方。
-
无处不在的前端性能优化
特别是在前端开发过程中,性能优化时无处不在的,例如请求资源时的网络、数据的传输方式,开发过程中所使用的的框架等。
本篇的核心是JavaScript语言的优化,具体来说就是认知内存空间的使用,垃圾回收的方式介绍。从而可以让我们编写出高效的JavaScript代码。
内容概要:
- 内存管理
- 为什么内存需要管理
- 内存管理的基本流程
- 垃圾回收与常见的GC算法
- V8引擎的垃圾回收
- V8中的GC算法实现垃圾回收
二、内存管理
Memory Management
1.内存为什么需要管理
随着近些年硬件技术的不断发展,同时高级编程语言中也都自带了GC(Garbage Collection)机制,这样的变化,让我们在不需要注意内存使用的情况下,也能够正常的完成相应的功能开发。
function fn() {
arrList = [];
arrList[100000] = 'Leo is a coder'
}
fn()
上述函数体内定义一个数组,数组长度足够大,为了当前函数在调用的时,程序可以向内存申请比较大的内存空间。执行函数过程中,我们使用性能检测工具,我们会发现,内存变化如下,内存持续升高,且并没有回落,这就是内存泄漏。内存泄漏会导致我们的页面处于卡顿状态,因此需要对内存进行人为管理。
2.内存管理介绍
- 内存:由可读写单元组成,表示一片可操作性空间
- 管理:人为的去操作一片空间的申请、使用和释放
- 内存管理:开发者主动申请空间、使用空间、释放空间
- 管理流程:申请-使用-释放
JavaScript中的内存管理
和其他语言相通,JavaScript内存管理的流程也是申请内存空间-使用内存空间-释放内存空间。但是由于ECMAScript中并没有提供操作内存的相关API,所以JavaScript语言不能像C或者C++那样,由开发者主动去调用相应的API来完成内存管理。不过,我们仍然可以通过js脚本去演示当前空间的生命周期是怎样完成的。
// 申请空间
let obj = {}
// 使用空间
obj.name = 'Leo'
// 释放空间
obj = null
3.JavaScript中的垃圾回收
JavaScript中的垃圾
- JavaScript中的内存管理时自动的
- 对象不再被引用时是垃圾
- 对象不能从根上访问到时是垃圾
JavaScript中的可达对象
- 可以访问到的对象就是科大对象(引用、作用域链)
- 可达的标准就是从根触发是否能够被找到
- JavaScript中的根可以理解为全局变量
引用说明代码示例:
let obj = {name:'leo'}; // obj引用leo对象,全局可达
let bai = obj; // bai引用leo内存地址
obj = null; // obj不再引用,但bai依然在引用
可达说明代码示例:
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', prev: [Circular]}},
// }
可达对象图示
如果我们在代码中做一些操作,比如使用delete将obj上的o1的应用以及o2中对obj1的应用删除掉,那么出现下面的情况:
三、GC算法介绍
1.GC定义与作用
- GC就是垃圾回收机制的简写(Garbage Collection)
- GC可以找到内存中的垃圾、并释放和回收空间
2.GC里的垃圾是什么
-
程序中不再需要使用的对象
function func() { name = 'leo'; return `${name} is a coder` } func()
上面例子中,当我们函数调用完成后,name不再被需要,因此它成为了一个垃圾
-
程序中不能再访问到的对象
function func() { const name = 'leo'; return `${name} is a coder` } func()
上面例子中,由于使用了const关键字进行声明变量,因此当函数执行结束后,外界无法再访问到它,它也会成为一个垃圾。
3.GC算法是什么
- GC是一种机制,垃圾回收器完成具体的工作
- 工作内容就是查找垃圾、释放空间、回收空间
- 算法就是工作时查找和回收所遵循的规则
常见的GC算法有以下几种:
- 引用计数
- 标记清除
- 标记整理
- 分代回收
4.引用计数算法
所谓的引用计数法就是给每个对象一个引用计数器,每当有一个地方引用它时,计数器就会加1;当引用失效时,计数器的值就会减1;任何时刻计数器的值为0的对象就是不可能再被使用的。
这个引用计数法时没有被Java所使用的,但是python有使用到它。而且最原始的引用计数法没有用到GC Roots。
- 核心思想:设置引用数,判断当前引用数是否为0
- 引用计数器
- 引用关系改变时修改引用数字
- 引用数字为0时立即回收
优点:
- 可即时回收垃圾,在该方法中,每个对象始终知道自己是否有被引用,当被引用的数值为0时,对象马上可以把自己当做空闲空间链接到空闲链表;
- 最大暂停时间短;
- 没有必要沿着指针查找;
缺点:
- 计数器的增减处理非常繁重;
- 计算器需要占用很多位;
- 实现繁琐;
- 循环引用无法回收;
5.标记清除算法
该算法分为标记和清除两个阶段。标记就是把所有活动对象都做上标记的阶段;清除就是将没有做上标记的对象进行回收的阶段。
- 核心思想:分标记和清除两个阶段完成
- 遍历所有对象找标记活动对象
- 遍历所有对象清除没有标记对象
- 回收相应的空间
优点:
- 实现简单
- 与保守式GC算法兼容(保守式GC在后面介绍)
缺点:
- 碎片化:如上图所示,在回收过程中会产生被细化的分块,到后面,即时堆中分块的总大小够用,但是却因为分块太小而不能执行分配
- 分配速度:因为分块不是连续的,因此每次分块都要遍历空闲链表,找到足够大的分块,从而造成时间短的浪费
- 与写时复制技术不兼容:所谓写时复制就是fork的时候,内存空间只引用而不复制,只有当该进程的数据发生变化时,才会将数据复制到该进程的内存空间。这样,当两个进程中的内存数据相同的时候,就能节约大量的内存空间了。而对于标记-清除算法,它的每个对象都有一个标志位来表示它是否被标记,在每一次运行标记-清除算法的时候,被引用的对象都会进行标记操作,这个仅仅标记位的改变,也会变成对象数据的改变,从而引发写时复制的复制过程,与写时复制的初衷就背道而驰了。
6.标记整理算法
标记-整理算法与标记-清理算法类似,只是后续步骤是让所有存活的对象移动到一端,然后直接清除掉端边界以外的内存。
- 标记整理可以看作是标记清除的增强
- 标记阶段的操作和标记清除一致
- 清除阶段会先执行整理,移动对象位置
优缺点:该算法可以有效的利用堆,但是整理需要花比较多的时间成本
四、V8引擎
1.认识V8
- V8引擎是一个JavaScript实现,最初由一些语言方面专家设计,后被谷歌收购,随后谷歌对其进行了开源;
- V8使用c++开发,在运行JavaScript之前,相比其他的JavaScript的引擎转换成字节码或解释执行,V8将其编译成原生机器码(IA-32, x86-64, ARM, or MIPS CPUs),并且使用了如内联缓存(Inline caching)等方法来提高性能
- 有了这些功能,JavaScript程序在V8引擎下的运行速度媲美二进制程序
- V8支持众多操作系统,如Windows、Linux、Android等,也支持其他硬件架构,如IA32、X64、ARM等,具有很好的可移植和跨平台特性
- V8内存设限(64位1.5GB,32位700MB)
2.V8垃圾回收策略
- 采用分代回收的思想
- 内存分为新生代、老生代
- 针对不同对象采用不同算法
V8 使用了分代和大数据的内存分配,在回收内存时使用精简整理的算法标记未引用的对象,然后消除没有标记的对象,最后整理和压缩那些还未保存的对象,即可完成垃圾回收。
V8中常用的GC算法
- 分代回收
- 空间复制
- 标记清除
- 标记整理
- 标记增量
3.V8回收新生代对象
年轻分代中的对象垃圾回收主要通过Scavenge算法进行垃圾回收。在Scavenge的具体实现中,主要采用了Cheney算法:通过复制的方式实现的垃圾回收算法。它将堆内存分为两个 semispace,一个处于使用中(From空间),另一个处于闲置状态(To空间)。当分配对象时,先是在From空间中进行分配。当开始进行垃圾回收时,会检查From空间中的存活对象,这些存活对象将被复制到To空间中,而非存活对象占用的空间将会被释放。完成复制后,From空间和To空间的角色发生对换。在垃圾回收的过程中,就是通过将存活对象在两个 semispace 空间之间进行复制。
年轻分代中的对象有机会晋升为年老分代,条件主要有两个:一个是对象是否经历过Scavenge回收,一个是To空间的内存占用比超过限制。
V8内存分配
- V8内存空间一分为二
- 小空间用于存储新生代对象(32M|16M)(From+To)
- 新生代值得是存货时间较短的对象
新生代对象回收实现
- 回收过程曹勇复制算法+标记整理算法
- 新生代内存分为两个等大小空间
- 使用空间为From,空闲空间为To
- 活动对象存储于From空间
- 标记整理后将活动对象拷贝至To
- From与To交换空间完成释放
- 回收细节说明:
- 拷贝过程中可能出现晋升
- 晋升就是将新生代对象移动至老生代
- 一轮GC还存活的新生代需要晋升
- To的使用率超过25%
4.V8回收老生代对象
- 老年代对象放在右侧老生代区域
- 64位操作系统1.4G,32位操作系统700M
- 老年代对象就是指存活时间较长的对象
对于年老分代中的对象,由于存活对象占较大比重,再采用上面的方式会有两个问题:一个是存活对象较多,复制存活对象的效率将会很低;另一个问题依然是浪费一半空间的问题。为此,V8在年老分代中主要采用了Mark-Sweep(标记清除)标记清除和Mark-Compact(标记整理)相结合的方式进行垃圾回收。
老年代对象回收实现
- 主要曹勇标记清除、标记整理、增量标记算法
- 首先使用标记清除完成垃圾空间的回收
- 采用标记整理进行空间优化
- 采用增量标记进行效率优化
老生代与新生代回收对象细节对比:
- 新生代区域垃圾回收使用空间换时间
- 老生代区域垃圾回收不适合复制算法
增量标记优化垃圾回收
图示中,程序在标记阶段被暂停运行,等待标记完成自动运行,当遇到大块需要标记的对象时,程序需要暂停很长一段时间,对用户体验很不友好,因此采用增量标记,将一大块分解为多个小块进行标记,减少每次程序暂停的时长,优化用户体验。最后标记完成后统一进行回收。
5.V8垃圾回收总结
- V8是一款主流的JavaScript引擎
- V8设置内存上限
- V8采用基于分代回收思想实现垃圾回收
- V8内存分为新生代和老生代
- V8垃圾回收常见的GC算法
今日分享就到了这里,上面很多的概念性问题,要完全的理解并使用这些新的知识,需要很长一段时间。多用、多查、多做!
下面一节我们讲一讲JavaScript的性能优化问题中工具的使用以及代码优化实例记录:2020/11/11