本文以V8为背景javascript
对以前的文章进行从新编辑,内容作了不少的调整,使其具备逻辑更加紧凑,内容更加全面。java
无论什么程序语言,内存生命周期基本是一致的:node
分配你所须要的内存git
使用分配到的内存(读、写)程序员
不须要时将其释放、归还github
在全部语言中第一和第二部分都很清晰。最后一步在低级语言(例如C语言)中很清晰,可是在像JavaScript等高级语言中,这一步依赖于垃圾回收机制,通常状况下不用程序员操心。web
咱们知道,内存空间能够分为栈空间和堆空间,其中算法
栈空间:由操做系统自动分配释放,存放函数的参数值,局部变量的值等。其操做方式相似于数据结构中的栈。chrome
堆空间:通常由程序员分配释放,这部分空间就要考虑垃圾回收的问题。segmentfault
在JavaScript中
基本类型:undefined,null,boolean,number,string,在内存中占有固定的大小,他们的值保存在栈空间中,咱们经过按值来访问。
引用类型:Object,Array,Function,则在堆内存中为这个值分配空间,而后把它的内存地址保存在栈内存中。(区分变量和对象)
handle
handle是指向对象的指针,在V8中,全部对象都是经过handle来引用,handle主要用于V8的垃圾回收机制。进一步的,handle分为两种:
持久化(Persistent handle),存放在堆上
本地化(Local handle),存放在栈上
scope
scope是handle的集合,能够包含若干个handle,这样就无需将每一个handle逐次释放,而是直接释放整个scope。
context
context是一个执行器环境,使用context能够将相互分离的JavaScript脚本在同一个V8实例中运行,而不互相干涉。在运行JavaScript脚本时,须要显示的指定context对象。
脚本中,绝大多数对象的生存期很短,只有某些对象的生存期较长。为利用这一特色,V8将堆进行了分代。对象起初会被分配在新生区。在新生区的内存分配很是容易:咱们只需保有一个指向内存区的指针,不断根据新对象的大小对其进行递增便可。当该指针达到了新生区的末尾,就会有一次清理(小周期),清理掉新生区中不活跃的死对象。对于活跃超过2个小周期的对象,则需将其移动至老生区。而在老生区则使用标记清除的算法来进行垃圾回收。V8经过分别对新生代对象和老生代对象使用不一样的垃圾回收算法来提高来及回收的效率。这就是所谓的分代策略
。
默认状况下,64位环境下的V8引擎的新生代内存大小32MB、老生代内存大小为1400MB,而32位则减半,分别为16MB和700MB
根据分代策略
,V8将堆空间进行了分隔:
新生区
大多数对象被分配在这里,新生区是一个很小的区域,垃圾回收在这个区域很是频繁,与其余区域相独立。
老生指针区
这里包含大多数可能存储指向其余对象的指针的对象,大多数在新生区存活了一段时间(2个周期)的对象都会被挪到这里。
老生数据区
这里存放只包含原始数据的对象,这些对象没有执行其余对象的指针,例如字符串,数字数组等,它们在新生区存活了一段时间后会被移动到这里。
大对象区
每个区域都是由一组内存页构成的。除大对象区的内存页较大以外,每一个区的内存页都是1MB大小,且按1MB内存对齐。对象超过必定大小时就会被放置到这个区,垃圾回收期从不移动这个区域的对象。
代码区
代码对象,也就是包含JIT以后指令的对象,会被分配到这里。这里是惟一拥有执行权限的内存区。(若是代码对象因过大而被放到大对象区,则该大对象所对应的内存也是可执行的。)
Cell区、属性Cell区、Map区
这些区域存放Cell、属性Cell和Map,每一个区域由于都是存储相同大小的元素,所以内存结构很简单,这里也是为了方便进行回收。
在 node-v4.x 以后,区域进行了合并为:新生区,老生区,大对象区,Map区,Code区
此外,对于一个对象所占的内存空间,也涉及两个概念:shallow size
和retained size
。
shallow size就是对象自己占用内存的大小,不包含其引用的对象。常规对象(非数组)的shallow size有其成员变量的数量和类型决定
retained size是该对象本身的shallow size,加上从该对象能直接或间接访问到对象的shallow size之和。换句话说,retained size是该对象被GC以后所能回收到内存的总和。
这两个概念在使用chrome的开发工具中会看到。
垃圾回收释放的内存即为Retained Size的大小。
新生代使用半空间(Semi-space)分配策略,其中新对象最初分配在新生代的活跃半空间内。一旦半空间已满,一个Scavenge操做将活跃对象移出到其余半空间中,被认为是长期驻存的对象,并被晋升为老生代。一旦活跃对象已被移出,则在旧的半空间中剩下的任何死亡对象被丢弃。
具体的以下:
YG被平分为两部分空间From和To,全部内存从To空间被分配出去,当To满时,开始触发GC。
例如说:
某时刻,To已经为A、B和C分配了内存,当前它只剩下一小块内存未分配。而From全部的内存都空闲着。
此时,一个程序须要为D分配内存,但D须要的内存大小超出了To未分配的内存,此时触发GC,页面中止执行
接着From和To进行对换,即原来的To空间被标志为From,From被标志为To。而且把活的变量值(B)标志出来,而垃圾(A、C)未被标志,它们将会被清掉。
活跃的变量(B)会被复制到To空间,而垃圾(A、C)则被回收。同时,D被分配到To空间,最后的状况以下。
至此,整个GC完成,此过程当中页面会阻塞,因此要尽量的快。
当一个新生代的对象在知足必定条件下,会重新生代被移到老生代,这就是对象的晋升。具体的移动的标准有两种
对象从From空间复制到To空间时,会检查它的内存地址来判断这个对象是否经历过一次新生代的清理结果,若是是(说明存活了两个周期了),则赋值到老生代中,不然则赋值到To空间中。
对象从From空间复制到To空间时,若是To空间已经被使用了超过25%,那么这个对象直接被复制到老生代。
V8在老生代中采用Mark-Sweep和Mark-Compact相结合的垃圾回收策略。
标记-清除算法分为标记和清除两个阶段。
标记阶段,全部堆上的活跃对象都会被标记,每一个内存页有一个用来标记对象的位图,位图中的每一位对应的内存页中的一个字,这个位图须要占据必定的空间。另外还有两位用来标记对象的状态:
若是一个对象为白对象,表示还未被垃圾回收器发现
若是一个对象为灰对象,表示已经被垃圾回收器发现,但其邻接对象还没有处理
若是一个对象为黑对象,表示已经被垃圾回收器发现,其邻接对象已所有处理
那么这里怎么理解标记的过程?这就必须知道:内存管理方式实际上基于图
的概念。
GC Root是内存的根节点,在浏览器中它是window,在Nodejs中则是global对象
图的节点名称是建立它的构造函数名
图的边是引用它的属性名或者变量名
有不少内部的GC Root对用户来讲都不是很重要,从应用的角度来讲有下面几种状况:
全局变量或者全局函数会一直被window这种全局对象所指向,它们会一直占据着内存
DOM节点只有在被javascript对象引用的状况下,会留在内存中。
在进行debug或者console的时候,可能会因为保留了上下文,致使本该被释放的对象被保留下来。
实际上,标记的过程正是以由GC Root创建的图为基础,来实现对象的标记,标记算法的核心是深度优先搜索,大体实现以下:
初始时,位图为空,全部对象都是白对象。
从根对象(GC Root)到达的对象会被染为灰色,放到一个单独的双端队列中。
标记阶段,每次都会从双端队列中取出一个对象,并将其转变为黑对象,其邻接对象转变为灰,而后把其邻接对象加入到双端队列中。
若是双端队列为空或者全部对象都变成黑对象,则结束。
这个算法实现起来仍是蛮繁琐的,从图
的角度来看,其实标记的过程其实是区分活节点和垃圾节点的过程。
从GC Root开始遍历图,全部能到达的节点称为活节点。
GC Root不能到达的节点,该节点就成为垃圾,将会被回收。
标记结束后,全部的对象非黑(活跃节点)即白(垃圾节点)。
标记时间取决于必须标记的活跃对象的数目,对于一个大的web应用,整个堆栈的标记可能须要超过100ms。因为全停顿会形成了浏览器一段时间无响应,因此V8使用了一种增量标记的方式标记活跃对象,将完整的标记拆分红不少小的步骤,每作完一部分就停下来,让JavaScript的应用线程执行一会,这样垃圾回收与应用线程交替执行。V8可让每一个标记步骤的持续时间低于5ms。
举个例子:
window.ob = 2; window.oa = { b1 : 3, b2 : { c1 : 4, c2 : "字符串" } }; window.ob = undefined;
例如图中灰色的节点,它原来表明ob变量值,当window.ob = undefined
后,此节点与GC Root链接的路径ob被切断了,它就成了垃圾,将会被回收。
因为标记完成后,全部对象都已经被标记,即不是活跃对象就是死亡对象,堆上有多少空间已经肯定。清除时,垃圾回收器会扫描连续存放的死对象,将其变成空闲空间。这个任务是由专门的清扫线程同步执行。
标记清除有一个问题就是进行一次标记清楚后,内存空间每每是不连续的,会出现不少的内存碎片。若是后续须要分配一个须要内存空间较多的对象时,若是全部的内存碎片都不够用,将会使得V8没法完成此次分配,提早触发垃圾回收。
标记整理正是为了解决标记清除所带来的内存碎片的问题。标记整理在标记清除的基础进行修改,将其的清除阶段变为紧缩极端。在整理的过程当中,将活着的对象向内存区的一段移动,移动完成后直接清理掉边界外的内存。紧缩过程涉及对象的移动,因此效率并非太好,可是能保证不会生成内存碎片。
新生代对象的Scavenge,这一般是快速的;
经过增量方式的标记步骤,依赖于须要标记的对象数量,时间能够任意长;
完整垃圾回收,这可能须要很长的时间;
带内存紧缩的完整垃圾回收,这也可能须要很长的时间,须要进行内存紧缩。
内存泄漏是指计算机可用内存的逐渐减小,缘由一般是程序持续没法释放其使用的临时内存。
先来一个最简单的DOM泄漏的例子
var el = document.getElementById("_p"); el.mark = "marked"; //移除P function removeP() { el.parentNode.removeChild(el); // el = null; }
程序很是简单,只是把id为_p的HTML元素从页面移除,在移除以前从GC Root遍历此P元素有两条路可走。在执行removeP()
以后,按理来讲该元素应该成为垃圾,所占有的内存应该被释放掉,可是因为还存在这路径el没有被切断,p元素占有的内存没法被释放,致使了内存泄漏。
这个问题很容易理解。例如使用事件代理来减小事件监听的函数,从而减小内存分配的开销。
若是你的页面垃圾回收很频繁,那说明你的页面可能内存使用分配太频繁了。频繁的GC可能也会致使页面卡顿。
在一些框架中,若是建立一个大对象以后,可能不会很快就将其释放,而是会缓存起来,直到没有用处为止。
在使用Chrome进行内存分析的时候,要先在chrome菜单-》工具,或者直接按shift+esc,找到内存管理器,而后选上JavaScript使用的内存(JavaScipt Memory)。
经过Timeline的内存模式,能够在宏观上观察到web应用的内存状况,通常咱们须要关注的点:
GC的时间长度是否正常?
GC频率是否正常?过于频繁会致使卡顿
内存趋势图是否正常?
DOM趋势图是否正常?
这些关注点均可以在timeline的内存视图中看到,如图
timeline统计的内存变化主要有:
js heap:堆空间
documents:文档计数
node:dom节点数
event listener:事件监听器
CPU:在手机端暂时没有
此外还能够经过event log
看到这期间页面执行的操做
profile
面板咱们关注的是Take Heap Snapshot
和Recode Heap Allocations
profile使用必须知道的:
标志为黄色的表示可能内存泄漏
标志为红色表示应该是发生内存泄漏
在profile中的几个概念:
(global property):全局对象,还有全局对象引用的对象
(closure):闭包,这里须要关注一下
(compiled code):V8会先代码编译成特定的语言,再执行
(array,string,number,regexp):这些内置对象的引用
HTML..Element:dom对象的引用
使用快照,必须知道:
每次进行快照时,chrome都会先自动执行一个gc
只有活跃的值,才会反映在快照里
快照有三个视图,它们分别有各自的做用
默认是以概要视图显示的,显示了对象总数,能够展开显示具体内容
该视图用来对照不一样的快照来找到快照之间的差别
在这个视图中,包括三个点
DOMWindow objects:js中的全局对象
GC Root:VM垃圾回收所使用的GC Root
Native Object:被放置到VM中的内置对象
好吧。暂时不知道有什么用?之后再补充。
这个功能能够动态监控,经过次工具能够看到
何时分配了内存,刚刚分配的内存会以深蓝色的柱子表示,柱子越高,内存越大
何时回收了内存,内存被回收的时候,柱子变为灰色
例子1:timeline来查看正常的内存
例子2:经过timeline来发现内存泄漏
能够看到随着时间的增加,页面占用的内存愈来愈多,
在这种状况下就能够怀疑有内存泄漏了,也有多是浏览器尚未进行gc,这个时候咱们能够强制进行垃圾回收(垃圾筒图标)
反复测试,若是发现不管怎么样,内存一直在增加,那么估计你就遇到内存泄漏的问题了。
若是页面中DOM节点的数量一直在攀升,那么确定出现DOM泄漏了
例子3:验证快照以前会进行gc
function Test (s) { this.s = s; } var _test1 = new Test("__________test___1_________"); var _test2 = new Test("__________test___2_________"); new Test("你看不到我,就是这么神奇");
例子4:经过snapshot来发现内存泄漏
打开例子以后,先进行一次快照
点击action,表明这用户的交互
再进行一次快照
使用comparison视图,对比两次快照,如图
能够看到,action以后,内存的数量是增长的(注意,已经gc过了),这说明web应用极有内存泄漏。
一个原则就是找到本不该该存在却还存在的那些值。
例子5:经过内存分配的状况来分析
点击蓝色的柱子,能够看到详细的状况,来进行分析
例子6:经过timeline来分析gc过于频繁致使卡顿的问题
此例子在移动手机的浏览器进行测试,页面仍是相对简单,在比较复杂的移动web应用,这种状况仍是比较危险的,可能会致使页面卡死。