jvm虛擬機原理1
二、JVM的內存管理和垃圾回收
JVM中的內存管理主要是指JVM對於Heap的管理,這是因為Stack,PC Register和Native Method Stack都是和線程一樣的生命周期,在線程結束時自然可以被再次使用。雖然說,Stack的管理不是重點,但是也不是完全不講究的。1.棧的管理
JVM允許棧的大小是固定的或者是動態變化的。在Oracle的關於參數設置的官方文檔中有關於Stack的設置(http://docs.oracle.com/cd/E13150_01/jrockit_jvm/jrockit/jrdocs/refman/optionX.html#wp1024112),是通過-Xss來設置其大小。關於Stack的默認大小對於不同機器有不同的大小,並且不同廠商或者版本號的jvm的實現其大小也不同,如下表是HotSpot的默認大小:Platform | Default |
---|---|
Windows IA32 | 64 KB |
Linux IA32 | 128 KB |
Windows x86_64 | 128 KB |
Linux x86_64 | 256 KB |
Windows IA64 | 320 KB |
Linux IA64 | 1024 KB (1 MB) |
Solaris Sparc | 512 KB |
關於棧一般會發生以下兩種異常: 1.當線程中的計算所需要的棧超過所允許大小時,會拋出StackOverflowError。 2.當Java棧試圖擴展時,沒有足夠的存儲器來實現擴展,JVM會報OutOfMemoryError。 我針對棧進行了實驗,由於遞歸的調用可以致使棧的引用增加,導致溢出,所以設計代碼如下: 我的機器是x86_64系統,所以Stack的默認大小是128KB,上述程序在運行時會報錯: 而當我在eclipse中調整了-Xss參數到3M之後,該異常消失。 。 另外棧上有一點得註意的是,對於本地代碼調用,可能會在棧中申請內存,比如C調用malloc(),而這種情況下,GC是管不著的,需要我們在程序中,手動管理棧內存,使用free()方法釋放內存。 2.堆的管理 堆的管理要比棧管理復雜的多,我通過堆的各部分的作用、設置,以及各部分可能發生的異常,以及如何避免各部分異常進行了學習。 上圖是 Heap和PermanentSapce的組合圖,其中Eden區裏面存著是新生的對象,From Space和To Space中存放著是每次垃圾回收後存活下來的對象 ,所以每次垃圾回收後,Eden區會被清空。存活下來的對象先是放到From Space,當From Space滿了之後移動到To Space。當To Space滿了之後移動到Old Space。Survivor的兩個區是對稱的,沒先後關系,所以同一個區中可能同時存在從Eden復制過來 對象,和從前一個Survivor復制過來的對象,而復制到年老區的只有從第一個Survivor復制過來的對象。而且,Survivor區總有一個是空的。同時,根據程序需要,Survivor區是可以配置為多個的(多於兩個),這樣可以增加對象在年輕代中的存在時間,減少被放到年老代的可能。 Old Space中則存放生命周期比較長的對象,而且有些比較大的新生對象也放在Old Space中。 堆的大小通過-Xms和-Xmx來指定最小值和最大值,通過-Xmn來指定Young Generation的大小(一些老版本也用-XX:NewSize指定), 即上圖中的Eden加FromSpace和ToSpace的總大小。然後通過-XX:NewRatio來指定Eden區的大小,在Xms和Xmx相等的情況下,該參數不需要設置。通過-XX:SurvivorRatio來設置Eden和一個Survivor區的比值。(參考自博文:http://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html) 堆異常分為兩種,一種是Out of Memory(OOM),一種是Memory Leak(ML)。Memory Leak最終將導致OOM。實際應用中表現為:從Console看,內存監控曲線一直在頂部,程序響應慢,從線程看,大部分的線程在進行GC,占用比較多的CPU,最終程序異常終止,報OOM。OOM發生的時間不定,有短的一個小時,有長的10天一個月的。關於異常的處理,確定OOM/ML異常後,一定要註意保護現場,可以dump heap,如果沒有現場則開啟GCFlag收集垃圾回收日誌,然後進行分析,確定問題所在。如果問題不是ML的話,一般通過增加Heap,增加物理內存來解決問題,是的話,就修改程序邏輯。 3.垃圾回收 JVM中會在以下情況觸發回收:對象沒有被引用,作用域發生未捕捉異常,程序正常執行完畢,程序執行了System.exit(),程序發生意外終止。 JVM中標記垃圾使用的算法是一種根搜索算法。簡單的說,就是從一個叫GC Roots的對象開始,向下搜索,如果一個對象不能達到GC Roots對象的時候,說明它可以被回收了。這種算法比一種叫做引用計數法的垃圾標記算法要好,因為它避免了當兩個對象啊互相引用時無法被回收的現象。 JVM中對於被標記為垃圾的對象進行回收時又分為了一下3種算法: 1.標記清除算法,該算法是從根集合掃描整個空間,標記存活的對象,然後在掃描整個空間對沒有被標記的對象進行回收,這種算法在存活對象較多時比較高效,但會產生內存碎片。 2.復制算法,該算法是從根集合掃描,並將存活的對象復制到新的空間,這種算法在存活對象少時比較高效。 3.標記整理算法,標記整理算法和標記清除算法一樣都會掃描並標記存活對象,在回收未標記對象的同時會整理被標記的對象,解決了內存碎片的問題。 JVM中,不同的 內存區域作用和性質不一樣,使用的垃圾回收算法也不一樣,所以JVM中又定義了幾種不同的垃圾回收器(圖中連線代表兩個回收器可以同時使用): 1.Serial GC。從名字上看,串行GC意味著是一種單線程的,所以它要求收集的時候所有的線程暫停。這對於高性能的應用是不合理的,所以串行GC一般用於Client模式的JVM中。 2.ParNew GC。是在SerialGC的基礎上,增加了多線程機制。但是如果機器是單CPU的,這種收集器是比SerialGC效率低的。 3.Parrallel Scavenge GC。這種收集器又叫吞吐量優先收集器,而吞吐量=程序運行時間/(JVM執行回收的時間+程序運行時間),假設程序運行了100分鐘,JVM的垃圾回收占用1分鐘,那麽吞吐量就是99%。Parallel Scavenge GC由於可以提供比較不錯的吞吐量,所以被作為了server模式JVM的默認配置。 4.ParallelOld是老生代並行收集器的一種,使用了標記整理算法,是JDK1.6中引進的,在之前老生代只能使用串行回收收集器。 5.Serial Old是老生代client模式下的默認收集器,單線程執行,同時也作為CMS收集器失敗後的備用收集器。 6.CMS又稱響應時間優先回收器,使用標記清除算法。他的回收線程數為(CPU核心數+3)/4,所以當CPU核心數為2時比較高效些。CMS分為4個過程:初始標記、並發標記、重新標記、並發清除。 7.GarbageFirst(G1)。比較特殊的是G1回收器既可以回收Young Generation,也可以回收Tenured Generation。它是在JDK6的某個版本中才引入的,性能比較高,同時註意了吞吐量和響應時間。 對於垃圾收集器的組合使用可以通過下表中的參數指定: (MarsYOungNote:圖中第三個寫錯了。UseConeMarkSweepGC) 默認的GC種類可以通過jvm.cfg或者通過jmap dump出heap來查看,一般我們通過jstat -gcutil [pid] 1000可以查看每秒gc的大體情況,或者可以在啟動參數中加入:-verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:./gc.log來記錄GC日誌。 GC中有一種情況叫做Full GC,以下幾種情況會觸發Full GC也叫MajorGC: 1.Tenured Space空間不足以創建打的對象或者數組,會執行FullGC,並且當FullGC之後空間如果還不夠,那麽會OOM:java heap space。 2.Permanet Generation的大小不足,存放了太多的類信息,在非CMS情況下回觸發FullGC。如果之後空間還不夠,會OOM:PermGen space。 3.CMS GC時出現promotion failed和concurrent mode failure時,也會觸發FullGC。promotion failed是在進行Minor GC時,survivor space放不下、對象只能放入舊生代,而此時舊生代也放不下造成的;concurrent mode failure是在執行CMS GC的過程中同時有對象要放入舊生代,而此時舊生代空間不足造成的。 4.判斷MinorGC後,要晉升到TenuredSpace的對象大小大於TenuredSpace的大小,也會觸發FullGC。 可以看出,當FullGC頻繁發生時,一定是內存出問題了。 三、JVM的數據格式規範和Class文件 1.數據類型規範 依據馮諾依曼的計算機理論,計算機最後處理的都是二進制的數,而JVM是怎麽把java文件最後轉化成了各個平臺都可以識別的二進制呢?JVM自己定義了一個抽象的存儲數據單位,叫做Word。一個字足夠大以持有byte、char、short、int、float、reference或者returnAdress的一個值,兩個字則足夠持有更大的類型long、double。它通常是主機平臺一個指針的大小,如32位的平臺上,字是32位。 同時JVM中定義了它所支持的基本數據類型,包括兩部分:數值類型和returnAddress類型。數值類型分為整形和浮點型。 整形:
byte | 值是8位的有符號二進制補碼整數 |
short | 值是16位的有符號二進制補碼整數 |
int | 值是32位的有符號二進制補碼整數 |
long | 值是64位的有符號二進制補碼整數 |
char | 值是表示Unicode字符的16位無符號整數 |
float | 值是32位IEEE754浮點數 |
double | 值是64位IEEE754浮點數 |
magic:
魔數,魔數的唯一作用是確定這個文件是否為一個能被虛擬機所接受的Class文件。魔數值固定為0xCAFEBABE,不會改變。
minor_version、major_version:
分別為Class文件的副版本和主版本。它們共同構成了Class文件的格式版本號。不同版本的虛擬機實現支持的Class文件版本號也相應不同,高版本號的虛擬機可以支持低版本的Class文件,反之則不成立。
constant_pool_count:
常量池計數器,constant_pool_count的值等於constant_pool表中的成員數加1。
constant_pool[]:
常量池,constant_pool是一種表結構,它包含Class文件結構及其子結構中引用的所有字符串常量、類或接口名、字段名和其它常量。常量池不同於其他,索引從1開始到constant_pool_count -1。
access_flags:
訪問標誌,access_flags是一種掩碼標誌,用於表示某個類或者接口的訪問權限及基礎屬性。access_flags的取值範圍和相應含義見下表:
this_class:
類索引,this_class的值必須是對constant_pool表中項目的一個有效索引值。constant_pool表在這個索引處的項必須為CONSTANT_Class_info類型常量,表示這個Class文件所定義的類或接口。
super_class:
父類索引,對於類來說,super_class的值必須為0或者是對constant_pool表中項目的一個有效索引值。如果它的值不為0,那constant_pool表在這個索引處的項必須為CONSTANT_Class_info類型常量,表示這個Class文件所定義的類的直接父類。當然,如果某個類super_class的值是0,那麽它必定是Java.lang.Object類,因為只有它是沒有父類的。
interfaces_count:
接口計數器,interfaces_count的值表示當前類或接口的直接父接口數量。
interfaces[]:
接口表,interfaces[]數組中的每個成員的值必須是一個對constant_pool表中項目的一個有效索引值,它的長度為interfaces_count。每個成員interfaces[i] 必須為CONSTANT_Class_info類型常量。
fields_count:
字段計數器,fields_count的值表示當前Class文件fields[]數組的成員個數。
fields[]:
字段表,fields[]數組中的每個成員都必須是一個fields_info結構的數據項,用於表示當前類或接口中某個字段的完整描述。
methods_count:
方法計數器,methods_count的值表示當前Class文件methods[]數組的成員個數。
methods[]:
方法表,methods[]數組中的每個成員都必須是一個method_info結構的數據項,用於表示當前類或接口中某個方法的完整描述。
attributes_count:
屬性計數器,attributes_count的值表示當前Class文件attributes表的成員個數。
attributes[]:
屬性表,attributes表的每個項的值必須是attribute_info結構。
四、一個java類的實例分析 為了了解JVM的數據類型規範和內存分配的大體情況,我新建了MemeryTest.java: 編譯為MemeryTest.class後,通過WinHex查看該文件,對應字節碼文件各個部分不同的定義,我了解了下面16進制數值的具體含義,盡管不清楚ClassLoader的具體實現邏輯,但是可以想象這樣一個嚴謹格式的文件給JVM對於內存管理和執行程序提供了多大的幫助。 運行程序後,我在windows資源管理器中找到對應的進程ID. 並且在控制臺通過jmap -heap 10016查看堆內存的使用情況: 輸出結果中表示當前java進程啟動的JVM是通過4個線程進行Parallel GC,堆的最小FreeRatio是40%,堆的最大FreeRatio是70%,堆的大小是4090M,新對象占用1.5M,Young Generation可以擴展到最大是1363M, Tenured Generation的大小是254.5M,以及NewRadio和SurvivorRadio中,下面更是具體給出了目前Young Generation中1.5M的劃分情況,Eden占用1.0M,使用了5.4%,Space占了0.5M,使用了93%,To Space占了0.5M,使用了0%。 下面我們通過jmap dump把heap的內容打印打文件中: 使用Eclipse的MAT插件打開對應的文件: 選擇第一項內存泄露分析報告打開test.bin文件,展示出來的是MAT關於內存可能泄露的分析。 從結果來看,有3個地方可能存在內存泄露,他們占據了Heap的22.10%,13.78%,14.69%,如果內存泄露,這裏一般會有一個比值非常高的對象。打開第一個Probem Suspect,結果如下: ShallowHeap是對象本身占用的堆大小,不包含引用,RetainedHeap是對象所持有的Shallowheap的大小,包括自己ShallowHeap和可以引用的對象的ShallowHeap。垃圾回收的時候,如果一個對象不再引用後被回收,那麽他的RetainedHeap是能回收的內存總和。通過上圖可以看出程序中並沒有什麽內存泄露,可以放心了。如果還有什麽不太確定的對象,則可以通過多個時間點的HeapDumpFile來研究某個對象的變化情況。 五、小結 以上便是我最近幾天對JVM相關資料的整理,主要圍繞他的基本組成和運行原理等,內存管理,基本數據類型和字節碼文件。JVM是一個非常優秀的JAVA程序,也是個不錯的規範,這次整理學習讓我對他有了更加清晰的認知,對Java語言的理解也更加加深。 這次學習過程,堅定了我對程序員發展的認知。知識一定要精,下一步我將邊工作邊仔細閱讀Oracle的3個版本的《JVM Specification》,並且結合實踐讓自己的Java基礎素養更上一個層次。(http://docs.oracle.com/javase/specs/)jvm虛擬機原理1