InterviewMap —— Javascript (五)
栏目: JavaScript · 发布时间: 5年前
内容简介:不想C语言那样,拥有原始底层的内存操作方法如即使是使用高级语言,开发者对内存管理也应该有所了解(至少要有基础的了解)。有时,开发者必须理解自动内存管理会遇到问题(例如:垃圾回收中的错误或者性能问题等),以便能够正确处理它们。(或者是找到适当的解决方法,用最小的代价去解决。)如果一个值不再需要了,但是垃圾回收机制确无法回收,这时候就是内存泄漏了。
不想 C语言 那样,拥有原始底层的内存操作方法如 malloc free
。js使用的是自动垃圾回收机制,也就是说js引擎会自动去判别变量的使用情况来自动回收那些不使用的内存块。
即使是使用高级语言,开发者对内存管理也应该有所了解(至少要有基础的了解)。有时,开发者必须理解自动内存管理会遇到问题(例如:垃圾回收中的错误或者性能问题等),以便能够正确处理它们。(或者是找到适当的解决方法,用最小的代价去解决。)
如果一个值不再需要了,但是垃圾回收机制确无法回收,这时候就是内存泄漏了。
const arr = [1, 2, 3, 4]; console.log('hello world'); 复制代码
上面代码中,数组 [1, 2, 3, 4]
是一个值,会占用内存。变量 arr
是仅有的对这个值的引用,因此引用次数为 1
。尽管后面的代码没有用到 arr
,它还是会持续占用内存。
如果增加一行代码,解除 arr
对 [1, 2, 3, 4]
引用,这块内存就可以被垃圾回收机制释放了。
const arr = [1, 2, 3, 4]; console.log('hello world'); arr = null; 复制代码
以上例子是在全局下的,arr为全局变量,它属于全局变量对象,全局变量对象只有在浏览器窗口关闭的时候才会被销毁,因此我们才会不推荐使用过多的全局变量。
因此,并不是说有了垃圾回收机制,程序员就轻松了。你还是需要关注内存占用:那些很占空间的值,一旦不再用到,你必须检查是否还存在对它们的引用。如果是的话,就必须手动解除引用。
1、内存的生命周期
内存往往经历: 操作系统分配内存 == 使用内存 == 内存释放 三个阶段。
2、垃圾回收机制
(1)标记清除
该算法由以下步骤组成:
- 垃圾回收器构建“roots”列表。Roots 通常是代码中保留引用的全局变量。在 JavaScript 中,“window” 对象可以作为 root 全局变量示例。
- 所有的 roots 被检查并标记为 active(即不是垃圾)。所有的 children 也被递归检查。从 root 能够到达的一切都不被认为是垃圾。
- 所有未被标记为 active 的内存可以被认为是垃圾了。收集器限制可以释放这些内存并将其返回到操作系统
如果是该算法,循环引用就不会出现。在函数调用后,两个对象不再被从全局对象可访问的东西所引用。因此,垃圾回收器将发现它们是不可达的。
(2)引用计数
如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。
上图中,左下角的两个值,没有任何引用,所以可以释放。
function f() { var o1 = {}; var o2 = {}; o1.p = o2; // o1 references o2 o2.p = o1; // o2 references o1. This creates a cycle. } f(); 复制代码
在函数调用之后,它们离开了作用域,因此它们实际上已经无用了,可以被释放了。然而,引用计数算法认为,由于两个对象中的每一个至少被引用了一次,所以也不能被垃圾回收。
3、什么是内存泄漏
实质上,内存泄漏可以被定义为应用程序不再需要的内存,但由于某种原因,内存不会返回到操作系统或可用内存池中。
4、内存泄漏的例子
(1)意外的全局变量
function foo(arg) { bar = "this is a hidden global variable"; //等同于window.bar="this is a hidden global variable" this.bar2= "potential accidental global"; //这里的this 指向了全局对象(window),等同于window.bar2="potential accidental global" } 复制代码
如果是在函数中未使用var声明的变量,那么会将其放到全局window上,会产生一个意外的全局变量。全局变量会一直驻留内存,一次我们要坚决避免这种意外发生。
解决办法就是使用'use strict'开启严格模式。
(2)循环引用
let obj1 = { a: 1 }; // 一个对象(称之为 A)被创建,赋值给 obj1,A 的引用个数为 1 let obj2 = obj1; // A 的引用个数变为 2 obj1 = null; // A 的引用个数变为 1 obj2 = null; // A 的引用个数变为 0,此时对象 A 就可以被垃圾回收了 复制代码
但是引用计数有个最大的问题: 循环引用。
function func() { let obj1 = {}; let obj2 = {}; obj1.a = obj2; // obj1 引用 obj2 obj2.a = obj1; // obj2 引用 obj1 } 复制代码
函数执行完毕之后,按道理是可以被销毁的。内部的变量也会被销毁。但根据引用计数方法,obj1 和 obj2 的引用次数都不为 0,所以他们不会被回收。要解决循环引用的问题,最好是在不使用它们的时候手工将它们设为空。上面的例子可以这么做:
obj1 = null; obj2 = null; 复制代码
(3)被遗忘的计时器和回调函数
let someResource = getData(); setInterval(() => { const node = document.getElementById('Node'); if(node) { node.innerHTML = JSON.stringify(someResource)); } }, 1000); 复制代码
每隔一秒执行一次匿名回调函数,该函数由于会被长期调用,因此其内部的变量都不会被回收,引用外部的someResource也不会被回收。那什么才叫结束呢?就是调用了 clearInterval。
比如开发SPA页面,当我们的某一个页面中存在这类定时器,跳转到另一个页面的时候,其实这里的定时器已经暂时没用了,但是我们在另一个页面的时候,内存中还是回你保留上一个页面的定时器资源,因此这就会导致内存泄漏。解决办法就是即使的使用clearInterval来清除定时器。
(4)闭包
JavaScript 开发的一个关键方面就是闭包:一个可以访问外部(封闭)函数变量的内部函数。
值得注意的是闭包本身不会造成内存泄漏,但闭包过多很容易导致内存泄漏。闭包会造成对象引用的生命周期脱离当前函数的上下文,如果闭包如果使用不当,可以导致环形引用(circular reference),类似于死锁,只能避免,无法发生之后解决,即使有垃圾回收也还是会内存泄露。
(5)console
console.log
:向web开发控制台打印一条消息,常用来在开发时调试分析。有时在开发时,需要打印一些对象信息,但发布时却忘记去掉 console.log
语句,这可能造成内存泄露。
在传递给 console.log
的对象是不能被垃圾回收 :recycle:,因为在代码运行之后需要在开发 工具 能查看对象信息。所以最好不要在生产环境中 console.log
任何对象。
(6)DOM泄漏
在Js中对DOM操作是非常耗时的。因为JavaScript/ECMAScript引擎独立于渲染引擎,而DOM是位于渲染引擎,相互访问需要消耗一定的资源。
假如将JavaScript/ECMAScript、DOM分别想象成两座孤岛,两岛之间通过一座收费桥连接,过桥需要交纳一定“过桥费”。JavaScript/ECMAScript每次访问DOM时,都需要交纳“过桥费”。因此访问DOM次数越多,费用越高,页面性能就会受到很大影响。
为了减少DOM访问次数,一般情况下,当需要多次访问同一个DOM方法或属性时,会将DOM引用缓存到一个局部变量中。但如果在执行某些删除、更新操作后,可能会忘记释放掉代码中对应的DOM引用,这样会造成DOM内存泄露。
var refA = document.getElementById('refA'); document.body.removeChild(refA); // #refA不能回收,因为存在变量refA对它的引用。将其对#refA引用释放,但还是无法回收#refA。 // 使用refA = null; 来释放内存 复制代码
var MyObject = {}; document.getElementById('myDiv').myProp = MyObject; 解决方法: 在window.onunload事件中写上: document.getElementById('myDiv').myProp = null; 复制代码
给DOM对象用attachEvent绑定事件:
function doClick() {} element.attachEvent("onclick", doClick); 解决方法: 在onunload事件中写上: element.detachEvent('onclick', doClick); 复制代码
从外到内执行appendChild。这时即使调用removeChild也无法释放。范例:
var parentDiv = document.createElement("div"); var childDiv = document.createElement("div"); document.body.appendChild(parentDiv); parentDiv.appendChild(childDiv); 解决方法: 从内到外执行appendChild: var parentDiv = document.createElement("div"); var childDiv = document.createElement("div"); parentDiv.appendChild(childDiv); document.body.appendChild(parentDiv); 复制代码
反复重写同一个属性会造成内存大量占用(但关闭IE后内存会被释放)。范例:
for(i = 0; i < 5000; i++) { hostElement.text = "asdfasdfasdf"; } 这种方式相当于定义了5000个属性! 解决方法: 其实没什么解决方法:P~~~就是编程的时候尽量避免出现这种情况咯~~ 复制代码
5、WeakMap 你了解吗?
前面说过,及时清除引用非常重要。但是,你不可能记得那么多,有时候一疏忽就忘了,所以才有那么多内存泄漏。
最好能有一种方法,在新建引用的时候就声明,哪些引用必须手动清除,哪些引用可以忽略不计,当其他引用消失以后,垃圾回收机制就可以释放内存。这样就能大大减轻 程序员 的负担,你只要清除主要引用就可以了。
ES6 考虑到了这一点,推出了两种新的数据结构:WeakSet 和 WeakMap。它们对于值的引用都是不计入垃圾回收机制的,是一种弱引用,所以名字里面才会有一个"Weak",表示这是弱引用。
const wm = new WeakMap(); const element = document.getElementById('example'); // 引用计数1 wm.set(element, 'some information'); // 此处是弱引用,不计数 wm.get(element) // "some information" 复制代码
WeakMap
里面对 element
的引用就是弱引用,不会被计入垃圾回收机制。
也就是说, DOM
节点对象的引用计数是 1
,而不是 2
。这时,一旦消除对该节点的引用,它占用的内存就会被垃圾回收机制释放。 Weakmap
保存的这个键值对,也会自动消失。
总结
虽然当下的浏览器已经对垃圾回收机制做出了一定的改进和提升,但是内存泄漏的问题我们还是需要关注的。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
JavaScript忍者秘籍
John Resig、Bear Bibeault / 徐涛 / 人民邮电出版社 / 2015-10 / 69.00
JavaScript语言非常重要,相关的技术图书也很多,但没有任何一本书对JavaScript语言的重要部分(函数、闭包和原型)进行深入、全面的介绍,也没有任何一本书讲述跨浏览器代码的编写。本书是jQuery库创始人编写的一本深入剖析JavaScript语言的书。 本书共分四个部分,从准入训练、见习训练、忍者训练和火影训练四个层次讲述了逐步成为JavaScript高手的全过程。全书从高级We......一起来看看 《JavaScript忍者秘籍》 这本书的介绍吧!