自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Web應(yīng)用內(nèi)存分析與內(nèi)存泄漏定位

開(kāi)發(fā) 開(kāi)發(fā)工具
內(nèi)存分析與內(nèi)存泄漏定位是筆者現(xiàn)代 Web 開(kāi)發(fā)工程化實(shí)踐之調(diào)試技巧的一部分,主要介紹 Web 開(kāi)發(fā)中需要了解的內(nèi)存分析與內(nèi)存泄露定位手段,本部分涉及的參考資料統(tǒng)一聲明在Web 開(kāi)發(fā)界面調(diào)試資料索引。

[[209282]]

內(nèi)存分析與內(nèi)存泄漏定位是筆者現(xiàn)代 Web 開(kāi)發(fā)工程化實(shí)踐之調(diào)試技巧的一部分,主要介紹 Web 開(kāi)發(fā)中需要了解的內(nèi)存分析與內(nèi)存泄露定位手段,本部分涉及的參考資料統(tǒng)一聲明在Web 開(kāi)發(fā)界面調(diào)試資料索引。

無(wú)論是分布式計(jì)算系統(tǒng)、服務(wù)端應(yīng)用程序還是 iOS、Android 原生應(yīng)用都會(huì)存在內(nèi)存泄漏問(wèn)題,Web 應(yīng)用自然也不可避免地存在著類似的問(wèn)題。雖然因?yàn)榫W(wǎng)頁(yè)往往都是即用即走,較少地存在某個(gè)網(wǎng)頁(yè)長(zhǎng)期運(yùn)行的問(wèn)題,即使存在內(nèi)存泄漏可能表現(xiàn)地也不明顯;但是在某些數(shù)據(jù)展示型的,需要長(zhǎng)期運(yùn)行的頁(yè)面上,如果不及時(shí)解決內(nèi)存泄漏可能會(huì)導(dǎo)致網(wǎng)頁(yè)占據(jù)過(guò)大地內(nèi)存,不僅影響頁(yè)面性能,還可能導(dǎo)致整個(gè)系統(tǒng)的崩潰。前端每周清單推薦過(guò)的 How JavaScript works 就是非常不錯(cuò)地介紹 JavaScript 運(yùn)行機(jī)制的系列文章,其也對(duì)內(nèi)存管理與內(nèi)存泄漏有過(guò)分析,本文部分圖片與示例代碼即來(lái)自此系列。

類似于 C 這樣的語(yǔ)言提供了 malloc() 與 free() 這樣的底層內(nèi)存管理原子操作,開(kāi)發(fā)者需要顯式手動(dòng)地進(jìn)行內(nèi)存的申請(qǐng)與釋放;而 Java 這樣的語(yǔ)言則是提供了自動(dòng)化的內(nèi)存回收機(jī)制,筆者在垃圾回收算法與 JVM 垃圾回收器綜述一文中有過(guò)介紹。JavaScript 也是采用的自動(dòng)化內(nèi)存回收機(jī)制,無(wú)論是 Object、String 等都是由垃圾回收進(jìn)程自動(dòng)回收處理。自動(dòng)化內(nèi)存回收并不意味著我們就可以忽略內(nèi)存管理的相關(guān)操作,反而可能會(huì)導(dǎo)致更不易發(fā)現(xiàn)的內(nèi)存泄漏出現(xiàn)。

內(nèi)存分配與回收

筆者在 JavaScript Event Loop 機(jī)制詳解與 Vue.js 中實(shí)踐應(yīng)用一文中介紹過(guò) JavaScript 的內(nèi)存模型,其主要也是由堆、棧、隊(duì)列三方面組成:

其中隊(duì)列指的是消息隊(duì)列、棧就是函數(shù)執(zhí)行棧,其基本結(jié)構(gòu)如下所示:

而主要的用戶創(chuàng)建的對(duì)象就存放在堆中,這也是我們內(nèi)存分析與內(nèi)存泄漏定位所需要關(guān)注的主要的區(qū)域。所謂內(nèi)存,從硬件的角度來(lái)看,就是無(wú)數(shù)觸發(fā)器的組合;每個(gè)觸發(fā)器能夠存放 1 bit 位的數(shù)據(jù),不同的觸發(fā)器由***的標(biāo)識(shí)符定位,開(kāi)發(fā)者可以根據(jù)該標(biāo)識(shí)符讀寫(xiě)該觸發(fā)器。抽象來(lái)看,我們可以將內(nèi)存當(dāng)做比特?cái)?shù)組,而數(shù)據(jù)就是在內(nèi)存中順序排布:

JavaScript 中開(kāi)發(fā)者并不需要手動(dòng)地為對(duì)象申請(qǐng)內(nèi)存,只需要聲明變量,JavaScript Runtime 即可以自動(dòng)地分配內(nèi)存:

  1. var n = 374; // allocates memory for a number 
  2. var s = 'sessionstack'; // allocates memory for a string  
  3. var o = { 
  4.   a: 1, 
  5.   b: null 
  6. }; // allocates memory for an object and its contained values 
  7. var a = [1, null'str'];  // (like object) allocates memory for the 
  8.                            // array and its contained values 
  9. function f(a) { 
  10.   return a + 3; 
  11. } // allocates a function (which is a callable object) 
  12. // function expressions also allocate an object 
  13. someElement.addEventListener('click'function() { 
  14.   someElement.style.backgroundColor = 'blue'
  15. }, false); 

某個(gè)對(duì)象的內(nèi)存生命周期分為了內(nèi)存分配、內(nèi)存使用與內(nèi)存回收這三個(gè)步驟,當(dāng)某個(gè)對(duì)象不再被需要時(shí),它就應(yīng)該被清除回收;所謂的垃圾回收器,Garbage Collector 即是負(fù)責(zé)追蹤內(nèi)存分配情況、判斷某個(gè)被分配的內(nèi)存是否有用,并且自動(dòng)回收無(wú)用的內(nèi)存。大部分的垃圾回收器是根據(jù)引用(Reference)來(lái)判斷某個(gè)對(duì)象是否存活,所謂的引用即是某個(gè)對(duì)象是否依賴于其他對(duì)象,如果存在依賴關(guān)系即存在引用;譬如某個(gè) JavaScript 對(duì)象引用了它的原型對(duì)象。最簡(jiǎn)單的垃圾回收算法即是引用計(jì)數(shù)(Reference Counting),即清除所有零引用的對(duì)象:

  1. var o1 = { 
  2.   o2: { 
  3.     x: 1 
  4.   } 
  5. }; 
  6. // 2 objects are created.  
  7. // 'o2' is referenced by 'o1' object as one of its properties. 
  8. // None can be garbage-collected 
  9.  
  10. var o3 = o1; // the 'o3' variable is the second thing that  
  11.             // has a reference to the object pointed by 'o1'.  
  12.                                                         
  13. o1 = 1;      // now, the object that was originally in 'o1' has a          
  14.             // single reference, embodied by the 'o3' variable 
  15.  
  16. var o4 = o3.o2; // reference to 'o2' property of the object. 
  17.                 // This object has now 2 references: one as 
  18.                 // a property.  
  19.                 // The other as the 'o4' variable 
  20.  
  21. o3 = '374'; // The object that was originally in 'o1' has now zero 
  22.             // references to it.  
  23.             // It can be garbage-collected. 
  24.             // However, what was its 'o2' property is still 
  25.             // referenced by the 'o4' variable, so it cannot be 
  26.             // freed. 
  27.  
  28. o4 = null; // what was the 'o2' property of the object originally in 
  29.            // 'o1' has zero references to it.  
  30.            // It can be garbage collected. 

不過(guò)這種算法往往受制于循環(huán)引用問(wèn)題,即兩個(gè)無(wú)用的對(duì)象相互引用:

  1. function f() { 
  2.   var o1 = {}; 
  3.   var o2 = {}; 
  4.   o1.p = o2; // o1 references o2 
  5.   o2.p = o1; // o2 references o1. This creates a cycle. 
  6.  
  7. f(); 

稍為復(fù)雜的算法即是所謂的標(biāo)記-清除(Mark-Sweep)算法,其根據(jù)某個(gè)對(duì)象是否可達(dá)來(lái)判斷某個(gè)對(duì)象是否可用。標(biāo)記-清除算法會(huì)從某個(gè)根元素開(kāi)始,譬如 window 對(duì)象開(kāi)始,沿著引用樹(shù)向下遍歷,標(biāo)記所有可達(dá)的對(duì)象為可用,并且清除其他未被標(biāo)記的對(duì)象。

2012 年之后,幾乎所有的主流瀏覽器都實(shí)踐了基于標(biāo)記-清除算法的垃圾回收器,并且各自也進(jìn)行有針對(duì)性地優(yōu)化。

內(nèi)存泄漏

所謂的內(nèi)存泄漏,即是指某個(gè)對(duì)象被無(wú)意間添加了某條引用,導(dǎo)致雖然實(shí)際上并不需要了,但還是能一直被遍歷可達(dá),以致其內(nèi)存始終無(wú)法回收。本部分我們簡(jiǎn)要討論下 JavaScript 中常見(jiàn)的內(nèi)存泄漏情境與處理方法。在新版本的 Chrome 中我們可以使用 Performance Monitor 來(lái)動(dòng)態(tài)監(jiān)測(cè)網(wǎng)頁(yè)性能的變化:

上圖中各項(xiàng)指標(biāo)的含義為:

  • CPU usage - 當(dāng)前站點(diǎn)的 CPU 使用量;
  • JS heap size - 應(yīng)用的內(nèi)存占用量;
  • DOM Nodes - 內(nèi)存中 DOM 節(jié)點(diǎn)數(shù)目;
  • JS event listeners- 當(dāng)前頁(yè)面上注冊(cè)的 JavaScript 時(shí)間監(jiān)聽(tīng)器數(shù)目;
  • Documents - 當(dāng)前頁(yè)面中使用的樣式或者腳本文件數(shù)目;
  • Frames - 當(dāng)前頁(yè)面上的 Frames 數(shù)目,包括 iframe 與 workers;
  • Layouts / sec - 每秒的 DOM 重布局?jǐn)?shù)目;
  • Style recalcs / sec - 瀏覽器需要重新計(jì)算樣式的頻次;

當(dāng)發(fā)現(xiàn)某個(gè)時(shí)間點(diǎn)可能存在內(nèi)存泄漏時(shí),我們可以使用 Memory 標(biāo)簽頁(yè)將此時(shí)的堆分配情況打印下來(lái):

 

 

 

全局變量

JavaScript 會(huì)將所有的為聲明的變量當(dāng)做全局變量進(jìn)行處理,即將其掛載到 global 對(duì)象上;瀏覽器中這里的 global 對(duì)象就是 window:

  1. function foo(arg) { 
  2.     bar = "some text"
  3.  
  4. // 等價(jià)于 
  5.  
  6. function foo(arg) { 
  7.     window.bar = "some text"

另一種常見(jiàn)的創(chuàng)建全局變量的方式就是誤用 this 指針:

  1. function foo() { 
  2.     this.var1 = "potential accidental global"
  3. // Foo called on its own, this points to the global object (window) 
  4. // rather than being undefined. 
  5. foo(); 

一旦某個(gè)變量被掛載到了 window 對(duì)象,就意味著它永遠(yuǎn)是可達(dá)的。為了避免這種情況,我們應(yīng)該盡可能地添加 use strict 或者進(jìn)行模塊化編碼(參考 JavaScript 模塊演化簡(jiǎn)史)。我們也可以擴(kuò)展類似于下文的掃描函數(shù),來(lái)檢測(cè)出 window 對(duì)象的非原生屬性,并加以判斷:

  1. function scan(o) { 
  2.   Object.keys(o).forEach(function(key) { 
  3.     var val = o[key]; 
  4.  
  5.     // Stop if object was created in another window 
  6.     if ( 
  7.       typeof val !== "string" && 
  8.       typeof val !== "number" && 
  9.       typeof val !== "boolean" && 
  10.       !(val instanceof Object) 
  11.     ) { 
  12.       debugger; 
  13.       console.log(key); 
  14.     } 
  15.  
  16.     // Traverse the nested object hierarchy 
  17.   }); 

定時(shí)器與閉包

我們經(jīng)常會(huì)使用 setInterval 來(lái)執(zhí)行定時(shí)任務(wù),很多的框架也提供了基于回調(diào)的異步執(zhí)行機(jī)制;這可能會(huì)導(dǎo)致回調(diào)中聲明了對(duì)于某個(gè)變量的依賴,譬如:

  1. var serverData = loadData(); 
  2. setInterval(function() { 
  3.     var renderer = document.getElementById('renderer'); 
  4.     if(renderer) { 
  5.         renderer.innerHTML = JSON.stringify(serverData); 
  6.     } 
  7. }, 5000); //This will be executed every ~5 seconds. 

定時(shí)器保有對(duì)于 serverData 變量的引用,如果我們不手動(dòng)清除定時(shí)器話,那么該變量也就會(huì)一直可達(dá),不被回收。而這里的 serverData 也是閉包形式被引入到 setInterval 的回調(diào)作用域中;閉包也是常見(jiàn)的可能導(dǎo)致內(nèi)存泄漏的元兇之一:

  1. var theThing = null
  2. var replaceThing = function () { 
  3.   var originalThing = theThing; 
  4.   var unused = function () { 
  5.     if (originalThing) // a reference to 'originalThing' 
  6.       console.log("hi"); 
  7.   }; 
  8.   theThing = { 
  9.     longStr: new Array(1000000).join('*'), 
  10.     someMethod: function () { 
  11.       console.log("message"); 
  12.     } 
  13.   }; 
  14. }; 
  15. setInterval(replaceThing, 1000); 

上述代碼中 replaceThing 會(huì)定期執(zhí)行,并且創(chuàng)建大的數(shù)組與 someMethod 閉包賦值給 theThing。someMethod 作用域是與 unused 共享的,unused 又有一個(gè)指向 originalThing 的引用。盡管 unused 并未被實(shí)際使用,theThing 的 someMethod 方法卻有可能會(huì)被外部使用,也就導(dǎo)致了 unused 始終處于可達(dá)狀態(tài)。unused 又會(huì)反向依賴于 theThing,最終導(dǎo)致大數(shù)組始終無(wú)法被清除。

DOM 引用與監(jiān)聽(tīng)器

有時(shí)候我們可能會(huì)將 DOM 元素存放到數(shù)據(jù)結(jié)構(gòu)中,譬如當(dāng)我們需要頻繁更新某個(gè)數(shù)據(jù)列表時(shí),可能會(huì)將用到的數(shù)據(jù)列表存放在 JavaScript 數(shù)組中;這也就導(dǎo)致了每個(gè) DOM 元素存在了兩個(gè)引用,分別在 DOM 樹(shù)與 JavaScript 數(shù)組中:

  1. var elements = { 
  2.     button: document.getElementById('button'), 
  3.     image: document.getElementById('image'
  4. }; 
  5. function doStuff() { 
  6.     elements.image.src = 'http://example.com/image_name.png'
  7. function removeImage() { 
  8.     // The image is a direct child of the body element. 
  9.     document.body.removeChild(document.getElementById('image')); 
  10.     // At this point, we still have a reference to #button in the 
  11.     //global elements object. In other words, the button element is 
  12.     //still in memory and cannot be collected by the GC. 

此時(shí)我們就需要將 DOM 樹(shù)與 JavaScript 數(shù)組中的引用皆刪除,才能真實(shí)地清除該對(duì)象。類似的,在老版本的瀏覽器中,如果我們清除某個(gè) DOM 元素,我們需要首先移除其監(jiān)聽(tīng)器,否則瀏覽器并不會(huì)自動(dòng)地幫我們清除該監(jiān)聽(tīng)器,或者回收該監(jiān)聽(tīng)器引用的對(duì)象:

  1. var element = document.getElementById('launch-button'); 
  2. var counter = 0; 
  3. function onClick(event) { 
  4.    counter++; 
  5.    element.innerHtml = 'text ' + counter; 
  6. element.addEventListener('click', onClick); 
  7. // Do stuff 
  8. element.removeEventListener('click', onClick); 
  9. element.parentNode.removeChild(element); 
  10. // Now when element goes out of scope, 
  11. // both element and onClick will be collected even in old browsers // that don't handle cycles well. 

現(xiàn)代瀏覽器使用的現(xiàn)代垃圾回收器則會(huì)幫我們自動(dòng)地檢測(cè)這種循環(huán)依賴,并且予以清除;jQuery 等第三方庫(kù)也會(huì)在清除元素之前首先移除其監(jiān)聽(tīng)事件。

iframe

iframe 是常見(jiàn)的界面共享方式,不過(guò)如果我們?cè)诟附缑婊蛘咦咏缑嬷刑砑恿藢?duì)于父界面某對(duì)象的引用,譬如:

  1. // 子頁(yè)面內(nèi) 
  2. window.top.innerObject = someInsideObject 
  3. window.top.document.addEventLister(‘click’, function() { … }); 
  4.  
  5. // 外部頁(yè)面 
  6.  innerObject = iframeEl.contentWindow.someInsideObject 

就有可能導(dǎo)致 iframe 卸載(移除元素)之后仍然有部分對(duì)象保留下來(lái),我們可以在移除 iframe 之前執(zhí)行強(qiáng)制的頁(yè)面重載:

  1. <a href="#">Remove</a> 
  2. <iframe src="url" /> 
  3.  
  4. $('a').click(function(){ 
  5.     $('iframe')[0].contentWindow.location.reload(); 
  6.     // 線上環(huán)境實(shí)測(cè)重置 src 效果會(huì)更好 
  7.     // $('iframe')[0].src = "javascript:false"
  8.     setTimeout(function(){ 
  9.        $('iframe').remove(); 
  10.     }, 1000); 
  11. }); 

或者手動(dòng)地執(zhí)行頁(yè)面清除操作:

  1. window.onbeforeunload = function(){ 
  2.     $(document).unbind().die();    //remove listeners on document 
  3.     $(document).find('*').unbind().die(); //remove listeners on all nodes 
  4.     //clean up cookies 
  5.     /remove items from localStorage 

Web Worker

現(xiàn)代瀏覽器中我們經(jīng)常使用 Web Worker 來(lái)運(yùn)行后臺(tái)任務(wù),不過(guò)有時(shí)候如果我們過(guò)于頻繁且不加容錯(cuò)地在主線程與工作線程之間傳遞數(shù)據(jù),可能會(huì)導(dǎo)致內(nèi)存泄漏:

  1. function send() { 
  2.  setInterval(function() {  
  3.     const data = { 
  4.      array1: get100Arrays(), 
  5.      array2: get500Arrays() 
  6.     }; 
  7.  
  8.     let json = JSON.stringify( data ); 
  9.     let arbfr = str2ab (json); 
  10.     worker.postMessage(arbfr, [arbfr]); 
  11.   }, 10); 
  12.  
  13.  
  14. function str2ab(str) { 
  15.    var buf = new ArrayBuffer(str.length*2); // 2 bytes for each char 
  16.    var bufView = new Uint16Array(buf); 
  17.    for (var i=0, strLen=str.length; i<strLen; i++) { 
  18.      bufView[i] = str.charCodeAt(i); 
  19.    } 
  20.    return buf; 
  21.  } 

在實(shí)際的代碼中我們應(yīng)該檢測(cè) Transferable Objects 是否正常工作:

  1. let ab = new ArrayBuffer(1); 
  2.  
  3. try { 
  4.    worker.postMessage(ab, [ab]); 
  5.  
  6.    if (ab.byteLength) { 
  7.       console.log('TRANSFERABLE OBJECTS are not supported in your browser!'); 
  8.    }  
  9.    else { 
  10.      console.log('USING TRANSFERABLE OBJECTS'); 
  11.    } 
  12. }  
  13. catch(e) { 
  14.   console.log('TRANSFERABLE OBJECTS are not supported in your browser!'); 
  15. }  

 【本文是51CTO專欄作者“張梓雄 ”的原創(chuàng)文章,如需轉(zhuǎn)載請(qǐng)通過(guò)51CTO與作者聯(lián)系】

戳這里,看該作者更多好文

責(zé)任編輯:武曉燕 來(lái)源: 51CTO專欄
相關(guān)推薦

2016-12-05 16:33:30

2024-03-11 08:22:40

Java內(nèi)存泄漏

2015-03-30 11:18:50

內(nèi)存管理Android

2022-09-09 15:58:29

HiveServerHive 組件Java 開(kāi)發(fā)

2012-08-13 10:14:36

IBMdW

2021-08-19 09:50:53

Java內(nèi)存泄漏

2009-06-10 22:03:40

JavaScript內(nèi)IE內(nèi)存泄漏

2018-10-25 15:24:10

ThreadLocal內(nèi)存泄漏Java

2017-03-19 16:40:28

漏洞Node.js內(nèi)存泄漏

2017-03-20 13:43:51

Node.js內(nèi)存泄漏

2023-12-18 10:45:23

內(nèi)存泄漏計(jì)算機(jī)服務(wù)器

2012-02-22 21:28:58

內(nèi)存泄漏

2010-10-25 10:10:27

ibmdwJava

2020-01-03 16:04:10

Node.js內(nèi)存泄漏

2021-06-03 21:13:03

內(nèi)存Python管理

2020-06-08 09:18:59

JavaScript開(kāi)發(fā)技術(shù)

2019-01-30 18:24:14

Java內(nèi)存泄漏編程語(yǔ)言

2021-08-09 09:54:37

內(nèi)存泄漏JS 阿里云

2021-08-05 15:28:22

JS內(nèi)存泄漏

2019-09-29 00:25:11

CC++內(nèi)存泄漏
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)