哈喽,我是前端菜鸟JL😄 下面分享一下垃圾回收机制这个专题
前言
内存生命周期分为三部分:
- 分配内存
- 使用内存(读、写)
释放\归还内存
所有语言第二部分都是明确的,第一和第三可能会比较隐含,或者在底层语言明确。
但JavaScript这些高级语言中,所以大部分是隐含的。
V8
V8是Google开源的JavaScript和WebAssembly引擎,用C++编写。它用于Chrome和Node.js等。 V8实现了ECMAScript和WebAssembly
为什么在Chrome浏览器中,V8被限制了内存的使用?
(64位约1.4G/1464MB , 32位约0.7G/732MB)
原因:V8最初为浏览器而设计,不太可能遇到大量内存的场景
深层原因:由于V8垃圾回收机制限制,清除大量内存垃圾很耗时间,这样会引起JavaScript线程阻塞暂停,影响到性能。
什么是内存泄漏
程序运行时候会像操作系统提出要求,在运行时供给内存。
过多的内存占用,如果不能及时释放,会影响到系统性能甚至导致进程崩溃。
不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)
常见的内存泄漏
良好的代码规范赏心悦目,不良好的则是狮山(= =。。),并且可能出现难以察觉且有害的内存泄露问题。
在内存有限的设备上,或者在函数会被调用很多次的情况下,内存泄漏可能是个大问题。JavaScript中的内存泄漏大部分是由不合理的引用导致的。
下面来讲一讲常见的内存泄漏:
- 意外声明的全局变量
function aa() {
name = '111'
}
解析器会把name当成window属性创建(相当于window.name = '111'),只要window不被清除就不消失
解决方案:变量声明加上var、let、const关键字,这样变量就会在函数执行完毕后离开作用域,然后被垃圾回收机制回收
- 被遗忘的定时器
// 定时器没有被释放,如赋值为null,就会造成内存泄露
let name = '111'
setInterval(() => {
console.log(name)
}, 100)
只要定时器一直运行,回调函数中引用的
name 就会一直占用内存。垃圾回收程序当然知道这一点,因而就不会清理外部变量。
- 使用不当的闭包
// 闭包创建变量执行结束后没有赋值为null容易造成内存泄露
let outer = function() {
let name = '111'
return function() {
return name
}
}
这会导致分配给
name 的内存被泄漏。以上代码创建了一个内部闭包,只要
outer 函数存在就不能清理
name ,因为闭包一直在引用着它。假如
name 的内容很大(不止是一个小字符串),那可能就是个大问题了。
注意:经jy指正,闭包造成内存泄漏是不对的说法
旧版本的浏览器的垃圾回收机制存在问题,通过一些处理就可以避免内存无法回收的问题
闭包并不会引起内存泄漏,只是由于IE9 之前的版本对JavaScript对象和COM对象使用不同的垃圾收集,从而导致内存无法进行回收
解决方法是,在退出函数之前,将不使用的局部变量赋值为null,全部删除
- 未清除的DOM引用
// 在对象中引用 DOM
var elements = {
btn: document.getElementById('btn'),
}
function doSomeThing() {
elements.btn.click()
}
function removeBtn() {
// 移除 DOM 树中的 btn
document.body.removeChild(document.getElementById('button'))
// 但是此时全局变量 elements 还是保留了对 btn 的引用, btn 还是存在于内存中,不能被 GC 回收
}
虽然别的地方删除了,但是对象中还存在对 dom 的引用。
解决方法是删除 DOM 节点时,也要释放 JS 对节点的引用:
elements.btn = null;
垃圾回收机制
有些语言(比如 C 语言)必须手动释放内存,程序员负责内存管理。
当然这很麻烦,所以大多数语言提供自动内存管理,减轻程序员的负担,这被称为"垃圾回收机制"(garbage collector)
原理
通过自动内存管理实现内存分配和闲置资源回收
基础思路:确定哪个变量不再使用(不被引用),释放它占用的内存
时长:周期性,每隔一段时间自动运行
存在问题:判断某块内存是否还在用,属于'不可判定的'问题,单纯靠算法解决不了
标记策略:标记清除和引用计数
标记清除(最常用)
例如变量进入上下文,函数内部声明一个变量时,就会被加入上下文的标记,离开时就会加上离开上下文标记等;
给变量加标记方式很多,比如进入上下文,反转某一位,或者维护一个标记列表等,实现方式并不重要,关键是策略。
工作流程:
- 垃圾收集器会在运行时候给存储在内存所有变量加上标记
- 从
根部(全局对象)出发将触及到的对象的标记清除 - 那些还存在标记的对象被视为待删除的变量
- 最后垃圾收集器会执行最后一步内存清理工作,销毁那些带标记的值并回收释放它们占用的内存
这里可能会有一个小误区,释放是不是意味着把整个内存每个字节进行一个清零操作呢?
注意:不会,它只需要把对象所占用内存的起始、结束的地址给记录下来,放在一个空闲的地址列表里就可以了,下次再分配新对象的时候就到空闲地址列表中去找看有没有一块足够的空间容纳新对象,如果有,那就进行一个内存分配,并不会把可以作为垃圾的对象占用的内存做一个清零的处理
引入计数(不常用)
思路:
对每个值都记录它被引用的次数,如声明变量则这个值引用值为1,被赋值或者被引用,引用值+1,;
类似反操作,当不被引用或者被覆盖就-1,直到引用值为0时,就会被认为不再被使用,下一次垃圾回收程序会将该值占据的内存释放。
遇到严重问题:循环引用
JavaScript 引擎不再使用这种算法,但某些旧版本的IE仍然会受这种算法的影响,原因是JavaScript会访问非原生JavaScript对象(如DOM元素)
解除引用(手动法)
将变量设置为 null ,从而释放其引用,这个建议最适合全局变量和全局对象的属性,局部变量在超出作用域后会被自动解除引用
注意:解除对一个值的引用并不会自动导致相关内存被回收。解除引用的关键在于确保相关的值已经不在上下文里了,因此它在下次垃圾回收时会被回收。
通过 const 和 let 声明提升性能
ES6增加这两个关键字不仅有助于改善代码风格,而且同样有助于改进垃圾回收的过程。
因为 const 和 let 都以块(而非函数)为作用域。
所以相比于使用 var ,使用这两个新关键字可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存。
结语
希望能给你带来帮助✨~
分享不易,点赞鼓励🤞