# JavaScript内存管理
点击关注本[公众号](https://www.kancloud.cn/book/dsh225/javascript_vue_css/edit#_118)获取文档最新更新,并可以领取配套于本指南的《**前端面试手册**》以及**最标准的简历模板**.
[TOC]
## 前言
像C语言这样的底层语言一般都有底层的内存管理接口,比如 malloc()和free()。另一方面,JavaScript创建变量(对象,字符串等)时分配内存,并且在不再使用它们时“自动”释放。 后一个过程称为垃圾回收。这个“自动”是混乱的根源,并让JavaScript(和其他高级语言)开发者感觉他们可以不关心内存管理,这是错误的。
> 本文主要参考了深入浅出nodejs中的内存章节
## 内存模型
平时我们使用的基本类型数据或者复杂类型数据都是如何存放的呢?
基本类型普遍被存放在『栈』中,而复杂类型是被存放在堆内存的。
> 如果你不了解执行栈和内存堆的概念,请先阅读[JavaScript执行机制](https://www.cxymsg.com/guide/memory.html#mechanism.html)
当你读完上述文章后,你会问,既然复杂类型被存放在内存堆中,执行栈的函数是如何使用内存堆的复杂类型?
实际上,执行栈的函数上下文会保存一个内存堆对应复杂类型对象的内存地址,通过引用来使用复杂类型对象。
一个例子:
~~~
function add() {
const a = 1
const b = {
num: 2
}
const sum = a + b.num
}
~~~
示意图如下(我们暂时不考虑函数本身的内存)![2019-06-20-12-38-57](https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/8f09ef156288fd2c9ee9b0b0296fd154.png)
还有一个问题是否所有的基本类型都储存在栈中呢?
并不是,当一个基本类型被闭包引用之后,就可以长期存在于内存中,这个时候即使他是基本类型,也是会被存放在堆中的。
## 生命周期
不管什么程序语言,内存生命周期基本是一致的:
1. 分配你所需要的内存
2. 使用分配到的内存(读、写)
3. 不需要时将其释放\\归还
![2019-06-20-12-18-16](https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/b9f8c025986dee6a49599c985cd15f2e.png)
所有语言第二部分都是明确的。第一和第三部分在底层语言中是明确的,但在像JavaScript这些高级语言中,大部分都是隐含的。
## 内存回收
V8的垃圾回收策略基于分代回收机制,该机制又基于**世代假说**,该假说有两个特点:
* 大部分新生对象倾向于早死
* 不死的对象,会活得更久
基于这个理论,现代垃圾回收算法根据对象的存活时间将内存进行了分代,并对不同分代的内存采用不同的高效算法进行垃圾回收
### V8的内存分代
在V8中,将内存分为了新生代(new space)和老生代(old space)。它们特点如下:
* 新生代:对象的存活时间较短。新生对象或只经过一次垃圾回收的对象。
* 老生代:对象存活时间较长。经历过一次或多次垃圾回收的对象。
### Stop The World (全停顿)
在介绍垃圾回收算法之前,我们先了解一下「全停顿」。
为避免应用逻辑与垃圾回收器看到的情况不一致,垃圾回收算法在执行时,需要停止应用逻辑。垃圾回收算法在执行前,需要将应用逻辑暂停,执行完垃圾回收后再执行应用逻辑,这种行为称为 「全停顿」(Stop The World)。例如,如果一次GC需要50ms,应用逻辑就会暂停50ms。
### Scavenge 算法
Scavenge 算法的缺点是,它的算法机制决定了只能利用一半的内存空间。但是新生代中的对象生存周期短、存活对象少,进行对象复制的成本不是很高,因而非常适合这种场景。
新生代中的对象主要通过 Scavenge 算法进行垃圾回收。Scavenge 的具体实现,主要采用了Cheney算法。
![2019-06-20-12-51-06](https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/b883571872f75fcf0157377003f57cf2.png)
Cheney算法采用复制的方式进行垃圾回收。它将堆内存一分为二,每一部分空间称为 semispace。这两个空间,只有一个空间处于使用中,另一个则处于闲置。使用中的 semispace 称为 「From 空间」,闲置的 semispace 称为 「To 空间」。
过程如下:
* 从 From 空间分配对象,若 semispace 被分配满,则执行 Scavenge 算法进行垃圾回收。
* 检查 From 空间的存活对象,若对象存活,则检查对象是否符合晋升条件,若符合条件则晋升到老生代,否则将对象从 From 空间复制到 To 空间。
* 若对象不存活,则释放不存活对象的空间。
* 完成复制后,将 From 空间与 To 空间进行角色翻转(flip)。
### 对象晋升
1. 对象是否经历过Scavenge回收。对象从 From 空间复制 To 空间时,会检查对象的内存地址来判断对象是否已经经过一次Scavenge回收。若经历过,则将对象从 From 空间复制到老生代中;若没有经历,则复制到 To 空间。
2. To 空间的内存使用占比是否超过限制。当对象从From 空间复制到 To 空间时,若 To 空间使用超过 25%,则对象直接晋升到老生代中。设置为25%的比例的原因是,当完成 Scavenge 回收后,To 空间将翻转成From 空间,继续进行对象内存的分配。若占比过大,将影响后续内存分配。
对象晋升到老生代后,将接受新的垃圾回收算法处理。下图为Scavenge算法中,对象晋升流程图。
![2019-06-20-12-52-37](https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/7d503b3c8b7619b0a4cceb34594fea03.png)
### Mark-Sweep & Mark-Compact
老生代中的对象有两个特点,第一是存活对象多,第二个存活时间长。若在老生代中使用 Scavenge 算法进行垃圾回收,将会导致复制存活对象的效率不高,且还会浪费一半的空间。因而,V8在老生代采用Mark-Sweep 和 Mark-Compact 算法进行垃圾回收。
Mark-Sweep,是标记清除的意思。它主要分为标记和清除两个阶段。
* 标记阶段,它将遍历堆中所有对象,并对存活的对象进行标记;
* 清除阶段,对未标记对象的空间进行回收。
与 Scavenge 算法不同,Mark-Sweep 不会对内存一分为二,因此不会浪费空间。但是,经历过一次 Mark-Sweep 之后,内存的空间将会变得不连续,这样会对后续内存分配造成问题。比如,当需要分配一个比较大的对象时,没有任何一个碎片内支持分配,这将提前触发一次垃圾回收,尽管这次垃圾回收是没有必要的。
![2019-06-20-12-55-15](https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/805b5b5cf48dc8299f7a8093fa2d4080.png)
为了解决内存碎片的问题,提高对内存的利用,引入了 Mark-Compact (标记整理)算法。Mark-Compact 是在 Mark-Sweep 算法上进行了改进,标记阶段与Mark-Sweep相同,但是对未标记的对象处理方式不同。与Mark-Sweep是对未标记的对象立即进行回收,Mark-Compact则是将存活的对象移动到一边,然后再清理端边界外的内存。
![2019-06-20-12-55-47](https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/847849c83fe8b3d4fea20017b28ef89b.png)
由于Mark-Compact需要移动对象,所以执行速度上,比Mark-Sweep要慢。所以,V8主要使用Mark-Sweep算法,然后在当空间内存分配不足时,采用Mark-Compact算法。
### Incremental Marking(增量标记)
在新生代中,由于存活对象少,垃圾回收效率高,全停顿时间短,造成的影响小。但是老生代中,存活对象多,垃圾回收时间长,全停顿造成的影响大。为了减少全停顿的时间,V8对标记进行了优化,将一次停顿进行的标记过程,分成了很多小步。每执行完一小步就让应用逻辑执行一会儿,这样交替多次后完成标记。如下图所示:
![2019-06-20-12-56-41](https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d42805a7a519dace93309411d32ccdb5.png)
长时间的GC,会导致应用暂停和无响应,将会导致糟糕的用户体验。从2011年起,v8就将「全暂停」标记换成了增量标记。改进后的标记方式,最大停顿时间减少到原来的1/6。
### lazy sweeping(延迟清理)
* 发生在增量标记之后
* 堆确切地知道有多少空间能被释放
* 延迟清理是被允许的,因此页面的清理可以根据需要进行清理
* 当延迟清理完成后,增量标记将重新开始
## 内存泄露
### 引起内存泄漏的几个禁忌
* 滥用全局变量:直接用全局变量赋值,在函数中滥用this指向全局对象
* 不销毁定时器和回调
* DOM引用不规范,很多时候, 我们对 Dom 的操作, 会把 Dom 的引用保存在一个数组或者 Map 中,往往无法对其进行内存回收,ES6中引入 WeakSet 和 WeakMap 两个新的概念, 来解决引用造成的内存回收问题. WeakSet 和 WeakMap 对于值的引用可以忽略不计, 他们对于值的引用是弱引用,内存回收机制, 不会考虑这种引用. 当其他引用被消除后, 引用就会从内存中被释放.
* 滥用闭包:
~~~
// 滥用闭包引起内存泄漏
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);
~~~
### 查看内存泄漏
* 打开开发者工具,选择 Timeline 面板
* 在顶部的Capture字段里面勾选 Memory
* 点击左上角的录制按钮。
* 在页面上进行各种操作,模拟用户的使用情况。
* 一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况。
* * *
参考:
[深入浅出Node.js](https://book.douban.com/subject/25768396/)
[MDN内存管理](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_Management)
* * *
## 公众号
想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新.
**简历模板**:关注公众号回复「模板」获取
**《前端面试手册》**:配套于本指南的突击手册,关注公众号回复「fed」获取
![2019-08-12-03-18-41](https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png)
- 前言
- 指南使用手册
- 为什么会有这个项目
- 面试技巧
- 面试官到底想看什么样的简历?
- 面试回答问题的技巧
- 如何通过HR面
- 推荐
- 书籍/课程推荐
- 前端基础
- HTML基础
- CSS基础
- JavaScript基础
- 浏览器与新技术
- DOM
- 前端基础笔试
- HTTP笔试部分
- JavaScript笔试部分
- 前端原理详解
- JavaScript的『预解释』与『变量提升』
- Event Loop详解
- 实现不可变数据
- JavaScript内存管理
- 实现深克隆
- 如何实现一个Event
- JavaScript的运行机制
- 计算机基础
- HTTP协议
- TCP面试题
- 进程与线程
- 数据结构与算法
- 算法面试题
- 字符串类面试题
- 前端框架
- 关于前端框架的面试须知
- Vue面试题
- React面试题
- 框架原理详解
- 虚拟DOM原理
- Proxy比defineproperty优劣对比?
- setState到底是异步的还是同步的?
- 前端路由的实现
- redux原理全解
- React Fiber 架构解析
- React组件复用指南
- React-hooks 抽象组件
- 框架实战技巧
- 如何搭建一个组件库的开发环境
- 组件设计原则
- 实现轮播图组件
- 性能优化
- 前端性能优化-加载篇
- 前端性能优化-执行篇
- 工程化
- webpack面试题
- 前端工程化
- Vite
- 安全
- 前端安全面试题
- npm
- 工程化原理
- 如何写一个babel
- Webpack HMR 原理解析
- webpack插件编写
- webpack 插件化设计
- Webpack 模块机制
- webpack loader实现
- 如何开发Babel插件
- git
- 比较
- 查看远程仓库地址
- git flow
- 比较分支的不同并保存压缩文件
- Tag
- 回退
- 前端项目经验
- 确定用户是否在当前页面
- 前端下载文件
- 只能在微信中访问
- 打开新页面-被浏览器拦截
- textarea高度随内容变化 vue版
- 去掉ios原始播放大按钮
- nginx在MAC上的安装、启动、重启和关闭
- 解析latex格式的数学公式
- 正则-格式化a链接
- 封装的JQ插件库
- 打包问题总结
- NPM UI插件
- 带你入门前端工程
- webWorker+indexedDB性能优化
- 多个相邻元素切换效果出现边框重叠问题的解决方法
- 监听前端storage变化