天天看點

JavaScript 垃圾回收機制

随着軟體開發行業的不斷發展,性能優化已經是一個不可避免的話題,那什麼樣的行為才能算得上是性能優化呢?

本質上任何一種可以提高運作效率,降低運作開銷的行為,都可以看做是一種優化操作。

這也就意味着,在軟體開放行業必然存在着很多值得優化的地方,特别是在前端開發過程中,性能優化可以認為是無處不在的。例如請求資源時所用到的網絡,以及資料的傳輸方式,再或者開發過程中所使用到的架構等都可以進行優化。

本章探索的是<code>javascript</code>語言本身的優化,是從認知記憶體空間的使用到垃圾回收的方式,進而可以編寫出高效的<code>javascript</code>代碼。

随着近些年硬體技術的不斷發展,進階程式設計語言中都自帶了<code>gc</code>機制,讓開發者在不需要特别注意記憶體空間使用的情況下,也能夠正常的去完成相應的功能開發。為什麼還要重提記憶體管理呢,下面就通過一段極簡單的代碼來進行說明。

首先定義一個普通的函數<code>fn</code>,然後在函數體内聲明一個數組,接着給數組指派,需要注意的是在指派的時候刻意選擇了一個比較大的數字來作為下标。這樣做的目的就是為了目前函數在調用的時候可以向記憶體盡可能多的申請一片比較大的空間。

在執行這個函數的過程中從文法上是不存在任何問題的,不過用相應的性能監控工具對記憶體進行監控的時候會發現,記憶體變化是持續程線性升高的,并且在這個過程當中沒有回落。這代表着記憶體洩露。如果在寫代碼的時候不夠了解記憶體管理的機制就會編寫出一些不容易察覺到的記憶體問題型代碼。

這種代碼多了以後程式帶來的可能就是一些意想不到的<code>bug</code>,是以掌握記憶體的管理是非常有必要的。是以接下來就去看一下,什麼是記憶體管理。

從這個詞語本身來說,記憶體其實就是由可讀寫的單元組成,他辨別一片可操作的空間。而管理在這裡刻意強調的是由人主動去操作這片空間的申請、使用和釋放,即使借助了一些<code>api</code>,但終歸可以自主的來做這個事。是以記憶體管理就認為是,開發者可以主動的向記憶體申請空間,使用空間,并且釋放空間。是以這個流程就顯得非常簡單了,一共三步,申請,使用和釋放。

回到<code>javascript</code>中,其實和其他的語言一樣,<code>javascript</code>中也是分三步來執行這個過程,但是由于<code>ecmascript</code>中并沒有提供相應的操作<code>api</code>。是以<code>javascript</code>不能像<code>c</code>或者<code>c++</code>那樣,由開發者主動調用相應的api來完成記憶體空間的管理。

不過即使如此也不能影響我們通過<code>javascript</code>腳本來示範一個空間的生命周期是怎樣完成的。過程很簡單首先要去申請空間,第二個使用空間,第三個釋放空間。

在<code>javascript</code>中并沒有直接提供相應的<code>api</code>,是以隻能在<code>javascript</code>執行引擎遇到變量定義語句的時候自動配置設定一個相應的空間。這裡先定義一個變量<code>obj</code>,然後把它指向一個空對象。對它的使用其實就是一個讀寫的操作,直接往這個對象裡面寫入一個具體的資料就可以了比如寫上一個<code>yd</code>。最後可以對它進行釋放,同樣的<code>javascript</code>裡面并沒有相應的釋放<code>api</code>,是以這裡可以采用一種間接的方式,比如直接把他設定為<code>null</code>。

這個時候就相當于按照記憶體管理的一個流程在<code>javascript</code>當中實作了記憶體管理。後期在這樣性能監控工具當中看一下記憶體走勢就可以了。

首先在<code>javascript</code>中什麼樣的内容會被當中是垃圾看待。在後續的<code>gc</code>算法當中,也會存在的垃圾的概念,兩者其實是完全一樣的。是以在這裡統一說明。

<code>javascript</code>中的記憶體管理是自動的。每建立一個對象、數組或者函數的時候,就會自動的配置設定相應的記憶體空間。等到後續程式代碼在執行的過程中如果通過一些引用關系無法再找到某些對象的時候那麼這些對象就會被看作是垃圾。再或者說這些對象其實是已經存在的,但是由于代碼中一些不合适的文法或者說結構性的錯誤,沒有辦法再去找到這些對象,那麼這種對象也會被稱之是垃圾。

發現垃圾之後<code>javascript</code>執行引擎就會出來工作,把垃圾所占據的對象空間進行回收,這個過程就是所謂的垃圾回收。在這裡用到了幾個小的概念,第一是引用,第二是從根上通路,這個操作在後續的<code>gc</code>裡面也會被頻繁的提到。

在這裡再提一個名詞叫可達對象,首先在<code>javascript</code>中可達對象了解起來非常的容易,就是能通路到的對象。至于通路,可以是通過具體的引用也可以在目前的上下文中通過作用域鍊。隻要能找得到,就認為是可達的。不過這裡邊會有一個小的标準限制就是一定要是從根上出發找得到才認為是可達的。是以又要去讨論一下什麼是根,在<code>javascript</code>裡面可以認為目前的全局變量對象就是根,也就是所謂的全局執行上下文。

簡單總結一下就是<code>javascript</code>中的垃圾回收其實就是找到垃圾,然後讓<code>javascript</code>的執行引擎來進行一個空間的釋放和回收。

這裡用到了引用和可達對象,接下來就盡可能的通過代碼的方式來看一下在<code>javascript</code>中的引用與可達是怎麼展現的。

首先定義一個變量,為了後續可以修改值采用<code>let</code>關鍵字定一個<code>obj</code>讓他指向一個對象,為了友善描述給他起一個名字叫<code>xiaoming</code>。

寫完這行代碼以後其實就相當于是這個空間被目前的<code>obj</code>對象引用了,這裡就出現了引用。站在全局執行上下文下<code>obj</code>是可以從根上來被找到的,也就是說這個<code>obj</code>是一個可達的,這也就間接地意味着目前<code>xiaoming</code>的對象空間是可達的。

接着再重新再去定義一個變量,比如<code>ali</code>讓他等于<code>obj</code>,可以認為小明的空間又多了一次引用。這裡存在着一個引用數值變化的,這個概念在後續的引用計數算法中是會用到的。

再來做一個事情,直接找到<code>obj</code>然後把它重新指派為<code>null</code>。這個操作做完之後就可以思考一下了。本身小明這對象空間是有兩個引用的。随着<code>null</code>指派代碼的執行,<code>obj</code>到小明空間的引用就相當于是被切斷了。現在小明對象是否還是可達呢?必然是的。因為<code>ali</code>還在引用着這樣的一個對象空間,是以說他依然是一個可達對象。

這就是一個引用的主要說明,順帶也看到了一個可達。

接下來再舉一個示例,說明一下目前<code>javascript</code>中的可達操作,不過這裡面需要提前說明一下。

為了友善後面<code>gc</code>中的标記清除算法,是以這個執行個體會稍微麻煩一些。

首先定義一個函數名字叫<code>objgroup</code>,設定兩個形參<code>obj1</code>和<code>obj2</code>,讓<code>obj1</code>通過一個屬性指向<code>obj2</code>,緊接着再讓<code>obj2</code>也通過一個屬性去指向<code>obj1</code>。再通過return關鍵字直接傳回一個對象,<code>obj1</code>通過<code>o1</code>進行傳回,再設定一個<code>o2</code>讓他找到<code>obj2</code>。完成之後在外部調用這個函數,設定一個變量進行接收,<code>obj</code>等于<code>objgroup</code>調用的結果。傳兩個參數分别是兩個對象<code>obj1</code>和<code>obj2</code>。

運作可以發現得到了一個對象。對象裡面分别有<code>obj1</code>和<code>obj2</code>,而<code>obj1</code>和<code>obj2</code>他們内部又各自通過一個屬性指向了彼此。

分析一下代碼,首先從全局的根出發,是可以找到一個可達的對象<code>obj</code>,他通過一個函數調用之後指向了一個記憶體空間,他的裡面就是上面看到的<code>o1</code>和<code>o2</code>。然後在<code>o1</code>和<code>o2</code>的裡面剛好又通過相應的屬性指向了一個<code>obj1</code>空間和<code>obj2</code>空間。<code>obj1</code>和<code>obj2</code>之間又通過<code>next</code>和<code>prev</code>做了一個互相的一個引用,是以代碼裡面所出現的對象都可以從根上來進行查找。不論找起來是多麼的麻煩,總之都能夠找到,繼續往下來再來做一些分析。

如果通過<code>delete</code>語句把<code>obj</code>身上<code>o1</code>的引用以及<code>obj2</code>對<code>obj1</code>的引用直接<code>delete</code>掉。此時此刻就說明了現在是沒有辦法直接通過什麼樣的方式來找到<code>obj1</code>對象空間,那麼在這裡他就會被認為是一個垃圾的操作。最後<code>javascript</code>引擎會去找到他,然後對其進行回收。

這裡說的比較麻煩,簡單來說就是目前在編寫代碼的時候會存在的一些對象引用的關系,可以從根的下邊進行查找,按照引用關系終究能找到一些對象。但是如果找到這些對象路徑被破壞掉或者說被回收了,那麼這個時候是沒有辦法再找到他,就會把他視作是垃圾,最後就可以讓垃圾回收機制把他回收掉。

<code>gc</code>可以了解為垃圾回收機制的簡寫,<code>gc</code>工作的時候可以找到記憶體當中的一些垃圾對象,然後對空間進行釋放還可以進行回收,友善後續的代碼繼續使用這部分記憶體空間。至于什麼樣的東西在<code>gc</code>裡邊可以被當做垃圾看待,在這裡給出兩種小的标準。

第一種從程式需求的角度來考慮,如果說某一個資料在使用完成之後上下文裡邊不再需要去用到他了就可以把他當做是垃圾來看待。

例如下面代碼中的<code>name</code>,當函數調用完成以後已經不再需要使用<code>name</code>了,是以從需求的角度考慮,他應該被當做垃圾進行回收。至于到底有沒有被回收現在先不做讨論。

第二種情況是目前程式運作過程中,變量能否被引用到的角度去考慮,例如下方代碼依然是在函數内部放置一個<code>name</code>,不過這次加上了一個聲明變量的關鍵字。有了這個關鍵字以後,當函數調用結束後,在外部的空間中就不能再通路到這個<code>name</code>了。是以找不到他的時候,其實也可以算作是一種垃圾。

說完了<code>gc</code>再來說一下<code>gc</code>算法。我們已經知道<code>gc</code>其實就是一種機制,它裡面的垃圾回收器可以完成具體的回收工作,而工作的内容本質就是查找垃圾釋放空間并且回收空間。在這個過程中就會有幾個行為:查找空間,釋放空間,回收空間。這樣一系列的過程裡面必然有不同的方式,<code>gc</code>的算法可以了解為垃圾回收器在工作過程中所遵循的一些規則,好比一些數學計算公式。

常見的<code>gc</code>算法有引用計數,可以通過一個數字來判斷目前的這個對象是不是一個垃圾。标記清除,可以在<code>gc</code>工作的時候給那些活動對象添加标記,以此判斷它是否是垃圾。标記整理,與标記清除很類似,隻不過在後續回收過程中,可以做出一些不一樣的事情。分代回收,<code>v8</code>中用到的回收機制。

引用計數算法的核心思想是在内部通過引用計數器來維護目前對象的引用數,進而判斷該對象的引用數值是否為<code>0</code>來決定他是不是一個垃圾對象。當這個數值為<code>0</code>的時候<code>gc</code>就開始工作,将其所在的對象空間進行回收和釋放。

引用計數器的存在導緻了引用計數在執行效率上可能與其它的<code>gc</code>算法有所差别。

引用的數值發生改變是指某一個對象的引用關系發生改變的時候,這時引用計數器會主動的修改目前這個對象所對應的引用數值。例如代碼裡有一個對象空間,有一個變量名指向他,這個時候數值<code>+1</code>,如果又多了一個對象還指向他那他再<code>+1</code>,如果是減小的情況就<code>-1</code>。當引用數字為<code>0</code>的時候,<code>gc</code>就會立即工作,将目前的對象空間進行回收。

通過簡單的代碼來說明一下引用關系發生改變的情況。首先定義幾個簡單的user變量,把他作為一個普通的對象,再定義一個數組變量,在數組的裡存放幾個對象中的<code>age</code>屬性值。再定義一個函數,在函數體内定義幾個變量數值<code>num1</code>和<code>num2</code>,注意這裡是沒有<code>const</code>的。在外層調用函數。

首先從全局的角度考慮會發現<code>window</code>的下邊是可以直接找到<code>user1</code>,<code>user2</code>,<code>user3</code>以及<code>namelist</code>,同時在<code>fn</code>函數裡面定義的<code>num1</code>和<code>num2</code>由于沒有設定關鍵字,是以同樣是被挂載在<code>window</code>對象下的。這時候對這些變量而言他們的引用計數肯定都不是<code>0</code>。

接着在函數内直接把<code>num1</code>和<code>num2</code>加上關鍵字的聲明,就意味着目前這個<code>num1</code>和<code>num2</code>隻能在作用域内起效果。是以,一旦函數調用執行結束之後,從外部全局的地方出發就不能找到<code>num1</code>和<code>num2</code>了,這個時候<code>num1</code>和<code>num2</code>身上的引用計數就會回到<code>0</code>。此時此刻隻要是<code>0</code>的情況下,<code>gc</code>就會立即開始工作,将<code>num1</code>和<code>num2</code>當做垃圾進行回收。也就是說這個時候函數執行完成以後内部所在的記憶體空間就會被回收掉。

那麼緊接着再來看一下其他的比如說<code>user1</code>,<code>user2</code>,<code>user3</code>以及<code>namelist</code>。由于<code>userlist</code>,裡面剛好都指向了上述三個對象空間,是以腳本即使執行完一遍以後<code>user1</code>,<code>user2</code>,<code>user3</code>他裡邊的空間都還被人引用着。是以此時的引用計數器都不是<code>0</code>,也就不會被當做垃圾進行回收。這就是引用計數算法實作過程中所遵循的基本原理。簡單的總結就是靠着目前對象身上的引用計數的數值來判斷是否為<code>0</code>,進而決定他是不是一個垃圾對象。

引用計數算法的優點總結出兩條。

第一是引用計數規則會在發現垃圾的時候立即進行回收,因為他可以根據目前引用數是否為<code>0</code>來決定對象是不是垃圾。如果是就可以立即進行釋放。

第二就是引用計數算法可以最大限度的減少程式的暫停,應用程式在執行的過程當中,必然會對記憶體進行消耗。目前執行平台的記憶體肯定是有上限的,是以記憶體肯定有占滿的時候。由于引用計數算法是時刻監控着記憶體引用值為<code>0</code>的對象,舉一個極端的情況就是,當他發現記憶體即将爆滿的時候,引用計數就會立馬找到那些數值為<code>0</code>的對象空間對其進行釋放。這樣就保證了目前記憶體是不會有占滿的時候,也就是所謂的減少程式暫停的說法。

引用計數的缺點同樣給出兩條說明。

第一個就是引用計數算法沒有辦法将那些循環引用的對象進行空間回收的。通過代碼片段示範一下,什麼叫做循環引用的對象。

定義一個普通的函數<code>fn</code>在函數體的内部定義兩個變量,對象<code>obj1</code>和<code>obj2</code>,讓<code>obj1</code>下面有一個<code>name</code>屬性然後指向<code>obj2</code>,讓<code>obj2</code>有一個屬性指向<code>obj1</code>。在函數最後的地方<code>return</code>傳回一個普通字元,當然這并沒有什麼實際的意義隻是做一個測試。接着在最外層調用一下函數。

那麼接下來分析還是一樣的道理,函數在執行結束以後,他内部所在的空間肯定需要有涉及到空間回收的情況。比如說<code>obj1</code>和<code>obj2</code>,因為在全局的地方其實已經不再去指向他了,是以這個時候他的引用計數應該是為<code>0</code>的。

但是這個時候會有一個問題,在裡邊會發現,當<code>gc</code>想要去把<code>obj1</code>删除的時候,會發現<code>obj2</code>有一個屬性是指向<code>obj1</code>的。換句話講就是雖然按照之前的規則,全局的作用域下找不到<code>obj1</code>和<code>obj2</code>了,但是由于他們兩者之間在作用域範圍内明顯還有着一個互相的指引關系。這種情況下他們身上的引用計數器數值并不是<code>0</code>,<code>gc</code>就沒有辦法将這兩個空間進行回收。也就造成了記憶體空間的浪費,這就是所謂的對象之間的循環引用。這也是引用計數算法所面臨到的一個問題。

第二個問題就是引用計數算法所消耗的時間會更大一些,因為目前的引用計數,需要維護一個數值的變化,在這種情況下要時刻的監控着目前對象的引用數值是否需要修改。對象數值的修改需要消耗時間,如果說記憶體裡邊有更多的對象需要修改,時間就會顯得很大。是以相對于其他的<code>gc</code>算法會覺得引用計數算法的時間開銷會更大一些。

相比引用計數而言标記清除算法的原理更加簡單,而且還能解決一些相應的問題。在<code>v8</code>中被大量的使用到。

标記清除算法的核心思想就是将整個垃圾回收操作分成兩個階段,第一個階段周遊所有對象然後找到活動對象進行标記。活動就像跟之前提到的可達對象是一個道理,第二個階段仍然會周遊所有的對象,把沒有标記的對象進行清除。需要注意的是在第二個階段當中也會把第一個階段設定的标記抹掉,便于<code>gc</code>下次能夠正常工作。這樣一來就可以通過兩次周遊行為把目前垃圾空間進行回收,最終再交給相應的空閑清單進行維護,後續的程式代碼就可以使用了。

這就是标記清除算法的基本原理,其實就是兩個操作,第一是标記,第二是清除。這裡舉例說明。

首先在全局<code>global</code>聲明<code>a</code>,<code>b</code>,<code>c</code>三個可達對象,找到這三個可達對象之後,會發現他的下邊還會有一些子引用,這也就是标記清除算法強大的地方。如果發現他的下邊有孩子,甚至孩子下邊還有孩子,這個時候他會用遞歸的方式繼續尋找那些可達的對象,比如說<code>d</code>,<code>e</code>分别是<code>a</code>和<code>c</code>的子引用,也會被标記成可達的。

這裡還有兩個變量<code>a1</code>和<code>b1</code>,他們在函數内的局部作用域,局部作用域執行完成以後這個空間就被回收了。是以從<code>global</code>鍊條下是找不到<code>a1</code>和<code>b1</code>的,這時候<code>gc</code>機制就會認為他是一個垃圾對象,沒有給他做标記,最終在<code>gc</code>工作的時候就會把他們回收掉。

這就是标記清除所謂的标記階段和清除階段,以及這兩個階段分别要做的事情。簡單的整理可以分成兩個步驟。在第一階段要找到所有可達對象,如果涉及到引用的層次關系,會遞歸進行查找。找完以後會将這些可達對象進行标記。标記完成以後進行第二階段開始做清除,找到那些沒有做标記的對象,同時還将第一次所做的标記清除掉。這樣就完成了一次垃圾回收,同時還要留意,最終會把回收的空間直接放在一個叫做空閑清單上面。友善後續的程式可以直接在這申請空間使用。

相對比引用計數而言标記清除具有一個最大的優點,就是可以解決對象循環引用的回收操作。在寫代碼的時候可能會在全局定義<code>a</code>、<code>b</code>、<code>c</code>這樣的可達對象,也會有一些函數的局部作用域,比如在函數内定義了<code>a1</code>和<code>b1</code>,而且讓他們互相引用。

函數的調用在結束之後必然要去釋放他們内部的空間,在這種情況下一旦當某一個函數調用結束之後他局部空間中的變量就失去了與全局<code>global</code>作用域上的連結。這個時候<code>a1</code>和<code>b1</code>在<code>global</code>根下邊就沒辦法通路到了,就是一個不可達的對象。不可達對象在做标記階段的時候不能夠完成标記,在第二個階段回收的時候就直接進行釋放了。

這是标記清除可以做到的,但是在引用計數裡面,函數調用結束同時也沒有辦法在全局進行通路。可是由于目前判斷的标準是引用數字是否為<code>0</code>,在這種情況下,就沒有辦法釋放<code>a1</code>和<code>b1</code>空間,這就是标記清除算法的最大優點,當然這是相對于引用計數算法而言的。

同時标記清除算法也會有一些缺點。比如模拟一個記憶體的存儲情況,從根進行查找,在下方有一個可達對象<code>a</code>對象, 左右兩側有一個從跟下無法直接查找的一個區域,<code>b</code>和<code>c</code>。這種情況下在進行第二輪清除操作的時候,就會直接将b和c所對應的空間進行回收。然後把釋放的空間添加到空閑清單上,後續的程式可以直接從空閑清單上申請相應的一個空間位址,進行使用。在這種情況下就會有一個問題。

比如我們認為,任何一個空間都會有兩部分組成,一個用來存儲空間一些元資訊比如他的大小,位址,稱之為頭。還有一部分是專門用于存放資料的叫做域,<code>b</code>、<code>c</code>空間認為<code>b</code>對象有<code>2</code>個字的空間,<code>c</code>對象有<code>1</code>個字的空間。這種情況下,雖然對他進行了回收,加起來好像是釋放了<code>3</code>個字的空間,但是由于它們中間被a對象去分割着。是以在釋放完成之後其實還是分散的也就是位址不連續。

這點很重要,後續想申請的空間位址大小剛好<code>1.5</code>個字。這種情況下,如果直接找到b釋放的空間會發現是多了的,因為還多了<code>0.5</code>個,如果直接去找<code>c</code>釋放的空間又發現不夠,因為是<code>1</code>個。是以這就帶來了标記清除算法中最大的問題,空間的碎片化。

所謂的空間碎片化,就是由于目前所回收的垃圾對象在位址上本身是不連續的,由于這種不連續進而造成了回收之後分散在各個角落,後續要想去使用的時候,如果新的生成空間剛好與他們的大小比對,就能直接用。一旦是多了或是少了就不太适合使用了。

這就是标記清除算法優點和缺點,簡單的整理一下就是優點是可以解決循環引用不能回收的問題,缺點是說會産生空間碎片化的問題,不能讓空間得到最大化的使用。

在<code>v8</code>中标記整理算法會被頻繁的使用到,下面來看一下是如何實作的。

首先認為标記整理算法是标記清除的增強操作,他們在第一個階段是完全一樣的,都會去周遊所有的對象,然後将可達活動對象進行标記。第二階段清除時,标記清除是直接将沒有标記的垃圾對象做空間回收,标記整理則會在清除之前先執行整理操作,移動對象的位置,讓他們能夠在位址上産生連續。

假設回收之前有很多的活動對象和非活動對象,以及一些空閑的空間,當執行标記操作的時候,會把所有的活動對象進行标記,緊接着會進行整理的操作。整理其實就是位置上的改變,會把活動對象先進行移動,在位址上變得連續。緊接着會将活動對象右側的範圍進行整體的回收,這相對标記清除算法來看好處是顯而易見的。

因為在記憶體裡不會大批量出現分散的小空間,進而回收到的空間都基本上都是連續的。這在後續的使用過程中,就可以盡可能的最大化利用所釋放出來的空間。這個過程就是标記整理算法,會配合着标記清除,在<code>v8</code>引擎中實作頻繁的gc操作。

首先是引用計數,他的可以及時回收垃圾對象,隻要數值<code>0</code>的就會立即讓<code>gc</code>找到這片空間進行回收和釋放。正是由于這個特點的存在,引用計數可以最大限度的減少程式的卡頓,因為隻要這個空間即将被占滿的時候,垃圾回收器就會進行工作,将記憶體進行釋放,讓記憶體空間總有一些可用的地方。

标記清除不能立即回收垃圾對象,而且他去清除的時候目前的程式其實是停止工作的。即便第一階段發現了垃圾,也要等到第二階段清除的時候才會回收掉。

标記整理也不能立即回收垃圾對象。

衆所周知<code>v8</code>引擎是目前市面上最主流的<code>javascript</code>執行引擎,日常所使用的<code>chrome</code>浏覽器以及<code>nodejavascript</code>平台都在采用這個引擎去執行<code>javascript</code>代碼。對于這兩個平台來看<code>javascript</code>之是以能高效的運轉,也正是因為<code>v8</code>的存在。<code>v8</code>的速度之是以快,除了有一套優秀的記憶體管理機制之外,還有一個特點就是采用及時編譯。

之前很多的<code>javascript</code>引擎都需要将源代碼轉成位元組碼才能執行,而<code>v8</code>可以将源碼翻譯成直接執行的機器碼。是以執行速度是非常快的。

<code>v8</code>還有一個比較大的特點就是他的記憶體是有上限的,在64位作業系統下,上限是不超過<code>1.5g</code>,在<code>32</code>位的作業系統中數值是不超過<code>800m</code>。

為什麼<code>v8</code>要采用這樣的做法呢,原因基本上可以從兩方面進行說明。

第一<code>v8</code>本身就是為了浏覽器制造的,是以現有的記憶體大小足夠使用了。再有<code>v8</code>内部所實作的垃圾回收機制也決定了他采用這樣一個設定是非常合理的。因為官方做過一個測試,當垃圾記憶體達到<code>1.5</code>個<code>g</code>的時候,<code>v8</code>去采用增量标記的算法進行垃圾回收隻需要消耗<code>50ms</code>,采用非增量标記的形式回收則需要<code>1s</code>。從使用者體驗的角度來說<code>1s</code>已經算是很長的時間了,是以就以<code>1.5g</code>為界了。

在程式的使用過程中會用到很多的資料,資料又可以分為原始的資料和對象類型的資料。基礎的原始資料都是由程式的語言自身來進行控制的。是以這裡所提到的回收主要還是指的是存活在堆區裡的對象資料,是以這個過程是離不開記憶體操作的。

<code>v8</code>采用的是分代回收的思想,把記憶體空間按照一定的規則分成兩類,新生代存儲區和老生代存儲區。有了分類後,就會針對不同代采用最高效的<code>gc</code>算法,進而對不同的對象進行回收操作。這也就意味着<code>v8</code>回收會使用到很多的<code>gc</code>算法。

首先,分代回收算法肯定是要用到的,因為他必須要做分代。緊接着會用到空間的複制算法。除此以外還會用到标記清除和标記整理。最後為了去提高效率,又用到了标記增量。

首先是要說明一下<code>v8</code>内部的記憶體配置設定。因為他是基于分代的垃圾回收思想,是以在<code>v8</code>内部是把記憶體空間分成了兩個部分,可以了解成一個存儲區域被分成了左右兩個區域。左側的空間是專門用來存放新生代對象,右側專門存放老生代對象。新生代對象空間是有一定設定的,在64位作業系統中大小是<code>32m</code>,在<code>32</code>位的作業系統中是<code>16m</code>。

新生代對象其實指的就是存活時間較短的。比如說目前代碼内有個局部的作用域,作用域中的變量在執行完成過後就要被回收,在其他地方比如全局也有一個變量,而全局的變量肯定要等到程式退出之後才會被回收。是以相對來說新生代就指的是那些存活時間比較短的那樣一些變量對象。

針對新生代對象回收所采用到的算法主要是複制算法和标記整理算法,首先會将左側一部分小空間也分成兩個部分,叫做<code>from</code>和<code>to</code>,而且這兩個部分的大小是相等的,将from空間稱為使用狀态,<code>to</code>空間叫做空閑狀态。有了這樣兩個空間之後代碼執行的時候如果需要申請空間首先會将所有的變量對象都配置設定至<code>from</code>空間。也就是說在這個過程中<code>to</code>是空閑的,一旦<code>from</code>空間應用到一定的程度之後,就要觸發<code>gc</code>操作。這個時候就會采用标記整理對<code>from</code>空間進行标記,找到活動對象,然後使用整理操作把他們的位置變得連續,便于後續不會産生碎片化空間。

做完這些操作以後,将活動對象拷貝至<code>to</code>空間,也就意味着<code>from</code>空間中的活動對象有了一個備份,這時候就可以考慮回收了。回收也非常簡單,隻需要把<code>from</code>空間完全釋放就可以了,這個過程也就完成了新生代對象的回收操作。

總結一下就是新生代對象的存儲區域被一分為二,而且是兩個等大的,在這兩個等大的空間中,起名<code>from</code>和<code>to</code>,目前使用的是<code>from</code>,所有的對象聲明都會放在這個空間内。觸發<code>gc</code>機制的時候會把活動對象全部找到進行整理,拷貝到<code>to</code>空間中。拷貝完成以後我們讓<code>from</code>和<code>to</code>進行空間交換(也就是名字的交換),原來的<code>to</code>就變成了<code>from</code>,原來的<code>from</code>就變成了<code>to</code>。這樣就算完成了空間的釋放和回收。

接下來針對過程的細節進行說明。首先在這個過程中肯定會想到的是,如果在拷貝時發現某一個變量對象所指的空間,在目前的老生代對象裡面也會出現。這個時候就會出現一個所謂的叫晉升的操作,就是将新生代的對象,移動至老生代進行存儲。

至于什麼時候觸發晉升操作一般有兩個判斷标準,第一個是如果新生代中的某些對象經過一輪<code>gc</code>之後他還活着。這個時候就可以把他拷貝至老年代存儲區,進行存儲。除此之外如果目前拷貝的過程中,發現<code>to</code>空間的使用率超過了<code>25%</code>,這個時候也需要将這一次的活動對象都移動至老生代中存放。

為什麼要選擇<code>25%</code>呢?其實也很容易想得通,因為将來進行回收操作的時候,最終是要把<code>from</code>空間和<code>to</code>空間進行交換的。也就是說以前的<code>to</code>會變成<code>from</code>,而以前的<code>from</code>要變成<code>to</code>,這就意味着<code>to</code>如果使用率達到了<code>80%</code>,最終變成活動對象的存儲空間後,新的對象好像存不進去了。簡單的說明就是<code>to</code>空間的使用率如果超過了一定的限制,将來變成使用狀态時,新進來的對象空間好像不那麼夠用,是以會有這樣的限制。

簡單總結一下就是目前記憶體一分為二,一部分用來存儲新生代對象,至于什麼是新生代對象可以認為他的存活時間相對較短。然後可以去采用标記整理的算法,對<code>from</code>空間進行活動對象的标記和整理操作,接着把他們拷貝<code>to</code>空間。最後再置換一下兩個空間的狀态,那此時也就完成了空間的釋放操作。

老生代對象存放在記憶體空間的右側,在<code>v8</code>中同樣是有記憶體大小的限制,在<code>64</code>位作業系統中大小是<code>1.4g</code>, 在<code>32</code>位作業系統中是<code>700m</code>。

老生代對象指的是存活時間較長的對象,例如之前所提到的在全局對象中存放的一些變量,或者是一些閉包裡面放置的變量有可能也會存活很長的時間。針對老生代垃圾回收主要采用的是标記清除,标記整理和增量标記三個算法。

使用時主要采用的是标記清除算法完成垃圾空間的釋放和回收,标記清除算法主要是找到老生代存儲區域中的所有活動對象進行标記,然後直接釋放掉那些垃圾資料空間就可以了。顯而易見這個地方會存在一些空間碎片化的問題,不過雖然有這樣的問題但是<code>v8</code>的底層主要使用的還是标記清除的算法。因為相對空間碎片來說他的提升速度是非常明顯的。

在什麼情況下會使用到标記整理算法呢?當需要把新生代裡的内容向老生代中移動的時候,而且這個時間節點上老生代存儲區域的空間又不足以存放新生代存儲區移過來的對象。這種情況下就會觸發标記整理,把之前的一些鎖片空間進行整理回收,讓程式有更多的空間可以使用。最後還會采用增量标記的方式對回收的效率進行提升。

這裡來對比一下新老生代垃圾回收。

新生代的垃圾回收更像是在用空間換時間,因為他采用的是複制算法,這也就意味着每時每刻他的内部都會有一個空閑空間的存在。但是由于新生代存儲區本身的空間很小,是以分出來的空間更小,這部分的空間浪費相比帶來的時間上的一個提升當然是微不足道的。

在老生代對象回收過程中為什麼不去采用這種一分二位的做法呢?因為老生代存儲空間是比較大的,如果一分為二就有幾百兆的空間浪費,太奢侈了。第二就是老生代存儲區域中所存放的對象資料比較多,是以在指派的過程中消耗的時間也就非常多,是以老生代的垃圾回收是不适合使用複制算法來實作的。

至于之前所提到的增量标記算法是如何優化垃圾回收操作的呢?首先分成兩個部分,一個是程式執行,另一個是垃圾回收。

首先明确垃圾回收進行工作的時候是會阻塞目前<code>javascript</code>程式執行的,也就是會出現一個空檔期,例如程式執行完成之後會停下來執行垃圾回收操作。所謂的标記增量簡單來講就是将整段的垃圾回收操作拆分成多個小步驟,組分片完成整個回收,替代之前一口氣做完的垃圾回收操作。

這樣做的好處主要是實作垃圾回收與程式執行交替完成,帶來的時間消耗會更加的合理一些。避免像以前那樣程式執行的時候不能做垃圾回收,程式做垃圾回收的時候不能繼續運作程式。

簡單的舉個例子說明一下增量标記的實作原理。

程式首先運作的時候是不需要進行垃圾回收的,一旦當他觸發了垃圾回收之後,無論采用的是何種算法,都會進行周遊和标記操作,這裡針對的是老生代存儲區域,是以存在周遊操作。在周遊的過程中需要做标記,标記之前也提到過可以不一口氣做完,因為存在直接可達和間接可達操作,也就是說如果在做的時候,第一步先找到第一層的可達對象。然後就可以停下來,讓程式再去執行一會。如果說程式執行了一會以後,再繼續讓<code>gc</code>機做第二步的标記操作,比如下面還有一些子元素也是可達的,那就繼續做标記。标記一輪之後再讓gc停下來,繼續回到程式執行,也就是交替的去做标記和程式執行。

最後标記操作完成以後再去完成垃圾回收,這段時間程式就要停下來,等到垃圾回收操作完成才會繼續執行。雖然這樣看起來程式停頓了很多次,但是整個<code>v8</code>最大的垃圾回收也就是當記憶體達到<code>1.5g</code>的時候,采用非增量标記的形式進行垃圾回收時間也不超過<code>1s</code>,是以這裡程式的間斷是合理的。而且這樣一來最大限度的把以前很長的一段停頓時間直接拆分成了更小段,針對使用者體驗會顯得更加流程一些。

首先要知道<code>v8</code>引擎是目前主流的<code>javascript</code>執行引擎,在<code>v8</code>的内部記憶體是設定上限的,這麼做的原因是第一他本身是為浏覽器而設定的,是以在<code>web</code>應用中這樣的記憶體大小是足夠使用的。第二就是由他内部的垃圾回收機制來決定的,如果把記憶體設定大一些這個時候回收時間最多可能就超過了使用者的感覺,是以這裡就設定了上限數值。

<code>v8</code>采用的是分代回收的思想,将記憶體分成了新生代和老生代。關于新生代和老生代在空間和存儲資料類型是不同的。新生代如果在<code>64</code>位作業系統下空間是<code>32m</code>,<code>32</code>位的系統下就是<code>16m</code>。

<code>v8</code>對不同代對象采用的是不同的<code>gc</code>算法來完成垃圾回收操作,具體就是針對新生代采用複制算法和标記整理算法,針對老生代對象主要采用标記清除,标記整理和增量标記這樣三個算法。

<code>gc</code>工作目的就是為了讓記憶體空間在程式運作的過程中,出現良性的循環使用。所謂良性循環的基礎其實就是要求開發者在寫代碼的時候能夠對記憶體空間進行合理的配置設定。但是由于<code>ecmascript</code>中并沒有給程式員提供相應的操作記憶體空間的<code>api</code>,是以是否合理好像也不知道,因為他都是由gc自動完成的。

如果想判斷整個過程記憶體使用是否合理,必須想辦法能夠時刻關注到記憶體的變化。是以就有了這樣一款工具可以提供給開發者更多的監控方式,在程式運作過程中幫助開發者完成對記憶體空間的監控。

通過使用<code>performance</code>可以對程式運作過程記憶體的變化實時的監控。這樣就可以在程式的記憶體出現問題的時候直接想辦法定位到出現問題的代碼快。下面來看一下<code>performance</code>工具的基本使用步驟。

首先打開浏覽器,在位址欄輸入網址。輸入完位址之後不建議立即進行通路,因為想把最初的渲染過程記錄下來,是以隻是打開界面輸入網址即可。緊接着打開開發人員工具面闆(<code>f12</code>),選擇性能選項。開啟錄制功能,開啟之後就可以通路目标網址了。在這個頁面上進行一些操作,過一段時間後停止錄制。

就可以得到一個報告,在報告當中就可以分析跟記憶體相關的資訊了。錄制後會有一些圖表的展示,資訊也非常的多,看起來比較麻煩。這裡主要關注與記憶體相關的資訊,有一個記憶體的選項(<code>memory</code>)。預設情況下如果沒有勾選需要将它勾選。頁面上可以看到一個藍色的線條。屬于整個過程中我記憶體所發生的變化,可以根據時序,來看有問題的地方。如果某個地方有問題可以具體觀察,比如有升有降就是沒問題的。

當程式的記憶體出現問題的時候,具體會表現出什麼樣的形式。

首先第一條,界面如果出現了延遲加載或者說經常性的暫停,首先限定一下網絡環境肯定是正常的,是以出現這種情況一般都會去判定記憶體是有問題的,而且與<code>gc</code>存在着頻繁的垃圾回收操作是相關的。也就是代碼中肯定存在瞬間讓記憶體爆炸的代碼。這樣的代碼是不合适的需要去進行定位。

第二個就是當界面出現了持續性的糟糕性能表現,也就是說在使用過程中,一直都不是特别的好用,這種情況底層一般會認為存在着記憶體膨脹。所謂的記憶體膨脹指的就是,目前界面為了達到最佳的使用速度,可能會申請一定的記憶體空間,但是這個記憶體空間的大小,遠超過了目前裝置本身所能提供的大小,這個時候就會感覺到一段持續性的糟糕性能的體驗,同樣肯定是假設目前網絡環境是正常的。

最後,當使用一些界面的時候,如果感覺到界面的使用流暢度,随着時間的加長越來越慢,或者說越來越差,這個過程就伴随着記憶體洩露,因為在這種情況下剛開始的時候是沒有問題的,由于我們某些代碼的出現,可能随着時間的增長讓記憶體空間越來越少,這也就是所謂的記憶體洩漏,是以,出現這種情況的時候界面會随着使用時間的增長表現出性能越來越差的現象。

這就是關于應用程式在執行過程中如果遇到了記憶體出現問題的情況,具體的展現可以結合<code>performance</code>進行記憶體分析操作,進而定位到有問題的代碼,修改之後讓應用程式在執行的過程中顯得更加流暢。

記憶體出現的問題一般歸納為三種:記憶體洩露,記憶體膨脹,頻繁的垃圾回收。當這些内容出現的時候,該以什麼樣的标準來進行界定呢?

記憶體洩露其實就是記憶體持續升高,這個很好判斷,目前已經有很多種方式可以擷取到應用程式執行過程中記憶體的走勢圖。如果發現記憶體一直持續升高的,整個過程沒有下降的節點,這也就意味着程式代碼中是存在記憶體洩露的。這個時候應該去代碼裡面定位相應的子產品。

記憶體膨脹相對的模糊,記憶體膨脹的本意指的是應用程式本身,為了達到最優的效果,需要很大的記憶體空間,在這個過程中也許是由于目前裝置本身的硬體不支援,才造成了使用過程中出現了一些性能上的差異。想要判定是程式問題還是裝置問題,應該多做一些測試。這個時候可以找到那些深受使用者喜愛的裝置,在他們上面運作應用程式,如果整個過程中所有的裝置都表現出了很糟糕的性能體驗。這就說明程式本身是有問題的,而不是裝置有問題。這種情況就需要回到代碼裡面,定位到記憶體出現問題的地方。

具體有哪些方式來監控記憶體的變化,主要還是采用浏覽器所提供的一些工具。

浏覽器所帶的任務管理器,可以直接以數值的方式将目前應用程式在執行過程中記憶體的變化展現出來。第二個是借助于<code>timeline</code>時序圖,直接把應用程式執行過程中所有記憶體的走勢以時間點的方式呈現出來,有了這張圖就可以很容易的做判斷了。再有浏覽器中還會有一個叫做堆快照的功能,可以很有針對性的查找界面對象中是否存在一些分離的<code>dom</code>,因為分離<code>dom</code>的存在也就是一種記憶體上的洩露。

至于怎樣判斷界面是否存在着頻繁的垃圾回收,這就需要借助于不同的工具來擷取目前記憶體的走勢圖,然後進行一個時間段的分析,進而得出判斷。

一個<code>web</code>應用在執行的過程中,如果想要觀察他内部的一個記憶體變化,是可以有多種方式的,這裡通過一段簡單的<code>demo</code>來示範一下,可以借助浏覽器中自帶的任務管理器監控腳本運作時記憶體的變化。

在界面中放置一個元素,添加一個點選事件,事件觸發的時候建立一個長度非常長的一個數組。這樣就會産生記憶體空間上的消耗。

完成之後打開浏覽器運作,在右上角的<code>更多</code>中找到<code>更多工具</code>找到<code>任務管理器</code>打開。

這個時候就可以在任務管理器中定位到目前正在執行的腳本,預設情況下是沒有<code>javascript</code>記憶體列的,如果需要可以直接右擊找到<code>javascript</code>記憶體展示出來。這裡最關注的是記憶體和<code>javascript</code>記憶體這兩列。

第一列記憶體表示的是原生記憶體,也就是目前界面會有很多<code>dom</code>節點,這個記憶體指的就是<code>dom</code>節點所占據的記憶體,如果這個數值在持續的增大,就說明界面中在不斷的建立dom元素。

<code>javascript</code>記憶體表示的是<code>javascript</code>的堆,在這列當中需要關注的是小括号裡面的值,表示的是界面中所有可達對象正在使用的記憶體大小,如果這個數值一直在增大,就意味着目前的界面中要麼在建立新對象,要麼就是現有對象在不斷的增長。

以這個界面為例,可以發現小括号的值一直是個穩定的數字沒有發生變化,也就意味着目前頁面是沒有記憶體增長的。此時可以再去觸發一下<code>click</code>事件(點選按鈕),多點幾次,完成以後就發現小括号裡面的數值變大了。

通過這樣的過程就可以借助目前的浏覽器任務管理器來監控腳本運作時整個記憶體的變化。如果目前<code>javascript</code>記憶體列小括号裡面的數值一直增大那就意味着記憶體是有問題的,當然這個工具是沒有辦法定位的,他隻能發現問題,無法定位問題。

在之前已經可以使用浏覽器自帶的任務管理器對腳本執行中記憶體的變化去進行監控,但是在使用的過程中可以發現,這樣的操作更多的是用于判斷目前腳本的記憶體是否存在問題。如果想要定位問題具體和什麼樣的腳本有關,任務管理器就不是那麼好用了。

這裡再介紹一個通過時間線記錄記憶體變化的方式來示範一下怎樣更精确的定位到記憶體的問題跟哪一塊代碼相關,或者在什麼時間節點上發生的。

首先放置一個<code>dom</code>節點,添加點選事件,在事件中建立大量的<code>dom</code>節點來模拟記憶體消耗,再通過數組的方式配合着其他的方法形成一個非常長的字元串,模拟大量的記憶體消耗。

先打開浏覽器的控制台工具,選擇性能面闆,預設是沒有運作的,也就是沒有記錄,需要先點選計時操作。點完以後就開始錄制了,點選幾次<code>add</code>按鈕,稍等幾秒後,點選停止按鈕。完成以後就生成了一個圖表,密密麻麻的東西看起來可能會有些頭疼,隻關注下想要看到的資訊就可以了。

記憶體如果沒有勾選的話是不會監控記憶體變化的,需要先勾選記憶體,勾選之後頁面上就出現了記憶體的走勢曲線圖。裡面會包含很多資訊,給出來了幾中顔色的解釋。藍色的是<code>javascript</code>堆,紅色表示目前的文檔,綠色是<code>dom</code>節點,棕色是監聽器,紫色是<code>cpu</code>記憶體。

為了便于觀察可以隻保留<code>javascript</code>堆,其他的取消勾選隐藏掉。可以看到這個腳本運作過程中到目前為止他的<code>javascript</code>堆的情況走勢。目前這個工具叫時序圖,也就是在第一欄,以毫秒為機關,記錄了整個頁面從空白到渲染結束到最終停狀态,這個過程中整個界面的變化。如果願意,可以點進去看一下目前的界面形态,如果隻是關注記憶體,隻看記憶體的曲線圖就可以了。

當這個頁面最開始打開的時候其實很長一段時間都是平穩的狀态,沒有太多的記憶體消耗。原因在根本沒有點選<code>add</code>。然後緊接着在某一個時間點上突然之間記憶體就上去了,上去之後是一段平穩的狀态,這是因為點選了<code>add</code>之後這裡的記憶體肯定是瞬間暴漲的,然後緊接着暴漲之後我們任何操作,是以這時候肯定是平穩。

然後緊接着平穩之後又下降了,這就是之前所提到的,浏覽器本身也是具有垃圾回收機制的,當的腳本運作穩定之後,<code>gc</code>可能在某個時間點上就開始工作了,會發現有一些對象是非活動的,就開始進行回收,是以一段平穩之後就降下去了。降下去之後又會有一些小的浮動,屬于正常的活動開銷。後來又有幾次連續的點選,這個連續的點選行為可能又造成記憶體的飙升,然後不操作之後又往下降。

通過這樣一張記憶體走勢圖,可以得出的結論是,腳本裡面記憶體是非常穩定的,整個過程有漲有降,漲是申請記憶體,降是用完之後我<code>gc</code>在正常的回收記憶體。

一旦看到記憶體的走勢是直線向上走,也就意味着他隻有增長而沒有回收,必然存在着記憶體消耗,更有可能是記憶體洩漏。可以通過上面的時序圖定位問題,當發現某一個節點上有問題的時候,可以直接在這裡面定位到那個時間節點,可以在時序圖上進行拖動檢視每一個時間節點上的記憶體消耗。還可以看到界面上的變化,就可以配合着定位到是哪一塊産生了這樣一個記憶體的問題。

是以相對任務管理器來說會更好用,不但可以看目前記憶體是否有問題,還可以幫助定位問題在哪個時候發生的,然後再配合目前的界面展示知道做了什麼樣的操作才出現了這個問題,進而間接地可以回到代碼中定位有問題的代碼塊。

這裡簡單說明一下堆快照功能工作的原理,首先他相當于找到<code>javascript</code>堆,然後對它進行照片的留存。有了照片以後就可以看到它裡面的所有資訊,這也就是監控的由來。堆快照在使用的時候非常的有用,因為他更像是針對分離dom的查找行為。

界面上看到的很多元素其實都是<code>dom</code>節點,而這些<code>dom</code>節點本應該存在于一顆存活的dom樹上。不過dom節點會有幾種形态,一種是垃圾對象,一種是分離<code>dom</code>。簡單的說就是如果這個節點從<code>dom</code>樹上進行了脫離,而且在<code>javascript</code>代碼當中沒有再引用的<code>dom</code>節點,他就成為了一個垃圾。如果<code>dom</code>節點隻是從<code>dom</code>樹上脫離了,但是在<code>javascript</code>代碼中還有引用,就是分離<code>dom</code>。分離<code>dom</code>在界面上是看不見的,但是在記憶體中是占據着空間的。

這種情況就是一種記憶體洩露,可以通過堆快照的功能把他們找出來,隻要能找得到,就可以回到代碼裡,針對這些代碼進行清除進而讓記憶體得到一些釋放,腳本在執行的時候也會變得更加迅速。

在<code>html</code>裡面放入<code>btn</code>按鈕,添加點選事件,點選按鈕的時候,通過<code>javascript</code>語句去模拟相應的記憶體變化,比如建立<code>dom</code>節點,為了看到更多類型的分離<code>dom</code>,采用<code>ul</code>包裹<code>li</code>的<code>dom</code>節點建立。先在函數中建立<code>ul</code>節點,然後使用循環的方式建立多個<code>li</code>放在<code>ul</code>裡面,建立之後不需要放在頁面上,為了讓代碼引用到這個<code>dom</code>使用變量<code>tmpele</code>指向<code>ul</code>。

簡單說明就是建立了<code>ul</code>和<code>li</code>節點,但是并沒有将他們放在頁面中,隻是通過<code>javascript</code>變量引用了這個節點,這就是分離<code>dom</code>。

打開浏覽器調試工具,選中記憶體面闆。進入以後可以發現堆快照的選項。這裡做兩個行為的測試,第一個是在沒有點選按鈕的情況下,直接擷取目前的快照,在這個快照裡面就是目前對象的具體展示,這裡有一個篩選的操作,直接檢索<code>deta</code>關鍵字,可以發現沒有内容。

回到界面中做另外一個操作,對按鈕進行點選,點完以後我再拍攝一張快照(點選左側的配置檔案文字,出現拍照界面),還是做和之前一樣的操作檢索<code>deta</code>。

這次就會發現,快照<code>2</code>裡面搜尋到了,很明顯這幾個就是代碼中所建立的dom節點,并沒有添加到界面中,但是他的确存在于堆中。這其實就是一種空間上的浪費,針對這樣的問題在代碼中對使用過後的dom節點進行清空就可以了。

在這裡我們簡單的總結就是,我們可以利用浏覽器當中提供的一個叫做堆快照的功能,然後去把我們目前的堆進行拍照,拍照過後我們要找一下這裡面是否存在所謂的分離<code>dom</code>。

因為分離<code>dom</code>在頁面中不展現,在記憶體中的确存在,是以這個時候他是一種記憶體的浪費,那麼我們要做的就是定位到我們代碼裡面那些個分離<code>dom</code>所在的位置,然後去想辦法把他給清除掉。

這裡說一下如何确定目前web應用在執行過程中是否存在着頻繁的垃圾回收。當<code>gc</code>去工作的時候應用程式是停止的。是以<code>gc</code>頻繁的工作對<code>web</code>應用很不友好,因為會處于死的狀态,使用者會感覺到卡頓。

這個時候就要想辦法确定目前的應用在執行時是否存在頻繁的垃圾回收。

這裡給出兩種方式,第一種是可以通過<code>timeline</code>時序圖的走勢來判斷,在性能工具面闆中對目前的記憶體走勢進行監控。如果發現藍色的走勢條頻繁的上升下降。就意味着在頻繁的進行垃圾回收。出現這樣的情況之後必須定位到相應的時間節點,然後看一下具體做了什麼樣的操作,才造成這樣現象的産生,接着在代碼中進行處理就可以了。

任務管理器在做判斷的時候會顯得更加簡單一些,因為他就是一個數值的變化,正常當界面渲染完成之後,如果沒有其他額外的操作,那麼無論是<code>dom</code>節點記憶體,還是我們<code>javascript</code>記憶體,都是一個不變化的數值,或者變化很小。如果這裡存在頻繁的<code>gc</code>操作時,這個數值的變化就是瞬間增大,瞬間減小,這樣的節奏,是以看到這樣的過程也意味着代碼存在頻繁的垃圾回收操作。

頻繁的垃圾回收操作表象上帶來的影響是讓使用者覺得應用在使用的時候非常卡頓,從内部看就是目前代碼中存在對記憶體操作不當的行為讓<code>gc</code>不斷的工作,來回收釋放相應的空間。

繼續閱讀