这是我参与8月更文挑战的第9天,活动详情查看:8月更文挑战
前言
TIP ? JS 内存机制相关的面试问题,常常紧跟在闭包之后出现。
JS内存生命周期
JS内存生命周期和大多数程序语言一样,分为三个阶段
- 分配内存
- 内存的读与写
- 内存的释放
栈内存和堆内存
JS数据类型
-
基本类型:Sting、Number、Boolean、null、undefined、Symbol。这类型的数据最明显的特征是大小固 定、体积轻量、相对简单,它们被放在 JS 的栈内存里存储。
-
引用类型:比如 Object、Array、Function 等等。这类数据比较复杂、占用空间较大、且大小不定,它们被放在 JS 的堆内存里存储。
堆与栈
栈是线性表的一种,而堆则是树形结构
let a = 0;
let b = "Hello World" let c = null;
let d = { name: '青莲' };
let e = ['青莲', '小明', '小白'];
复制代码
5个变量,在内存中的形态如图所示:
在访问 a、b、c 三个变量时,过程非常简单:从栈中直接获取该变量的值。
而在访问 d 和 e 时,则需要分两步走:
1. 从栈中获取变量对应对象的引用(即它在堆内存中的地址)
2. 拿着 1 中获取到的地址,再去堆内存空间查询,才能拿到我们想要的数据
垃圾回收机制
内存如何释放呢?
每隔一段时间,JS 的垃圾收集器就会对变量做 “巡检”。当它判断一个变量不再被需要之后,它就会把这个变量所占用的内存空间给释放掉,这个过程叫做垃圾回收。
那么 JS 是如何知道一个变量是否不被需要的呢? 这里就引出了内存管理的又一个考点 —— 垃圾回收算法。在 JS 中,我们讨论的垃圾回收算法有两种 —— 引用计数法和标记清除法。
引用计数法
这是最初级的垃圾回收算法,它在现在的浏览器机会呗淘汰了,因为引用计数法无法甄别循环引用场景下的“垃圾”
标记清除法
标记清除法是现在浏览器的标准垃圾回收算法,在标记清除法中,一个变量是否被需要的判断标准,是它是否可抵达。
这个算法有两个阶段,分别是标记阶段和清除阶段:
- 标记阶段:垃圾收集器会先找到根对象,在浏览器里,根对象是Window,在Node里,根对象是Global,从根对象出发,垃圾收集器会扫描所有可以通过根对象触及的变量,这些对象会被标记为“可抵达”。
- 清除阶段:没有被标记为“可抵达”的变量,就会被认为是不需要的变量,这波变量会被清除
复制代码
function badCycle() {
var obj1 = {}
var obj2 = {}
obj1.target = obj2
obj2.tartget = obj1
}
badCycle()
复制代码
badCycle执行完毕后,从根对象window出发,obj1和obj2都会被识别为不可达的对象,他们会按照预期被清除掉。这样以来,循环引用的问题,就被标记清除解决掉了。
闭包与内存泄漏
啥是内存泄漏?
该释放的变量(内存垃圾)没有被释放,仍然占着内存,导致内存占用不断攀高,带来性能恶化等一系列问题,这种现象就叫内存泄漏。
var theThing = null
var replaceThing = function() {
var originalThing = theThing
var unused = function() {
if(originalThing) //originalThing的引用
console.log('haha')
}
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function() {
console.log('hehehe')
}
}
}
setInterval(replaceThing, 1000)
复制代码
这段代码有什么问题?
在这段代码里,unused是一个不会被使用的闭包,但和它共享同一个父级作用域的someMethod,则是一个可以被使用的闭包。unused引用了originalThing,这导致和它共享作用域的someMethod也间接地引用了originalThing。结果就是someMethod“被迫”产生了对originalThing的持续引用,originalThing虽然没有任何意义和作用,却永远不会被回收。不仅如此,originalThing每次setInterval都会改变一次指向,所以即使我们用非有效的方式替换旧的theThing值,但是旧值不会被清理。这导致无法被回收的无用originalThing越堆积越多,最终导致严重的内存泄漏。
内存泄漏的原因
1.全局变量
function test() {
name = 'qinglian'
}
复制代码
2.没有清除的setInterval和setTimeout
3.清除不当的DOM
const myDiv = document.getElementById('myDiv')
function handleMyDiv() {
}
handleMyDiv()
document.body.removeChild(document.getElementById('myDiv'))
复制代码
myDiv这个变量对这个DOM的引用仍然存在,它仍然是一块“可抵达”的内存。
各位码农在工作的时候都应该保持严谨的态度!!!!!