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
保存的这个键值对,也会自动消失。
总结
虽然当下的浏览器已经对垃圾回收机制做出了一定的改进和提升,但是内存泄漏的问题我们还是需要关注的。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Is Parallel Programming Hard, And, If So, What Can You Do About
Paul E. McKenney
The purpose of this book is to help you understand how to program shared-memory parallel machines without risking your sanity.1 By describing the algorithms and designs that have worked well in the pa......一起来看看 《Is Parallel Programming Hard, And, If So, What Can You Do About 》 这本书的介绍吧!