垃圾回收 及 内存泄漏
一、JS 垃圾回收策略:
垃圾回收策略有:标记清除 和 引用计数 。 (详细介绍请参考J《avaScript高级程序设计》)
- 标记清除:
- 引用计数:
引用计数(Reference Counting),这其实是早先的一种垃圾回收算法,它把 对象是否不再需要 简化定义为 对象有没有其他对象引用到它,如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收,目前很少使用这种算法了,因为它的问题很多,不过我们还是需要了解一下。
【这种策略主要是在 循环引用时,会出现清除不了的现象】 - 总结:
- js中最常用的垃圾收集方式是标记清除。但是COM对象(BOM和DOM对象就是使用C++以COM对象的形式实现的)垃圾收集机制采用的就是引用计数策略。
使用标记清除的方式,不会引起内存泄漏的问题(哪怕对象相互引用也没有关系)。因为变量离开执行环境就会被回收。
(个人认为)现在的浏览器基本不用引用计数,只有早期的ie中com对象是使用引用计数的,现在应该都是使用标记清除,所以不要考虑内存泄漏的问题。 - 使用引用计数策略, 在函数中出现循环引用的时候,当函数执行完毕,可是函数里的变量引用计数不为零,垃圾回收机制无法回收,这个对象将一直存在内存中。如果这个函数被多次调用的话,就会导致大量内存得不到回收。从而出现内存泄漏问题。
- 解决循环引用的问题,在不使用变量的时候,手工断开原生JavaScript对象(获取DOM对象的变量)与DOM元素之间的链接。用element = null;
- JavaScript内存泄漏的质疑 : http://www.cnblogs.com/iyangyuan/p/4310601.html (个人觉得现代浏览器对垃圾回收已经做的很极致了。只是个别情况需要注意下手动释放下)
- js中最常用的垃圾收集方式是标记清除。但是COM对象(BOM和DOM对象就是使用C++以COM对象的形式实现的)垃圾收集机制采用的就是引用计数策略。
总结(个人观点):原生js对象不存在内存泄漏问题(原生js对象常用标记清除的垃圾回收策略),引起内存泄漏的问题主要还是在函数中对DOM的操作,同时DOM对象的属性指向函数的对象。内存泄漏不一定是闭包引起的,只是闭包函数不注意就会在闭包中出现对DOM的循环引用,重新内存泄漏的问题。
参考: https://www.cnblogs.com/yhf286/p/4918095.html 或 https://www.cnblogs.com/sunhuahuaa/p/7655587.html (推荐)
具体 闭包函数的问题 有空看 JavaScript高级程序设计
二、js中给对象设置 null值 与 垃圾回收 的关系
参考:https://www.cnblogs.com/cwWeb/archive/2012/07/14/2591956.html 或 https://zhidao.baidu.com/question/1175437169245726939.html
- 垃圾清理是针对对象的,不是针对某个标示符的。对于值类型,函数执行后,如果其所在作用域并未被应用,会立即释放;要销毁一个对象,必须 要消除一个对象 的所有外部 引用。
所以 将变量 设置 为 null,对象 不一定会被 垃圾回收。对象 只要 没有 外部 引用,自动回 被回收的。 - 非JS 创建的 对象(如DOM对象),js 的标识符(变量) 只是对它的引用。无法 通过设置 标识符 为 null ,将这样的对象 垃圾回收。因为 整个 web 环境还是有 引用他们的。
这样的对象有:DOM 树上DOM对象【js创建的DOM对象没有挂DOM树,就可以被垃圾回收】,BOM对象等。
这种 web 环境 带有 的对象,要清除,还要看自身 是否有 这样的 API 去处理。
三、绑定事件 的对象,如果 对象被 回收了。那他所绑定的事件还 在吗,会被 回收吗。
- 首先 确认 对象是否真的被回收了,如果只是 指向的 标识符 设置 为了 null ,这个对象不一定 被回收了。 https://www.imooc.com/wenda/detail/490415
var wraper = document.querySelector('#wraper'); // 获取 DOM 树上的DOM对象 wraper.onclick = handle; wraper = null; // 这里只是 把 wraper 标准 对 DOM 的引用断开了,但是 web 环境 对 这个DOM对象的 引用还是 存在的,所以这个DOM 对象并没有被回收。DOM对象的事件还是存在的。
- 确认 DOM 对象已经被 清除 回收了,那他所绑定的 事件函数 也会 被 垃圾回收掉。因为没有指向这个 事件函数的 东西了。
不过 这点 可能 和 浏览器的 垃圾 回收 机制不同,好像低版本 的 IE 浏览器,需要手动设置 具名函数,函数名 为 null。 - js中事件是 具名函数 和 匿名函数是有区别的:https://developer.aliyun.com/ask/66988?spm=a2c6h.13159736
四、内存泄露的4种方式及如何避免
参考:https://www.pianshen.com/article/16561228160/ 或 https://blog.csdn.net/weixin_57092157/article/details/118596807
【个人理解】正常的内存占用 属于 内存开销;该回收的内存无法回收,才叫内存泄漏。
function foo(){ bar = "我是全局变量" } // 正常情况 foo函数执行完毕,foo的执行栈内存都要释放出去。但是bar意外的变成 全局变量,bar 变量无法被回收。
-
四种类型的常见 JavaScript 内存泄露
-
意外的全局变量
全局作用域下的变量,会在页面的生命周期内存续(即 一直存在)。《js高级程序设计第4版》function foo(arg){ bar = "全局变量" // 全局变量在生命周期中,都会存在,不会被回收的。需要手动设置 null 才会被回收 }
全局变量存储简单数据倒是无伤大雅的,不用考虑回收的问题。当全局变量用于临时存储和处理大量信息时,需要多加小心。如果必须使用全局变量存储大量数据时,确保用完以后把它设置为 null 或者重新定义。
-
被遗忘的定时器或回调 【定时器本身不是什么问题,主要是回调函数,在函数中形成了闭包】
function fn(){ let name = 'jake' setInterval(() => { console.log(name); }, 100) }
如果这个name是复制的引用类型数据,就比较占用内存了。如下
var someResouce=getData(); setInterval(function(){ var node=document.getElementById('Node'); if(node){ node.innerHTML=JSON.stringify(someResouce) } },1000)
这样的代码很常见,如果 id 为 Node 的元素从 DOM 中移除,该定时器仍会存在,同时,因为回调函数中包含对 someResource 的引用,定时器外面的 someResource 也不会被释放。
【题外话】setTimeout 和 setInterval 调用时,会返回一个ID值(就是普通数字)。取消计时器,必须使用这个ID。这个ID只是基本数据类型,不占内存。
-
脱离 DOM 的引用,【js引用的DOM对象,在DOM树中已经删除了,但是js保持对它的引用。js中这个DOM对象不会被回收掉。】
var elements ={ button: document.getElementById('button'), image: document.getElementById('image'), text: document.getElementById('text') } function doStuff() { image.src="http://some.url/image" button.click(); console.log(text,innerHTML) } //更多逻辑 function removeButton () { //按钮是body的后代元素 document.body.remoyeChild(document.getElementById('button')) // 这里虽然删除了DOM树上的这个DOM对象,但是js中还是保持者对它的引用 //此时,仍日存在一个全局的button的引用 //elements字典·button元素仍旧在内存中,不能被GC回收jdvd一日一条 }
-
闭包 【针对闭包函数, 如何避免内存泄露,请看 《闭包函数》文章】
主要是函数里面给DOM对象添加事件函数,形成闭包。这时DOM对象一直引用着闭包函数,导致包含函数无法被回收。function assignHandler(){ var element = document.getElementById('someElement') // assignHandler作用域中,element变量引用了 document对象的属性 element.onclick = () => console.log(element.id); // 闭包中引用着 assignHandler() 的活动对象,阻止了对element的引用计数归零。 }
-
- 总结:JS的内存泄漏 都是因为 全局作用域 下,还是保持着 对 函数 活动对象的引用。导致函数执行完,而 活动对象无法被回收。如下说明
- 被遗忘的定时器或回调 :这个还是本质上还是 因为形成闭包引起的。
- 脱离 DOM 的引用:全局下变量,引用了 DOM对象。DOM对象 在document文档上移除了,但是这个全局变量还是保存着这个 DOM对象。
- 闭包:全局下变量 引用 着 闭包函数。document对象也是全局变量。
- 对循环引用的说明 【这种循环引用,现在浏览器可能已经做了优化处理了】 参考:https://www.cnblogs.com/greatluoluo/p/5930685.html
【闭包中的循环引用指的是,闭包中引用了 包含函数的 对象。注意不是基本类型 】
function leakMemory() { var el = document.getElementById('el'); var obj= { 'el': el }; el.obj= obj; } // 这段代码中(还不是闭包函数),函数中引用了外部的dom对象,同时, 函数中的 el.obj 给外部的dom对象添加了一个属性,引用了函数内的obj对象。 // 这样就存在了相互引用的问题,js垃圾回收机制无法回收这个函数。
我们常常像下面这样,在函数中给DOM对象创建事件,这样无意识中就存在内存泄漏问题。(个人觉得解决内存泄漏问题,可以把变量使用后设置为 null ,解除引用)
function addHandler() { var el = document.getElementById('el'); el.onclick = function() { el.style.backgroundColor = 'red'; } }
https://www.cnblogs.com/yakun/p/3932026.html
https://blog.csdn.net/johnny0991/article/details/51778436 (能导致内存泄漏的一定是引用类型的变量,而值类型的变量是不存在内存泄漏的)
五、chrome浏览器调试内存泄漏工具:
- F12控制台 - Performance :看内存的 时间线
- F12控制台 - Memory :保存内存快照,可以对比两次内存的差异对象。
需要调用 API 释放内存的对象(而非设置null)
- URL.createObjectURL() :这个API生成的对象,在内存中是会常驻的。需要手动释放内存的。
释放内存:var objectURL = URL.createObjectURL(object); URL.revokeObjectURL(objectURL ) // 释放 内存
- DOM 对象,JS中对某个DOM对象引用了。后面 document文档中删除了这个DOM,JS对这个DOM的引用也要断开。
var button = document.getElementById('button'), document.body.remoyeChild(document.getElementById('button')) // 这里document文档删除了DOM树上的这个DOM对象,但是js中还是保持者对它的引用,需要手动释放对他的引用 button = null