Redian新闻
>
如何避免JavaScript中的内存泄漏?

如何避免JavaScript中的内存泄漏?

公众号新闻

来源 | OSCHINA 社区

作者 | 葡萄城技术团队

原文链接:https://my.oschina.net/powertoolsteam/blog/10122640

前言

过去,我们浏览静态网站时无须过多关注内存管理,因为加载新页面时,之前的页面信息会从内存中删除。然而,随着单页 Web 应用(SPA)的兴起,应用程序消耗的内存越来越多,这不仅会降低浏览器性能,甚至会导致浏览器卡死。因此,在编码实践中,开发人员需要更加关注与内存相关的内容。因此,小编今天将为大家介绍 JavaScript 内存泄漏的编程模式,并提供一些内存管理的改进方法。
什么是内存泄漏以及如何发现它?
什么是内存泄漏?
JavaScript 对象被保存在浏览器内存的堆中,并通过引用方式访问。值得一提的是,JavaScript 垃圾回收器则运行于后台,并通过识别无法访问的对象来释放并恢复底层存储空间,从而保证 JavaScript 引擎的良好运行状态。
当内存中的对象在垃圾回收周期中应该被清理时,若它们被另一个仍然存在于内存中的对象通过一个意外的引用所持有,就会引发内存泄漏问题。这种情况下,冗余对象会继续占据内存空间,导致应用程序消耗过多的内存资源,并可能导致性能下降和表现不佳的情况出现。因此,及时清理无用对象并释放内存资源是至关重要的,以确保应用程序的正常运行和良好的性能表现。
如何发现内存泄漏?
那么如何知道代码中是否存在内存泄漏?内存泄漏往往隐蔽且很难检测和定位。即使代码中存在内存泄漏,浏览器在运行时也不会返回任何错误。如果注意到页面的性能逐渐下降,可以使用浏览器内置的工具来确定是否存在内存泄漏以及是哪个对象引起的。
任务管理器(不要与操作系统的任务管理器混淆)提供了浏览器中所有选项卡和进程的概览。Chrome 中,可以通过在 Linux 和 Windows 操作系统上按 Shift+Esc 来打开任务管理器;而在 Firefox 中,通过在地址栏中键入 about:performance 则可以访问内置的管理器,它可以显示每个标签的 JavaScript 内存占用情况。如果网站停留在那里什么都不做,但 JavaScript 内存使用量逐渐增加,那很可能是存在内存泄漏。
开发者工具提供了一些先进的内存管理方法,例如,使用 Chrome 浏览器的性能记录工具,可以对页面的性能进行可视化分析。在这个过程中,可以通过一些指标来判断是否存在内存泄漏问题,比如堆内存使用量增加的情况,并及时采取措施解决这些问题,以确保应用程序的正常运行和良好的性能表现。
另外,通过 Chrome 和 Firefox 的开发者工具提供的内存工具,可以进一步探索内存使用情况。队列内存使用快照的比较可以显示在两个快照之间分配了多少内存以及分配的位置,并提供额外信息来帮助识别代码中存在问题的对象。这些工具为开发者提供了便利,能够更好地进行内存管理和性能优化,提高应用程序的质量和性能。
JavaScript 代码中常见的内存泄漏的常见来源:
研究内存泄漏问题就相当于寻找符合垃圾回收机制的编程方式,有效避免对象引用的问题。下面小编就为大家介绍几个常见的容易导致内存泄漏的地方:
1. 全局变量
全局变量始终存储在根目录下,且永远不会被回收。而在 JavaScript 的开发中,一些错误会导致局部变量被转换到了全局,尤其是在非严格的代码模式下。下面是两个常见的局部变量被转化到全局变量的情况:
  1. 为未声明的变量赋值

  2. 使用 this 指向全局对象。

function createGlobalVariables() {
leaking1 = 'I leak into the global scope'; // 为未声明的变量赋值
this.leaking2 = 'I also leak into the global scope'; // 使用this指向全局对象
};
createGlobalVariables();
window.leaking1;
window.leaking2;
** 注意:** 严格模式("use strict")将帮助您避免上面示例中的内存泄漏和控制台错误。
2. 闭包
函数中定义的变量会在函数退出调用栈并且在函数外部没有指向它的引用时被清除。而闭包则会保持被引用的变量一直存在,即便函数的执行已经终止。
function outer() {
const potentiallyHugeArray = [];

return function inner() {
potentiallyHugeArray.push('Hello'); // function inner is closed over the potentiallyHugeArray variable
console.log('Hello');
};
};
const sayHello = outer(); // contains definition of the function inner

function repeat(fn, num) {
for (let i = 0; i < num; i++){
fn();
}
}
repeat(sayHello, 10); // each sayHello call pushes another 'Hello' to the potentiallyHugeArray

// now imagine repeat(sayHello, 100000)
在这个例子中,potentiallyHugeArray 从未被任何函数返回,也无法被访问,但它的大小会随着调用 inner 方法的次数而增长。
3. 定时器
在 JavaScript 中,使用使用 setTimeout 或 setInterval 函数引用对象是防止对象被垃圾回收的最常见方法。当在代码中设置循环定时器(可以使 setTimeout 表现得像 setInterval,即使其递归)时,只要回调可调用,定时器回调对象的引用就会永远保持活动状态。
例如下面的这段代码,只有在移除定时器后,data 对象才会被垃圾回收。在没有移除 setInterval 之前,它永远不会被删除,并且 data.hugeString 会一直保留在内存中,直到应用程序停止。
function setCallback() {
const data = {
counter: 0,
hugeString: new Array(100000).join('x')
};

return function cb() {
data.counter++; // data object is now part of the callback's scope
console.log(data.counter);
}
}

setInterval(setCallback(), 1000); // how do we stop it?
那么应该如何避免上述这种情况的发生呢?可以从以下两个方法入手:
  1. 注意定时器回调引用的对象。

  2. 必要时取消定时器。

如下方的代码所示:
function setCallback() {
// 'unpacking' the data object
let counter = 0;
const hugeString = new Array(100000).join('x'); // gets removed when the setCallback returns

return function cb() {
counter++; // only counter is part of the callback's scope
console.log(counter);
}
}

const timerId = setInterval(setCallback(), 1000); // saving the interval ID

// doing something ...

clearInterval(timerId); // stopping the timer i.e. if button pressed
4. 事件监听
活动的事件监听器会阻止其范围内的所有变量被回收。一旦添加,事件监听器会一直生效,直到下面两种情况的发生:
  1. 通过 removeEventListener () 移除。

  2. 相关联的 DOM 元素被移除。

在下面的示例中,使用匿名内联函数作为事件监听器,这意味着它不能与 removeEventListener () 一起使用。此外,由于 document 不能被移除,触发方法中的内容会一直驻留内存,即使只使用它触发一次。
const hugeString = new Array(100000).join('x');

document.addEventListener('keyup', function() { // anonymous inline function - can't remove it
doSomething(hugeString); // hugeString is now forever kept in the callback's scope
});

那么如何避免这种情况呢?可以通过 removeEventListener () 释放监听器:

function listener() {
doSomething(hugeString);
}

document.addEventListener('keyup', listener); // named function can be referenced here...
document.removeEventListener('keyup', listener); // ...and here

如果事件监听器只需要运行一次,addEventListener () 可以带有第三个参数,一个提供附加选项的对象。只要将 {once: true} 作为第三个参数传递给 addEventListener (),监听器将在事件处理一次后自动删除。

document.addEventListener('keyup', function listener() {
doSomething(hugeString);
}, {once: true}); // listener will be removed after running once
5. 缓存
如果不断向缓存中添加内容,而未使用的对象也没有移除,也没有限制缓存的大小,那么缓存的大小就会无限增长:
let user_1 = { name: "Peter", id: 12345 };
let user_2 = { name: "Mark", id: 54321 };
const mapCache = new Map();

function cache(obj){
if (!mapCache.has(obj)){
const value = `${obj.name} has an id of ${obj.id}`;
mapCache.set(obj, value);

return [value, 'computed'];
}

return [mapCache.get(obj), 'cached'];
}

cache(user_1); // ['Peter has an id of 12345', 'computed']
cache(user_1); // ['Peter has an id of 12345', 'cached']
cache(user_2); // ['Mark has an id of 54321', 'computed']

console.log(mapCache); // ((…) => "Peter has an id of 12345", (…) => "Mark has an id of 54321")
user_1 = null; // removing the inactive user

// Garbage Collector
console.log(mapCache); // ((…) => "Peter has an id of 12345", (…) => "Mark has an id of 54321") // first entry is still in cache
为了解决这个问题,需要清除不需要的缓存:
一种有效的解决内存泄漏问题的方法是使用 WeakMap。它是一种数据结构,其中键引用被保持为弱引用,并且仅接受对象作为键。如果使用对象作为键,并且它是唯一引用该对象的引用,相关条目将从缓存中移除,并进行垃圾回收。在下面的示例中,当替换 user_1 后,与之关联的条目将在下一次垃圾回收时自动从 WeakMap 中移除。
let user_1 = { name: "Peter", id: 12345 };
let user_2 = { name: "Mark", id: 54321 };
const weakMapCache = new WeakMap();

function cache(obj){
// ...same as above, but with weakMapCache

return [weakMapCache.get(obj), 'cached'];
}

cache(user_1); // ['Peter has an id of 12345', 'computed']
cache(user_2); // ['Mark has an id of 54321', 'computed']
console.log(weakMapCache); // ((…) => "Peter has an id of 12345", (…) => "Mark has an id of 54321"}
user_1 = null; // removing the inactive user

// Garbage Collector

console.log(weakMapCache); // ((…) => "Mark has an id of 54321") - first entry gets garbage collected
结论
对于复杂的应用程序,检测和修复 JavaScript 内存泄漏问题可能是一项非常艰巨的任务。了解内存泄漏的常见原因以防止它们发生是非常重要的。在涉及内存和性能方面,最重要的是用户体验,这才是最重要的。


往期推荐



Linus曾给Windows 7热情点赞
云原生 IDE 将成为常态
优先展示冒牌货且定向至恶意软件,网友:是时候摆脱Google了




活动推荐

10 月 28 日,本周六,源创会苏州站暨 Techo TVP 技术沙龙将正式拉开帷幕。

这一次,我们以“寻宝 AI 时代”为主题,希望能给每一位开发者新的启发,一起在 AI 新时代更上一层楼。同时,我们将沙龙地点定在了轰趴馆,希望各位玩得开心!

快扫描下方二维码,或点击文末“阅读原文”即刻报名 🎁🎁


阅读原文立即报名>>

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
战国故事《定风波》卷二(25):春梦1折入!穿过国际重奢𝘼𝙦𝙪𝙖𝙨𝙘𝙪𝙩𝙪𝙢的男人,才会明白什么是品质!1折入!英国重奢𝘼𝙦𝙪𝙖𝙨𝙘𝙪𝙩𝙪𝙢,专柜同款Polo衫,舒适、透气、高品质!1折入!国际重奢𝘼𝙦𝙪𝙖𝙨𝙘𝙪𝙩𝙪𝙢雅格狮丹三防夹克,下单送长袖T恤!九十八 婚礼真没想到!国际重奢𝘼𝙦𝙪𝙖𝙨𝙘𝙪𝙩𝙪𝙢(雅格狮丹)风衣都让我们找来了(1折限时抢)双11特惠|国际重奢疯了?!𝘼𝙦𝙪𝙖𝙨𝙘𝙪𝙩𝙪𝙢(雅格狮丹)90%白鸭绒羽绒服,保暖防泼水,1折开抢!国际重奢𝘼𝙦𝙪𝙖𝙨𝙘𝙪𝙩𝙪𝙢中长款风衣,品位与优雅,兼顾帅气和女人味!Issaquah一青少年感染汉坦病毒肺综合症!HPS症状类似流感,但死亡率极高,如何避免?PPT中的“分割”排版,简直是YYDS!刚刚!2023年诺贝尔化学奖揭晓,但疑遭提前泄漏?英国百年贵族奢牌𝘼𝙦𝙪𝙖𝙨𝙘𝙪𝙩𝙪𝙢休闲卫衣!经典、优雅不凡的英伦风~真没想到风衣1折!国际重奢𝘼𝙦𝙪𝙖𝙨𝙘𝙪𝙩𝙪𝙢(雅格狮丹)风衣都让我们找来了国际重奢卷疯了?!𝘼𝙦𝙪𝙖𝙨𝙘𝙪𝙩𝙪𝙢(雅格狮丹)90%白鸭绒羽绒服,保暖防泼水,1折开抢!如何避免“流动性陷阱”?公交公司如何避免“停运”命运?真没想到!国际重奢𝘼𝙦𝙪𝙖𝙨𝙘𝙪𝙩𝙪𝙢雅格狮丹的风衣都让我们找来了,1折限时抢厚重的历史书,雄伟的大教堂1折入!穿过国际重奢𝘼𝙦𝙪𝙖𝙨𝙘𝙪𝙩𝙪𝙢的人,才是真正的有品!𝘼𝙦𝙪𝙖𝙨𝙘𝙪𝙩𝙪𝙢三防夹克,经典、优雅不凡的英伦风!还送长袖T恤!换季大捡漏!国际重奢𝘼𝙦𝙪𝙖𝙨𝙘𝙪𝙩𝙪𝙢经典纯色T恤,买一送一!火了172年!国际重奢𝘼𝙦𝙪𝙖𝙨𝙘𝙪𝙩𝙪𝙢长袖Polo衫,又好穿又有品!英国百年奢牌 𝘼𝙦𝙪𝙖𝙨𝙘𝙪𝙩𝙪𝙢 90%白鸭绒羽绒服!保暖防泼水,4种款式!思想者如何避免因痛苦而自杀:习瑞跳楼与我的自恋红色日记 1967年 18岁 1.1-16投资房地产不懂税-早晚你要交学费!买房自住出租炒楼花如何合法避税?房地产投资核心是:资金来源是否经得起查税?如何避免被罚款?火了172年!国际重奢𝘼𝙦𝙪𝙖𝙨𝙘𝙪𝙩𝙪𝙢来了,又好穿又有品!!九十七 过扬州【房产】非法押金、假房东猖狂,租房骗局如何避免?1折入!国际重奢𝘼𝙦𝙪𝙖𝙨𝙘𝙪𝙩𝙪𝙢(雅格狮丹)三防夹克,防风防污防泼水,好穿又有品!做医生有多难?26岁男子入院不到3天死亡,医患双方各执一词!医生该如何避免风险?丨医眼看法大捡漏!国际重奢𝘼𝙦𝙪𝙖𝙨𝙘𝙪𝙩𝙪𝙢雅格狮丹出系列了!五防保暖夹克、POLO衫、西裤随便搭换季大捡漏!国际重奢𝘼𝙦𝙪𝙖𝙨𝙘𝙪𝙩𝙪𝙢短袖买一送一!库存不多,手慢无!好东西真不一样!国际重奢𝘼𝙦𝙪𝙖𝙨𝙘𝙪𝙩𝙪𝙢雅格狮丹卫裤,1折限量抢!Kubernetes 中的 Java 应用的内存调优
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。