十一:JVM的內存模型與GC算法

一、JVM結構

根據《java虛擬機規範》規定,JVM的基本結構一般如下圖所示:

img

1.類加載器(ClassLoader)

​ 在JVM啓動時或者在類運行時將需要的class加載到JVM中。(右圖表示了從java源文件到JVM的整個過程,可配合理解。 關於類的加載機制,可以參考http://blog.csdn.net/tonytfjing/article/details/47212291

2.執行引擎

​ 負責執行class文件中包含的字節碼指令(執行引擎的工作機制,這裏也不細說了,這裏主要介紹JVM結構);

3.內存區(也叫運行時數據區)

是在JVM運行的時候操作所分配的內存區。運行時內存區主要可以劃分爲5個區域,如圖:

img

  • 方法區(Method Area,jdk8之前可以理解爲永久區):用於存儲類結構信息的地方,(包括常量池、靜態變量)、構造函數等。雖然JVM規範把方法區描述爲堆的一個邏輯部分, 但它卻有個別名non-heap(非堆),所以大家不要搞混淆了。方法區還包含一個運行時常量池。對Java7及以前版本的Hotspot中方法區位於永久代中。同時,永久代和堆是相互隔離的,但它們使用的物理內存是連續的。 但在Java7中永久代中存儲的部分數據已經開始轉移到堆內存或本地內存中了。比如,符號引用轉移到了本地內存;字符串常量池轉移到了堆內存;類的靜態變量轉移到了堆內存。

  • java堆(Heap管存儲):存儲java實例或者對象的地方。這塊是GC的主要區域(後面解釋)。從存儲的內容我們可以很容易知道,方法區和堆是被所有java線程共享的。

  • java棧(Stack管運行):java棧總是和線程關聯在一起,每當創建一個線程時,JVM就會爲這個線程創建一個對應的java棧。在這個java棧中又會包含多個棧幀,每運行一個方法就創建一個棧幀,用於存儲局部變量表、操作棧、方法返回值等。每一個方法從調用直至執行完成的過程,就對應一個棧幀在java棧中入棧到出棧的過程。所以java棧是線程私有的。

  • PC計數器(PC Register):用於保存當前線程執行的內存地址,及方法的執行順序。由於JVM程序是多線程執行的(線程輪流切換),所以爲了保證線程切換回來後,還能恢復到原先狀態,就需要一個獨立的計數器,記錄之前中斷的地方,可見程序計數器也是線程私有的。

  • 本地方法棧(Native Method Stack):和java棧的作用差不多,只不過是爲JVM使用到的native方法服務的。

4.本地方法接口

主要是調用C或C++實現的本地方法及返回結果。

二、內存分配

​ Java的內存分配原理與C/C++不同,C/C++每次申請內存時都要malloc進行系統調用,而系統調用發生在內核空間,每次都要中斷進行切換,這需要一定的開銷,而Java虛擬機是先一次性分配一塊較大的空間,然後每次new時都在該空間上進行分配和釋放,減少了系統調用的次數,節省了一定的開銷,這有點類似於內存池的概念;二是有了這塊空間過後,如何進行分配和回收就跟GC機制有關了。

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

總之Stack的內存管理是順序分配的,而且定長,不存在內存回收問題;而Heap 則是爲java對象的實例隨機分配內存,不定長度,所以存在內存分配和回收的問題;

三、垃圾檢測、回收算法

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

引用計數法:給一個對象添加引用計數器,每當有個地方引用它,計數器就加1;引用失效就減1。

​ 好了,問題來了,如果我有兩個對象A和B,互相引用,除此之外,沒有其他任何對象引用它們,實際上這兩個對象已經無法訪問,即是我們說的垃圾對象。但是互相引用,計數不爲0,導致無法回收,所以還有另一種方法:

可達性分析算法:以根集對象爲起始點進行搜索,如果有對象不可達的話,即是垃圾對象。這裏的根集一般包括java棧中引用的對象、方法區常良池中引用的對象本地方法中引用的對象等。

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

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

算法和名字一樣,分爲兩個階段:標記和清除。標記所有需要回收的對象,然後統一回收。這是最基礎的算法,後續的收集算法都是基於這個算法擴展的。

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

img

2.複製(Copying)

此算法把內存空間劃爲兩個相等的區域,每次只使用其中一個區域。垃圾回收時,遍歷當前使用區域,把正在使用中的對象複製到另外一個區域中。此算法每次只處理正在使用中的對象,因此複製成本比較小,同時複製過去以後還能進行相應的內存整理,不會出現“碎片”問題。當然,此算法的缺點也是很明顯的,就是需要兩倍內存空間。效果圖如下:

img

3.標記-整理(Mark-Compact)

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

img

(1,2,3 圖文摘自 http://pengjiaheng.iteye.com/blog/520228,感謝原作者。)

4.分代收集算法

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

4.1爲什麼要運用分代垃圾回收策略?

​在java程序運行的過程中,會產生大量的對象,因每個對象所能承擔的職責不同所具有的功能不同所以也有着不一樣的生命週期,有的對象生命週期較長,比如Http請求中的Session對象,線程,Socket連接等;有的對象生命週期較短,比如String對象,由於其不變類的特性,有的在使用一次後即可回收。

4.2如何劃分?

年輕代:

​ 所有新對象產生的地方。年輕代被分爲3個部分——Enden區和兩個Survivor區(From和to,默比例是8:1:1)當Eden區被對象填滿時,就會執行Minor GC。並把所有存活下來的對象轉移到其中一個survivor區(假設爲from區)。Minor GC同樣會檢查存活下來的對象,並把它們轉移到另一個survivor區(假設爲to區)。這樣在一段時間內,總會有一個空的survivor區;所以年輕代GC主要是採用複製算法。經過多次GC週期後,仍然存活下來的對象會被轉移到年老代內存空間。通常這是在年輕代有資格提升到年老代前通過設定年齡閾值來完成的(通過-XX:MaxTenuringThreshold來設置)。需要注意,Survivor的兩個區是對稱的,沒先後關係,from和to是相對的(複製要交換,誰空誰爲to)。

年老代:

​ 在年輕代中經歷了N次回收後仍然沒有被清除的對象,就會被放到年老代中,都是生命週期較長的對象。對於年老代和永久代,就不能再採用像年輕代中那樣搬移騰挪的回收算法,因爲那些對於這些回收戰場上的老兵來說是小兒科。通常會在老年代內存被佔滿時將會觸發Full GC,回收整個堆內存(內存溢出只會出現在這個區域)。

永久代:(1.8之後無,元空間代替)

​ 用於存放靜態文件,比如java類、方法等。持久代對垃圾回收沒有顯著的影響。

分代回收的效果圖如下:

​ 伊甸區 倖存區0 倖存區1 老年代 永久代(1.8元空間)

​ 新生代

img

發佈了30 篇原創文章 · 獲贊 42 · 訪問量 7815
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章