【进阶1-4期】JavaScript深入之带你走进内存机制

栏目: JavaScript · 发布时间: 7年前

内容简介:本期的主题是如果觉得本系列不错,欢迎点赞、评论、转发,您的支持就是我坚持的最大动力。JS内存空间分为

本期的主题是 调用堆栈 ,本计划一共28期, 每期重点攻克一个面试重难点 ,如果你还不了解本进阶计划,文末点击查看全部文章。

如果觉得本系列不错,欢迎点赞、评论、转发,您的支持就是我坚持的最大动力。

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

昨天文章介绍了堆和栈,小结一下:

栈
堆

今日补充一个知识点,就是闭包中的变量并不保存中栈内存中,而是保存在 堆内存 中,这也就解释了函数之后之后为什么闭包还能引用到函数内的变量。

function A() {
  let a = 1
  function B() {
      console.log(a)
  }
  return B
}
复制代码

闭包 的简单定义是:函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。

函数 A 弹出调用栈后,函数 A 中的变量这时候是存储在堆上的,所以函数B依旧能引用到函数A中的变量。现在的 JS 引擎可以通过逃逸分析辨别出哪些变量需要存储在堆上,哪些需要存储在栈上。

闭包的介绍点到为止,会详细介绍,敬请期待。

今天文章的重点是 内存回收内存泄漏

内存回收

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

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

垃圾回收算法

对垃圾回收算法来说,核心思想就是如何判断内存已经不再使用,常用垃圾回收算法有下面两种。

  • 引用计数(现代浏览器不再使用)
  • 标记清除(常用)

引用计数

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

// 创建一个对象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 函数执行完成之后,对象 o1o2 实际上已经不再需要了,但根据引用计数的原则,他们之间的相互引用依然存在,因此这部分内存不会被回收。所以现代浏览器 不再使用 这个算法。

但是IE依旧使用。

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

上面的写法很常见,但是上面的例子就是一个循环引用。

变量div有事件处理函数的引用,同时事件处理函数也有div的引用,因为div变量可在函数内被访问,所以循环引用就出现了。

标记清除(常用)

标记清除算法将“不再使用的对象”定义为“ 无法到达的对象 ”。即从根部(在JS中就是全局对象)出发定时扫描内存中的对象,凡是能从根部到达的对象, 保留 。那些从根部出发无法触及到的对象被标记为 不再使用 ,稍后进行回收。

无法触及的对象包含了没有引用的对象这个概念,但反之未必成立。

所以上面的例子就可以正确被垃圾回收处理了。

所以现在对于主流浏览器来说,只需要切断需要回收的对象与根部的联系。最常见的内存泄露一般都与DOM元素绑定有关:

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

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

上面代码中, div 元素已经从DOM树中清除,但是该 div 元素还绑定在email对象中,所以如果email对象存在,那么该 div 元素就会一直保存在内存中。

内存泄漏

对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。 对于不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)

内存泄漏识别方法

1、浏览器方法

  1. 打开开发者工具,选择 Memory
  2. 在右侧的Select profiling type字段里面勾选 timeline
  3. 点击左上角的录制按钮。
  4. 在页面上进行各种操作,模拟用户的使用情况。
  5. 一段时间后,点击左上角的 stop 按钮,面板上就会显示这段时间的内存占用情况。

2、命令行方法

使用 Node 提供的 process.memoryUsage 方法。

console.log(process.memoryUsage());

// 输出
{ 
  rss: 27709440,		// resident set size,所有内存占用,包括指令区和堆栈
  heapTotal: 5685248,   // "堆"占用的内存,包括用到的和没用到的
  heapUsed: 3449392,	// 用到的堆的部分
  external: 8772 		// V8 引擎内部的 C++ 对象占用的内存
}
复制代码

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

详细的JS内存分析将在详细介绍,敬请期待。

WeakMap

ES6 新出的两种数据结构: WeakSetWeakMap ,表示这是弱引用,它们对于值的引用都是不计入垃圾回收机制的。

const wm = new WeakMap();
const element = document.getElementById('example');

wm.set(element, 'some information');
wm.get(element) // "some information"
复制代码

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

昨日思考题解答

昨天文章留了一道思考题,群里讨论很热烈,大家应该都知道原理了,现在来简单解答下。

var a = {n: 1};
var b = a;
a.x = a = {n: 2};

a.x 	// --> undefined
b.x 	// --> {n: 2}
复制代码

答案已经写上面了,这道题的关键在于

  • 1、优先级。 . 的优先级高于 = ,所以先执行 a.x ,堆内存中的 {n: 1} 就会变成 {n: 1, x: undefined} ,改变之后相应的 b.x 也变化了,因为指向的是同一个对象。
  • 2、赋值操作是 从右到左 ,所以先执行 a = {n: 2}a 的引用就被改变了,然后这个返回值又赋值给了 a.x需要注意 的是这时候 a.x 是第一步中的 {n: 1, x: undefined} 那个对象,其实就是 b.x ,相当于 b.x = {n: 2}
【进阶1-4期】JavaScript深入之带你走进内存机制

今日份思考题

问题一:

从内存来看 null 和 undefined 本质的区别是什么?

问题二:

ES6语法中的 const 声明一个只读的常量,那为什么下面可以修改const的值?

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

问题三:

哪些情况下容易产生内存泄漏?

参考

JavaScript 内存机制

MDN之运算符优先级

由ES规范学JavaScript(二):深入理解“连等赋值”问题

InterviewMap

进阶系列目录

  • 【进阶1期】 调用堆栈
  • 【进阶2期】 作用域闭包
  • 【进阶3期】 this全面解析
  • 【进阶4期】 深浅拷贝原理
  • 【进阶5期】 原型Prototype
  • 【进阶6期】 高阶函数
  • 【进阶7期】 事件机制
  • 【进阶8期】 Event Loop原理
  • 【进阶9期】 Promise原理
  • 【进阶10期】Async/Await原理
  • 【进阶11期】防抖/节流原理
  • 【进阶12期】模块化详解
  • 【进阶13期】ES6重难点
  • 【进阶14期】计算机网络概述
  • 【进阶15期】浏览器渲染原理
  • 【进阶16期】webpack配置
  • 【进阶17期】webpack原理
  • 【进阶18期】前端监控
  • 【进阶19期】跨域和安全
  • 【进阶20期】性能优化
  • 【进阶21期】VirtualDom原理
  • 【进阶22期】Diff算法
  • 【进阶23期】MVVM双向绑定
  • 【进阶24期】Vuex原理
  • 【进阶25期】Redux原理
  • 【进阶26期】路由原理
  • 【进阶27期】VueRouter源码解析
  • 【进阶28期】ReactRouter源码解析

交流

进阶系列文章汇总: github.com/yygmind/blo… ,内有优质前端资料,欢迎领取,觉得不错点个star。

我是木易杨,网易高级前端工程师,跟着我 每周重点攻克一个前端面试重难点 。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!

【进阶1-4期】JavaScript深入之带你走进内存机制

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Practical JavaScript, DOM Scripting and Ajax Projects

Practical JavaScript, DOM Scripting and Ajax Projects

Frank Zammetti / Apress / April 16, 2007 / $44.99

http://www.amazon.com/exec/obidos/tg/detail/-/1590598164/ Book Description Practical JavaScript, DOM, and Ajax Projects is ideal for web developers already experienced in JavaScript who want to ......一起来看看 《Practical JavaScript, DOM Scripting and Ajax Projects》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具