JS 性能优化
前言
内存管理:申请 -> 使用 -> 释放
垃圾回收:不可达对象(没有使用到且不可访问到的对象)就是垃圾对象,会被JS垃圾回收引擎自动回收。
GC算法:不可达对象的内存空间会被GC算法回收。大概是这四种算法 引用计数、标记清除、标记整理、分代回收。
性能优化:细节上的留心,使程序更自信。
GC算法
浏览器可分配的内存是有上限的,当内存占满时,程序就会暂停。
引用计数
引用关系发生变化时,引用计数就会随之变化。
每当有一个引用指向某个对象空间时,这个对象空间的引用计数器就会累加,反之取消引用时就会削减。
当引用计数器变成0,就会去进行回收操作。
也就是通过判断某个对象的引用计数器是否为0,从而决定这个对象是否是一个垃圾对象。
优点:发现垃圾立即回收。最大程度的减少程序的暂停。
缺点:无法回收循环引用的对象,因为循环引用的对象的引用计数不为0,则不会回收。同时由于它需要维护引用计数器数值并且时刻监控着变化,所以在时间上的开销也比较大。
标记清除
会对活动的对象进行标记,对于那些没有标记的对象,会被当作垃圾对象来清除。
第一个阶段是遍历所有对象,然后找到那些活动的对象(可达对象)来进行标记。
第二个阶段是对那些没有进行标记的对象进行清除,从而回收那部分被垃圾对象占用的空间。
优点:可以将引用计数算法中无法回收的循环引用的对象进行垃圾回收。
缺点:由于它回收的内存空间不全是连续的,所以就会造成空间碎片化的问题,从而造成这部分内存空间的浪费。
标记整理
会对活动的对象进行标记,在标记的过程中会将活动的对象与非活动的对象进行整理。
第一个阶段,遍历所有的对象,找到活动的对象来进行标记。
第二个阶段,是将活动的对象与不活动的对象的空间分别整理成两块,之后回收不活动对象的那部分的空间。
优点:可以解决回收的内存空间碎片化的问题。
缺点:由于它需要整理不连续的内存空间,所以在时间和空间上的损耗会大一些,所以会蛮一些。
分代回收
是V8引擎的回收机制
V8引擎
在64位操作系统中,内存不超过1.5G。
在32位操作系统中,内存不超过800M。
以上是经过官方测试的,也是最优的内存占用,在垃圾回收过程中也不容易被用户感知到。
垃圾回收机制-新生代回收机制
采用分代回收的机制,分为新生代和老生代。
64位操作系统中新生代的存储空间为64M,32位操作系统中新生代的存储空间为32M。
新对象的创建会被分到新生代的内存空间中,不够的话,也会被分配到老生代的内存空间中。
新生代中的对象是存活时间比较短,如果存活时间长,就会晋升分配到老生代的内存空间中。
新生代中存活时间比较短的对象一般都是“局部作用域”中的变量,而全局作用域中的对象一般都是在程序结束后才会被回收,所以它们的存活时间较长。
第一步:将新生代对象的存储空间一分为2,分为暂存区和闲置区。
第二步:新创建的对象放到左边暂存区,然后再对活动的对象进行标记整理,之后将标记的对象复制到右边闲置区,最后将左边暂存区进行一个清空回收的操作。
第三步:暂存区清空完毕后,开始将左边暂存区和右边闲置区的空间进行交换,这样左边暂存区中就又存放了之前标记的对象。之后再有新创建的对象,还会被存放到左边暂存区。最后继续第二步的操作,也就是一轮一轮的循环第二步第三步的操作。
多次交换之后,依然存活下来的对象,就会被作为存活时间较长的对象,从而晋升分配到老生代的内存空间中。
垃圾回收机制-老生代回收机制
新生代内存空间中的对象晋升条件:
- 该对象已经经过一轮新生代的回收机制,并且存活下来了,而且正准备要经历第二轮新生代的回收机制,这时直接晋升分配到老生代的内存空间区。
- 从左边暂存区进行标记处理和标记整理后,在复制标记的活动对象时,如果发现右边闲置区的容量已经使用超过25%,这时候会将这个待复制的这个对象直接晋升分配到老生代的内存空间区。之所以是25%,这是防止左边暂存区和右边闲置区进行空间交换时后续的内存分配空间不够用。
64位操作系统中老生代的存储空间为1.4G,32位操作系统中老生代的存储空间为700M。
老生代的垃圾回收算法:标记清除、标记整理、增量标记。它们是结合起来使用的,主要使用标记清除,发现空间不够了,就使用标记整理算法。
标记清除阶段:对老生代存储空间中活动的对象进行一个标记,然后将未标记的对象进行一个回收清除。
标记整理阶段:对老生代存储空间中活动的对象进行一个标记,然后将标记的对象都整理移动到老生代存储空间中最左侧,最后将老生代存储空间最右侧的这部分进行一个整体的回收清除。这样一来,存储空间就是连续,同时也释放了那些非活动的对象所占用的空间。
JS性能实践笔录
全局变量的优化
- 全局变量查找比较消耗时间。先查找局部变量,再查找全局变量。
- 全局变量会一直存储在内存中,直到程序退出,才会被GC回收,这就降低了内存的使用。
- 局部变量和全局变量重名,容易造成变量污染。
实践经验:
- var 改成let 或者 const。
- 将全局变量缓存到局部变量中。
方法的优化
- 在构造函数中定义成员函数,每次new都会创建新的函数,不如直接在原型对象上定义一次成员函数。
- 在函数中定义一个函数,每次调用外层函数,都会创建新的函数,不如在外层定义一次函数。
闭包的优化
闭包会导致变量内存不会被回收,从而导致内存泄漏的问题。
闭包是一种强大的编程方式,但是稍微不注意就会遇到内存泄漏。
闭包:
- 函数外部具有指向函数内部的引用。
- 在外部作用域中可以访问内部作用域中的数据值。
实践经验:
- 在函数内部,进行事件注册时,将全局变量缓存到局部变量中并且在事件绑定的函数中使用到了该局部变量,要记得在函数内部的末尾将局部变量设置为null。反之就算全局变量被销毁,这个闭包中的数据也不会被回收。
循环的优化
在循环语句里,不要在for(;;)的第二部分里使用计算相关或者查找相关的表达式,可以将这类表达式写在第一部分,第二部分直接使用第一部分中计算所得的变量即可。
例如for(let i=0,len=arr.length;i<len;i++) {...}
。
在使用循环的时候,可以减少非必要的重复运算或重复查找,这样的细节可以提高程序运行的效率。
代码层级的优化
当代码中存在if-else多层嵌套时,可以尝试通过return语句和if中取反的逻辑语句来及时终止程序往下运行,从而优化代码层级以及执行效率。
写代码的过程中,过多的层级嵌套,会使得逻辑变得复杂起来,也影响程序的执行效率。
作用域的优化
多使用局部变量,少使用全局变量。
不要在函数中进行非必要的全局变量二次赋值,这样会使得查找变量时作用域链被拉长,影响程序的执行效率,还不如直接声明局部变量,当函数执行完,马上被GC回收。
面向对象的优化
使用ES6的面向对象写法,然后使用babeljs取转换,比你直接使用ES5的面向对象写法要更友好也更简洁一些。
总结
垃圾回收的大致原理,以及js代码层面性能优化的常规实践,编程经验蹭蹭往上涨。
虽然过去也知道要这样,但是有时也会违背,因为现在的硬件很发达,所以人懒了也粗心了点。
回顾一下,很是不错😂。