JavaScript的内存和内存管理

简介: JavaScript的内存和内存管理

内存管理



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 内失去对用户的响应,造成假死现象。如果有动画效果的话,动画的展现也将显著受到影响。


V8 垃圾回收策略


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

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


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


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


新生代对象回收实现


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


网络异常,图片无法展示
|


晋升


将新生代对象移到老生代

晋升条件

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


老生代对象回收实现


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


细节对比


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


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


V8 的优化


增量标记


由于全停顿会造成了浏览器一段时间无响应,所以 V8 使用了一种增量标记的方式,将完整的标记拆分成很多部分,每做完一部分就停下来,让 JS 的应用逻辑执行一会,这样垃圾回收与应用逻辑交替完成。经过增量标记的改进后,垃圾回收的最大停顿时间可以减少到原来的 1/6 左右


网络异常,图片无法展示
|


惰性清理

由于标记完成后,所有的对象都已经被标记,不是死对象就是活对象,堆上多少空间格局已经确定。我们可以不必着急释放那些死对象所占用的空间,而延迟清理过程的执行。垃圾回收器可以根据需要逐一清理死对象所占用的内存空间


其他

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 结束录制之后,开启内存勾选,拖动截图到指定时间段查看发生内存问题时候到页面展示,并定位问题。同时可以查看对应出现红点到执行脚本,定位问题代码。


网络异常,图片无法展示
|


利用浏览器内存模块,查找分离 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;
}
复制代码


网络异常,图片无法展示
|


如何确定频繁对垃圾回收


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


代码优化



jsPerf(JavaScript 性能测试)


基于 Benchmark.js


慎用全局变量


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


网络异常,图片无法展示
|


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


避免全局查找


网络异常,图片无法展示
|


上图可以看出,test2 的性能要比 test1 的性能要好,从而得知,缓存全局变量后使用可以提升性能


通过原型对象添加附加方法提高性能


网络异常,图片无法展示
|


上图可以看出,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 不需属性的访问方法,所有属性都是外部可见的
  • 使用属性访问方法只会增加一层重定义,没有访问的控制力


网络异常,图片无法展示
|


上图可以看出,test2 的性能要比 test1 的性能要好不少,从而得知,直接访问属性,会比通过方法访问属性速度来的快。


遍历速度


网络异常,图片无法展示
|


上图可以看出,loop 遍历速度 forEach > 优化 for > for of > for > for in


dom 节点操作


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


采用字面量替换 New 操作


网络异常,图片无法展示
|


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

相关文章
|
2月前
|
Web App开发 监控 JavaScript
监控和分析 JavaScript 内存使用情况
【10月更文挑战第30天】通过使用上述的浏览器开发者工具、性能分析工具和内存泄漏检测工具,可以有效地监控和分析JavaScript内存使用情况,及时发现和解决内存泄漏、过度内存消耗等问题,从而提高JavaScript应用程序的性能和稳定性。在实际开发中,可以根据具体的需求和场景选择合适的工具和方法来进行内存监控和分析。
|
2月前
|
JavaScript 前端开发 Java
避免 JavaScript 中的内存泄漏
【10月更文挑战第30天】避免JavaScript中的内存泄漏问题需要开发者对变量引用、事件监听器管理、DOM元素操作以及异步操作等方面有深入的理解和注意。通过遵循良好的编程实践和及时清理不再使用的资源,可以有效地减少内存泄漏的风险,提高JavaScript应用程序的性能和稳定性。
|
2月前
|
监控 JavaScript 算法
如何使用内存监控工具来定位和解决Node.js应用中的性能问题?
总之,利用内存监控工具结合代码分析和业务理解,能够逐步定位和解决 Node.js 应用中的性能问题,提高应用的运行效率和稳定性。需要耐心和细致地进行排查和优化,不断提升应用的性能表现。
194 77
|
2月前
|
监控 JavaScript
选择适合自己的Node.js内存监控工具
选择合适的内存监控工具是优化 Node.js 应用内存使用的重要一步,它可以帮助你更好地了解内存状况,及时发现问题并采取措施,提高应用的性能和稳定性。
120 76
|
2月前
|
监控 JavaScript 数据库连接
解读Node.js内存监控工具生成的报告
需要注意的是,不同的内存监控工具可能会有不同的报告格式和内容,具体的解读方法可能会有所差异。因此,在使用具体工具时,还需要参考其相关的文档和说明,以更好地理解和利用报告中的信息。通过深入解读内存监控报告,我们可以不断优化 Node.js 应用的内存使用,提高其性能和稳定性。
106 74
|
2月前
|
存储 缓存 JavaScript
如何优化Node.js应用的内存使用以提高性能?
通过以上多种方法的综合运用,可以有效地优化 Node.js 应用的内存使用,提高性能,提升用户体验。同时,不断关注内存管理的最新技术和最佳实践,持续改进应用的性能表现。
131 62
|
2月前
|
监控 JavaScript Java
Node.js中内存泄漏的检测方法
检测内存泄漏需要综合运用多种方法,并结合实际的应用场景和代码特点进行分析。及时发现和解决内存泄漏问题,可以提高应用的稳定性和性能,避免潜在的风险和故障。同时,不断学习和掌握内存管理的知识,也是有效预防内存泄漏的重要途径。
158 52
|
2月前
|
存储 缓存 监控
如何使用内存监控工具来优化 Node.js 应用的性能
需要注意的是,不同的内存监控工具可能具有不同的功能和特点,在使用时需要根据具体工具的要求和操作指南进行正确使用和分析。
77 31
|
1月前
|
存储 监控 算法
Java内存管理深度剖析:从垃圾收集到内存泄漏的全面指南####
本文深入探讨了Java虚拟机(JVM)中的内存管理机制,特别是垃圾收集(GC)的工作原理及其调优策略。不同于传统的摘要概述,本文将通过实际案例分析,揭示内存泄漏的根源与预防措施,为开发者提供实战中的优化建议,旨在帮助读者构建高效、稳定的Java应用。 ####
40 8
|
2月前
|
缓存 监控 JavaScript
避免在Node.js中出现内存泄漏
总之,避免内存泄漏需要在开发过程中保持谨慎和细心,遵循最佳实践,不断优化和改进代码。同时,定期进行内存管理的检查和维护也是非常重要的。通过采取这些措施,可以有效地降低 Node.js 应用中出现内存泄漏的风险,确保应用的稳定和性能。