说起来已经有两次面试问到这个了!
一度天真的以为内存泄露是内存丢失…
其实没用的内存却没有及时释放就成为内存泄露。
参考链接/书籍
《JavaScript 高级程序设计》
JavaScript 内存泄漏教程
4 种 JavaScript 内存泄漏浅析及如何用谷歌工具查内存泄露
JS 是单线程,你了解其运行机制吗 ?
内存管理
An interesting kind of JavaScript memory leak
进程和线程
进程是 CPU 资源分配的最小单位
线程是 CPU 调度的最小单位
线程是基于进程的一次程序运行单位,一个进程可以有多个线程(至少有一个),以及进程拥有独立内存,而多个线程共享内存。
一个操作系统上可以做很多事 -> 多进程
一个程序可以做很多事(比如一个 word 文档可以进行很多操作) -> 多线程
浏览器和 JavaScript
浏览器是多线程的,而 JavaScript 是单线程的,也就是说同一个时间只能做一个事情。
至少 Chrome 是多进程的,也侧面体现了为什么 Chrome 是吃内存大户
至于 JavaScript 为什么是单线程的。
这也算是语言特色吧,也不能说是为什么,JavaScript 作为浏览器脚本语言,主要用途是与用户互动,以及操作 DOM。
假如是多线程,就会有很多麻烦的同步问题,比如说有两个线程一个增加了某个 DOM 节点,另一个删除了同一个 DOM 节点,就会有冲突。
内存管理
生命周期
- 分配
- 使用 (读、写)
- 回收
。。。有点被降智打击的感觉
分配内存
虽然和词法作用域没有什么关系,但是 JavaScript 的内存是在定义变量的时候就完成了内存分配。
内存回收
因为其实根据算法来确定哪些内存已经不再需要是不可能的。所以 JavaScript 的垃圾回收具有一定的局限性。
垃圾回收的原理也比较简单:找到那些不再继续使用的变量,然后释放他们的内存。
而垃圾收集器会按照固定的时间间隔或者代码执行中预定的收集时间周期性的执行这一操作。
标记清除
最常见的垃圾收集方式
把对象是否不再需要定义为对象是否可获得。
垃圾收集器在运行的时候会给存储在内存里的所有变量都增加上标记(当然,可以使用任何标记方法)。然后,他会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后垃圾收集器完成内存清除工作,销毁那些带标记的值并回收他们所占用的内存空间。 – 《JavaScript 高级程序设计》第 79 页
这个算法假定设置一个叫做根(root)的对象(在 Javascript 里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。 – MDN
其实就是回收一下那些不会被引用到的对象。
引用计数
不太常见
引用计数的含义是跟踪每个值被引用的次数。
当声明了一个变量并将一个引用类型赋值给该变量时,这个值的引用次数就是 1。如果同一个值又被赋给另一个变量,则该值的引用次数+1 。相反如果包含这个值引用的变量又取得了另一个只,则这个值的引用次数-1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存回收。 – 《JavaScript 高级程序设计》第 79 页
这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。– MDN
内存泄露
闭包
网路上的资料看下来,对于闭包会不会导致内存泄露存在比较大的争议。
主要是围绕"占用内存算不算内存泄露"来说的吧。
但是还会支持闭包不会引起内存泄露,不正确的闭包才会这样的说法。
1 | var theThing = null; |
上面的代码就是利用了闭包引起的一个内存泄露。
一个关键是"同一父级作用域下的闭包创建的作用域是共享的"
所以 originalThing 会挂载 someMethod 的作用域上。
所以每 .1 秒运行一次函数,都会给 theThing 重新赋值,而 theThing 的 someMethod 又保持了对 originalThing 的引用权限。所以每次运行 replaceThing, 都引用了上一次运行的 theThing,而 someMethod 的作用域里有上一次的 originalThing,就一直不会中断(或者说回收)之前一次的旧值。
这样 originalThing 就一直在,旧的不去新的又一直来,引发了内存泄露。
解决方法嘛,就是删掉 unused() 。
或者在 replaceThing 函数的最后设置 originalThing = null。
If you have a large object that is used by some closures, but not by any closures that you need to keep using, just make sure that the local variable no longer points to it once you’re done with it. Unfortunately, these bugs can be pretty subtle; it would be much better if JavaScript engines didn’t require you to have to think about them.
全局变量
就是还是关于不使用 var / let / const 就直接对变量进行操作的问题。
不使用关键字进行变量定义会导致变量直接挂在 window 对象上(浏览器里,若是 nodejs 则挂在 global 对象上)。
虽然说得是没有明确的全局变量会引发内存泄露,但是其实使用了 var 的全局变量,也有可能会引发内存泄露。
可以考虑使用'use strict'来避免无意生成全局变量。
定/计时器未回收
用过 ReactJs 应该知道在 ComponentDidMount 里定义的计时器、定时器需要在 ComponentWillUnmoment 里回收。
因为定/计时器可能会引用到已经被卸载的节点或者数据,如果没能回收定/计时器,则其引用的数据、节点也无法被 GC 回收。
DOM 引用泄露
思路就是在 js 里引用了一个 DOM,然后对这个 DOM 元素从 HTML 里删除了之后应该对 js 里的引用设置为 null,否则 GC 不会回收他。就导致了 DOM 其实在 HTML 里已经不存在了,但是还存在于内存里。