本文目錄一覽:
怎麼解決內存泄漏js
意外的全局變量
js中如果不用var聲明變量,該變量將被視為window對象(全局對象)的屬性,也就是全局變量.
function foo(arg) {
bar = “this is a hidden global variable”;
}123
// 上面的函數等價於
function foo(arg) {
window.bar = “this is an explicit global variable”;
}123
所以,你調用完了函數以後,變量仍然存在,導致泄漏.
如果不注意this的話,還可能會這麼漏:
function foo() {
this.variable = “potential accidental global”;
}123
// 沒有對象調用foo, 也沒有給它綁定this, 所以this是window
foo();
你可以通過加上’use strict’啟用嚴格模式來避免這類問題, 嚴格模式會組織你創建意外的全局變量.
被遺忘的定時器或者回調
var someResource = getData();
setInterval(function() {
var node = document.getElementById(‘Node’); if(node) {
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);1234567
這樣的代碼很常見, 如果id為Node的元素從DOM中移除, 該定時器仍會存在, 同時, 因為回調函數中包含對someResource的引用, 定時器外面的someResource也不會被釋放.
沒有清理的DOM元素引用
var elements = { button: document.getElementById(‘button’), image: document.getElementById(‘image’), text: document.getElementById(‘text’)
};function doStuff() {
image.src = ”;
button.click(); console.log(text.innerHTML);
}function removeButton() { document.body.removeChild(document.getElementById(‘button’)); // 雖然我們用removeChild移除了button, 但是還在elements對象里保存着#button的引用
// 換言之, DOM元素還在內存裡面.
}123456789101112131415161718
閉包
先看這樣一段代碼:
var theThing = null;var replaceThing = function () {
var someMessage = ‘123’
theThing = {
someMethod: function () {
console.log(someMessage);
}
};
};123456789
調用replaceThing之後, 調用theThing.someMethod, 會輸出123, 基本的閉包, 我想到這裡應該不難理解.
解釋一下的話, theThing包含一個someMethod方法, 該方法引用了函數中的someMessage變量, 所以函數中的someMessage變量不會被回收, 調用someMethod可以拿到它正確的console.log出來.
接下來我這麼改一下:
var theThing = null;var replaceThing = function () {
var originalThing = theThing; var someMessage = ‘123’
theThing = {
longStr: new Array(1000000).join(‘*’), // 大概佔用1MB內存
someMethod: function () {
console.log(someMessage);
}
};
};1234567891011
我們先做一個假設, 如果函數中所有的私有變量, 不管someMethod用不用, 都被放進閉包的話, 那麼會發生什麼呢.
第一次調用replaceThing, 閉包中包含originalThing = null和someMessage = ‘123’, 我們設函數結束時, theThing的值為theThing_1.
第二次調用replaceThing, 如果我們的假設成立, originalThing = theThing_1和someMessage = ‘123’.我們設第二次調用函數結束時, theThing的值為theThing_2.注意, 此時的originalThing保存着theThing_1, theThing_1包含着和theThing_2截然不同的someMethod, theThing_1的someMethod中包含一個someMessage, 同樣如果我們的假設成立, 第一次的originalThing = null應該也在.
所以, 如果我們的假設成立, 第二次調用以後, 內存中有theThing_1和theThing_2, 因為他們都是靠longStr把佔用內存撐起來, 所以第二次調用以後, 內存消耗比第一次多1MB.
如果你親自試了(使用Chrome的Profiles查看每次調用後的內存快照), 會發現我們的假設是不成立的, 瀏覽器很聰明, 它只會把someMethod用到的變量保存下來, 用不到的就不保存了, 這為我們節省了內存.
但如果我們這麼寫:
var theThing = null;var replaceThing = function () {
var originalThing = theThing; var unused = function () {
if (originalThing)
console.log(“hi”);
}; var someMessage = ‘123’
theThing = {
longStr: new Array(1000000).join(‘*’),
someMethod: function () {
console.log(someMessage);
}
};
};123456789101112131415
unused 這個函數我們沒有用到, 但是它用了 originalThing 變量, 接下來, 如果你一次次調用 replaceThing , 你會看到內存1MB 1MB的漲.
也就是說, 雖然我們沒有使用 unused , 但是因為它使用了 originalThing , 使得它也被放進閉包了, 內存漏了.
強烈建議讀者親自試試在這幾種情況下產生的內存變化.
這種情況產生的原因, 通俗講, 是因為無論 someMethod 還是 unused , 他們其中所需要用到的在 replaceThing 中定義的變量是保存在一起的, 所以就漏了.
如何自己檢查NodeJS的代碼是否存在內存泄漏
首先,我們來看一個簡單的內存泄漏
var http = require(‘http’);var server = http.createServer(function (req, res) {
for (var i=0; i1000; i++) {
server.on(‘request’, function leakyfunc() {});
}
res.end(‘Hello World\n’);}).listen(1337, ‘127.0.0.1’);server.setMaxListeners(0);console.log(‘Server running at . Process PID: ‘, process.pid);
每一個請求我們增加了1000個導致泄漏的監聽器。如果我們在一個shell控制台中執行以下命令:
while true; do curl ; done
然後在另外一個shell控制台中查看我們的進程
top -pid
我們會看到node進程產生異常高的內存佔用,我們的node進程看起來失控了。那麼,當我們的node進程出現這種情況的時候,通常我們該怎樣診斷出問題的根源?
內存泄露的檢測
npm模塊 memwatch 是一個非常好的內存泄漏檢查工具,讓我們先將這個模塊安裝到我們的app中去,執行以下命令:
npm install –save memwatch
然後,在我們的代碼中,添加:
var memwatch = require(‘memwatch’);memwatch.setup();
然後監聽 leak 事件
memwatch.on(‘leak’, function(info) {
console.error(‘Memory leak detected: ‘, info);});
這樣當我們執行我們的測試代碼,我們會看到下面的信息:
{
start: Fri Jan 02 2015 10:38:49 GMT+0000 (GMT),
end: Fri Jan 02 2015 10:38:50 GMT+0000 (GMT),
growth: 7620560,
reason: ‘heap growth over 5 consecutive GCs (1s) – -2147483648 bytes/hr’}
memwatch發現了內存泄漏!memwatch 判定內存泄漏事件發生的規則如下:
當你的堆內存在5個連續的垃圾回收周期內保持持續增長,那麼一個內存泄漏事件被派發
了解更加詳細的內容,查看 memwatch
內存泄漏分析
使用memwatch我們發現了存在內存泄漏,這非常好,但是現在呢?我們還需要定位內存泄漏出現的實際位置。要做到這一點,有兩種方法可以使用。
memwatch heap diff
通過memwatch你可以得到堆內存使用量和內存隨程序運行產生的差異。詳細的文檔在這裡
例如,我們可以在兩個leak事件發生的間隔中做一個heap dump:
var hd;memwatch.on(‘leak’, function(info) {
console.error(info);
if (!hd) {
hd = new memwatch.HeapDiff();
} else {
var diff = hd.end();
console.error(util.inspect(diff, true, null));
hd = null;
}});
執行這段代碼會輸出更多的信息:
{ before: {
nodes: 244023,
time: Fri Jan 02 2015 12:13:11 GMT+0000 (GMT),
size_bytes: 22095800,
size: ‘21.07 mb’ },
after: {
nodes: 280028,
time: Fri Jan 02 2015 12:13:13 GMT+0000 (GMT),
size_bytes: 24689216,
size: ‘23.55 mb’ },
change: {
size_bytes: 2593416,
size: ‘2.47 mb’,
freed_nodes: 388,
allocated_nodes: 36393,
details:
[ { size_bytes: 0,
‘+’: 0,
what: ‘(Relocatable)’,
‘-‘: 1,
size: ‘0 bytes’ },
{ size_bytes: 0,
‘+’: 1,
what: ‘Arguments’,
‘-‘: 1,
size: ‘0 bytes’ },
{ size_bytes: 2856,
‘+’: 223,
what: ‘Array’,
‘-‘: 201,
size: ‘2.79 kb’ },
{ size_bytes: 2590272,
‘+’: 35987,
what: ‘Closure’,
‘-‘: 11,
size: ‘2.47 mb’ },…
所以在內存泄漏事件之間,我們發現堆內存增長了2.47MB,而導致內存增長的罪魁禍首是閉包。如果你的泄漏是由某個class造成的,那麼what字段可能會輸出具體的class名字,所以這樣的話,你會獲得足夠的信息來幫助你最終定位到泄漏之處。
然而,在我們的例子中,我們唯一獲得的信息只是泄漏來自於閉包,這個信息非常有用,但是仍不足以在一個複雜的應用中迅速找到問題的來源(複雜的應用往往有很多的閉包,不知道哪一個造成了內存泄漏——譯者注)
所以我們該怎麼辦呢?這時候該Heapdump出場了。
Heapdump
npm模塊node-heapdump是一個非凡的模塊,它可以使用來將v8引擎的堆內存內容dump出來,這樣你就可以在Chrome的開發者工具中查看問題。你可以在開發工具中對比不同運行階段的堆內存快照,這樣可以幫助你定位到內存泄漏的位置。要想了解heapdump的更多內容,可以閱讀這篇文章
現在讓我們來試試 heapdump,在每一次發現內存泄漏的時候,我們都將此時的內存堆棧快照寫入磁盤中:
memwatch.on(‘leak’, function(info) {
console.error(info);
var file = ‘/tmp/myapp-‘ + process.pid + ‘-‘ + Date.now() + ‘.heapsnapshot’;
heapdump.writeSnapshot(file, function(err){
if (err) console.error(err);
else console.error(‘Wrote snapshot: ‘ + file);
});});
運行我們的代碼,磁盤上會產生一些.heapsnapshot的文件到/tmp目錄下。現在,在Chrome瀏覽器中,啟動開發者工具(在mac下的快捷鍵是alt+cmd+i),點擊Profiles標籤並點擊Load按鈕載入我們的快照。
我們能夠很清晰地發現原來leakyfunc()是內存泄漏的元兇。
我們依然還可以通過對比兩次記錄中heapdump的不同來更加迅速確認兩次dump之間的內存泄漏:
想要進一步了解開發者工具的memory profiling功能,可以閱讀 Taming The Unicorn: Easing JavaScript Memory Profiling In Chrome DevTools 這篇文章。
Turbo Test Runner
我們給Turbo – FeedHenry開發的測試工具提交了一個小補丁 — 使用了上面所說的內存泄漏檢查技術。這樣就可以讓開發者寫針對內存的單元測試了,如果模塊有內存問題,那麼測試結果中就會產生相應的警告。詳細了解具體的內容,可以訪問Turbo模塊。
結論和其他細節
上面的內容討論了一種檢測NodeJS內存泄漏的基本方法,以下是一些結論:
heapdump有一些潛規則,例如快照大小等。仔細閱讀說明文檔,並且生成快照也是比較消耗CPU資源的。
還有些其他方法也能生成快照,各有利弊,針對你的項目選擇最適合的方式。(例如,發送sigusr2到進程等等,這裡有一個memwatch-sigusr2項目)
需要考慮在什麼情況下開啟memwatch/heapdump。只有在測試環境中有開啟它們的必要,另外也需要考慮heapdump的頻度以免耗盡了CPU。總之,選擇最適合你項目的方式。
也可以考慮其他的方式來檢測內存的增長,比如直接監控process.memoryUsage()是一個可以考慮的方法。
當內存問題被探測到之後,你應該要確定這確實是個內存泄漏問題,然後再告知給相關人員。
當心誤判,短暫的內存使用峰值表現得很像是內存泄漏。如果你的app突然要佔用大量的CPU和內存,處理時間可能會跨越數個垃圾回收周期,那樣的話memwatch很有可能將之誤判為內存泄漏。但是,這種情況下,一旦你的app使用完這些資源,內存消耗就會降回正常的水平。所以,你其實需要注意的是持續報告的內存泄漏,而可以忽略一兩次突發的警報。
memwatch目前僅支持node 0.10.x,node 0.12.x(可能還有io.js)支持的版本在這個分支
js循環引用引起的內存泄漏示例
Js中存在和OC同等意義的閉包(block closure)閉包可看作匿名函數,例如:
函數中 給element的onclick屬性賦值了一個閉包,閉包要訪問element的id屬性。閉包在js中也是對象,函數即對象。閉包會持有外部傳入的變量,因此閉包持有了element對象,而element對象通過onclick屬性持有了閉包,因此兩個對象相互持有,造成內存泄漏。
與OC類比,OC中使用weak對象引用,來解決循環引用的問題,js中也有類似操作,例如:
因為var id是由賦值得到的,js的賦值操作是值或者引用的拷貝,並不持有對象。此時element持有閉包,閉包持有id對象,並未造成循環引用。
autojs死巡環內存爆炸
內存溢出是一種程序運行會出現的錯誤,當程序所需要的內存大於剩餘內存(機器能提供給你的內存),就會拋出內存溢出的錯誤
var obj = {}
for (var i = 0; i 100000000; i++) {
obj[i] = new Array[100000000]
}
登錄後複製
內存泄漏
佔用的內存沒有及時的釋放從而失去控制,從而造成內存的浪費。內存泄漏多了就容易引發內存溢出。
常見的內存泄漏案例:
1、意外的全局變量
function fn() {
var name = ‘張三’
var age = 18
address = ‘上海’ // 沒有用var定義,這時候address是全局的
}
fn() // 因為address會被變量提升到了全局變量,fn調用完成後address還保留在內存中
登錄後複製
2、沒有及時清除定時器
// 沒有及時清理定時器
var timer = setInterval(() = {
console.log(new Date())
}, 1000);
// clearInterval(timer) 及時清理定時器
登錄後複製
3、沒有及時清理閉包
// 函數執行完後, 函數內的局部變量沒有釋放, 佔用內存時間會變長,容易造成內存泄露
function fun() {
var a = 5
function getA() {
return a
}
return getA
}
var f = fun()
f() // 5
// f = null 讓內部函數成為垃圾對象,釋放閉包
登錄後複製
4、沒有及時清理清理dom元素的引用
var dom = document.getElementById(‘box’)
document.body.removeChild(dom) // dom刪除後,下面依然能打印出整個div
console.log(dom) // div id=”box”嘿嘿嘿/div
dom = null
console.log(dom) // 釋放資源,解除引用
登錄後複製
5、addEventListener
監聽事件的解除,監聽的時候addEventListener,在不監聽的時候要使用removeEventListener。
原創文章,作者:小藍,如若轉載,請註明出處:https://www.506064.com/zh-hant/n/189271.html