一、内存基本概念

1.1、生命周期

不管什么程序语言,内存生命周期基本是一致的:

  • 分配你所需要的内存
var n = 123; // 给数值变量分配内存
var s = "azerty"; // 给字符串分配内存 var o = {
a: 1,
b: null
}; // 给对象及其包含的值分配内存 // 给数组及其包含的值分配内存(就像对象一样)
var a = [1, null, "abra"]; function f(a){
return a + 2;
} // 给函数(可调用的对象)分配内存 // 函数表达式也能分配一个对象
someElement.addEventListener('click', function(){
someElement.style.backgroundColor = 'blue';
}, false);
  • 使用分配到的内存(读、写)
// 有些函数调用结果是分配对象内存:
var d = new Date(); // 分配一个 Date 对象
var e = document.createElement('div'); // 分配一个 DOM 元素
// 有些方法分配新变量或者新对象:

var s = "azerty";
var s2 = s.substr(0, 3); // s2 是一个新的字符串
// 因为字符串是不变量,
// JavaScript 可能决定不分配内存,
// 只是存储了 [0-3] 的范围。 var a = ["ouais ouais", "nan nan"];
var a2 = ["generation", "nan nan"];
var a3 = a.concat(a2);
// 新数组有四个元素,是 a 连接 a2 的结果

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

  • 不需要时将其释放、归还

在所有语言中第一和第二部分都很清晰。最后一步在低级语言(例如C语言)中很清晰,但是在像JavaScript等高级语言中,这一步依赖于垃圾回收机制,一般情况下不用程序员操心。垃圾回收算法我会在后续介绍。

1.2 堆与栈

我们知道,内存空间可以分为栈空间和堆空间,其中

栈空间:由操作系统自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。栈空间主要存储基本数据类型,如undefined,null,boolean,number,string,在内存中占有固定的大小,我们通过按值来访问。

堆空间:一般由程序员分配释放,这部分空间就要考虑垃圾回收的问题。堆空间主要存储引用类型,如Object,Array,Function,在堆内存中为这个值分配空间,然后把它的内存地址保存在栈内存中。(区分变量和对象)

1.3 垃圾回收算法

垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。

垃圾收集算法中,IE 6, 7采用的是引用计数垃圾收集算法。该算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。该算法有一个弊端就是“循环引用”,是导致内存泄漏的重要原因。

// A不引用B,B和C会被销毁
A ---------> B ------------> C
// A不引用B,B和C不会销毁
A ---------> B ------------> C
       ^、_ _ _ _ _ _ _|

而从2012年起,所有的现代浏览器都换成了标记-清除算法,这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。是否可获得的判断标准就是这个对象是否被root引用(包含直接引用和间接引用),如果不被引用到,就被收回。这样就很好的避免了循环引用的问题。

var a = new A(); //创建A的实例
var b = new B(); //创建B的实例 a.link = b;
b.link = a; a = null;
b = null; /*
上面的例子中 A ,B的实例形成循环引用, 最后把a ,b设为null。 在引用计数垃圾收集算法中,A ,B的实例相互引用,各自的引用数不为0,所以不会被收回。 而在标记-清除算法中,由于a, b设为null,A ,B的实例都不会被root也就是window对象引用到,会被收回。 */

二、内存泄漏

本质上,内存泄漏可以定义为:应用程序不再需要占用内存的时候,由于某些原因,内存没有被操作系统或可用内存池回收。编程语言管理内存的方式各不相同。只有开发者最清楚哪些内存不需要了,操作系统可以回收。一些编程语言提供了语言特性,可以帮助开发者做此类事情。另一些则寄希望于开发者对内存是否需要清晰明了。下面介绍了4种常见的内存泄漏:

2.1、全局变量

JavaScript 处理未定义变量的方式比较宽松:未定义的变量会在全局对象创建一个新变量。在浏览器中,全局对象是 window 。

function foo(arg) {
bar = "some text";
}
// 等同于
function foo(arg) {
window.bar = "some text";
}

如果bar被假定只在foo函数的作用域里引用变量,但是你忘记了使用var去声明它,一个意外的全局变量就被声明了。

在这个例子里,泄漏一个简单的字符串不会造成很大的伤害,但是它确实有可能变得更糟。

另外一个意外创建全局变量的方法是通过this:

function foo() {
this.var1 = "potential accidental global";
} // Foo作为函数调用,this指向全局变量(window)
// 而不是undefined
foo();

为了防止这些问题发生,可以在你的JaveScript文件开头使用'use strict';。这个可以使用一种严格的模式解析JavaScript来阻止意外的全局变量。

除了意外创建的全局变量,明确创建的全局变量同样也很多。这些当然属于不能被回收的(除非被指定为null或者重新分配)。特别那些用于暂时存储数据的全局变量,是非常重要的。如果你必须要使用全局变量来存储大量数据,确保在是使用完成之后为其赋值null或者重新赋其他值。

2.2、被遗忘的定时器或者回调

在JavaScript中使用setInterval是十分常见的。

大多数库,特别是提供观察器或其他接收回调的实用函数的,都会在自己的实例无法访问前把这些回调也设置为无法访问。但涉及setInterval时,下面这样的代码十分常见:

var serverData = loadData();
setInterval(function() {
var renderer = document.getElementById('renderer');
if (renderer) {
renderer.innerHTML = JSON.stringify(serverData);
}
}, 5000); //每5秒执行一次

定时器可能会导致对不需要的节点或者数据的引用。

renderer对象在将来有可能被移除,让interval处理器内部的整个块都变得没有用。但由于interval仍然起作用,处理程序并不能被回收(除非interval停止)。如果interval不能被回收,它的依赖也不可能被回收。这就意味着serverData,大概保存了大量的数据,也不可能被回收。

在观察者的情况下,在他们不再被需要(或相关对象需要设置成不能到达)的时候明确的调用移除是非常重要的。

在过去,这一点尤其重要,因为某些浏览器(旧的IE6)不能很好的管理循环引用(更多信息见下文)。如今,大部分的浏览器都能而且会在对象变得不可到达的时候回收观察处理器,即使监听器没有被明确的移除掉。然而,在对象被处理之前,要显式地删除这些观察者仍然是值得提倡的做法。例如:

var element = document.getElementById('launch-button');
var counter = 0; function onClick(event) {
counter++;
element.innerHtml = 'text ' + counter;
} element.addEventListener('click', onClick); // 做点事 element.removeEventListener('click', onClick);
element.parentNode.removeChild(element); // 当元素被销毁
//元素和事件都会即使在老的浏览器里也会被回收

如今的浏览器(包括IE和Edge)使用现代的垃圾回收算法,可以立即发现并处理这些循环引用。换句话说,先调用removeEventListener再删节点并非严格必要。

jQuery等框架和插件会在丢弃节点前删除监听器。这都是它们内部处理,以保证不会产生内存泄漏,甚至是在有问题的浏览器(没错,IE6)上也不会。

2.3、闭包

闭包是JavaScript开发的一个关键方面:一个内部函数使用了外部(封闭)函数的变量。由于JavaScript运行时实现的不同,它可能以下面的方式造成内存泄漏:

var theThing = null;

var replaceThing = function() {

    var originalThing = theThing;
var unused = function() {
if (originalThing) // 引用'originalThing'
console.log("hi");
}; theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function() {
console.log("message");
}
};
}; setInterval(replaceThing, 1000);

这段代码做了一件事:每次ReplaceThing被调用,theThing获得一个包含大数组和新的闭包(someMethod)的对象。同时,变量unused保持了一个引用originalThing(theThing是上次调用replaceThing生成的值)的闭包。已经有点困惑了吧?最重要的事情是一旦为同一父域中的作用域产生闭包,则该作用域是共享的

这里,作用域产生了闭包,someMethod和unused共享这个闭包中的内存。unused引用了originalThing。尽管unused不会被使用,someMethod可以通过theThing来使用replaceThing作用域外的变量(例如某些全局的)。而且someMethod和unused有共同的闭包作用域,unused对originalThing的引用强制oriiginalThing保持激活状态(两个闭包共享整个作用域)。这阻止了它的回收。

当这段代码重复执行,可以观察到被使用的内存在持续增加。垃圾回收运行的时候也不会变小。从本质上来说,闭包的连接列表已经创建了(以theThing变量为根),这些闭包每个作用域都间接引用了大数组,导致大量的内存泄漏。

这个问题被Meteor团队发现,他们有一篇非常好的文章描述了闭包大量的细节。

2.4、DOM外引用

有的时候在数据结构里存储DOM节点是非常有用的,比如你想要快速更新一个表格几行的内容。此时存储每一行的DOM节点的引用在一个字典或者数组里是有意义的。此时一个DOM节点有两个引用:一个在dom树中,另外一个在字典中。如果在未来的某个时候你想要去移除这些排,你需要确保两个引用都不可到达。

var elements = {
button: document.getElementById('button'),
image: document.getElementById('image')
}; function doStuff() {
image.src = 'http://example.com/image_name.png';
} function removeImage() {
//image是body元素的子节点
document.body.removeChild(document.getElementById('image'));
//这个时候我们在全局的elements对象里仍然有一个对#button的引用。
//换句话说,buttom元素仍然在内存中而且不能被回收。
}

当涉及DOM树内部或子节点时,需要考虑额外的考虑因素。例如,你在JavaScript中保持对某个表的特定单元格的引用。有一天你决定从DOM中移除表格但是保留了对单元格的引用。人们也许会认为除了单元格其他的都会被回收。实际并不是这样的:单元格是表格的一个子节点,子节点保持了对父节点的引用。确切的说,JS代码中对单元格的引用造成了整个表格被留在内存中了,所以在移除有被引用的节点时候要当心。

三、Chrome Devtools

3.1、任务管理器

可以了解各个页面的内存的使用总量,发现内存是否占用过高。

3.2、performance

performance的好处是可以看到随着时间的变化,看到内存的使用的情况。通过performance,我们很容易了解到GC操作和内存的分配,从而发现内存是否泄漏和GC是否频繁的问题。

https://developers.google.com/web/tools/chrome-devtools/evaluate-performance

3.3、memory

内存快照的优点是详细的展示了某一时刻的内存的使用情况,包括:什么类型的数据占用了多大的内存,以及变量之间的引用关系。通过这些,我们就可以找到内存使用的问题所在,找到解决内存问题的方法。

https://developers.google.com/web/tools/chrome-devtools/memory-problems/

参考资料

http://web.jobbole.com/92652/

https://jinlong.github.io/2016/05/01/4-Types-of-Memory-Leaks-in-JavaScript-and-How-to-Get-Rid-Of-Them/

http://www.imooc.com/article/13489

http://www.ayqy.net/blog/js内存泄漏排查方法/#articleHeader0

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_Management

https://segmentfault.com/a/1190000006104910#articleHeader11

JavaScript 内存相关知识的更多相关文章

  1. JavaScript的相关知识

      Oject.assign()   // Cloning an object var obj = { a: 1 }; var copy = Object.assign({}, obj); conso ...

  2. javascript 字符串相关知识汇总

    ① charAt(): 选中字符串内第几个元素 <script> var str="1234567389"; alert( str.charAt(1) ); // 2 ...

  3. 【转】java NIO 相关知识

    原文地址:http://www.iteye.com/magazines/132-Java-NIO Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的 ...

  4. HTML入门基础教程相关知识

    HTML入门基础教程 html是什么,什么是html通俗解答: html是hypertext markup language的缩写,即超文本标记语言.html是用于创建可从一个平台移植到另一平台的超文 ...

  5. javascript的基础知识及面向对象和原型属性

    自己总结一下javascript的基础知识,希望对大家有用,也希望大家来拍砖,毕竟是个人的理解啊 1.1 类型检查:typeof(验证数据类型是:string) var num = 123; cons ...

  6. PySpark SQL 相关知识介绍

    title: PySpark SQL 相关知识介绍 summary: 关键词:大数据 Hadoop Hive Pig Kafka Spark PySpark SQL 集群管理器 PostgreSQL ...

  7. 【Python五篇慢慢弹(5)】类的继承案例解析,python相关知识延伸

    类的继承案例解析,python相关知识延伸 作者:白宁超 2016年10月10日22:36:57 摘要:继<快速上手学python>一文之后,笔者又将python官方文档认真学习下.官方给 ...

  8. iOS网络相关知识总结

    iOS网络相关知识总结 1.关于请求NSURLRequest? 我们经常讲的GET/POST/PUT等请求是指我们要向服务器发出的NSMutableURLRequest的类型; 我们可以设置Reque ...

  9. Chrome开发者工具之JavaScript内存分析

    阅读目录 对象大小(Object sizes) 对象的占用总内存树 支配对象(Dominators) V8介绍 Chrome 任务管理器 通过DevTools Timeline来定位内存问题 内存回收 ...

随机推荐

  1. 获取或设置当前窗口contextmenu事件的事件处理函数

    在浏览器中 鼠标右键点击会显示默认的 自带的菜单,那么如何禁止 和更改呢? 1)  禁止右键 window.oncontextmenu = funcRef; //funcRef是个函数引用 列子: w ...

  2. centos7 忘记mysql root密码办法

    1.首先确认服务器出于安全的状态,也就是没有人能够任意地连接MySQL数据库. 因为在重新设置MySQL的root密码的期间,MySQL数据库完全出于没有密码保护的状态下,其他的用户也可以任意地登录和 ...

  3. python 正则的使用 —— 编写一个简易的计算器

    在 Alex 的博客上看到的对正则这一章节作业是编写一个计算器,要求能计算出下面的算式. 1 - 2 * ( (60-30 +(-40/5) * (9-2*5/3 + 7 /3*99/4*2998 + ...

  4. 【new File(String Path)加载资源问题】

    2017-12-17   15:07:38  [原创-wx] 一.我们在用IO流加载资源的时候,创建文件资源 1 File file = New File("String Path" ...

  5. Java开发小技巧(四):配置文件敏感信息处理

    前言 不知道在上一篇文章中你有没有发现,jdbc.properties中的数据库密码配置是这样写的: jdbc.password=5EF28C5A9A0CE86C2D231A526ED5B388 其实 ...

  6. php两个问号??表示什么意思,PHP两个问号运算符,双问号表达式

    有同学给子恒老师留言, 说在php源代码中看到有两个问号?? 不知道是什么意思. 其实两个问题??是php7新推出的表达式, c = a ?? b; 表示如果a非空,则c = a, 如果a为空,则 c ...

  7. 基于winpcap的以太网流量分析器(java)

    开发工具 IDE:eclipse -neon JDK:1.8 OS:Win10-64bit 主要功能 1.要求完成一个基于Winpcap的网络流量统计分析系统,具有易用.美观的界面. 2.完成局域网( ...

  8. Vue深度学习(5)-过渡效果

    简介 通过 Vue.js 的过渡系统,你可以轻松的为 DOM 节点被插入/移除的过程添加过渡动画效果.Vue 将会在适当的时机添加/移除 CSS 类名来触发 CSS3 过渡/动画效果,你也可以提供相应 ...

  9. 入门vue----(vue的安装)

    1.安装node.js 2.基于node.js,利用淘宝npm镜像安装相关依赖 在cmd里直接输入:npm install -g cnpm –registry=https://registry.npm ...

  10. 【SqlServer系列】语法定义符号解析

    1   概述 在数据库函数定义中,经常用到这些符号:<>,::=,[],{},|,..,(),!!   .这篇文章简要概述这些符号. 2   具体内容 2.1  <> < ...