100字范文,内容丰富有趣,生活中的好帮手!
100字范文 > JavaScript内存管理和内存泄漏

JavaScript内存管理和内存泄漏

时间:2020-11-11 20:39:28

相关推荐

JavaScript内存管理和内存泄漏

简介

像C语言这样的底层语言,一般是由应用程序手动分配内存和释放内存的。而JavaScript是由引擎自动完成内存管理的。虽然JavaScript会自动释放内存,但如果编码不当,也会造成内存泄漏问题。文章分析了JavaScript的内存管理机制,同时,总结了几种典型的内存泄漏的场景,最后,介绍了排查内存泄漏的相关实践。希望能够帮助我们在编码中尽量避免内存泄漏的问题,即使有问题了也可以快速定位。

程序执行

内存是用来存储程序运行过程中的数据

程序的执行是一系列函数调用的过程。函数可以嵌套调用,当子函数执行完毕,其占用的内存资源也应该得到释放,所以用栈结构来存储上下文信息(包括基本类型变量)。

如果把所有数据都放入栈中,将会影响上下文切换的效率。所有还需要另外的空间来存储比较大的数据(引用类型变量),即堆空间

内存空间

根据程序运行的需要,JavaScript引擎把内存空间划分如下:

代码空间栈空间: 程序执行上下文信息(包括基本类型数据)。堆空间: 引用类型数据。

栈空间

栈空间是一种先进后出的结构。当调用函数时,把执行上下文压入栈,函数内定义的基本类型变量也会被存在栈中。函数执行完毕后,再把数据弹出栈

采用静态分配方式,即在编译时就能确定变量占用内存大小。

堆空间

内存分配

当创建一个引用类型变量时,引擎会在堆空间分配内存,并通过一个栈变量索引到该堆地址

采用动态分配方式,即在运行时确定变量占用内存大小。

垃圾回收

回收对象

垃圾回收策略有一个重要的环节,如何确定哪些内存是可回收的

通常有两种方法:引用计数法访问可达法

引用计数法:当一个对象被引用次数为零时,就认为该对象是可被回收的。但存在循环引用的问题。访问可达法:从根对象(window/global)出发,递归遍历子对象,把不可访问到的对象认为是可被回收的。

循环引用:

访问可达法:

代际假说

JavaScript引擎的垃圾回收机制是基于"代际假说"的:即大部分对象的生命周期比较短,在一个回收周期内可被回收。

基于"代际假说",把堆内存分为"新生代""老生代"。新对象被放入"新生代",经过两轮垃圾回收依然存活的对象被移动到"老生代"。

"新生代"和"老生代"的垃圾回收策略有所不同:"新生代"使用Minor GC,"老生代"使用Major GC

Major GC

主回收器的工作过程包括: marking、sweeping、defragmenting三个阶段。

marking: 完成标记。把不可访问到的对象,标记为非活动对象(垃圾)。sweeping: 回收垃圾。记录非活动对象的地址信息,下次有新对象需要分配内存时可以直接使用。defragment: 整理内存碎片。经过多次的内存回收后,原本连续的内存块会被分割得比较零散。这时候把所有对象复制到连续的内存片段,可以提高内存利用效率。

Minor GC

“新生代"又分为"对象区""空闲区":新对象放入"对象区”;当对象区快写满了,进行垃圾回收,把活动对象复制到"空闲区";然后交换"对象区"和"空闲区"的角色。

副回收器的工作过程包括: marking、copying、 exchange role。

marking:标记阶段。同Major GC。copying:回收垃圾。把活动对象从"对象区"复制到"空闲区",此时的"对象区"空间被当做垃圾回收。exchange role:交换"对象区"和"空闲区"的角色。即"空闲区"变成"对象区",可以继续分配给新对象。

Major GC VS Minor GC

相同点:

marking阶段是相同的。当复制对象到新的内存时,需要更新引用地址,保证该对象能够被正常访问。

不同点:

sweeping阶段是不同的:

Minor GC是复制活动对象到空闲区,留下来的非活动对象会被当作垃圾回收。由于新生代的对象一般占内存小且存活周期短,所以复制并不会带来太大的开销。

Major GC不是每次都复制,而是把非活动对象的地址信息存起来,下次可以直接分配给新对象。当内存碎片过多时再进行复制。

GC执行时机

GC是运行在主线程的,会阻塞JS脚本的执行,被称为全停顿(stop-the-world)。

针对全停顿问题,常见的有以下两种方案:

增量(increment)执行

把GC任务拆分成多个小任务,碎片化执行。虽然没有提高GC工作效率,但可以给JS脚本更高优先级的响应,避免页面卡顿。

并发(concurrent)执行

GC运行在另外的辅助线程,不再占用主线程。理论上避免了全停顿问题。

内存泄漏

少量的内存泄露是不容易被察觉的,用户一刷新页面,问题也就被隐藏了。当页面交互越多,用户停留时间越长,特别是SPA程序,就更容易暴露内存泄露问题: 导致可用内存减少,GC工作频率和时长增加,主线程被占用,最终可能导致页面卡顿甚至程序崩溃。

典型场景

在了解内存管理机制之后,我们知道当一个变量是可访问的/被引用的,那么GC将不会回收内存。所以开发中应该做到:变量不用之后立刻解除引用。下面介绍几种典型的内存泄漏场景,大家在开发中可以规避。

不必要的全局变量

未使用关键字声明的变量

// 非严格模式下,memoryLeak会被挂在全局上function testGlobalValue() {memoryLeak = new MemoryLeak();}

解决方法:

开启严格模式或者lint工具检查 未声明的变量。尽量避免使用全局变量存储大量的数据。

不恰当的闭包(closure)

闭包是JavaScript的一大特性:当一个对象被内部函数引用了,就会形成闭包。即使外部函数已经执行完毕,内部函数依然可以访问到该对象。由于闭包对象是存储在堆空间的,如果内部函数被引用了,闭包对象将不会被释放。

举个栗子:

let globalArray = [];function testClosure() {function wrap() {let dateArray = new Array(5 * 1000).map(() => {return new Date();});return function inner() {return dateArray;};}globalArray.push(wrap());}

由于内部函数inner()引用了dateArray,形成闭包。并且inner()最终被全局对象globalArray引用,导致dateArray无法被释放。如果这不是预期行为,那就是内存泄漏了。

解决方法:

尽量避免在闭包中使用大量的数据。

detached DOM/EventListener

<body><div id="node"></div><script>function onclick() {}let node = document.getElementById("detachedNode");node.addEventListener("click", onclick);//node.removeEventListener("click", onclick); // 删除节点后,未移除的事件监听也会造成内存泄漏。node.parentNode.removeChild(node); // 虽然从dom树移除了节点,但该节点还被JavaScript变量(node)引用着,所以导致dom节点无法被回收。</script></body>

解决方法:

在操作完DOM之后,及时清除对DOM节点的引用,即赋值null。在释放DOM节点之前,记得移除节点上的事件监听。

未移除的事件监听

添加事件监听会占用内存,所以在事件处理完成后立刻移除事件监听。

function testUnremoveEventListener() {document.addEventListener("mousedown", onMousedown);}function onMousedown() {document.addEventListener("mousemove", onMousemove);document.addEventListener("mouseup", onMouseup);}function onMousemove() {console.log("mousemove");}function onMouseup() {console.log("mouseup");// 事件处理完后应该立刻释放 事件监听// document.removeEventListener("mousedown", onMousedown);// document.removeEventListener("mousemove", onMousemove);// document.removeEventListener("mouseup", onMouseup);}

类似的,还有setTimeout和setInterval函数。

注意:setInterval运行期间,入参对象始终被定时器引用。setTimeout在触发回调之后,入参会被解除引用。

function testTimer() {let couter = new MemoryLeak();// 在定时器被clearInterval之前,couter对象不会被释放。window.timeoutID = setInterval(onTimeout, 1 * 1000, couter);}function onTimeout(couter) {console.log("timeout");couter.value++;}

排查方法

思路

目前是没有工具可以自动检查出内存泄漏的,但是,内存泄漏有个明显的特征:当执行某个操作之后,占用内存会变大。所以排查问题的思路就是逐一排查可能存在内存泄漏的页面组件,观察内存使用情况

在浏览器开发环境中,有两个辅助工具:timelineheap snapshot

timeline直观地展示了内存分配随时间的变化情况。heap snapshot记录了当前内存中所有对象占用内存的大小。

过程

具体排查过程如下:

通过timeline排查出页面中可能存在内存泄漏的组件。审查对应模块的代码,结合内存泄漏的典型场景,找出可能存在问题的代码(业务数据/listener/detached dom)。记录操作前后的heap snapshot,通过对比可疑对象的内存大小变化,进一步锁定可疑对象。找出可能存在问题的代码进行修复,重复上述操作进行验证。

这里以上面的detached DOM/EventListener为例做分析:

第一步,操作页面组件,观察timeline。我们观察到执行动作后,timeline出现了蓝线且一直未消失。

第二步,选中蓝线附近区域,查看下面的内存对象列表。观察Shallow Size大的对象是否为应用程序数据对象。由于例子中的数据量不大,所以这里很难看出问题。

第三步,审查对应模块的代码。发现是DOM操作和事件监听相关的,可能是detached DOM和Listener造成。所以尝试搜索detached对象,果然出现了detached DOM。

第四步,记录操作前后的heap snapshot,进行对比。观察到存在未释放的DOM节点和事件监听。

第五步,找出可能存在问题的代码进行修复,然后再通过timeline和heap snapshot进行验证。

参考资料

MDN内存管理

图文并茂讲清楚 JavaScript 内存管理

浅谈V8垃圾回收机制

浅谈JS内存机制

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。