内存溢出分析及解决实践

开发 项目管理
在项目过程中,如果遇到了某些性能问题可能跟内存泄漏有关时,就可以参照本文方法去排查,一定能找到问题所在并给到解决办法的。

 

[[407593]]

如果页面卡顿、或者半夜内存飙升,你觉得可能是什么原因造成的?有什么办法锁定原因并解决吗?(项目常用)

这是一个非常宽泛而又有深度的问题,涉及到很多的页面性能优化问题,我依稀还记得当初面试被问到这个问题时是这么回答的:

1)先会检查是否是网络请求太多,导致数据返回较慢,可以适当做一些缓存;

2)也有可能是某块资源的bundle太大,可以考虑拆分一下;3)然后排查一下js代码,是不是某处有过多循环导致占用主线程时间过长;

4)浏览器某帧渲染的东西太多,导致的卡顿;

5)在页面渲染过程中,可能有很多重复的重排重绘;

后来了解到了,感官上的长时间运行页面卡顿也有可能是因为内存泄漏,内存溢出引起的。

一、内存溢出与内存泄露

1、内存溢出

1)一种程序运行出现的错误。

2)当程序运行所需内存超过了计算机剩余的内存时,程序崩溃并抛出内存溢出的错误。

例如:系统已经不能再分配出你所需要的空间,比如你需要100M的空间,系统只剩90M了,这就叫内存溢出。

2、内存泄漏

1)指内存被占用且没有及时释放。

2)内存泄漏越多,计算机剩余的内存就越小,此时越容易发生内存溢出。

常见的内存泄露情况:

1)意外的全局变量

2)未及时清理计时器或回调函数

3)闭包

例如:用资源的时候为他开辟了一段空间,当你用完时忘记释放资源了,这时内存还被占用着,一次没关系,但是内存泄漏次数多了就会导致内存溢出。内存泄漏是导致迟缓,崩溃的根本原因,甚至会导致其他应用问题。

二、垃圾回收机制

垃圾回收机制(GC)

根据内存泄漏的定义,有些变量或数据不再被使用或不需要了,那么它就是垃圾变量或垃圾数据,如果其一直保存在内存中,最终可能会导致内存占用过多的情况。那么此时就需要对这些垃圾数据进行回收,这里引入了垃圾回收机制的概念。

1、GC定义与作用

GC 就是垃圾回收机制的简写;

GC 可以找到内存中的垃圾、并释放和回收空间;

2、GC里的垃圾是什么

1) 程序中不再需要使用对象

 

  1. function func() { 
  2.  name = 'lg' 
  3.   return `${nameis a coder` 
  4. func() 

2) 程序中不再需要使用对象

 

  1. function func() { 
  2.   const name = 'lg' 
  3.   return `${nameis a coder` 
  4. func() 

3、常见 GC 算法- 引用计数

引用计数算法实现原理

引用计数算法(重要)

  • 核心思想:设置引用数,判断当前引用数是否为 0;
  • 引用计数器;
  • 引用关系改变时修改引用数字;
  • 引用数字为0时立即回收;

 

  1. const user1 = {age: 11} 
  2. const user2 = {age: 22} 
  3. const user3 = {age: 33} 
  4.  
  5. const nameList = [user1.age, user2.age, user3.age] 
  6.  
  7. function fn() { 
  8.  const num1 = 1; 
  9.   const num2 = 2; 
  10. fn(); 

说明:fn() 的num1,num2会被回收。

4、常见 GC 算法- 标记清除

标记清除算法实现原理

  • 核心思想:分标记和清除二个阶段完成;
  • 遍历所有对象找标记活动对象;
  • 遍历所有对象清除没有标记对象;
  • 回收相应的空间;

 

  1. function speakLines(){ 
  2.   let night="天黑";//做个标记 ,进入环境  
  3.   let closeEyes="闭眼";//做个标记 ,进入环境  
  4.   let speak=`开始狼人杀,${night}请${closeEyes}`;//做个标记 ,进入环境  
  5.   console.log(speak); 
  6. speakLines() //代码执行完毕  里面被标记过的变量,又被标记 离开环境 最后被回收 

三、V8引擎- node内存溢出

用户反馈的说是半夜没人用的时候内存飙升了,node内存的溢出的问题分析。

1、先期排查(重要)

先期排查的内容,信息如下:

1) 日志:数据分析大小,内存溢出日志。

2) 监控:开启Sentry监控,Sonar代码质量,阿里云监控。

1) 报错日志

a. 数据分析

 

 

 

 

b. 报错日志

 

  1. 2021-05-13 22:26:48:198S: FATAL ERROR:  
  2. CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory 
  3.  
  4. 2021-05-15 00:18:33:670S: FATAL ERROR:  
  5. Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory 

2、问题分析 (重要)

1) V8引擎对Node.js(和Chrome V8)中的内存限制大小。

2) 代码分析:内存泄漏出现情况(重要)

  • redis服务(没有使用去掉);
  • 无用的node安装依赖包;
  • node的回收机制没有开启;
  • websocket机制;
  • socket在node里面是否销毁;
  • node端是否存储了api返回的数据(导致内存溢出);
  • node代理的时候是否发送给dns,或者带域名的地址(网络慢导致代理失败);

3、Nodejs内存 - 扩大容量

V8引擎对Node.js(和Chrome V8)中的内存限制大小。

原因

因为在Node中,通过JavaScript使用内存时只能使用部分内存(64位系统:1.4 GB,32位系统:0.7 GB),这个时候,如果前端项目非常的庞大,Webpack编译时就会占用很多的系统资源,如果超出了V8引擎对Node默认的内存限制大小时,就会产生内存溢出的错误。

解决方案一:通过 package.json 中的 "build" 加大内存

 

  1. "scripts": { 
  2.     "dev""node build/dev-server.js"
  3.     "start""node build/dev-server.js"
  4.     "build""set NODE_ENV=production && node --max_old_space_size=4096 build/build.js"
  5.     "e2e""node test/e2e/runner.js"
  6.     "test""npm run e2e" 
  7.   }, 

解决方案二:安装插件,加大内存

第一步:在packagejson里添加这两个插件

 

  1. npm install -g increase-memory-limit 
  2. npm install cross-env 

第二步:在package.json里的scripts里进行配置

 

  1. "scripts": { 
  2.     "fix-memory-limit""cross-env LIMIT=8192 increase-memory-limit" 
  3.   }, 

LIMIT是你想分配的内存大小,这里的8192单位是M也就是8G(具体的大小根据实际情况而定);

第三步:

 

  1. npm run fix-memory-limit 

*说明:增加内存限制大小,并不能够实质解决问题。

扩展知识

1、认识 V8

  • V8 是一款主流的 JavaScript 执行引擎
  • V8 采用即时编译
  • V8 内存设限

因为在Node中,通过JavaScript使用内存时只能使用部分内存(64位系统:1.4 GB,32位系统:0.7 GB)

2、V8 垃圾回收策略

  • 采用分代回收的思想
  • 内存分为新生代、老生代
  • 针对不同对象采用不同算法

3、V8 如何回收新生代对象

  • V8内存分配
  • V8内存空间一分为二
  • 小空间用于存储新生代对象(32M | 16M)
  • 新生代指的是存活时间较短的对象

V8 内存分配

 

 

 

 

新生代对象回收实现

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

回收细节说明

  • 拷贝过程中可能出现晋升
  • 晋升就是将新生代对象移动至老生代
  • 一轮 GC 还存活的新生代需要晋升
  • To 空间的使用率超过 25%

4、V8 如何回收老生代对象

老年代对象说明

  • 老年代对象存放在右侧老生代区域
  • 64位操作系统1.4G,32操作系统 700M
  • 老年代对象就是指存活时间较长的对象

老年代对象回收实现

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

细节对比

  • 新生代区域垃圾回收使用空间换时间
  • 老生代区域垃圾回收不适合复制算法

标记增量如何优化垃圾回收

 

 

 

 

四、Chrome devTools查看内存情况

监控内存的几种方式(重要)

  • 浏览器任务管理器
  • Timeline 时序图记录
  • 堆快照查找分离 DOM
  • 判断是否存在频繁的垃圾回收

chrome内存泄露、内存泄漏分析工具

内存分析使用的工具包括chrome任务管理器、chrome时间轴(低版本是Timeline,高版本对应performance)、chrome memory(低版本是chrome profiles,主要用JS堆快照、JS堆动态分配时间轴)。

1) 使用Chrome任务管理器,了解网页使用的内存量。

2) 使用时间轴记录可视化内存使用。

3) 使用堆快照标识分离的DOM树(内存泄漏的常见原因)。

4) 通过堆动态分配时间轴记录了解在JS堆中分配及回收情况。

首先打开Chrome的无痕模式,这样做的目的是为了屏蔽掉Chrome插件对我们之后测试内存占用情况的影响。

 

 

 

 

然后打开开发者工具,找到Performance这一栏,可以看到其内部带着一些功能按钮,例如:开始录制按钮;刷新页面按钮;清空记录按钮;记录并可视化js内存、节点、事件监听器按钮;触发垃圾回收机制按钮等等。

 

 

 

 

简单录制一下百度页面,看看我们能获得什么,如下动图所示:

 

 

 

 

从上图中我们可以看到,在页面从零到加载完成这个过程中JS Heap(js堆内存)、documents(文档)、Nodes(DOM节点)、Listeners(监听器)、GPU memory(GPU内存)的最低值、最高值以及随时间的走势曲线,这也是我们主要关注的点。再来看看开发者工具中的Memory一栏,其主要是用于记录页面堆内存的具体情况以及js堆内存随加载时间线动态的分配情况

 

 

 

 

五、代码优化

代码优化介绍(重要)

  • JavaScript中的内存管理自动完成;
  • 执行引擎会使用不同的 GC 算法;
  • 算法工作的目的是为了实现内存空间良性循环;
  • Performance 工具监测内存变化;
  • JavaScript 是单线程机制的解释型语言;

 递归导致内存溢出 (函数执行中再次调用自己执行)

 

  1. // 下面案例是“死递归”  Uncaught RangeError: Maximum call stack size exceeded “内存溢出” 
  2. function fn(x) { 
  3.     // console.log(x); 
  4.     fn(x + 1); 
  5. fn(1);  

虽然JavaScript会自动垃圾收集,但是如果我们的代码没有及时清空一些被调用的东西,就会让变量一直处于无法回收状态。

 全局变量引起

 

  1. function leaks(){   
  2.     leak = 'xxxxxx';//leak 成为一个全局变量,不会被回收 

 闭包

 

  1. var leaks = (function(){   
  2.     var leak = 'xxxxxx';// 被闭包所引用,不会被回收 
  3.     return function(){ 
  4.         console.log(leak); 
  5.     } 
  6. })() 

 被遗忘的计时器

 

  1. var someResource = getData(); 
  2. setInterval(function() { 
  3.     var node = document.getElementById('Node'); 
  4.     if(node) { 
  5.         // Do stuff with node and someResource. 
  6.         node.innerHTML = JSON.stringify(someResource)); 
  7.     } 
  8. }, 1000); 

 eval 使用

使用eval()函数会带来安全隐患,eval()函数的作用是返回任意字符串,当作js代码来处理。

原来代码:

 

  1. this.formColor = eval('(' + resp.responsePageData.sysColor + ')'); 

 

 

 

 

处理后代码:

 

  1. this.formColor = JSON.parse(resp.responsePageData.sysColor); 

 

 

 

 

*总结

在项目过程中,如果遇到了某些性能问题可能跟内存泄漏有关时,就可以参照本文方法去排查,一定能找到问题所在并给到解决办法的。

虽然JavaScript的垃圾回收是自动的,但我们有时也是需要考虑要不要手动清除某些变量的内存占用的,例如你明确某个变量在一定条件下再也不需要,但是还会被外部变量引用导致内存无法得到释放时,你可以用null对该变量重新赋值就可以在后续垃圾回收阶段释放该变量的内存了。

责任编辑:姜华 来源: 前端学苑
相关推荐

2009-08-18 16:45:50

Tomcat内存溢出

2012-05-29 16:30:33

Tomcat内存溢出

2013-08-02 10:06:36

Android内存溢出

2018-04-17 14:41:41

Java堆内存溢出

2019-08-29 14:29:42

JVM内存 Java

2023-05-29 07:17:48

内存溢出场景

2010-09-26 16:04:48

JVM内存溢出

2018-12-20 10:17:35

JVM模型内存溢出

2023-09-22 17:34:37

内存remove方法

2021-05-26 08:02:03

ThreadLocal多线程多线程并发安全

2009-06-16 11:01:14

Java内存溢出

2012-03-14 10:58:27

Java

2024-03-11 08:22:40

Java内存泄漏

2021-08-26 05:00:44

生产环境内存

2015-03-30 11:18:50

内存管理Android

2024-03-25 12:38:00

MySQL内存参数

2012-05-15 02:04:22

JVMJava

2022-07-20 22:55:39

直播OOM抖动

2018-05-15 08:44:44

TensorFlowKeras内存

2020-05-09 13:49:00

内存空间垃圾
点赞
收藏

51CTO技术栈公众号