JavaScript的内存和内存管理

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战web

内存管理

V8 内存限制

限制大小算法

64 位为 1.4GB,32 位为 0.7GB浏览器

限制缘由缓存

V8 之因此限制了内存的大小,表面上的缘由是 V8 最初是做为浏览器的 JavaScript 引擎而设计,不太可能遇到大量内存的场景,而深层次的缘由则是因为 V8 的垃圾回收机制的限制。因为 V8 须要保证 JavaScript 应用逻辑与垃圾回收器所看到的不同,V8 在执行垃圾回收时会阻塞 JavaScript 应用逻辑,直到垃圾回收结束再从新执行 JavaScript 应用逻辑,这种行为被称为“全停顿”(stop-the-world)。若 V8 的堆内存为 1.5GB,V8 作一次小的垃圾回收须要 50ms 以上,作一次非增量式的垃圾回收甚至要 1 秒以上。这样浏览器将在 1s 内失去对用户的响应,形成假死现象。若是有动画效果的话,动画的展示也将显著受到影响。markdown

V8 垃圾回收策略

  • 采用分代回收的思想
  • 内存分为新生代、老生代
  • 针对新、老生代采用不一样算法来提高垃圾回收的效率

新生代的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。闭包

V8 新生代、老生代内存大小

V8 引擎的新生代内存大小 32MB(64 位)、16MB(32 位),老生代内存大小为 1400MB(64 位)、700MB( 32 位)。app

新生代对象回收实现

  • 回收过程采用复制算法+标记整理
  • 新生代内存区被等分为两个空间
  • 使用空间为 From,空闲空间为 To
  • 标记整理后将活动对象拷贝至 To
  • From 和 To 交换空间完成释放

image.png

晋升

将新生代对象移到老生代dom

晋升条件jsp

  • 一轮 GC 还存活的新生代须要晋升
  • 对象从 From 空间复制到 To 空间时,若是 To 空间已经被使用了超过 25%,那么这个对象直接被复制到老生代

老生代对象回收实现

  • 主要采起标记清除、标记整理、增量标记算法
  • 首先使用标记清除完成垃圾空间的回收
  • 采用标记整理进行空间优化
  • 采用增量标记进行效率优化

细节对比

新生代区域,采用复制算法, 所以其每时每刻内部都有空闲空间的存在(为了完成 From 到 To 的对象复制),可是新生代区域空间较小(32M)且被一分为二,因此这种空间上的浪费也是比较微不足道的。oop

老生代因其空间较大(1.4G),若是一样采用一分为二的作法则对空间大小是比较浪费,且老生代空间较大,存放对对象也较多,若是进行复制算法,则其消耗对时间也会更大。也就是是否使用复制算法来进行垃圾回收,是一个时间 T 关于内存大小的关系,当内存大小较小时,使用复制算法消耗的时间是比较短的,而当内存较大时,采用复制算法对时间对消耗也就更大。

V8 的优化

增量标记

因为全停顿会形成了浏览器一段时间无响应,因此 V8 使用了一种增量标记的方式,将完整的标记拆分红不少部分,每作完一部分就停下来,让 JS 的应用逻辑执行一会,这样垃圾回收与应用逻辑交替完成。通过增量标记的改进后,垃圾回收的最大停顿时间能够减小到原来的 1/6 左右

image.png

惰性清理

因为标记完成后,全部的对象都已经被标记,不是死对象就是活对象,堆上多少空间格局已经肯定。咱们能够没必要着急释放那些死对象所占用的空间,而延迟清理过程的执行。垃圾回收器能够根据须要逐一清理死对象所占用的内存空间

其余

V8 后续还引入了增量式整理(incremental compaction),以及并行标记和并行清理,经过并行利用多核 CPU 来提高垃圾回收的性能

监控内存

内存问题的外在表现

  • 页面出现延迟加载或常常性暂停: 可能存在频繁当 GC 操做,存在一些代码瞬间吃满了内存。
  • 页面出现持续性的糟糕性能: 程序为了达到最优的运行速度,向内存申请了一片较大的内存空间,但空间大小超过了设备所能提供的大小。
  • 页面使用随着时间延长愈来愈卡: 可能存在内存泄漏。

界定内存问题的标准

  • 内存泄漏:内存使用持续升高
  • 内存膨胀:在多数设备上都存在性能问题
  • 频繁垃圾回收:经过内存变化时序图进行分析

监控内存方式

任务管理器

这里以 Google 浏览器为例,使用 Shift + Esc 唤起 Google 浏览器自带的任务管理器

  • Memory(内存) 列表示原生内存。DOM 节点存储在原生内存中。 若是此值正在增大,则说明正在建立 DOM 节点。
  • JavaScript Memory(JavaScript 内存) 列表示 JS 堆。此列包含两个值。 您感兴趣的值是实时数字(括号中的数字)。 实时数字表示您的页面上的可到达对象正在使用的内存量。 若是此数字在增大,要么是正在建立新对象,要么是现有对象正在增加。

模拟内存泄漏

在任务管理器里能够看到 JavaScript 内存持续上升

document.body.innerHTML = `<button id="add">add</button>`;
document.getElementById('add').addEventListener('click', function (e) {
  simulateMemoryLeak();
});
let result = [];
function simulateMemoryLeak() {
  setInterval(function () {
    result.push(new Array(1000000).join('x'));
    document.body.innerHTML = result;
  }, 100);
}
复制代码

Timeline 记录内存

这里以 Google 浏览器为例,使用 F12 开启调式,选择 Performance,点击 record(录制),进行页面操做,点击 stop 结束录制以后,开启内存勾选,拖动截图到指定时间段查看发生内存问题时候到页面展现,并定位问题。同时能够查看对应出现红点到执行脚本,定位问题代码。

image.png

利用浏览器内存模块,查找分离 dom

这里以 Google 浏览器为例,在页面上进行相关操做后,使用 F12 开启调式,选择 Memory,点击 Take snapshot(拍照),在快照中查找 Detached HTMLElement,回到代码中查找对应的分离 dom 存在的代码,在相关操做代码以后,对分离 dom 进行释放,防止内存泄漏。

只有页面的 DOM 树或 JavaScript 代码再也不引用 DOM 节点时,DOM 节点才会被做为垃圾进行回收。 若是某个节点已从 DOM 树移除,但某些 JavaScript 仍然引用它,咱们称此节点为“已分离”。已分离的 DOM 节点是内存泄漏的常见缘由。

模拟已分离 DOM 节点

document.body.innerHTML = `<button id="add">add</button>`;
document.getElementById('add').addEventListener('click', function (e) {
  create();
});
let detachedTree;
function create() {
  let ul = document.createElement('ul');
  for (let i = 0; i < 10; i++) {
    let li = document.createElement('li');
    ul.appendChild(li);
  }
  detachedTree = ul;
}
复制代码

image.png

如何肯定频繁对垃圾回收

  • GC 工做时,程序是暂停的,频繁/过长的 GC 会致使程序假死,用户会感知到卡顿。
  • 查看 Timeline 中是否存在内存走向在短期内频繁上升降低的区域。浏览器任务管理器是否频繁的增长减小。

代码优化

jsPerf(JavaScript 性能测试)

基于 Benchmark.js

慎用全局变量

  • 全局变量定义在全局执行的上下文,是全部做用域链的顶端
  • 全局执行上下文一直存在于上下文执行栈,直到程序退出
  • 若是某个局部做用域出现了同名变量则会屏蔽或者污染全局做用域
  • 全局变量的执行速度,访问速度要低于局部变量,所以对于一些须要常常访问的全局变量能够在局部做用域中进行缓存

image.png

上图能够看出,test2 的性能要比 test1 的性能要好,从而得知,全局变量的执行速度,访问速度要低于局部变量

避免全局查找

image.png 上图能够看出,test2 的性能要比 test1 的性能要好,从而得知,缓存全局变量后使用能够提高性能

经过原型对象添加附加方法提升性能

image.png 上图能够看出,test2 的性能要比 test1 的性能要好,从而得知,经过原型对象添加方法与直接在对象上添加成员方法相比,原型对象上的属性访问速度较快。

避开闭包陷阱

闭包特色

  • 外部具备指向内部的引用
  • 在“外”部做用域访问“内”部做用域的数据
function foo() {
  let name = 'heath';
  function fn() {
    console.log(name);
  }
  return fn;
}
let a = foo();
a();
复制代码

闭包使用不当很容易出现内存泄漏

function f5() {
  // el 引用了全局变量document,假设btn节点被删除后,由于这里被引用着,因此这里不会被垃圾回收,致使内存泄漏
  let el = document.getElementById('btn');
  el.onclick = function (e) {
    console.log(e.id);
  };
}
f5();

function f6() {
  // el 引用了全局变量document,假设btn节点被删除后,由于这里被引用着,因此这里不会被垃圾回收,致使内存泄漏
  let el = document.getElementById('btn');
  el.onclick = function (e) {
    console.log(e.id);
  };
  el = null; // 咱们这里手动将el内存释放,从而当btn节点被删除后,能够被垃圾回收
}
f6();
复制代码

避免属性访问方法使用

JavaScript 中的面向对象

  • JS 不需属性的访问方法,全部属性都是外部可见的
  • 使用属性访问方法只会增长一层重定义,没有访问的控制力

image.png 上图能够看出,test2 的性能要比 test1 的性能要好很多,从而得知,直接访问属性,会比经过方法访问属性速度来的快。

遍历速度

image.png 上图能够看出,loop 遍历速度 forEach > 优化 for > for of > for > for in

dom 节点操做

upload-images.jianshu.io/upload_imag… 上图能够看出,节点克隆(cloneNode)生成节点速度要快于建立节点。

采用字面量替换 New 操做

image.png 上图能够看出,字面量声明的数据生成速度要快于单独属性赋值行为生成的数据。

最后最后:

公众号:小何成长,佛系更文,都是本身曾经踩过的坑或者是学到的东西

有兴趣的小伙伴欢迎关注我哦,我是:何小玍。 你们一块儿进步鸭

相关文章
相关标签/搜索