JVM架構和工作原理及GC工作機制

2022-09-11 17:30:15 字數 3930 閱讀 3670

學習j**a,理解弄懂了jvm和gc,對於學習j**a開發有很大幫助。

借助前人之鑑博文,這裡主要講四個部分:jvm結構、記憶體分配、垃圾**演算法、垃圾收集器

jvm主要包括四個部分:

1.類載入器(classloader):在jvm啟動時或者在類執行時將需要的class載入到jvm中。

2.執行引擎:負責執行class檔案中包含的位元組碼指令。

3.記憶體區(也叫執行時資料區):是在jvm執行的時候操作所分配的記憶體區。

4.本地方法介面:主要是呼叫c或c++實現的本地方法及返回結果。

了解垃圾**之前,得先了解jvm是怎麼分配記憶體的,然後識別哪些記憶體是垃圾需要**,最後才是用什麼方式**。

j**a的記憶體分配原理與c/c++不同,c/c++每次申請記憶體時都要malloc進行系統呼叫,而系統呼叫發生在核心空間,每次都要中斷進行切換,這需要一定的開銷,而j**a虛擬機器是先一次性分配一塊較大的空間,然後每次new時都在該空間上進行分配和釋放,減少了系統呼叫的次數,節省了一定的開銷,這有點類似於記憶體池的概念;二是有了這塊空間過後,如何進行分配和**就跟gc機制有關了。

j**a一般記憶體申請有兩種:靜態記憶體和動態記憶體。很容易理解,編譯時就能夠確定的記憶體就是靜態記憶體,即記憶體是固定的,系統一次性分配,比如int型別變數;動態記憶體分配就是在程式執行時才知道要分配的儲存空間大小,比如j**a物件的記憶體空間。根據上面我們知道,j**a棧、程式計數器、本地方法棧都是執行緒私有的,執行緒生就生,執行緒滅就滅,棧中的棧幀隨著方法的結束也會撤銷,記憶體自然就跟著**了。所以這幾個區域的記憶體分配與**是確定的,我們不需要管的。但是j**a堆和方法區則不一樣,我們只有在程式執行期間才知道會建立哪些物件,所以這部分記憶體的分配和**都是動態的。一般我們所說的垃圾**也是針對的這一部分。

總之stack的記憶體管理是順序分配的,而且定長,不存在記憶體**問題;而heap 則是為j**a物件的例項隨機分配記憶體,不定長度,所以存在記憶體分配和**的問題;

垃圾收集器一般必須完成兩件事:檢測出垃圾;**垃圾。怎麼檢測出垃圾?一般有以下幾種方法:

引用計數法:給乙個物件新增引用計數器,每當有個地方引用它,計數器就加1;引用失效就減1。

但是問題是,如果我有兩個物件a和b,互相引用,除此之外,沒有其他任何物件引用它們,實際上這兩個物件已經無法訪問,即是我們說的垃圾物件。但是互相引用,計數不為0,導致無法**,所以還有另一種方法:

可達性分析演算法:以根集物件為起始點進行搜尋,如果有物件不可達的話,即是垃圾物件。這裡的根集一般包括j**a棧中引用的物件、方法區常良池中引用的物件、本地方法中引用的物件等。

總之,jvm在做垃圾**的時候,會檢查堆中的所有物件是否會被這些根集物件引用,不能夠被引用的物件就會被垃圾收集器**。一般**演算法也有如下幾種:

1.標記-清除(mark-sweep)

演算法和名字一樣,分為兩個階段:標記和清除。標記所有需要**的物件,然後統一**。這是最基礎的演算法,後續的收集演算法都是基於這個演算法擴充套件的。

不足:效率低;標記清除之後會產生大量碎片。效果圖如下:

2.複製(copying)

此演算法把記憶體空間劃為兩個相等的區域,每次只使用其中乙個區域。垃圾**時,遍歷當前使用區域,把正在使用中的物件複製到另外乙個區域中。此演算法每次只處理正在使用中的物件,因此複製成本比較小,同時複製過去以後還能進行相應的記憶體整理,不會出現「碎片」問題。當然,此演算法的缺點也是很明顯的,就是需要兩倍記憶體空間。效果圖如下:

3.標記-整理(mark-compact)

此演算法結合了「標記-清除」和「複製」兩個演算法的優點。也是分兩階段,第一階段從根節點開始標記所有被引用物件,第二階段遍歷整個堆,把清除未標記物件並且把存活物件「壓縮」到堆的其中一塊,按順序排放。此演算法避免了「標記-清除」的碎片問題,同時也避免了「複製」演算法的空間問題。效果圖如下:

4.分代收集演算法

這是當前商業虛擬機器常用的垃圾收集演算法。分代的垃圾**策略,是基於這樣乙個事實:不同的物件的生命週期是不一樣的。因此,不同生命週期的物件可以採取不同的收集方式,以便提高**效率。

為什麼要運用分代垃圾**策略?在j**a程式執行的過程中,會產生大量的物件,因每個物件所能承擔的職責不同所具有的功能不同所以也有著不一樣的生命週期,有的物件生命週期較長,比如http請求中的session物件,執行緒,socket連線等;有的物件生命週期較短,比如string物件,由於其不變類的特性,有的在使用一次後即可**。試想,在不進行物件存活時間區分的情況下,每次垃圾**都是對整個堆空間進行**,那麼消耗的時間相對會很長,而且對於存活時間較長的物件進行的掃瞄工作等都是徒勞。因此就需要引入分治的思想,所謂分治的思想就是因地制宜,將物件進行代的劃分,把不同生命週期的物件放在不同的代上使用不同的垃圾**方式。

如何劃分?將物件按其生命週期的不同劃分成:年輕代(young generation)、年老代(old generation)、持久代(permanent generation)。其中持久代主要存放的是類資訊,所以與j**a物件的**關係不大,與**息息相關的是年輕代和年老代。

這裡有個比喻很形象

「假設你是乙個普通的 j**a 物件,你出生在 eden 區,在 eden 區有許多和你差不多的小兄弟、小姐妹,可以把 eden 區當成幼兒園,在這個幼兒園裡大家玩了很長時間。eden 區不能無休止地放你們在裡面,所以當年紀稍大,你就要被送到學校去上學,這裡假設從小學到高中都稱為 survivor 區。開始的時候你在 survivor 區裡面劃分出來的的「from」區,讀到高年級了,就進了 survivor 區的「to」區,中間由於學習成績不穩定,還經常來回折騰。直到你 18 歲的時候,高中畢業了,該去社會上闖闖了。於是你就去了年老代,年老代裡面人也很多。在年老代里,你生活了 20 年 (每次 gc 加一歲),最後壽終正寢,被 gc **。有一點沒有提,你在年老代遇到了乙個同學,他的名字叫愛德華 (慕光之城裡的帥哥吸血鬼),他以及他的家族永遠不會死,那麼他們就生活在永生代。」

年輕代:是所有新物件產生的地方。年輕代被分為3個部分——enden區和兩個survivor區(from和to)當eden區被物件填滿時,就會執行minor gc。並把所有存活下來的物件轉移到其中乙個survivor區(假設為from區)。minor gc同樣會檢查存活下來的物件,並把它們轉移到另乙個survivor區(假設為to區)。這樣在一段時間內,總會有乙個空的survivor區。經過多次gc週期後,仍然存活下來的物件會被轉移到年老代記憶體空間。通常這是在年輕代有資格提公升到年老代前通過設定年齡閾值來完成的。需要注意,survivor的兩個區是對稱的,沒先後關係,from和to是相對的。

年老代:在年輕代中經歷了n次**後仍然沒有被清除的物件,就會被放到年老代中,可以說他們都是久經沙場而不亡的一代,都是生命週期較長的物件。對於年老代和永久代,就不能再採用像年輕代中那樣搬移騰挪的**演算法,因為那些對於這些**戰場上的老兵來說是小兒科。通常會在老年代記憶體被佔滿時將會觸發full gc,**整個堆記憶體。

持久代:用於存放靜態檔案,比如j**a類、方法等。持久代對垃圾**沒有顯著的影響。 

垃圾收集演算法是記憶體**的方**,而實現這些方**的則是垃圾收集器。不同廠商不同版本jvm所提供的垃圾收集器可能不同。可以參考博文

PKI CA工作原理及架構

圖1 數字簽名 用傳送發私鑰生成數字簽名 用傳送方公鑰解密,可以證明訊息確實是由公鑰擁有者發出的。兩份摘要的比對結果,可以證明訊息在傳輸的過程中是否被改動。數字簽名要發揮作用,首先需要接收方獲取傳送方的公鑰。如何證明獲取的公鑰確實是傳送方的公鑰而不是假冒的呢?數字證書提供了一種發布公鑰的簡便方法。圖...

Ansible工作架構和原理

確認安裝 ansible version 例 用ping模組判斷主機是否存活 ansible 目標ip m ping k。對方必須在ansible hosts裡,且需要帶密碼。若填寫多個ip,只會要求填寫第乙個的口令 用此口令訪問所有主機 則可能會導致其他的出錯。切訪問次序不會按期望執行。上次連線會...

Ansible 工作架構和原理

ansible應用舉例 場景 公司計畫在年代做一次大型的市場 活動,全面衝刺下交易額,為明年上市做市場準備,公司要求所有業務組對年底大 做準備,運維部要求所有業務容量進行三倍擴容,並搭建出多套環境可以供開發和測試人員做測試。運維老大為了年底表現,要求所有運維部同學盡快實現。使用者 控制端 被控端三層...