js内存深刻学习(一)

一. 内存空间储存

某些状况下,调用堆栈中函数调用的数量超出了调用堆栈的实际大小,浏览器会抛出一个错误终止运行。这个就涉及到内存问题了。html

1. 数据结构类型

  • 栈: 后进先出(LIFO)的数据结构 栈
  • 堆: 一种树状结构
  • 队列: 先进先出(FIFO)的数据结构 队列

2. 变量的存放

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

一、基本类型 --> 保存在栈内存中,由于这些类型在内存中分别占有固定大小的空间,经过按值来访问。基本类型一共有6种:Undefined、Null、Boolean、Number 、String和Symbol数组

二、引用类型 --> 保存在堆内存中,由于这种值的大小不固定,所以不能把它们保存到栈内存中,但内存地址大小的固定的,所以保存在堆内存中,在栈内存中存放的只是该对象的访问地址。当查询引用类型的变量时, 先从栈中读取内存地址, 而后再经过地址找到堆中的值。对于这种,咱们把它叫作按引用访问。浏览器

变量的存放

在计算机的数据结构中,栈比堆的运算速度快,Object是一个复杂的结构且能够扩展:数组可扩充,对象可添加属性,均可以增删改查。将他们放在堆中是为了避免影响栈的效率。而是经过引用的方式查找到堆中的实际对象再进行操做。因此查找引用类型值的时候先去栈查找再去堆查找。服务器

例子:数据结构

<script>
var a = {n:1}; 
var b = a;  
a.x = a = {n:2}; 
console.log(a.x);// --> undefined 
console.log(b.x);// --> {n:2}
</script>

  

解析:闭包

  1. var a = {n:1}; var b = a; 在这里a指向了一个对象{n:1}(咱们姑且称它为对象A),b指向了a所指向的对象,也就是说,在这时候a和b都是指向对象A的。app

  2. a.x = a = {n:2};函数

    • 咱们知道js的赋值运算顺序永远都是从右往左的,不过因为“.”是优先级最高的运算符,因此这行代码先“计算”了a.x。a指向的对象{n:1}新增了属性x(虽然这个x是undefined的)
    • 依循“从右往左”的赋值运算顺序先执行 a={n:2} ,这时候,a指向的对象发生了改变,变成了新对象{n:2}(咱们称为对象B)
    • 接着继续执行 a.x=a, 因为一开始js已经先计算了a.x,便已经解析了这个a.x是对象A的x,因此在同一条公式的状况下再回来给a.x赋值,因此应理解为对象A的属性x指向了对象B。

另外, 闭包中的变量并不保存中栈内存中,而是保存在堆内存中,这也就解释了函数以后以后为何闭包还能引用到函数内的变量。学习

function A() {
  let a = 1
  function B() {
      console.log(a)
  }
  return B
}

  

函数 A 弹出调用栈后,函数 A 中的变量这时候是存储在堆上的,因此函数B依旧能引用到函数A中的变量。如今的 JS 引擎能够经过逃逸分析辨别出哪些变量须要存储在堆上,哪些须要存储在栈上。

二. 内存空间管理

1. 内存生命周期

JavaScript的内存生命周期是

一、分配你所须要的内存

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

三、不须要时将其释放、归还

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

  • 局部变量和全局变量的销毁
    • 局部变量:局部做用域中,当函数执行完毕,局部变量也就没有存在的必要了,所以垃圾收集器很容易作出判断并回收。
    • 全局变量:全局变量何时须要自动释放内存空间则很难判断,因此在开发中尽可能避免使用全局变量。
  • 以Google的V8引擎为例,V8引擎中全部的JS对象都是经过堆来进行内存分配的
    • 初始分配:当声明变量并赋值时,V8引擎就会在堆内存中分配给这个变量。
    • 继续申请:当已申请的内存不足以存储这个变量时,V8引擎就会继续申请内存,直到堆的大小达到了V8引擎的内存上限为止。
  • V8引擎对堆内存中的JS对象进行分代管理
    • 新生代:存活周期较短的JS对象,如临时变量、字符串等。
    • 老生代:通过屡次垃圾回收仍然存活,存活周期较长的对象,如主控制器、服务器对象等。

2. 垃圾回收算法

  • 2.1 引用计数(现代浏览器再也不使用)

引用计数算法简单理解,就是看一个对象是否有指向它的引用。若是没有其余对象指向它了,说明该对象已经再也不须要了。

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

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

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

p = null;           //原person对象已经没有引用,很快会被回收

  

引用计数有一个致命的问题,那就是循环引用

若是两个对象相互引用,尽管他们已再也不使用,可是垃圾回收器不会进行回收,最终可能会致使内存泄露。

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

    return "cycle reference!"
}

cycle();

  

cycle函数执行完成以后,对象o1和o2实际上已经再也不须要了,但根据引用计数的原则,他们之间的相互引用依然存在,所以这部份内存不会被回收。因此现代浏览器再也不使用这个算法。

可是IE依旧使用,以下,变量div有事件处理函数的引用,同时事件处理函数也有div的引用,由于div变量可在函数内被访问,因此循环引用就出现了。

var div = document.createElement("div");
div.onclick = function() {
    console.log("click");
};

  

  • 2.2 标记清除(经常使用)

标记清除算法将“再也不使用的对象”定义为“没法到达的对象”。即从根部(在JS中就是全局对象)出发定时扫描内存中的对象,凡是能从根部到达的对象,保留。那些从根部出发没法触及到的对象被标记为再也不使用,稍后进行回收。因此像上面的例子,虽然是循环引用,但从全局来讲并无被使用到,因此就能够正确被垃圾回收处理了。

算法由如下几步组成:

  • 垃圾回收器建立了一个“roots”列表。roots一般是代码中全局变量的引用。JavaScript 中,“window”对象是一个全局变量,被看成 root 。window对象老是存在,所以垃圾回收器能够检查它和它的全部子对象是否存在(即不是圾);
  • 全部的 roots 被检查和标记为激活(即不是垃圾)。全部的子对象也被递归地查。从 root 开始的全部对象若是是可达的,它就不被看成垃圾。
  • 全部未被标记的内存会被当作垃圾,收集器如今能够释放内存,归还给操做系了。

对于主流浏览器来讲,只须要切断须要回收的对象与根部的联系。但可能还存在着与DOM元素绑定有关的内存问题:

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

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

 

上面代码中,div元素已经从DOM树中清除,可是该div元素还绑定在email对象中,因此若是email对象存在,那么该div元素就会一直保存在内存中。若是再也不须要使用的话,须要手动设置email.message = null。

另外ES6 新出的两种数据结构:WeakSet 和 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的引用就是弱引用,不会被计入垃圾回收机制。

 

续篇 js内存深刻学习(二)

相关文章
相关标签/搜索