那些操作会造成内存泄漏

2021-02-24 20:37发布

12条回答
20200921文 - 做更棒的自己!
2楼 · 2021-02-25 09:22

严格意义上的内存泄露的原因只有一种:没有释放向系统申请的内存,因为不申请内存,就谈不上什么泄露,搞清楚内存泄露的原因,应当从汇编语言的角度考虑问题。

当然没有释放内存的原因是多种的:

有可能是你自己代码写的不好,忘记了释放自己代码里申请的内存,

也有可能是你使用了一个写的不好的库,库本身有问题,这里说的库不仅仅是第三方库,甚至于各种语言的运行时库也有可能出现(再高的人都免不了出BUG),还甚至于操作系统的库,因为操作系统的BUG也多的很(当然系统一般情况不会出现这些低级的错误)。

死循环不能说是内存泄露,概念上应称为死锁,死锁的确是有可能会导致内存无限量增长,但其与内存泄露有本质区别,尽管有时候它们导致的结果在内存层面是相同的。


ban_gank
3楼 · 2021-02-25 09:26

1、JS的回收机制

JavaScript垃圾回收的机制很简单:找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是实时的,因为其开销比较大,所以垃圾回收系统(GC)会按照固定的时间间隔,周期性的执行。

到底哪个变量是没有用的?所以垃圾收集器必须跟踪到底哪个变量没用,对于不再有用的变量打上标记,以备将来收回其内存。用于标记的无用变量的策略可能因实现而有所区别,通常情况下有两种实现方式:标记清除引用计数。引用计数不太常用,标记清除较为常用。


2、标记清除

js中最常用的垃圾回收方式就是标记清除。当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。


function test(){var a=10;//被标记,进入环境var b=20;//被标记,进入环境}test();//执行完毕之后a、b又被标记离开环境,被回收


3、引用此时


引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值(function object array)赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为0的值所占用的内存。


function test(){var a={};//a的引用次数为0var b=a;//a的引用次数加1,为1var c=a;//a的引用次数加1,为2var b={};//a的引用次数减1,为1}


4、哪些操作会造成内存泄露


1)意外的全局变量引起的内存泄露


function leak(){leak="xxx";//leak成为一个全局变量,不会被回收}


2)闭包引起的内存泄露



function bindEvent(){var obj=document.createElement("XXX");obj.οnclick=function(){//Even if it's a empty function}}

闭包可以维持函数内局部变量,使其得不到释放。 上例定义事件回调时,由于是函数内定义函数,并且内部函数--事件回调的引用外暴了,形成了闭包。


解决之道,将事件处理函数定义在外部,解除闭包,或者在定义事件处理函数的外部函数中,删除对dom的引用。

//将事件处理函数定义在外部function onclickHandler(){//do something}function bindEvent(){var obj=document.createElement("XXX");obj.οnclick=onclickHandler;}


//在定义事件处理函数的外部函数中,删除对dom的引用function bindEvent(){var obj=document.createElement("XXX");obj.οnclick=function(){//Even if it's a empty function}obj=null;}


3)没有清理的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[removed])}function removeButton(){document.body.removeChild(document.getElementById('button'))}


4)被遗忘的定时器或者回调



var someResouce=getData();setInterval(function(){var node=document.getElementById('Node');if(node){node[removed]=JSON.stringify(someResouce)}},1000)

这样的代码很常见, 如果 id 为 Node 的元素从 DOM 中移除, 该定时器仍会存在, 同时, 因为回调函数中包含对 someResource 的引用, 定时器外面的 someResource 也不会被释放。



5)子元素存在引起的内存泄露



黄色是指直接被 js变量所引用,在内存里,红色是指间接被 js变量所引用,如上图,refB 被 refA 间接引用,导致即使 refB 变量被清空,也是不会被回收的子元素 refB 由于 parentNode 的间接引用,只要它不被删除,它所有的父元素(图中红色部分)都不会被删除。


6)IE7/8引用计数使用循环引用产生的问题


function fn(){var a={};var b={};a.pro=b;b.pro=a;}fn();

fn()执行完毕后,两个对象都已经离开环境,在标记清除方式下是没有问题的,但是在引用计数策略下,因为a和b的引用次数不为0,所以不会被垃圾回收器回收内存,如果fn函数被大量调用,就会造成内存泄漏。在IE7与IE8上,内存直线上升。


IE中有一部分对象并不是原生js对象。例如,其内存泄漏DOM和BOM中的对象就是使用C++以COM对象的形式实现的,而COM对象的垃圾回收机制采用的就是引用计数策略。因此,即使IE的js引擎采用标记清除策略来实现,但js访问的COM对象依然是基于引用计数策略的。换句话说,只要在IE中涉及COM对象,就会存在循环引用的问题。


var element=document.getElementById("some_element");var myObject=new Object();myObject.e=element;element.o=myObject;

上面的例子在一个DOM元素(element)与一个原生js对象(myObject)之间创建了循环引用。其中,变量myObject有一个名为e的属性指向element对象;而变量element也有一个属性名为o回指myObject。由于存在这个循环引用,即使例子中的DOM从页面中移除,它也永远不会被回收。


看上面的例子,有人会觉得太弱了,谁会做这样无聊的事情,但是其实我们经常会这样做


window.οnlοad=function outerFunction(){var obj=document.getElementById("element"):obj.οnclick=function innerFunction(){};};

这段代码看起来没什么问题,但是obj引用了document.getElementById(“element”),而document.getElementById(“element”)的onclick方法会引用外部环境中的变量,自然也包括obj,是不是很隐蔽啊。


最简单的解决方式就是自己手工解除循环引用,比如刚才的函数可以这样

myObject.element=null;element.o=null;window.οnlοad=function outerFunction(){var obj=document.getElementById("element"):obj.οnclick=function innerFunction(){};obj=null;};

将变量设置为null意味着切断变量与它此前引用的值之间的连接。当垃圾回收器下次运行时,就会删除这些值并回收它们占用的内存。 要注意的是,IE9+并不存在循环引用导致Dom内存泄漏问题,可能是微软做了优化,或者Dom的回收方式已经改变


5、如何分析内存的使用情况

Google Chrome浏览器提供了非常强大的JS调试工具,Memory 视图  profiles 视图让你可以对 JavaScript 代码运行时的内存进行快照,并且可以比较这些内存快照。它还让你可以记录一段时间内的内存分配情况。在每一个结果视图中都可以展示不同类型的列表,但是对我们最有用的是 summary 列表和 comparison 列表。  summary 视图提供了不同类型的分配对象以及它们的合计大小:shallow size (一个特定类型的所有对象的总和)和 retained size (shallow size 加上保留此对象的其它对象的大小)。distance 显示了对象到达 GC 根(校者注:最初引用的那块内存,具体内容可自行搜索该术语)的最短距离。 comparison 视图提供了同样的信息但是允许对比不同的快照。这对于找到泄漏很有帮助。


6、怎样避免内存泄露

1)减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收;

2)注意程序逻辑,避免“死循环”之类的 ;

3)避免创建过多的对象  原则:不用了的东西要及时归还。


思禹小姐姐y
4楼 · 2021-02-25 09:47

1)意外的全局变量引起的内存泄露

2)闭包引起的内存泄露

3)没有清理的DOM元素引用

4)被遗忘的定时器或者回调

5)子元素存在引起的内存泄露

6)IE7/8引用计数使用循环引用产生的问题

lemon
5楼 · 2021-02-25 09:55

内存泄漏指任何对象在您不再拥有或需要它之后仍然存在。

垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量。如果一个对象的引用数量为 0(没有其他对象引用过该对象),或对该对象的惟一引用是循环的,那么该对象的内存即可回收。

setTimeout 的第一个参数使用字符串而非函数的话,会引发内存泄漏。

闭包、控制台日志、循环(在两个对象彼此引用且彼此保留时,就会产生一个循环)


浅浅77
6楼 · 2021-02-25 13:55

意外的全局变量引起的内存泄漏

image.png

你可以通过加上 'use strict' 启用严格模式来避免这类问题, 严格模式会阻止你创建意外的全局变量.

闭包引起的内存泄漏

image.png

闭包可以维持函数内局部变量,使其得不到释放。 上例定义事件回调时,由于是函数内定义函数,并且内部函数--事件回调的引用外暴了,形成了闭包 解决之道,将事件处理函数定义在外部,解除闭包,或者在定义事件处理函数的外部函数中,删除对dom的引用

image.png

image.png

没有清理的DOM元素引用

image.png

虽然我们用removeChild移除了button, 但是还在elements对象里保存着#button的引用,换言之, DOM元素还在内存里面

被遗忘的定时器或者回调

image.png

这样的代码很常见, 如果 id 为 Node 的元素从 DOM 中移除, 该定时器仍会存在, 同时, 因为回调函数中包含对 someResource 的引用, 定时器外面的 someResource 也不会被释放.

子元素存在引用引起的内存泄漏

image.png

黄色是指直接被 js变量所引用,在内存里,红色是指间接被 js变量所引用,如上图,refB 被 refA 间接引用,导致即使 refB 变量被清空,也是不会被回收的子元素 refB 由于 parentNode 的间接引用,只要它不被删除,它所有的父元素(图中红色部分)都不会被删除。

我想吃肉
7楼 · 2021-02-25 18:30

首先让我们从另一个角度来看,如何主动发生内存泄漏呢?当然是想办法给他一个一直存在的强引用了。


static

static这个关键字使一个变量变为只和这个类相关的类变量,和实例无关。他的生命周期是很长的,贯穿于app的启动到关闭。因此只要用一个static引用一个大对象,就可以泄漏了!举个例子:


static Activity activity;


这是最简单粗暴的持有一个activity的引用,这样这个activity退出之后对象并没有被销毁。


static View view;


一个View初始化时会用到context,我们在自定义View,重写构造方法时就知道这个了。因此如果一个View也像这样被持有,那个context也不会被释放。


innerClass

内部类有个特性,是他会持有一个外部类的引用。如果内部类的实例一直存活,那么外部类activity的实例也就一直在。比如持有一个static的内部类引用:

图片

或者以前我们用asynctask时喜欢搞一个匿名内部类执行异步任务,那当我们activity退出后这个异步任务还在执行的话,就会泄露了。

图片

还有自己开个匿名线程:

图片

还有在使用handler时,如果用了匿名handler,那么这个handler会带着activity的引用藏到消息队列中。消息没有被处理,就会造成内存泄漏。类似的,还有timertask等。


register

我们平时会用到很多第三方库,比如ButterKnife EventBus RxJava等等,有的时候要获取系统服务,getSystemService。在使用的时候,都有一个先registerd或者bind的操作,而且在创建的时候会把activity的引用传过去。如果在activity结束时没有unregister或者unbind,就会造成内存泄漏。


刘家起 - coding
8楼 · 2021-02-26 09:08

内存泄漏是指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束。在C++中,因为是手动管理内存,内存泄漏是经常出现的事情。而现在流行的C#和Java等语言采用了自动垃圾回收方法管理内存,正常使用的情况下几乎不会发生内存泄漏。浏览器中也是采用自动垃圾回收方法管理内存,但由于浏览器垃圾回收方法有bug,因此会产生内存泄漏

1、意外的全局变量引起的内存泄漏

2、闭包引起的内存泄漏

3、没有清理的DOM元素引用

4、被遗忘的定时器或者回调

5、子元素存在引用引起的内存泄漏


熊晓燕
9楼 · 2021-02-26 09:47

严格意义上的内存泄露的原因只有一种:没有释放向系统申请的内存,因为不申请内存,就谈不上什么泄露,搞清楚内存泄露的原因,应当从汇编语言的角度考虑问题。

当然没有释放内存的原因是多种的:

有可能是你自己代码写的不好,忘记了释放自己代码里申请的内存,

也有可能是你使用了一个写的不好的库,库本身有问题,这里说的库不仅仅是第三方库,甚至于各种语言的运行时库也有可能出现(再高的人都免不了出BUG),还甚至于操作系统的库,因为操作系统的BUG也多的很(当然系统一般情况不会出现这些低级的错误)。

死循环不能说是内存泄露,概念上应称为死锁,死锁的确是有可能会导致内存无限量增长,但其与内存泄露有本质区别,尽管有时候它们导致的结果在内存层面是相同的。