有过一种经历,网页打开挂在那里,打开PC任务管理器,发现浏览器占用的内存在不断飙升,直至后面浏览器崩溃卡死了;
这就是内存泄漏导致的,很多时候还很难找到原因所在,so,认识一下js的内存管理机制,在前端开发过程中可以避免不必要的问题。
1. 内存的⽣命周期
分配: 在js中,申明变量、函数、对象的时候,系统会⾃动为他们分配内存
使⽤: 使⽤变量、函数等,即读写内存
回收: 垃圾回收机制⾃动回收不再使⽤的内存
2. 内存分配
const num = 111;
const str = “abc”;
const obj = {
a: 111,
b: null
}; // 给对象及其包含的属性分配内存
3. 内存使⽤
使⽤内存:实际上是对分配的内存进⾏读、写操作
读写操作:写⼊⼀个变量或者⼀个对象的属性值,又或者传递函数的参数。
var a = 123; // 分配内存
console.log(a); // 使用内存
4. js中的垃圾回收机制
垃圾回收算法主要依赖于引⽤的概念。
在内存管理的环境中,⼀个对象如果有访问另⼀个对象的权限(隐式或者显式),叫做⼀个对象引⽤另⼀个对象。
例如,⼀个Javascript对象具有对它原型的引⽤(隐式引⽤)和对它属性的引⽤(显式引⽤)。
在这⾥,“对象”的概念不仅特指 JavaScript 对象,还包括函数作⽤域(或者全局词法作⽤域)。
4.1 引⽤计数垃圾回收
不再使用的对象:
就是看⼀个对象是否有指向它的引⽤, 如果没有其他对象指向它了,说明该对象已经不再需要了。
但如果两个对象相互引⽤(即循环引用),尽管他们已不再使⽤,垃圾回收不会进⾏回收,导致内存泄露。
4.2 标记清除算法
标记清除算法将“不再使⽤的对象”定义为“⽆法达到的对象”:就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。
凡是能从根部到达的对象,都是还需要使⽤的。 那些⽆法由根部出发触及到的对象被标记为不再使⽤,稍后进⾏回收。
4.2.1 垃圾收集器在运⾏的时候会给存储在内存中的所有变量都加上标记。
4.2.2 从根部出发将能触及到的对象的标记清除。
4.2.3 那些还存在标记的变量被视为准备删除的变量。
4.2.4 最后垃圾收集器会执⾏最后⼀步内存清除的⼯作,销毁那些带标记的值并回收它们所占⽤的内存空间。
5. js中有哪些常见的内存泄露
5.1 全局变量
function fn() {
text = ‘text content’; // 没有声明变量 实际上是全局变量 => window.text
this.text2 = ‘text2 content’ // 全局变量 => window.text2
}
fn();
5.2 未被清理的定时器、回调函数等
var resp = getRich();
setInterval(function() {
var app = document.getElementById(‘app’);
if(app) {
app.innerHTML = JSON.stringify(resp);
}
}, 5000); // 每 5 秒调⽤⼀次
如果后续 app 元素被移除,整个定时器实际上没有任何作⽤。
但如果没有回收定时器,整个定时器依然有效, 不但定时器⽆法被内存回收,定时器函数中的依赖也⽆法回收。
在上述案例中的 resp 也⽆法被回收。
5.3 闭包
在 JS 中,⼀个内部函数,有权访问包含其的外部函数中的变量(闭包)如下情况:闭包也会造成内存泄露
var some = null;
var fn = function () {
var quoted = bigData;
var not = function () {
if (quoted) console.log(“hi”);
};
bigData = {
longStr: new Array(1000000).join(‘*’),
myMethod: function () {
console.log(“myMethod”);
}
};
};
setInterval(fn, 1000);
上述这段代码,每次调⽤ fn 时,bigData 引用了包含⼀个巨⼤的数组和⼀个对于新闭包 myMethod 的对象。
同时 not 是⼀个引⽤了 quoted 的闭包。
由于闭包之间是共享作⽤域的,尽管 not 可能⼀直没有被调⽤,但是myMethod 可能会被调⽤,就会导致⽆法对其内存进⾏回收。
当这段代码被反复执⾏时,内存会持续增⻓。
5.4 DOM引⽤
很多时候, 我们对 Dom 的操作, 会把 Dom node 的引⽤保存在⼀个数组或者 Map 中。
var elements = {
image: document.getElementById(‘imageId’)
};
function editPro() {
elements.image.src = ‘http://xxxxx.png’;
}
function removeEl() {
document.body.removeChild(document.getElementById(‘imageId’));
// 这个时候我们对于 #imageId 仍然有⼀个引⽤, Image 元素, 仍然⽆法被内存回收.
}
如上述:即使我们通过操作 dom 把image元素给移除了,但是仍然有对 该image元素 的引用,仍然无法对其进行内存回收。
6. 如何避免内存泄露
1、减少不必要的全局变量,可使⽤严格模式避免意外创建全局变量。
2、在使⽤完数据后,及时解除引⽤:赋值null(闭包中的变量,dom引⽤,定时器清除)。
3、组织好代码逻辑,避免死循环等造成浏览器卡顿,崩溃的问题。
最后:实现一个objectSize函数, 计算传⼊的对象所占的Bytes数值.
基本类型所占Bytes:
number: 8字节
string: 每个长度 2 字节
boolean: 4 字节
const test1 = {}
const test2 = {
a: 12345,
b: 'string',
54321: false,
c: test1,
d: test1,
}
// 相同的引用不重复占用内存空间,所以先缓存对象的引用,用于过滤掉已经计算过的引用
const seen = new WeakSet();
function objectSize(object){
if(object === null){
return 0
}
let bytes = 0;
// 对象的key也占内存
const properties = Object.keys(object)
for(let i = 0; i < properties.length; i++){
const key = properties[i];
bytes += calculator(key);
if(typeof object[key] = 'object' && object[key] !== null{
if(seen.has(object[key])){
continue;
}
seen.add(object[key]);
}
bytes += calculator(object[key]);
}
return bytes
}
function calculator(object){
const objectType = typeof object; // Object.prototype.toString.call(object) === '[object Object]'
switch(objectType){
case 'string':{
return object.lengh * 2
}
case 'boolean':{
return 4
}
case 'number':{
return 8
}
case 'object':{
if(Array.isArray(object){
// 对数组的处理
// [1,2,3,4,5,6]
// [{x: 1},{y:2}]
return object.map(calculator).reduce((res, currnt) => res + current, 0)
}else{
// 对对象的处理
return objectSize(object);
}
}
defalut:{
return 0
}
}
}