我的总结:javascript
1.两种垃圾回收机制:html
1)引用标记算法:若是检测到一个对象没有被引用了,就清除它。 ***这种算法不能处理循环引用的状况***前端
2)标记—清除算法:从根(全局变量)开始向后代变量检测,任何根变量所到达不了的变量会被标记为内存垃圾。java
这是 JavaScript 工做原理的第三章。git
咱们将会讨论平常使用中另外一个被开发者愈来愈忽略的重要话题,这都是日益成熟和复杂的编程语言的锅,即内存管理问题。github
像 C 语言拥有底层的内存管理原语如 malloc()
和 free()
。开发者使用这些原语来显式从操做系统分配和释放内存。web
与此同时,当建立事物(对象,字符串等)的时候,JavaScript 分配内存而且当它们再也不使用的时候 "自动释放" 内存,这一过程称为内存垃圾回收。这个乍看起来本质上是 "自动化释放内存" 的释放资源是引发混乱的缘由,而且给予 JavaScript(及其它高级语言)开发者一个错误的印象即他们能够选择忽略内存管理。这是一个巨大的错误。算法
即便是当使用高级语言的时候,开发者也应该要理解内存管理(或者至少是一些基础)。有时候自动化内存管理会存在一些问题(好比垃圾回收中的 bugs 或者实施的局限性等等),为了可以合理地处理内存泄漏问题(或者以最小代价和代码缺陷来寻找一个合适的方案),开发者就必须理解内存管理。编程
无论你使用哪一种编程语言,内存生命周期几乎是同样的:设计模式
如下是每一步生命周期所发生事情的一个概述:
为快速浏览调用堆栈和动态内存管理的概念,你能够阅读第一篇文章。
在直接跳向 JavaScript 内存管理以前,先来简要地介绍一下内存及其工做原理。
从硬件层面看,计算机内存是由大量的 flip flops 所组成的(这里大概查了下,即大量的二进制电路所组成的)。每一个 flip flop 包含少许晶体管并可以存储一个比特位。单个的 flip flops 能够经过一个惟一标识符寻址,因此就能够读和覆写它们。所以,理论上,咱们能够把整个计算机内存当作是由一个巨大的比特位数组所组成的,这样就能够进行读和写。
做为猿类,咱们并不擅长用位来进行全部的逻辑思考和计算,因此咱们把位组织成一个更大的组,这样就能够用来表示数字。8 位称为一字节。除了字节还有字(16 或 32 位)。
内存中存储着不少东西:
编译器和操做系统一块儿协做来为你进行内存管理,可是建议你了解一下底层是如何实现的。
当编译代码的时候,编译器会检查原始数据类型并提早计算出程序运行所须要的内存大小。在所谓的静态堆栈空间中,所需的内存大小会被分配给程序。这些变量所分配到的内存所在的空间之因此被称为静态内存空间是由于当调用函数的时候,函数所需的内存会被添加到现存内存的顶部。当函数中断,它们被以 LIFO(后进先出) 的顺序移出内存。好比,考虑以下代码:
int n; // 4 字节 int x[4]; // 4 个元素的数组,每一个数组元素 4 个字节 double m; // 8 字节
编译器会当即计算出代码所需的内存:4 + 4 x 4 + 8 = 28 字节。
编译器是这样处理当前整数和浮点数的大小的。大约 20 年前,整数通常是 2 字节而 浮点数是 4 字节。代码不用依赖于当前基础数据类型的字节大小。
编译器会插入标记,标记会和操做系统协商从堆栈中获取所须要的内存大小,以便在堆栈中存储变量。
在以上示例中,编译知道每一个变量的准确内存地址。事实上,当你编写变量 n
的时候,会在内部把它转换为相似 "内存地址 412763" 的样子。
注意到这里当咱们试图访问 x[4]
时候,将会访问到 m 相关的数据。这是由于咱们访问了数组中不存在的数组元素-它超过了最后一个实际分配到内存的数组元素 x[3]
4 字节,而且有可能会读取(或者覆写) m
的位。这几乎能够肯定会产生其它程序所预料不到的后果。
当函数调用其它函数的时候,各个函数都会在被调用的时候取得其在堆栈中的各自分片内存地址。函数会把保存它全部的本地变量,但也会有一个程序计数器用来记住函数在其执行环境中的地址。当函数运行结束时,其内存块能够再次被用做其它用途。
不幸的是,想要知道编译时一个变量须要多少内存并无想象中那般容易。设想一下若要作相似以下事情:
int n = readInput(); // 从用户读取信息 ... // 建立一个含有 n 个元素的数组
这里,编译器并不知道编译时数组须要多少内存,由于这是由用户输入的数组元素的值所决定的。
所以,就不可以在堆栈中为变量分配内存空间。相反,程序须要在运行时显式地从操做系统分配到正确的内存空间。这里的内存是由动态内存空间所分配的。静态和动态内存分配的差别总结以下图表:
*静态和动态分配内存的区别*
为了彻底理解动态内存分配的工做原理,咱们须要花点时间了解指针,这个就可能有点跑题了 ^.^。若是你对指针感兴趣,请留言,而后咱们将会在之后的章节中讨论更多关于指针的内容。
如今,咱们将会介绍在 JavaScript 中是如何分配内存的((第一步)。
JavaScript 经过声明变量值,本身处理内存分配工做而不须要开发者干涉。
var n = 374; // 为数字分配内存 var s = 'sessionstack'; // 为字符串分配内存 var o = { a: 1, b: null }; // 为对象及其值分配内存 var a = [1, null, 'str']; // (相似对象)为数组及其数组元素值分配内存 function f(a) { return a + 3; } // 分配一个函数(一个可调用对象) // 函数表达式也分配一个对象 someElement.addEventListener('click', function() { someElement.style.backgroundColor = 'blue'; }, false);
一些函数调用也会分配一个对象:
var d = new Date(); // 分配一个日期对象 var e = document.createElement('div'); // 分配一个 DOM 元素
能够分配值或对象的方法:
var s1 = 'sessionstack'; var s2 = s1.substr(0, 3); // s2 为一个新字符串 // 由于字符串是不可变的,因此 JavaScript 可能会选择不分配内存而只是存储数组 [0, 3] 的内存地址范围。 var a1 = ['str1', 'str2']; var a2 = ['str3', 'str4']; var a3 = a1.concat(a2); // 包含 4 个元素的新数组由 a1 和 a2 数组元素所组成
JavaScript 中使用分配的内存主要指的是内存读写。
能够经过为变量或者对象属性赋值,亦或是为函数传参来使用内存。
大多数的内存管理问题是出如今这一阶段。
痛点在于检测出什么时候分配的内存是闲置的。它常常会要求开发者来决定程序中的这段内存是否已经再也不使用,而后释放它。
高级程序语言集成了一块称为垃圾回收器的软件,该软件的工做就是追踪内存分配和使用状况以便找出并自动释放闲置的分配内存片断。
不幸的是,这是个近似的过程,由于断定一些内存片断是否闲置的广泛问题在于其不可断定性(不能为算法所解决)。
大多数的垃圾回收器会收集那些再也不被访问的内存,好比引用该内存的全部变量超出了内存寻址范围。然而仍是会有低于近似值的内存空间被收集,由于在任何状况下仍然可能会有变量在内存寻址范围内引用该内存地址,即便该内存是闲置的。
因为找出 "再也不使用" 的内存的不可断定性,针对这一广泛问题,垃圾回收实现了一个有限的解决方案。本小节将会阐述必要的观点来理解主要的内存垃圾回收算法及其局限性。
引用是内存垃圾回收算法所依赖的主要概念之一。
在内存管理上下文中,若是对象 A 访问了另外一个对象 B 表示 A 引用了对象 B(能够隐式或显式)。举个栗子,一个 JavaScript 对象有引用了它的原型(隐式引用)和它的属性值(显式引用)。
在这个上下文中,"对象" 的概念被拓展超过了通常的 JavaScript 对象而且包含函数做用域(或者全局词法做用域)。
词法做用域定义了如何在嵌套函数中解析变量名。即便父函数已经返回,内部的函数仍然会包含父函数的做用域。
这是最简单的内存垃圾回收算法。当一个对象被 0 引用,会被标记为 "可回收内存垃圾"。
看下以下代码:
var o1 = { o2: { x: 1 } }; // 建立两个对象。 // 'o1' 引用对象 'o2' 做为其属性。所有都是不可回收的。 // 'o3' 是第二个引用 'o1' 对象的变量 var o3 = o1; o1 = 1; // 如今,原先在 'o1' 中的对象只有一个单一的引用,以变量 'o3' 来表示 // 引用对象的 'o2' 属性。 // 该对象有两个引用:一个是做为属性,另外一个是 'o4' 变量 var o4 = o3.o2; // 'o1' 对象如今只有 0 引用,它能够被做为内存垃圾回收。 // 然而,其 'o2' 属性仍然被变量 'o4' 所引用,因此它的内存不可以被释放。 o3 = '374'; o4 = null; // 'o1' 中的 'o2' 属性如今只有 0 引用了。因此 'o1' 对象能够被回收。
循环引用会形成限制。在如下的示例中,建立了两个互相引用的对象,这样就会形成循环引用。函数调用以后他们将会超出范围,因此,实际上它们是无用且能够释放对他们的引用。然而,引用计数算法会认为因为两个对象都至少互相引用一次,因此他们都不可回收的。
function f() { var o1 = {}; var o2 = {}; o1.P = O2; // O1 引用 o2 o2.p = o1; // o2 引用 o1. 这就形成循环引用 } f();
为了判断是否须要释放对对象的引用,算法会肯定该对象是否可得到。
标记-清除算法包含三个步骤:
标记-清除算法的动态图示
该算法比以前的算法要好,由于对象零引用可让对象不可得到。反之则否则,正如以前所看到的循环引用。
从 2012 年起,全部的现代浏览器都内置了一个标记-清除垃圾回收器。前些年全部对于 JavaScript 内存垃圾收集(分代/增量/并发/并行 垃圾收集)的优化都是针对标记-清除算法的实现的优化,但既没有提高垃圾收集算法自己,也没有提高断定对象是否可得到的能力。
你能够查看这篇文章 来了解追踪内存垃圾回收的详情及包含优化了的标记-清除算法。
在以前的第一个示例中,当函数返回,全局对象再也不引用这两个对象。结果,内存垃圾回收器发现它们是不可得到的。
即便两个对象互相引用,也不可以从根变量得到他们。
虽然内存垃圾回收器很方便,可是它们也有其一系列的代价。其中之一即是不肯定性。意思即内存垃圾回收具备不可预见性。你不能肯定内存垃圾收集的确切时机。这意味着在某些状况下,程序会使用比实际须要更多的内存。在其它状况下,在特定的交互敏感的程序中,你也许须要注意那些内存垃圾收集短暂停时间。虽然不肯定性意味着不可以肯定何时能够进行内存垃圾收集,可是大多数 GC 的实现都是在内存分配期间进行内存垃圾回收的通常模式。若是没有进行内存分配,大多数的内存垃圾回收就会保持闲置状态。考虑如下状况:
在该状况下,大多数的内存垃圾回收器不会再运行任何的内存垃圾回收。换句话说,即便能够对该不可得到的引用进行垃圾回收,可是内存收集器不会进行标记。虽然这不是严格意义上的内存泄漏,可是这会致使高于日常的内存使用率。
正如内存管理所说的那样,内存泄漏即一些程序在过去时使用但处于闲置状态,却没有返回给操做系统或者可用的内存池。
编程语言喜欢多种内存管理方法。然而,某个内存片断是否被使用是一个不肯定的问题。换句话说,只有开发人员清楚某个内存片断是否能够返回给操做系统。
某些编程语言会为开发者提供功能函数来解决这个问题。其它的编程语言彻底依赖于开发者全权掌控哪一个内存片断是可回收的。维其百科上有关于手动和自动内存管理的好文章。
JavaScript 以一种有趣的方式来处理未声明变量:当引用一个未声明的变量,会在全局对象上建立一个新的变量。在浏览器中,全局对象是 window
,这意味着以下代码:
function foo(arg) { bar = "some text"; }
等同于:
function foo(arg) { window.bar = "some text"; }
变量 bar
本意是只能在 foo 函数中被引用。可是若是你没有用 var
来声明变量,那么将会建立一个多余的全局变量。在上面的例子中,并不会形成大的事故。但你能够很天然地想象一个更具破坏性的场景。
你也可使用 this
关键字不经意地建立一个全局变量。
function foo() { this.var1 = "potential accidental global"; } // 调用 foo 函数自身,this 会指向全局对象(window)而不是未定义
你能够经过在 JavaScript 文件的顶部添加
'use strict'
来避免以上的全部问题,'use strict'
会切换到更加严格的 JavaScript 解析模式,这样就能够防止建立意外的全局变量。
意外的全局变量的确是个问题,而代码常常会被显式定义的全局变量所污染,根据定义这些全局变量是不会被内存垃圾回收器所收集的。你须要特别注意的是使用全局变量来临时存储和处理大型的位信息。只有在必要的时候使用全局变量来存储数据,记得一旦你再也不使用的时候,把它赋值为 null 或者对其再分配。
由于常常在 JavaScript 中使用 setInterval
,因此让咱们以它为例。
框架中提供了观察者和接受回调的其它指令一般会确保当他们的实例不可得到的时候,全部对回调的引用都会变成不可得到。很容易找到以下代码:
var serverData = loadData(); setInterval(function() { var renderer = document.getElementById('renderer'); if (renderer) { renderer.innerHTML = JSON.stringify(serverData); } }, 5000); // 这将会每隔大约 5 秒钟执行一次
以上代码片断展现了使用定时器来引用再也不须要的节点或数据的后果。
renderer
对象会在某些时候被替换或移除,这样就会致使由定时处理程序封装的代码变得冗余。当这种状况发生的时候,无论是定时处理程序仍是它的依赖都不会被垃圾回收,这是因为须要先中止定时器(记住,定时器仍然处于激活状态)。这能够归结为保存和处理数据加载的 serverData
变量也不会被垃圾回收。
当使用观察者的时候,你须要确保一旦你再也不须要它们的时候显式地移除它们(再也不须要观察者或者对象变得不可得到)。
幸运的是,大多数现代浏览器都会替你进行处理:当被观察者对象变得不可得到时,即便你忘记移除事件监听函数,浏览器也会自动回收观察者处理程序。之前,一些老掉牙的浏览器处理不了这些状况(如老旧的 IE6)。
那么,最佳实践是当对象被废弃的时候,移除观察者处理程序。查看以下例子:
var element = document.getElementById('launch-button'); var counter = 0; function onClick(event) { counter++; element.innerHTML = 'text' + counter; } element.addEventListener('click', onClick); // Do stuff element.removeEventListener('click', onClick); element.parentNode.removeChild(element); // 如今当元素超出范围 // 即便在不能很好处理循环引用的浏览器中也会回收元素和 onClick 事件
在让一个 DOM 节点不可得到以前,你再也不须要调用 removeEventListener
,由于现代浏览器支持用内存垃圾回收器来检测并适当地处理 DOM 节点的生命周期。
若是你使用 jQuery
API(其它的库和框架也支持的 API),你能够在废弃节点以前移除事件监听函数。jQuery 也会确保即便在老旧的浏览器之中,也不会产生内存泄漏。
闭包是 JavaScript 的一个重要功能:嵌套函数能够访问外部(封闭)函数的变量。鉴于 JavaScript 运行时的实现细节,如下方法可能会形成内存泄漏:
var theThing = null var replaceThing = function () { var originalThing = theThing; var unused = function () { if (originalThing) // 引用 originalThing console.log("hi"); }; theThing = { longStr: new Array(1000000).join('*'), someMethod: function () { console.log("message"); } }; }; setInterval(replaceThing, 1000);
当调用 replaceThing
的时候,theThing
对象由一个大数组和新的闭包(someMethod
)所组成。而 originalThing
由 unused
变量建立的闭包所引用(即引用 replaceThing
函数以前的 theThing
变量)。须要记住的是当一旦为同一个父做用域中的闭包建立闭包做用域的时候,该闭包做用域是共享的。
在这样的状况下,闭包 someMethod
和 unused
共享相同的做用域。unused
引用了 origintalThing
。即便 unused
永不使用,也能够在 replaceThing
的做用域外使用 someMethod
函数。而后因为 someMethod
和 unused
共享相同的闭包做用域,unused
变量引用 originalThing
会强迫 unused
保持激活状态(两个闭包共享做用域)。这会阻止内存垃圾回收。
在以上例子中,闭包 someMethod
和 unused
共享做用域,而 unused
引用 origintalThing
。能够在 replaceThing
做用域外经过 theThing
使用 someMethod
,即便 unused
从未被使用。事实上,因为 someMethod
和 unused
共享闭包做用域,unused 引用 origintalThing
要求 unused 保持激活状态。
全部的这些行为会致使内存泄漏。当你不断地运行如上代码片断,你将会发现飙升的内存使用率。当内存垃圾回收器运行的时候,这些内存使用率不会降低。这里会建立出一份闭包链表(当前状况下,其根变量是 theThing
),每一个闭包做用域都间接引用了大数组。
该问题是由 Metor 小组发现的而且他们写了一篇很好的文章来详细描述该问题。
有时候,开发者会在数据结构中存储 DOM 节点。
假设你想要快速更新几行表格内容。若是你在一个字典或者数组中保存对每一个表格行的引用,这将会形成重复引用相同的 DOM 元素:一个在 DOM 树中而另外一个在字典中。若是你想要释放对这些表格行的引用,你须要记得让这些引用变成不可得到。
var elements = { button: document.getElementById('button'), image: document.getElementById('image') }; function doStuff() { elements.image.src = 'http://example.com/image_name.png'; } function removeImage() { // image 元素是 body 元素的直系后代元素 document.body.removeChild(document.getElementById('image')); // 这时,咱们仍然在 elements 全局对象中引用了 #button 元素 // 换句话说,按钮元素仍然在内存中且不可以被垃圾回收器收集 }
你还须要额外考虑的状况是引用 DOM 树中的内节点或者叶节点。若是你在代码中保存着对一个单元格的引用,这时候当你决定从 DOM 中移除表格,却仍然会保持对该单元格的引用,这就会致使大量的内存泄漏。你能够认为内存垃圾回收器将会释放除了该单元格之外的内存。而这还没完。由于单元格是表格的一个后代元素然后代元素保存着对其父节点的引用,对一个单元格的引用会致使没法释放整个表格所占用的内存。
如下内容为我的原创分享。By 三月。
尽量减小内存占用,尽量减小 GC。
减小 GC 次数
浏览器会不定时回收垃圾内存,称为 GC,不定时触发,通常在向浏览器申请新内存时,浏览器会检测是否到达一个临界值再进行触发。通常来讲,GC 会较为耗时,GC 触发时可能会致使页面卡顿及丢帧。故咱们要尽量避免GC的触发。GC 没法经过代码触发,但部分浏览器如 Chrome,可在 DevTools -> TimeLine 页面手动点击 CollectGarbage 按钮触发 GC。
减小内存占用
下降内存占用,可避免内存占用过多致使的应用/系统卡顿,App 闪退等,在移动端尤其明显。当内存消耗较多时,浏览器可能会频繁触发 GC。而如前所述,GC 发生在申请新内存时,若能避免申请新内存,则可避免GC 触发。
对象池**(英语:object pool pattern)是一种设计模式。**一个对象池包含一组已经初始化过且可使用的对象,而能够在有需求时建立和销毁对象。池的用户能够从池子中取得对象,对其进行操做处理,并在不须要时归还给池子而非直接销毁它。这是一种特殊的工厂对象。
若初始化、实例化的代价高,且有需求须要常常实例化,但每次实例化的数量较少的状况下,使用对象池能够得到显著的效能提高。从池子中取得对象的时间是可预测的,但新建一个实例所需的时间是不肯定。
以上摘自维基百科。
使用对象池技术能显著优化需频繁建立对象时的内存消耗,但建议按不一样使用场景作如下细微优化。
按需建立
默认建立空对象池,按需建立对象,用完归还池子。
预建立对象
如在高频操做下,如滚动事件、TouchMove事件、resize事件、for 循环内部等频繁建立对象,则可能会触发GC的发生。故在特殊状况下,可优化为提早建立对象放入池子。
高频状况下,建议使用截流/防抖及任务队列相关技术。
定时释放
对象池内的对象不会被垃圾回收,若极端状况下建立了大量对象回收进池子却不释放只会拔苗助长。
故池子需设计定时/定量释放对象机制,如以已用容量/最大容量/池子使用时间等参数来定时释放对象。
尽量避免建立对象,非必要状况下避免调用会建立对象的方法,如 Array.slice
、Array.map
、Array.filter
、字符串相加、$('div')
、ArrayBuffer.slice
等。
再也不使用的对象,手动赋为 null,可避免循环引用等问题。
使用 Weakmap
生产环境勿用 console.log
大对象,包括 DOM、大数组、ImageData、ArrayBuffer 等。由于 console.log
的对象不会被垃圾回收。详见Will console.log prevent garbage collection?。
合理设计页面,按需建立对象/渲染页面/加载图片等。
避免以下问题:
使用重复 DOM 等,如重复使用同一个弹窗而非建立多个。
如 Vue-Element 框架中,PopOver/Tooltip 等组件用于表格内时会建立 m * n 个实例,可优化为只建立一个实例,动态设置位置及数据。
ImageData 对象是 JS 内存杀手,避免重复建立 ImageData 对象。
重复使用 ArrayBuffer。
压缩图片、按需加载图片、按需渲染图片,使用恰当的图片尺寸、图片格式,如 WebP 格式。
假设渲染一张 100KB 大小,300x500 的透明图片,粗略的可分为三个过程:
加载图片
加载图片二进制格式到内存中并缓存,此时消耗了100KB 内存 & 100KB 缓存。
解码图片
将二进制格式解码为像素格式,此时占用宽 * 高 * 24(透明为32位)比特大小的内存,即 300 * 500 * 32,约等于 585 KB,这里约定名为像素格式内存。我的猜想此时浏览器会回收加载图片时建立的 100KB 内存。
渲染图片
经过 CPU 或 GPU 渲染图片,若为 GPU 渲染,则还需上传到 GPU 显存,该过程较为耗时,由图片尺寸 / 显存位宽决定,图片尺寸越大,上传时间越慢,占用显存越多。
其中,较旧的浏览器如Firefox回收像素内存时机较晚,若渲染了大量图片时会内存占用太高。
PS:浏览器会复用同一份图片二进制内存及像素格式内存,浏览器渲染图片会按如下顺序去获取数据:
显存 >> 像素格式内存 >> 二进制内存 >> 缓存 >> 从服务器获取。咱们需控制和优化的是二进制内存及像素内存的大小及回收。
总结一下,浏览器渲染图片时所消耗内存由图片文件大小内存、宽高、透明度等所决定,故建议:
使用 CSS三、SVG、IconFont、Canvas 替代图片。展现大量图片的页面,建议使用 Canvas 渲染而非直接使用img标签。具体详见 Javascript的Image对象、图像渲染与浏览器内存两三事。
适当压缩图片,可减少带宽消耗及图片内存占用。
使用恰当的图片尺寸,即响应式图片,为不一样终端输出不一样尺寸图片,勿使用原图缩小代替 ICON 等,好比一些图片服务如 OSS。
使用恰当的图片格式,如使用WebP格式等。详细图片格式对比,使用场景等建议查看web前端图片极限优化策略。
按需加载及按需渲染图片。
预加载图片时,切记要将 img 对象赋为 null,不然会致使图片内存没法释放。
当实际渲染图片时,浏览器会从缓存中再次读取。
将离屏 img 对象赋为 null,src 赋为 null,督促浏览器及时回收内存及像素格式内存。
将非可视区域图片移除,须要时再次渲染。和按需渲染结合时实现很简单,切换 src 与 v-src 便可。