资讯专栏INFORMATION COLUMN

【重温基础】22.内存管理

Pandaaa / 928人阅读

摘要:内存泄露内存泄露概念在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。判断内存泄漏,以字段为准。

本文是 重温基础 系列文章的第二十二篇。
今日感受:优化学习方法。

系列目录:

【复习资料】ES6/ES7/ES8/ES9资料整理(个人整理)

【重温基础】1-14篇

【重温基础】15.JS对象介绍

【重温基础】16.JSON对象介绍

【重温基础】17.WebAPI介绍

【重温基础】18.相等性判断

【重温基础】19.闭包

【重温基础】20.事件

【重温基础】21.高阶函数

本章节复习的是JS中的内存管理,这对于我们开发非常有帮助。

前置知识
绝大多数的程序语言,他们的内存生命周期基本一致:

分配所需使用的内存 ——(分配内存

使用分配到的内存(读、写) ——(使用内存

不需要时将其释放归还 ——(释放内存

对于所有的编程语言,第二部分都是明确的。而第一和第三部分在底层语言中是明确的。
但在像JavaScript这些高级语言中,大部分都是隐含的,因为JavaScript具有自动垃圾回收机制(Garbage collected)。
因此在做JavaScript开发时,不需要关心内存的使用问题,所需内存分配和无用内存回收,都完全实现自动管理。

1.概述
像C语言这样的高级语言一般都有底层的内存管理接口,比如 malloc()free()。另一方面,JavaScript创建变量(对象,字符串等)时分配内存,并且在不再使用它们时“自动”释放。 后一个过程称为垃圾回收。这个“自动”是混乱的根源,并让JavaScript(和其他高级语言)开发者感觉他们可以不关心内存管理。 这是错误的。
——《MDN JavaScript 内存管理》

MDN中的介绍告诉我们,作为JavaScript开发者,还是需要去了解内存管理,虽然JavaScript已经给我们做好自动管理。

2.JavaScript内存生命周期 2.1 分配内存

在做JavaScript开发时,我们定义变量的时候,JavaScript便为我们完成了内存分配:

var num = 100;      // 为数值变量分配内存
var str = "pingan"; // 为字符串变量分配内存
var obj = {
    name : "pingan"
};                  // 为对象变量及其包含的值分配内存
var arr = [1, null, "hi"]; // 为数组变量及其包含的值分配内存

function fun(num){
  return num + 2;
};                  // 为函数(可调用的对象)分配内存

// 函数表达式也能分配一个对象
someElement.addEventListener("click", function(){
  someElement.style.backgroundColor = "blue";
}, false);

另外,通过调用函数,也会分配内存:

// 类型1. 分配对象内存
var date = new Date();   // 分配一个Date对象
var elem = document.createElement("div"); // 分配一个DOM元素

// 类型2. 分配新变量或者新对象
var str1 = "pingan";
var str2 = str1.substr(0, 3); // str2 是一个新的字符串

var arr1 = ["hi", "pingan"];
var arr2 = ["hi", "leo"];
var arr3 = arr1.concat(arr2); // arr3 是一个新的数组(arr1和arr2连接的结果)
2.2 使用内存

使用内存的过程实际上是对分配的内存进行读取与写入的操作。
通常表现就是使用定义的值。
读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。

var num = 1;
num ++; // 使用已经定义的变量,做递增操作
2.3 释放内存

当我们前面定义好的变量或函数(分配的内存)已经不需要使用的时候,便需要释放掉这些内存。这也是内存管理中最难的任务,因为我们不知道什么时候这些内存不使用。
很好的是,在高级语言解释器中,已经嵌入“垃圾回收器”,用来跟踪内存的分配和使用,以便在内存不使用时自动释放(这并不是百分百跟踪到,只是个近似过程)。

3.垃圾回收机制

就像前面提到的,“垃圾回收器”只能解决一般情况,接下来我们需要了解主要的垃圾回收算法和它们局限性。

3.1 引用

垃圾回收算法主要依赖于引用的概念。
即在内存管理环境中,一个对象如果有权限访问另一个对象,不论显式还是隐式,称为一个对象引用另一个对象。
例如:一个JS对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。
注意:
这里的对象,不仅包含JS对象,也包含函数作用域(或全局词法作用域)。

3.2 引用计数垃圾收集

这个算法,把“对象是否不再需要”定义为:当一个对象没有被其他对象所引用的时候,回收该对象。这是最初级的垃圾收集算法。

var obj = {
    leo : {
        age : 18
    };
};

这里创建2个对象,一个作为leo的属性被引用,另一个被分配给变量obj

// 省略上面的代码
/*
我们将前面的
    {
        leo : {
            age : 18
        };
    };
称为“这个对象”
*/
var obj2 = obj;  // obj2变量是第二个对“这个对象”的引用
obj = "pingan";  // 将“这个对象”的原始是引用obj换成obj2

var leo2 = obj2.leo;  // 引用“这个对象”的leo属性

可以看出,现在的“这个对象”已经有2个引用,一个是obj2,另一个是leo2

obj2 = "hi"; 
// 将obj2变成零引用,因此,obj2可以被垃圾回收
// 但是它的属性leo还在被leo2对象引用,所以还不能回收

leo2 = null;
// 将leo变成零引用,这样obj2和leo2都可以被垃圾回收

这个算法有个限制
无法处理循环引用。即两个对象创建时相互引用形成一个循环。

function fun(){
    var obj1 = {}, obj2 = {};
    obj1.leo = obj2;  // obj1引用obj2
    obj2.leo = obj1;  // obj2引用obj1
    return "hi pingan";
}
fun();

可以看出,它们被调用之后,会离开函数作用域,已经没有用了可以被回收,然而引用计数算法考虑到它们之间相互至少引用一次,所以它们不会被回收。

实际案例
在IE6,7中,使用引用计数方式对DOM对象进行垃圾回收,常常造成对象被循环引用导致内存泄露:

var obj;
window.onload = function(){
    obj = document.getElementById("myId");
    obj.leo = obj;
    obj.data = new Array(100000).join("");
};

可以看出,DOM元素obj中的leo属性引用了自己obj,造成循环引用,若该属性(leo)没有移除或设置为null,垃圾回收器总是且至少有一个引用,并一直占用内存,即使从DOM树删除,如果这个DOM元素含大量数据(如data属性)则会导致占用内存永远无法释放,出现内存泄露。

3.3 标记清除算法

这个算法,将“对象是否不再需要”定义为:对象是否可以获得。

标记清除算法,是假定设置一个根对象(root),在JS中是全局对象。垃圾回收器定时找所有从根开始引用的对象,然后再找这些对象引用的对象...直到找到所有可以获得的对象搜集所有不能获得的对象

它比引用计数垃圾收集更好,因为“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”。

循环引用不再是问题:

function fun(){
    var obj1 = {}, obj2 = {};
    obj1.leo = obj2;  // obj1引用obj2
    obj2.leo = obj1;  // obj2引用obj1
    return "hi pingan";
}
fun();

还是这个代码,可以看出,使用标记清除算法来看,函数调用之后,两个对象无法从全局对象获取,因此将被回收。相同的,下面案例,一旦 obj 和其事件处理无法从根获取到,他们将会被垃圾回收器回收。

var obj;
window.onload = function(){
    obj = document.getElementById("myId");
    obj.leo = obj;
    obj.data = new Array(100000).join("");
};

注意: 那些无法从根对象查询到的对象都将被清除。

3.4 个人小结

在日常开发中,应该注意及时切断需要回收对象与根的联系,虽然标记清除算法已经足够强壮,就像下面代码:

var obj,ele=document.getElementById("myId");
obj.div = document.createElement("div");
ele.appendChild(obj.div);
// 删除DOM元素
ele.removeChild(obj.div);

如果我们只是做小型项目开发,JS用的比较少的话,内存管理可以不用太在意,但是如果是大项目(SPA,服务器或桌面应用),那就需要考虑好内存管理问题了。

4.内存泄露(Memory Leak) 4.1 内存泄露概念
在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。 ——维基百科

其实简单理解:一些不再使用的内存无法被释放
当内存占用越来越多,不仅影响系统性能,严重的还会导致进程奔溃。

4.2 内存泄露案例

全局变量

未定义的变量,会被定义到全局,当页面关闭才会销毁,这样就造成内存泄露。如下:

function fun(){
    name = "pingan";
};

未销毁的定时器和回调函数

如果这里举一个定时器的案例,如果定时器没有回收,则不仅整个定时器无法被内存回收,定时器函数的依赖也无法回收:

var data = {};
setInterval(function(){
    var render = document.getElementById("myId");
    if(render){
        render.innderHTML = JSON.stringify(data);
    }
}, 1000);

闭包

var str = null;
var fun = function(){
    var str2 = str;
    var unused = function(){
        if(str2) console.log("is unused");
    };
    str = {
        my_str = new Array(100000).join("--");
        my_fun = function(){
            console.log("is my_fun");
        };
    };
};
setInterval(fun, 1000);

定时器中每次调用funstr都会获得一个包含巨大的数组和一个对于新闭包my_fun的对象,并且unused是一个引用了str2的闭包。
整个案例中,闭包之间共享作用域,尽管unused可能一直没有调用,但my_fun可能被调用,就会导致内存无法回收,内存增长导致泄露。

DOM引用

当我们把DOM的引用保存在一个数组或Map中,即使移除了元素,但仍然有引用,导致无法回收内存。例如:

var ele = {
    img : document.getElementById("my_img")
};
function fun(){
    ele.img.src = "http://www.baidu.com/1.png";
};
function foo(){
    document.body.removeChild(document.getElementById("my_img"));
};

即使foo方法将my_img元素移除,但fun仍有引用,无法回收。

4.3 内存泄露识别方法

浏览器

通过Chrome浏览器查看内存占用:

步骤如下:

打开开发者工具,选择 Timeline 面板

在顶部的Capture字段里面勾选 Memory

点击左上角的录制按钮

在页面上进行各种操作,模拟用户的使用情况

一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况

如果内存占用基本平稳,接近水平,就说明不存在内存泄漏。

反之,就是内存泄漏了。

命令行

命令行可以使用 Node 提供的process.memoryUsage方法。

console.log(process.memoryUsage());
// { rss: 27709440,
//  heapTotal: 5685248,
//  heapUsed: 3449392,
//  external: 8772 }

process.memoryUsage返回一个对象,包含了 Node 进程的内存占用信息。该对象包含四个字段,单位是字节,含义如下。

rss(resident set size):所有内存占用,包括指令区和堆栈。

heapTotal:"堆"占用的内存,包括用到的和没用到的。

heapUsed:用到的堆的部分。

external: V8 引擎内部的 C++ 对象占用的内存。

判断内存泄漏,以heapUsed字段为准。

参考文章

MDN JavaScript指南 内存管理

精读《JS 中的内存管理》

阮一峰老师JavaScript 内存泄漏教程

本部分内容到这结束

Author 王平安
E-mail pingan8787@qq.com
博 客 www.pingan8787.com
微 信 pingan8787
每日文章推荐 https://github.com/pingan8787...
JS小册 js.pingan8787.com
微信公众号 前端自习课

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/101725.html

相关文章

  • 重温JS基础--变量、作用域和内存问题

    摘要:作用域链是保证对执行环境有权访问的所有变量和函数的有序访问。如上,包含的作用域链包含它自己的变量对象和全局环境的变量对象,为什么能在函数内访问,这就是通过作用域链找的。 前言JavaScript的变量类型是弱类型的,在特定的时间内保存一个特定的值,变量的值和数据类型可以在脚本的生命周期内随意改变。 1. 基本类型和引用类型的值 JavaScript包含两种不同类型的值:基本类型和引用类...

    huaixiaoz 评论0 收藏0
  • 重温基础】9.正则表达式

    摘要:前置知识中的正则表达式是用来匹配字符串中指定字符组合的模式。另外需要记住正则表达式也是对象。在正则表达式创建时更新,不执行。替换与正则表达式匹配的子串。查找以十六进制数规定的字符。正则表达式拓展介绍在中有两种情况。 本文是 重温基础 系列文章的第九篇。 今日感受:时间管理-角色管理法。 系列目录: 【复习资料】ES6/ES7/ES8/ES9资料整理(个人整理) 【重温基础】1.语...

    ispring 评论0 收藏0
  • 重温基础】19.闭包

    摘要:系列目录复习资料资料整理个人整理重温基础篇重温基础对象介绍重温基础对象介绍重温基础介绍重温基础相等性判断本章节复习的是中的关于闭包,这个小哥哥呀,看看。这里随着闭包函数的结束,执行环境销毁,变量回收。 本文是 重温基础 系列文章的第十九篇。今日感受:将混乱的事情找出之间的联系,也是种能力。 系列目录: 【复习资料】ES6/ES7/ES8/ES9资料整理(个人整理) 【重温基础】...

    nanfeiyan 评论0 收藏0
  • 重温基础】12.使用对象

    摘要:本文是重温基础系列文章的第十二篇。注意对象的名称,对大小写敏感。基础用法第一个参数是目标对象,后面参数都是源对象。用途遍历对象属性。用途将对象转为真正的结构。使用场景取出参数对象所有可遍历属性,拷贝到当前对象中。类似方法合并两个对象。 本文是 重温基础 系列文章的第十二篇。 今日感受:需要总结下2018。 这几天,重重的感冒发烧,在家休息好几天,伤···。 系列目录: 【复习资料...

    garfileo 评论0 收藏0
  • 重温JS基础--引用类型(二)

    摘要:创建一个日期对象中国标准时间在调用构造函数而不传参数的情况下,新创建的对象自动获得当前日期和时间。日期格式化方法类型还有一些专门用于将日期格式化为字符串的方法中国标准时间下午以上的这些方法都会根据系统环境而异。 咱们接着上面一篇继续~ 1. Date类型 JavaScript中的Date类型使用自UTC时间,1970年1月1日零时开始的毫秒数来保存日期。创建一个日期对象: var no...

    NusterCache 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<