排查内存泄漏方法(Chrome)
- 利用 Chrome 调试工具的内存快照:
- F12 => 内存 => 拍摄堆快照
- => 摘要、比较、控制、统计信息
- 重点检查 VueComponent
- 利用 Chrome 调试工具的性能录制:
- JS 堆、文档、节点、监听器、GPU 内存
注意: console 控制台信息造成的内存泄漏、注释大法好
闭包会造成内存泄漏?
- 测试 1:下面两种情况,函数执行时,造成内存溢出了么?
// 1.
var outerFunction = function (n) {
var counter = 0;
var bigStr = new Array(100000000).join("*");
return function () {
bigStr += n;
};
};
var add = outerFunction(5);
var interval = setInterval(add, 10);
// 2.
var outerFunction = function (n) {
var counter = 0;
var bigStr = 0;
return function () {
bigStr = new Array(100000000).join("*");
bigStr += n;
};
};
var add = outerFunction(5);
var interval = setInterval(add, 10);
- 测试 2:2 秒后,下面三种情况内存占用从小到大的顺序是?
// 1.
var str = new Array(100000000).join("*");
var interval = setInterval(() => {
console.log(str[0]);
}, 200);
var timeout= setTimeout(() => {
clearInterval(interval);
}, 2000);
// 2.
var str = new Array(100000000).join("*");
var interval = setInterval(() => {
console.log(str[0]);
}, 200);
var timeout= setTimeout(() => {
clearInterval(interval);interval=null;
}, 2000);
// 3.
var str = new Array(100000000).join("*");
var interval = setInterval(() => {
console.log(str[0]);
}, 200);
var timeout= setTimeout(() => {
clearInterval(interval);interval=null;str="";
}, 2000);
- 测试 3:节流函数 1、节流函数 2、防抖函数都是闭包函数;当其内部再嵌套闭包函数时;会影响闭包可被回收么?
// 节流函数1
var throttle1 = function (fn, wait) {
var lastTime = 0;
var obj = {};
return function () {
var nowTime = new Date().getTime();
if (nowTime - lastTime > wait) {
// fn.bind(obj);
fn.apply(this, arguments);
lastTime = nowTime;
}
};
};
// 节流函数2
var throttle = function (fn, delay) {
let timer = null;
return function () {
if (timer) return;
timer = setTimeout(() => {
fn.apply(this, arguments);
clearTimeout(timer);
timer = null;
}, delay);
};
};
// 防抖函数
var debounce = function (fn, wait) {
var timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, wait);
};
};
// 执行测试
var fn = function (index) {
var str = index;
console.log(index);
};
var t = throttle1(fn, 300);//throttle、debounce
var index = 300;
var interval = setInterval(function () {
if (index > 0) {
index--;
t(index);
} else {
clearInterval(interval);
// t = null;
}
}, 20);
- 测试 1 结论:
- 闭包每执行一次都会造成内存增加
- 当 V8 引擎内存达到 1464MB 时,会触发垃圾回收
- 垃圾回收机制能正常回收闭包函数执行时累加的内存占用
- 垃圾回收机制不能回收闭包函数作用域(堆空间)
- 测试 2 结论:
- 打断闭包的引用能使闭包可被回收
- 栈变量(全局变量属于栈变量)不会被回收
- 测试 3 结论:
- 闭包嵌套闭包也能正常回收
- 闭包调用完毕后,将引用打断(置为 null),根据闭包实现方式不同,存在差异;但不置为 null 影响不大
- 控制台内容、注释占用内存
- setInterval、setTimeout 第一个参数为匿名函数时,默认会造成闭包;bind 方法是闭包
注:为保证测试结果准确,使用浏览器无痕模式,步骤需清空 console、手动触发垃圾回收,然后开启新的录制=>刷新页面
V8 引擎测试结论:
- Jquery 的 ajax 方法调用完毕后,残留引用 XMLHttprequest,置不置为 null,对垃圾回收无影响。
- 随着循环调用 ajax,内存会缓慢溢出
- 对于长期不刷新的网页应用,尽量避免定时不断调用闭包
项目内存泄漏问题分析:
注意事项
- 针对性配置菜单、权限(单独测试“设备运维-人工巡检-巡检任务页”)
- 步骤:清空 GC =>截取初始堆快照=>操作=>截取堆快照=>对比分析(配合“注释大法”)
- 排查顺序:VueComponent、Object.constructor、Array
泄漏问题记录
- VueComponent:aw-echarts 组件销毁前未销毁 echarts 实例(dispose 方法)
- Array:$backToLogin 方法引用
window.backToLogin = function () {
location.href = "/index.html#/login";
};
// 原写法
Vue.prototype.$backToLogin = window.backToLogin;
// 改后
Vue.prototype.$backToLogin = function () {
location.href = "/index.html#/login";
};
- svg-sprite-loader 拓展插件造成,需全局引入 svg
// 统一引入svg
import "@/assets/images/logo-l.svg";
import "@/assets/images/logo-r.svg";
import "@/assets/images/logoA.svg";
import "@/assets/images/e403.svg";
import "@/assets/images/noData.svg";
import "@/assets/images/noCP.svg";
import "@/assets/images/e404.svg";
import "@/assets/images/portaHome/play.svg";
- 地图绘制点线组件 MapLineAndMarkers
排查中…
参考文档
-
- 栈空间(stack):原始数据类型、引用关系
- 堆空间(heap):引用数据类型
- 垃圾回收机制:
- 引用计数算法
- 标记清除算法(Mark-Sweep)
- 标记合并算法(Mark-Compact)
-
- Chrome 垃圾回收算法:
- 副垃圾回收器(Scavenge):新生代的垃圾回收–20%
- Scavenge 执行频率快,会造成全停顿(stop-the-world):暂停 JavaScript 应用、暂停主线程,造成卡顿<并发、并行>
- 主垃圾回收器(Mark-Sweep & Mark-Compact):老生代的垃圾回收–80%
- 堆中的内存大小超过某个阈值之后触发
- 针对卡顿问题, 衍生出 Orinoco - 项目代号的优化项目:
- 增量标记 (Incremental marking):将原本的标记全堆对象拆分为一个一个任务,让其穿插在 JavaScript 应用逻辑之间执行,增量标记在堆的大小达到一定的阈值时启用
- 懒性清理 (Lazy sweeping):优先照顾 JavaScript 逻辑代码先执行,按增量标记顺序逐一清理非活动对象内存
- (增量标记&懒性清理)- 逐一清理,衍生出“写屏障技术”:记录这些引用关系的变化
- (增量标记&懒性清理)造成的问题:
- 并没有减少主线程的总暂停的时间,甚至会略微增加
- 由于写屏障(Write-barrier)机制的成本,增量标记可能会降低应用程序的吞吐量
- 并发(Concurrent):主线程与 GC 线程
- 并行(Parallel)GC 线程再拆分辅助线程
- 副垃圾回收器、主垃圾回收器都采用并行+并发方式
- 副垃圾回收器(Scavenge):新生代的垃圾回收–20%
- Chrome 垃圾回收算法:
-
- 增量标记衍生“三色标记算法”:
- 白色:可回收
- 灰色:待处理 存在栈引用的对象
- 黑色:不可回收
- 增量标记衍生“三色标记算法”:
-
- 执行上下文:函数每次执行,都会生成一个会创建一个称为执行上下文的内部对象(AO 对象,可理解为函数作用域),这个 AO 对象会保存这个函数中所有的变量值和该函数内部定义的函数的引用。
- 作用域链:链状执行上下文集合
-
- 内存泄露:是指你用不到(访问不到)的变量,依然占居着内存空间
- 个人理解:被引用上下文作用域的作用域
-
- 仅列举 V8:
- 闭包内未被引用变量 - 回收
- 闭包内未被引用变量引用引用变量- 回收
- eval - 不回收
- 间接调用(window.eval)回收
- 多个匿名函数 不回收
- with 表达式 不回收
- catch 表达式 看原文 -_-|||
- 嵌套函数中声明的同名变量 回收
- 仅列举 V8:
Vue 项目内存泄漏分析:
-
- 子组件嵌套组件避免太深,太多层级。
- 作者未验证观点:
- 子组件 beforeDestroy 的时候,把变量置 null?
- IView 的 Modal 本身是有内存泄漏
- Ztree 本身有少许泄漏,需调用 API 手动销毁
- 父组件要传值给子组件的 props 变量,在关闭时,能置空的尽量置空
- watch 部分,也会有可能导致父组件一直 watch 挂载着导致子组件没有被销毁
- v-show 控制子组件,要在它的外层 div 加个 v-if 控制。
- 递归渲染?
-
- 集中在 beforeDestroy 解绑组件绑定事件
-
- 2017 年解决过 v-model 内存泄漏问题
- 如果数据庞大,不要用 vue 的双向绑定
-