这是我参与「第四届青训营 」笔记创作活动的的第3天
内存的管理的生命周期:内存管理就是分配、使用、释放内存的过程。
javascript是自动管理内存的编程语言。
分配内存:在定义数据的时候,Javascript自动为数据分配内存。对于基本数据类型的数据,为其分配栈内存;对于复杂数据类型的数据,为其分配堆内存空间,并将这片内存空间的地址指针作为变量引用存在栈中。
垃圾回收(Garbage Collection)GC
Javascript自动管理内存,有其垃圾回收机制。
垃圾:定期对那些不再使用或者很久没有使用的对象回收,释放更多内存空间,减少内存泄漏。
java的运行环境JVM和js的运行环境js引擎都内存了垃圾回收器。
垃圾回收程序必须跟踪记录哪个变量还会使用,以及哪个变量不会再使用,以便回收内存。如何标记未使用的变量也许有不同的实现方式。
不过,在浏览器的发展史上,用到过两种主要的标记策略:标记清除和引用计数。
两种垃圾回收算法:引用计数法和标记清除法
引用计数法
引用计数法基本已经被浏览器淘汰,本质就是对于变量的引用次数进行计数。
使用一个对象给一个新的对象赋值的时候,就创建了指向新的对象的引用;而当一个对象有一个引用指向它的时候,该对象的引用计数+1;
当一个对象被赋值为null的时候,就销毁该对象,释放其内存空间。
引用计数法的弊端是会产生循环引用。
标记清除法
Js最常用的垃圾回收策略是标记清理(mark-and-sweep)。当变量进入上下文(变量处于执行环境中),就会被加上存在于上下文中的标记。而在上下文中的变量,逻辑上讲,永远不应该释放它们的内存,因为只要上下文中的代码在运行,就有可能用到它们。当变量离开上下文时,也会被加上离开上下文的标记。
给变量加标记的方式,例如给变量使用Boolean类型标记位,在进入离开上下文的时候反转标记;或者维护“在上下文中”和“不在上下文中”的两个变量列表。
垃圾回收程序运行。
①首先标记所有内存中存储的变量;
②将在上下文中的变量和被在上下文中的变量引用的变量去除标记(在上下文中说明执行程序时候仍旧是有用的);
③将带标记的变量销毁并释放内存;
④循环以上操作
不同浏览器就是垃圾回收的频率有差异;
标记清除法的核心是可达性,遍历。
垃圾回收器从设置的根对象出发,查找引用的对象。对于不可达的对象,就认为无用,然后销毁该对象,释放内存空间。
标记清除法的缺点就是清除对象之后,空闲出来的内存空间不连续,产生内存碎片,需要通过内存分配来管理内存。 js引擎中采用最多的就是可达性中的标记清除法,同时js引擎为了优化,实现细节上还搭配了其他算法。
性能
如果内存中分配了很多变量,垃圾回收程序的循环执行会造成性能损耗,垃圾回收有可能会明显拖慢渲染的速度和帧速率,为避免这些损耗,我们应当使得代码尽快运行完垃圾回收程序;
现代垃圾回收程序会基于对JavaScript运行时环境的探测来决定何时运行。探测机制因引擎而异,但基本上都是根据已分配对象的大小和数量来判断的.
IE曾饱受诟病。它的策略是根据分配数;实现的问题在于,分配那么多变量的脚本,很可能在其整个生命周期内始终需要那么多变量,结果就会导致垃圾回收程序过于频繁地运行。
调优为动态改变分配变量。不是根据数量,而是根据被回收的内存占总分配内存的占比动态调整分配数阈值;如果没有达到15%,就将阈值翻倍;如果占比达到85%,就将阈值重置。
js虽然有内存自动管理机制,但我们也需要关心内存管理;因为分配给浏览器、移动浏览器的内存要少,防止不重要的(网页)耗尽了系统内存导致系统崩溃(重要的瘫痪)。内存限制包括变量分配(减少无所谓的变量定义),栈调用,同一个线程中执行的语句数量。
优化内存的手段
- 如果全局变量或者全局变量属性不再被需要,设置为null,解除引用。局部变量会在超出作用域后自动被解除引用。
解除对一个值的引用并不会自动导致相关内存被回收。解除引用的关键在于确保相关的值已经不在上下文里了,因此它在下次垃圾回收时会被回收。 - 使用const和let声明代替使用var声明。const和let的作用域为块级,在块级作用域比函数作用域更早终止的情况下,就更早被回收内存。
- 运行期间,V8引擎会将创建的对象与隐藏类关联起来,以跟踪它们的属性特征。能够共享相同隐藏类的对象性能会更好,共享一个隐藏类的实例共享同一个构造函数和原型,但是如果在实例对象上追加了属性,实例就会对应两个不同的隐藏类。解决办法避免JavaScript的“先创建再补充”(ready-fire-aim)式的动态属性赋值,并在构造函数中一次性声明所有属性。delete删除属性导致的结果一样,所以最佳实践是将不需要的属性设置为null。
- 避免内存泄露。
1、意外声明全局变量。应当使用关键字声明变量,let、const
2、定时器使用了回调函数外的变量,定时器不结束,变量占用内存就不会被销毁。应当将定时器使用的变量都在回调函数内定义声明。
3、闭包很容易造成内存泄漏。 - 静态分配和对象池。 减少垃圾回收的次数,通过控制垃圾回收的条件; 1、函数返回一个被新构造的对象;如果这个函数被频繁调用,那么函数内的这个对象就会多次重复被新建和销毁。我们应当在函数外新建一个对象,然后以参数形式传进来。在哪里定义这种对象合适呢?一个策略是使用对象池。在初始化的某一时刻,可以创建一个对象池,用来管理一组可回收的对象。应用程序可以向这个对象池请求一个对象、设置其属性、使用它,然后在操作完成后再把它还给对象池。由于没发生对象初始化,垃圾回收探测就不会发现有对象更替,因此垃圾回收程序就不会那么频繁地运行。如果对象池只按需分配矢量(在对象不存在时创建新的,在对象存在时则复用存在的),那么这个实现本质上是一种贪婪算法,有单调增长但为静态的内存。这个对象池必须使用某种结构维护所有对象,数组是比较好的选择。
let arr=[]
arr.push(obj)
如果担心数组的动态分配内存,会导致垃圾回收机制来回收,可以静态分配内存,在定义对象池的时候就分配一个大小够用的内存。
不过,静态分配是优化的一种极端形式。如果你的应用程序被垃圾回收严重地拖了后腿,可以利用它提升性能。但这种情况并不多见。大多数情况下,这都属于过早优化,因此不用考虑