內存泄漏js代碼,內存泄露代碼

本文目錄一覽:

怎麼解決內存泄漏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

(0)
打賞 微信掃一掃 微信掃一掃 支付寶掃一掃 支付寶掃一掃
小藍的頭像小藍
上一篇 2024-11-29 08:02
下一篇 2024-11-29 08:02

相關推薦

  • JS Proxy(array)用法介紹

    JS Proxy(array)可以說是ES6中非常重要的一個特性,它可以代理一個數組,監聽數據變化並進行攔截、處理。在實際開發中,使用Proxy(array)可以方便地實現數據的監…

    編程 2025-04-29
  • Python周杰倫代碼用法介紹

    本文將從多個方面對Python周杰倫代碼進行詳細的闡述。 一、代碼介紹 from urllib.request import urlopen from bs4 import Bea…

    編程 2025-04-29
  • Python字符串寬度不限制怎麼打代碼

    本文將為大家詳細介紹Python字符串寬度不限制時如何打代碼的幾個方面。 一、保持代碼風格的統一 在Python字符串寬度不限制的情況下,我們可以寫出很長很長的一行代碼。但是,為了…

    編程 2025-04-29
  • Python基礎代碼用法介紹

    本文將從多個方面對Python基礎代碼進行解析和詳細闡述,力求讓讀者深刻理解Python基礎代碼。通過本文的學習,相信大家對Python的學習和應用會更加輕鬆和高效。 一、變量和數…

    編程 2025-04-29
  • Python創建分配內存的方法

    在python中,我們常常需要創建並分配內存來存儲數據。不同的類型和數據結構可能需要不同的方法來分配內存。本文將從多個方面介紹Python創建分配內存的方法,包括列表、元組、字典、…

    編程 2025-04-29
  • 倉庫管理系統代碼設計Python

    這篇文章將詳細探討如何設計一個基於Python的倉庫管理系統。 一、基本需求 在着手設計之前,我們首先需要確定倉庫管理系統的基本需求。 我們可以將需求分為以下幾個方面: 1、庫存管…

    編程 2025-04-29
  • Python滿天星代碼:讓編程變得更加簡單

    本文將從多個方面詳細闡述Python滿天星代碼,為大家介紹它的優點以及如何在編程中使用。無論是剛剛接觸編程還是資深程序員,都能從中獲得一定的收穫。 一、簡介 Python滿天星代碼…

    編程 2025-04-29
  • 寫代碼新手教程

    本文將從語言選擇、學習方法、編碼規範以及常見問題解答等多個方面,為編程新手提供實用、簡明的教程。 一、語言選擇 作為編程新手,選擇一門編程語言是很關鍵的一步。以下是幾個有代表性的編…

    編程 2025-04-29
  • Python實現簡易心形代碼

    在這個文章中,我們將會介紹如何用Python語言編寫一個非常簡單的代碼來生成一個心形圖案。我們將會從安裝Python開始介紹,逐步深入了解如何實現這一任務。 一、安裝Python …

    編程 2025-04-29
  • 怎麼寫不影響Python運行的長段代碼

    在Python編程的過程中,我們不可避免地需要編寫一些長段代碼,包括函數、類、複雜的控制語句等等。在編寫這些代碼時,我們需要考慮代碼可讀性、易用性以及對Python運行性能的影響。…

    編程 2025-04-29

發表回復

登錄後才能評論