Java自動記憶體管理詳解

來源:互聯網
上載者:User

最近找了兩本Java虛擬機器方面的書,看了看其中對於Java自動記憶體管理的章節,寫的都大同小異,在此總結一下,主要是三個方面:記憶體劃分、記憶體配置、記憶體回收。

記憶體劃分(運行時資料區)

JVM運行時資料區

從線程的角度來分,可分為線程私人和線程共用的,上圖中左邊的灰色地區就是線程共用的地區,包括堆、方法區、運行時常量池。而右邊的地區則是線程私人的,包括程式計數器、虛擬機器棧。

堆是虛擬機器管理的記憶體中最大的一塊,是被線程共用的一塊地區,主要用於存放對象執行個體,但並不是所有對象都是在堆上分配的。同時堆也是垃圾收集器管理的主要區域。

方法區

方法區與堆一樣,也是各個線程共用的記憶體地區,用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的代碼等資料。方法區雖然邏輯上與堆獨立,但物理上屬於堆。

運行時常量池

運行時常量池屬於方法區的一部分,用於存放class檔案中的常量池資訊,主要是各種字面值和符號引用。另外,運行時常量池並不要求常量一定只能在編譯期產生,運行期間也可能將新的常量放入池中,例如String類的intern()方法。

程式計數器

類似於作業系統中的程式計數器,不過這裡的程式計數器指示的是正在執行的位元組碼指令的地址。位元組碼解譯器的執行完一條指令後,會改變程式計數器的值,指向下一條需要執行的指令地址。之所以需要每個線程都使用一個獨立的程式計數器,是因為能夠讓多線程程式正確執行,各條線程之間的計數器互不影響。

虛擬機器棧

虛擬機器棧描述的是Java方法執行的記憶體模型,其基本單位是棧幀,每個方法執行的時候都會建立一個棧幀。虛擬機器一直在執行棧頂的棧幀所對應的方法,當一個方法中調用另一個方法時,就會建立一個被呼叫者法的棧幀,push進虛擬機器棧,被呼叫者法執行結束,會將傳回值寫入調用他的棧幀,並將自己的棧幀從棧中彈出。
而方法的棧幀中,存放了局部變數表、運算元棧、動態連結、方法出口等資訊。局部變數表所需的記憶體空間都是在編譯器就能夠確定的,用於存放方法內部的本地變數;運算元棧則是用來進行運算操作,將兩個運算元從棧頂彈出,計算結果,壓入棧。

還有一個沒有提及的是本地方法棧,與虛擬機器棧相似,不過是為本地方法服務的,虛擬機器規範中對其沒有強制規定,可由虛擬機器具體實現。

記憶體配置

java堆分代

上面說到,堆是記憶體管理的主要區域,堆中存放了各種各樣的對象,進一步可以劃分為新生代和老年代。其中,新生代裡有Eden空間、From Survivor空間、To Survivor空間。這樣劃分主要是為了方便記憶體回收。具體各個空間的用途,到記憶體回收就會知道。

從Java代碼中new一個對象說起,JVM首先會檢查這個new指令的參數能夠在常量池中定位到一個類的符號引用,然後檢查與這個符號引用相對應的類是否已經成功經曆過載入、解析、初始化等步驟,當類完成裝載之後,就可以完全確定建立該類執行個體所需要的空間大小。然後JVM就會為該執行個體進行記憶體配置。

下面就是分配在哪的問題。一般會分配在堆中的Eden空間,如果啟動了本地線程分配緩衝,會優先在TLAB(Tread Local Allocation Buffer,即本地線程分配緩衝區)中分配,TLAB是Eden空間中線程私人的部分,大約佔據Eden總空間的1%。 如果分配到Eden空間失敗,就會進行一次新生代的垃圾收集工作。對於需要大量連續記憶體的大對象,會直接分配到老年代。

另外涉及到的一個概念是逃逸分析。上文也提到,並不是所有的對象都在堆中分配,其中有一部分對象是在棧上分配的,這裡說的棧就是指虛擬機器棧幀中的局部變數表部分。逃逸分析是JVM執行效能最佳化之前的一種分析技術,具體目標是分析出對象的範圍。如果一個對象的作用於僅限於方法體內部,就會在棧上為其分配記憶體,棧幀隨著方法退出而銷毀,不需要參與到垃圾收集中去。但一旦方法內部的對象被外部對象引用,這個對象就因此發生了逃逸,就不會在棧上分配。

記憶體回收

記憶體回收涉及到幾個方面:哪些記憶體需要回收?什麼時候回收?如何回收?

可回收對象判定

常用的有引用計數演算法和根搜尋演算法。

引用計數就是為每一個對象添加一個引用計數器,每當有一個地方引用它時,就將計數器的值加1,當引用失效時,計數器的值減1。任何時刻計數器值為0,說明對象不再被使用。此方法的缺陷在於,很難解決對象之間相互循環參考,如果兩個需要回收的對象分別引用彼此,就無法被垃圾收集器回收。

根搜尋演算法通過一系列名為“GC Roots”的對象作為起始點,從這些節點開始向下搜尋,搜尋走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連,就證明此對象是停用。GC Roots對象包括棧幀中本地變數表中引用的對象、方法區中類靜態屬性引用的對象、方法區中常量引用的對象、本地方法棧中引用的對象。

垃圾收集演算法

主要有標記-清除演算法、複製演算法、標記-壓縮演算法。

標記-清楚演算法:首先標記出所有需要回收的對象,在標記完成後統一回收掉所有被標記的對象。標記和清除過程的效率都不高,而且標記清除之後會產生大量不連續的記憶體片段,片段太多會導致需要分配大對象時無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集操作。閒置記憶體片段可以用空閑列表來表示,從而提供下一次指派至的記憶體位址。

複製演算法:將記憶體劃分為大小相等的兩塊,每次只使用其中的一塊,當一塊記憶體用完了,就將還活著的對象複製到另外一塊上面,然後把已使用過的記憶體空間一次清理掉。運行高效,代價是損失了一般的記憶體空間。在堆中的新生代垃圾收集演算法中,就使用了複製演算法。將Eden空間、From Survivor空間中存活的對象複製到To Survivor空間,然後將From Survivor空間和To Survivor空間互換。(如果Eden空間和From Survivor空間的存活對象的分代年齡大於一定閾值或者To Survivor空間已滿,會直接被分配到老年代)Eden空間和兩個Survivor空間的預設比例是8:1:1。之所以可以在新生代使用複製演算法,是因為大多數新生代對象的生命週期都非常短暫。

標記-整理演算法:與標記-清除演算法差不多,不過此演算法將所有存活對象向記憶體的一端移動,然後直接清理掉另一端的記憶體。此演算法應用於老年代的垃圾收集。由於能夠整理出一大塊連續的空閑記憶體地區,所以用一個指標指向空閑記憶體地區的起點,用於指向下一次記憶體配置的位置。

垃圾收集器

垃圾收集器有很多,而且虛擬機器裡整合了很多種垃圾收集器,本文不再贅述,值得一提的是Stop-the-World機制,通俗來說,垃圾收集進行的時候,背景工作執行緒必須停止一段時間,無論以哪種收集器進行垃圾收集,都會有或多或少的Stop-the-World時間。

另外一個是程式輸送量與低延遲的權衡。所謂輸送量就是CPU用於運行使用者代碼的時間與CPU總消耗時間的比值。可通過-XX:MaxGcPauseMillis設定垃圾收集造成的Stop-the-World的時間,但為了低延遲而將該值調小之後,會導致相應的新生代記憶體空間變小,記憶體空間越小越容易被耗盡,會導致GC更加頻繁,總的用於GC的時間可能反而會變多,導致程式輸送量下降。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.