整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          javascript 的垃圾回收機制

          javascript 的垃圾回收機制

          S自帶一套內存管理引擎,負責創建對象、銷毀對象,以及垃圾回收。這期探討一下垃圾回收機制。垃圾回收機制主要是由一個叫垃圾收集器(garbage collector,簡稱GC)的后臺進程負責監控、清理對象,并及時回收空閑內存。

          和C#、Java一樣JavaScript有自動垃圾回收機制,也就是說執行環境會負責管理代碼執行過程中使用的內存,在開發過程中就無需考慮內存分配及無用內存的回收問題了。
          而 JavaScript 在創建對象(對象、字符串等)時會為它們分配內存,不再使用時會“自動”釋放內存,這個過程稱為垃圾收集。

          內存生命周期中的每一個階段:

          分配內存 —? 內存是由操作系統分配的,它允許您的程序使用它。在低級語言(例如 C 語言)中,這是一個開發人員需要自己處理的顯式執行的操作。然而,在高級語言中,系統會自動為你分配內在。
          使用內存 — 這是程序實際使用之前分配的內存,在代碼中使用分配的變量時,就會發生讀和寫操作。
          釋放內存 — 釋放所有不再使用的內存,使之成為自由內存,并可以被重新利用。與分配內存操作一樣,這一操作在低級語言中也是需要顯式地執行。

          四種常見的內存泄漏:全局變量,未清除的定時器,閉包,以及 dom 的引用

          1. 全局變量 不用 var 聲明的變量,相當于掛載到 window 對象上。如:b=1; 解決:使用嚴格的模式
          2. 被遺忘的定時器和回調函數
          3. 閉包
          4. 沒有清理的 DOM 元素引用

          S垃圾收集機制

          JS會在創建變量時自動分配內存,在不使用地時候會自動周期性的釋放內存,釋放的過程就叫 "垃圾回收"。這個機制有好的一面,當然也也有不好的一面。一方面自動分配內存減輕了開發者的負擔,開發者不用過多的去關注內存使用,但是另一方面,正是因為因為是自動回收,所以如果不清楚回收的機制,會很容易造成混亂,而混亂就很容易造成"內存泄漏".由于是自動回收,所以就存在一個 "內存是否需要被回收的" 的問題,但是這個問題的判定在程序中意味著無法通過某個算法去準確完整的解決,后面探討的回收機制只能有限地去解決一般的問題。

          回收算法

          垃圾回收對是否需要回收的問題主要依賴于對變量的判定是否可訪問,由此衍生出兩種主要的回收算法:

          • 標記清理
          • 引用計數

          標記清理

          標記清理是js最常用的回收策略,2012年后所有瀏覽器都使用了這種策略,此后的對回收策略的改進也是基于這個策略的改進。其策略是:

          1. 變量進入上下文,也可理解為作用域,會加上標記,證明其存在于該上下文;
          2. 將所有在上下文中的變量以及上下文中被訪問引用的變量標記去掉,表明這些變量活躍有用;
          3. 在此之后再被加上標記的變量標記為準備刪除的變量,因為上下文中的變量已經無法訪問它們;
          4. 執行內存清理,銷毀帶標記的所有非活躍值并回收之前被占用的內存;

          局限

          • 由于是從根對象(全局對象)開始查找,對于那些無法從根對象查詢到的對象都將被清除
          • 回收后會形成內存碎片,影響后面申請大的連續內存空間

          引用計數

          引用計數策略相對而言不常用,因為弊端較多。其思路是對每個值記錄它被引用的次數,通過最后對次數的判斷(引用數為0)來決定是否保留,具體的規則有

          • 聲明一個變量,賦予它一個引用值時,計數+1;
          • 同一個值被賦予另外一個變量時,引用+1;
          • 保存對該值引用的變量被其他值覆蓋,引用-1;
          • 引用為0,回收內存;

          局限

          最重要的問題就是,循環引用 的問題

          function refProblem () {
          	let a=new Object();
          	let b=new Object();
          	a.c=b;
          	b.c=a;  //互相引用
          }
          復制代碼

          根據之前提到的規則,兩個都互相引用了,引用計數不為0,所以兩個變量都無法回收。如果頻繁地調用改函數,則會造成很嚴重的內存泄漏。

          Nodejs V8回收機制

          V8的回收機制基于 分代回收機制 ,將內存分為新生代(young generation)和老生代(tenured generation),新生代為存活時間較短的對象,老生代為存活時間較長或者常駐內存的變量。

          V8堆的構成

          V8將堆分成了幾個不同的區域

          • 新生代(New Space/Young Generation): 大多數新生對象被分配到這,分為兩塊空間,整體占據小塊空間,垃圾回收的頻率較高,采用的回收算法為 Scavenge 算法
          • 老生代(Old Space/Old Generation):大多數在新生區存活一段時間后的對象會轉移至此,采用的回收算法為 標記清除 & 整理(Mark-Sweep & Mark-Compact,Major GC) 算法,內部再細分為兩個空間 指針空間(Old pointer space): 存儲的對象含有指向其他對象的指針 數據空間(Old data space):存儲的對象僅包含數據,無指向其他對象的指針
          • 大對象空間(Large Object Space):存放超過其他空間(Space)限制的大對象,垃圾回收器從不移動此空間中的對象
          • 代碼空間(Code Space): 代碼對象,用于存放代碼段,是唯一擁有執行權限的內存空間,需要注意的是如果代碼對象太大而被移入大對象空間,這個代碼對象在大對象空間內也是擁有執行權限的,但不能因此說大對象空間也有執行權限
          • Cell空間、屬性空間、Map空間 (Cell ,Property,Map Space): 這些區域存放Cell、屬性Cell和Map,每個空間因為都是存放相同大小的元素,因此內存結構很簡單。

          Scavenge 算法

          Scavenge 算法是新生代空間中的主要算法,該算法由 C.J. Cheney 在 1970 年在論文 A nonrecursive list compacting algorithm 提出。 Scavenge 主要采用了 Cheney算法,Cheney算法新生代空間的堆內存分為2塊同樣大小的空間,稱為 Semi space,處于使用狀態的成為 From空間 ,閑置的稱為 To 空間。垃圾回收過程如下:

          • 檢查From空間,如果From空間被分配滿了,則執行Scavenge算法進行垃圾回收
          • 如果未分配滿,則檢查From空間的是否有存活對象,如果無存活對象,則直接釋放未存活對象的空間
          • 如果存活,將檢查對象是否符合晉升條件,如果符合晉升條件,則移入老生代空間,否則將對象復制進To空間
          • 完成復制后將From和To空間角色互換,然后再從第一步開始執行

          晉升條件

          1. 經歷過一次Scavenge 算法篩選;
          2. To空間內存使用超過25%;

          標記清除 & 整理(Mark-Sweep & Mark-Compact,Major GC)算法

          之前說過,標記清除策略會產生內存碎片,從而影響內存的使用,這里 標記整理算法(Mark-Compact)的出現就能很好的解決這個問題。標記整理算法是在 標記清除(Mark-Sweep )的基礎上演變而來的,整理算法會將活躍的對象往邊界移動,完成移動后,再清除不活躍的對象。

          由于需要移動移動對象,所以在處理速度上,會慢于Mark-Sweep。

          全停頓(Stop The World )

          為了避免應用邏輯與垃圾回收器看到的邏輯不一樣,垃圾回收器在執行回收時會停止應用邏輯,執行完回收任務后,再繼續執行應用邏輯。這種行為就是 全停頓,停頓的時間取決于不同引擎執行一次垃圾回收的時間。這種停頓對新生代空間的影響較小,但對老生代空間可能會造成停頓的現象。

          增量標記(Incremental Marking)

          為了解決全停頓的現象,2011年V8推出了增量標記。V8將標記過程分為一個個的子標記過程,同時讓垃圾回收標記和JS應用邏輯交替進行,直至標記完成。

          內存泄漏

          內存泄漏的問題難以察覺,在函數被調用很多次的情況下,內存泄漏可能是個大問題。常見的內存泄漏主要有下面幾個場景。

          意外聲明全局變量

          function hello (){
          	name='tom'
          }
          hello();
          復制代碼

          未聲明的對象會被綁定在全局對象上,就算不被使用了,也不會被回收,所以寫代碼的時候,一定要記得聲明變量。

          定時器

          let name='Tom';
          setInterval(()=> {
            console.log(name);
          }, 100);
          復制代碼

          定時器的回調通過閉包引用了外部變量,如果定時器不清除,name會一直占用著內存,所以用定時器的時候最好明白自己需要哪些變量,檢查定時器內部的變量,另外如果不用定時器了,記得及時清除定時器。

          閉包

          let out=function() {
            let name='Tom';
            return function () {
              console.log(name);
            }
          }
          復制代碼

          由于閉包會常駐內存,在這個例子中,如果out一直存在,name就一直不會被清理,如果name值很大的時候,就會造成比較嚴重的內存泄漏。所以一定要慎重使用閉包。

          事件監聽

          mounted() {
          window.addEventListener("resize",  ()=> {
          	//do something
          });
          }
          復制代碼

          在頁面初始化時綁定了事件監聽,但是在頁面離開的時候未清除事監聽,就會導致內存泄漏。

          最后

          文章為參考資料總結的筆記文章,我最近在重學js,會將復習總結的文章記錄在Github,有想一起復習的小伙伴可私信一起參與復習總結!

          過去,我們瀏覽靜態網站時無須過多關注內存管理,因為加載新頁面時,之前的頁面信息會從內存中刪除。 然而,隨著單頁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 內存泄漏問題可能是一項非常艱巨的任務。了解內存泄漏的常見原因以防止它們發生是非常重要的。在涉及內存和性能方面,最重要的是用戶體驗,這才是最重要的。


          主站蜘蛛池模板: 99国产精品一区二区| 人妻体内射精一区二区三区| 国产福利电影一区二区三区久久老子无码午夜伦不 | 内射白浆一区二区在线观看| 在线精品日韩一区二区三区| 成人区人妻精品一区二区三区 | 精品一区狼人国产在线| 麻豆精品一区二区综合av| 亚洲片国产一区一级在线观看| 亚洲AV色香蕉一区二区| 久久伊人精品一区二区三区| 美女视频一区三区网站在线观看| 亚洲一区二区三区久久久久| 天堂一区二区三区在线观看| 国产AⅤ精品一区二区三区久久| 国产自产V一区二区三区C| 国产精品香蕉在线一区| 日本一区二区三区精品国产| 久久无码一区二区三区少妇| 亚洲美女一区二区三区| 久久久久女教师免费一区| 亚洲毛片αv无线播放一区| 黑巨人与欧美精品一区| 日韩一区二区电影| 精品一区二区三区四区在线| 久久se精品一区二区国产| 美女免费视频一区二区| 3d动漫精品一区视频在线观看| 成人国产精品一区二区网站| 亚洲色无码专区一区| 久久精品国产一区二区电影| 另类国产精品一区二区| 91福利视频一区| 狠狠色婷婷久久一区二区| 国产日韩综合一区二区性色AV| 人妻体体内射精一区二区| 国产午夜福利精品一区二区三区| 日本香蕉一区二区三区| 精品视频一区二区三区四区| 精品无码人妻一区二区三区| 人妻少妇久久中文字幕一区二区 |