JavaScript内存模型

1.简介

小弟也是后端开发的,工做中接触JavaScript好久了.想借助本文讲解一下JavaScript内存.同时也为后续的设计模式作铺垫.javascript

每种编程语言都有它的内存管理机制,比java也有本身的内存和GC。一样咱们在学习JavaScript的时候,颇有必要了解JavaScript的内存管理机制。html

JavaScript的内存管理机制是:内存基元在变量(对象,字符串等等)建立时分配,而后在他们再也不被使用时“自动”释放。后者被称为垃圾回收。这个“自动”是混淆并给JavaScript(和其余高级语言)开发者一个错觉:他们能够不用考虑内存管理。对于前端开发来讲,内存空间并非一个常常被说起的概念,很容易被你们忽视。固然也包括我本身。(原本不想长篇大论的W( ̄_ ̄)W);前端

2.内存模型

JS内存空间分为栈(stack)堆(heap)池(通常也会归类为栈中)
其中存放变量,存放复杂对象,存放常量。java

2.1基础数据类型与栈内存

数据在栈内存中的存储与使用方式相似于数据结构中的堆栈数据结构,遵循后进先出的原则。
基础数据类型: Number String Null Undefined Boolean程序员

2.2引用数据类型与堆内存

为了更好的搞懂栈内存与堆内存,咱们能够结合如下例子与图解进行理解。
引用类型的值都是按引用访问的。这里的引用,咱们能够粗浅地理解为保存在栈内存中的一个地址,该地址与堆内存的实际值相关联。算法

var a1 = 0; // 栈
var a2 = 'this is string'; // 栈
var a3 = null; // 栈
var b = { m: 20 }; // 变量b存在于栈中,{m: 20} 做为对象存在于堆内存中
var c = [1, 2, 3]; // 变量c存在于栈中,[1, 2, 3] 做为对象存在于堆内存中编程

// demo01.js
var a = 20;
var b = a;
b = 30;
// 这时a的值是多少?

// demo02.js
var m = { a: 10, b: 20 };
var n = m;
n.a = 15;
// 这时m.a的值是多少

相信上面的问题可想而知 ,a仍是20,可是m.a是15后端

 

 

3.内存的生命周期

JS环境中分配的内存通常有以下生命周期:设计模式

  1. 内存分配:当咱们申明变量、函数、对象的时候,系统会自动为他 们分配内存
  2. 内存使用:即读写内存,也就是使用变量、函数等
  3. 内存回收:使用完毕,由垃圾回收机制自动回收再也不使用的内存

废话很少说看代码浏览器

var a = 20;  // 在内存中给数值变量分配空间
alert(a + 100);  // 使用内存
var a = null; // 使用完毕以后,释放内存空间

//注意这行代码的意识
console.info(typeof(null))//object
console.info( typeof(undefined) )//undefined

第一步和第二步咱们都很好理解,JavaScript在定义变量时就完成了内存分配。第三步释放内存空间则是咱们须要重点理解的一个点。

如今想一想,从内存来看 null 和 undefined 本质的区别是什么?

为何typeof(null) //object typeof(undefined) //undefined

如今再想一想,构造函数和当即执行函数的声明周期是什么?

ES6语法中的 const 声明一个只读的常量。一旦声明,常量的值就不能改变。可是下面的代码能够改变 const 的值,这是为何?

const foo = {}; 
foo.prop = 123;
foo.prop // 123
foo = {}; // TypeError: "foo" is read-only

4.内存回收

JavaScript有自动垃圾收集机制,就是找出那些再也不继续使用的值,而后释放其占用的内存。垃圾收集器会每隔固定的时间段就执行一次释放操做.

在JavaScript中,最经常使用的是经过标记清除的算法来找到哪些对象是再也不继续使用的,所以 a = null 其实仅仅只是作了一个释放引用的操做,让 a 本来对应的值失去引用,脱离执行环境,这个值会在下一次垃圾收集器执行操做时被找到并释放。而在适当的时候解除引用,是为页面得到更好性能的一个重要方式。

  • 在局部做用域中,当函数执行完毕,局部变量也就没有存在的必要了,所以垃圾收集器很容易作出判断并回收。可是全局变量何时须要自动释放内存空间则很难判断,所以在咱们的开发中,须要尽可能避免使用全局变量,以确保性能问题
  • 以Google的V8引擎为例,在V8引擎中全部的JAVASCRIPT对象都是经过堆来进行内存分配的。当咱们在代码中声明变量并赋值时,V8引擎就会在堆内存中分配一部分给这个变量。若是已申请的内存不足以存储这个变量时,V8引擎就会继续申请内存,直到堆的大小达到了V8引擎的内存上限为止(默认状况下,V8引擎的堆内存的大小上限在64位系统中为1464MB,在32位系统中则为732MB)。

  • 另外,V8引擎对堆内存中的JAVASCRIPT对象进行分代管理。新生代:新生代即存活周期较短的JAVASCRIPT对象,如临时变量、字符串等;老生代:老生代则为通过屡次垃圾回收仍然存活,存活周期较长的对象,如主控制器、服务器对象等。

请各位老铁see一下如下的代码,来分析一下垃圾回收。

function fun1() {
    var obj = {name: 'csa', age: 24};
}
 
function fun2() {
    var obj = {name: 'coder', age: 2}
    return obj;
}
 
var f1 = fun1();
var f2 = fun2();
/*在上述代码中,当执行var f1 = fun1();的时候,执行环境会建立一个{name:'csa', age:24}这个对象,
 *当执行var f2 = fun2();的时候,执行环境会建立一个{name:'coder', age=2}这个对象,
 *而后在下一次垃圾回收来临的时候,会释放{name:'csa', age:24}这个对象的内存,
 *但并不会释放{name:'coder', age:2}这个对象的内存。
 *这就是由于在fun2()函数中将*{name:'coder, age:2'}这个对象返回,
 *而且将其引用赋值给了f2变量,又因为f2这个对象属于全局变量,
 *因此在页面没有卸载的状况下,f2所指向的对象{name:'coder', age:2}是不会被回收的。
 *因为JavaScript语言的特殊性(闭包...),
 *致使如何判断一个对象是否会被回收的问题上变的异常艰难,各位老铁看看就行。
 */

4.1垃圾回收算法

对垃圾回收算法来讲,核心思想就是如何判断内存已经再也不使用了。

4.1.1引用计数算法

熟悉或者用C语言搞过事的同窗的都明白,引用无非就是指向某一物体的指针。对不熟悉这个语言的同窗来讲,可简单将引用视为一个对象访问另外一个对象的路径。(这里的对象是一个宽泛的概念,泛指JS环境中的实体.本人最初作Java开发,java里面会有环形引用的问题.JavaScript也有这样的问题)。

引用计数算法定义“内存再也不使用”的标准很简单,就是看一个对象是否有指向它的引用。若是没有其余对象指向它了,说明该对象已经再也不需了。

// 建立一个对象person,他有两个指向属性age和name的引用
var person = {
    age: 12,
    name: 'aaaa'
};

person.name = null; // 虽然设置为null,但由于person对象还有指向name的引用,所以name不会回收

var p = person; 
person = 1;         //原来的person对象被赋值为1,但由于有新引用p指向原person对象,所以它不会被回收

p = null;

由上面能够看出,引用计数算法是个简单有效的算法。但它却存在一个致命的问题:循环引用。若是两个对象相互引用,尽管他们已再也不使用,垃圾回收器不会进行回收,致使内存泄露。

function cycle() {
    var o1 = {};
    var o2 = {};
    o1.a = o2;
    o2.a = o1; 

    return "Cycle reference!"
}

cycle();
//出现循环引用
/*上面咱们申明了一个cycle方程,其中包含两个相互引用的对象。在调用函数结束后,
*对象o1和o2实际上已离开函数范围,所以再也不须要了。但根据引用计数的原则,
*他们之间的相互引用依然存在,所以这部份内存不会被*回收,内存泄露不可避免了。
*正是由于有这个严重的缺点,这个算法在现代浏览器中已经被下面要介绍的标记清除算法所取代了。
*但毫不可认为该问题已经再也不存在了,由于还占有大量市场的IE老祖宗们使用的正是这一算法。
*在须要照顾兼容性的时候,某些看起来很是普通的写法也可能形成意想不到的问题:
*/

正是由于有这个严重的缺点,这个算法在现代浏览器中已经被下面要介绍的标记清除算法所取代了。但毫不可认为该问题已经再也不存在了,由于还占有大量市场的IE老祖宗们使用的正是这一算法。在须要照顾兼容性的时候,某些看起来很是普通的写法也可能形成意想不到的问题:

var div = document.createElement("div");
 div.onclick = function() {
     console.log("click");
 };
/*
上面这种JS写法再普通不过了,建立一个DOM元素并绑定一个点击事件。
那么这里有什么问题呢?请注意,变量div有事件处理函数的引用,
同时事件处理函数也有div的引用!(div变量可在函数内被访问)。
一个循序引用出现了,按上面所讲的算法,该部份内存无可避免地泄露哦了。
如今你明白为啥前端程序员都讨厌IE了吧?拥有超多BUG并依然占有大量市场的IE是前端开发一辈子之敌!
亲,没有买卖就没有杀害。
*/

4.1.2标记清除算法

上面说过,现代的浏览器已经再也不使用引用计数算法了。现代浏览器通用的大可能是基于标记清除算法的某些改进算法,整体思想都是一致的。

标记清除算法将“再也不使用的对象”定义为“没法达到的对象”。简单来讲,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还须要使用的。那些没法由根部出发触及到的对象被标记为再也不使用,稍后进行回收。

从这个概念能够看出,没法触及的对象包含了没有引用的对象这个概念(没有任何引用的对象也是没法触及的对象)。但反之未必成立。

根据这个概念,上面的例子能够正确被垃圾回收处理了(亲,想一想为何?)。

当div与其时间处理函数不能再从全局对象出发触及的时候,垃圾回收器就会标记并回收这两个对象。

4.2如何写出对内存管理友好的JS代码?

若是还须要兼容老旧浏览器,那么就须要注意代码中的循环引用问题。或者直接采用保证兼容性的库来帮助优化代码。

对现代浏览器来讲,惟一要注意的就是明确切断须要回收的对象与根部的联系。有时候这种联系并不明显,且由于标记清除算法的强壮性,这个问题较少出现。最多见的内存泄露通常都与DOM元素绑定有关:

email.message = document.createElement(“div”);
displayList.appendChild(email.message);

// 稍后从displayList中清除DOM元素
displayList.removeAllChildren();

div元素已经从DOM树中清除,也就是说从DOM树的根部没法触及该div元素了。可是请注意,div元素同时也绑定了email对象。因此只要email对象还存在,该div元素将一直保存在内存中.

小结

若是你的引用只包含少许JS交互,那么内存管理不会对你形成太多困扰。一旦你开始构建中大规模的SPA好比服务器和桌面端的应用,那么就应当将内存泄露提上日程了。不要知足于写出能运行的程序,也不要认为机器的升级就能解决一切

我的笔记:浏览器在解析html会把全部的标签所有解析成dom或则bom结构,全都加载到堆里面去,这些标签都是对象,typeof 判断出来是object的必定是在堆里面的

<!DOCTYPE HTML>
<html lang="en-US">
<head>
	<meta charset="UTF-8">
	<title></title>
</head>
<body>
	<div id="myDiv"></div>
	<script type="text/javascript">
    //DIV标签的一个实例
	var myDiv= document.getElementById("myDiv");
	var num =123;
	
	console.info(typeof myDiv); //object
	console.info(typeof num); //number
	</script>
</body>
</html>

5.内存泄漏的识别方法

   5.1浏览器方法

  1. 打开开发者工具,选择 Timeline 面板
  2. 在顶部的Capture字段里面勾选 Memory
  3. 点击左上角的录制按钮。
  4. 在页面上进行各类操做,模拟用户的使用状况。
  5. 一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用状况。

若是内存占用基本平稳,接近水平,就说明不存在内存泄漏。反之,就是内存泄漏了。

   5.2命令行方法

 

console.log(process.memoryUsage());
// { rss: 27709440,
//  heapTotal: 5685248,
//  heapUsed: 3449392,
//  external: 8772 }
//process.memoryUsage返回一个对象,包含了 Node 进程的内存占用信息。该对象包含四个字段,单位是字节,含义以下。

//Resident Set(常驻内存)
//Code Segment(代码区)
//Stack(Local Variables, Pointers)
//Heap(Objects, Closures)
//Used Heap
//rss(resident set size):全部内存占用,包括指令区和堆栈。
//heapTotal:"堆"占用的内存,包括用到的和没用到的。
//heapUsed:用到的堆的部分。
//external: V8 引擎内部的 C++ 对象占用的内存。
//判断内存泄漏,以heapUsed字段为准。

5.3WeakMap

前面说过,及时清除引用很是重要。可是,你不可能记得那么多,有时候一疏忽就忘了,因此才有那么多内存泄漏。

最好能有一种方法,在新建引用的时候就声明,哪些引用必须手动清除,哪些引用能够忽略不计,当其余引用消失之后,垃圾回收机制就能够释放内存。这样就能大大减轻程序员的负担,你只要清除主要引用就能够了。

ES6 考虑到了这一点,推出了两种新的数据结构:WeakSet 和 WeakMap。它们对于值的引用都是不计入垃圾回收机制的,因此名字里面才会有一个"Weak",表示这是弱引用。

下面以 WeakMap 为例,看看它是怎么解决内存泄漏的。

const wm = new WeakMap(); 
const element = document.getElementById('example'); 
wm.set(element, 'some information'); 
wm.get(element) // "some information"

上面代码中,先新建一个 Weakmap 实例。而后,将一个 DOM 节点做为键名存入该实例,并将一些附加信息做为键值,一块儿存放在 WeakMap 里面。这时,WeakMap 里面对element的引用就是弱引用,不会被计入垃圾回收机制。

也就是说,DOM 节点对象的引用计数是1,而不是2。这时,一旦消除对该节点的引用,它占用的内存就会被垃圾回收机制释放。Weakmap 保存的这个键值对,也会自动消失。

基本上,若是你要往对象上添加数据,又不想干扰垃圾回收机制,就可使用 WeakMap

本章也是为了跟好的去理解JavaScript,目前移动开发也能够用JavaScript,同时跟方便去讲解后面的内容

<JavaScript 高级特性 做用域详解>

特别感谢:

相关文章
相关标签/搜索