java虛擬機在執(zhí)行java程序的過程中會把他所管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域,運行時數(shù)據(jù)區(qū)分別為:程序計數(shù)器,java虛擬機棧,本地方法棧,java堆,方法區(qū)。還有一個非運行時數(shù)據(jù)區(qū):直接內(nèi)存(Direct Memory)。 1)程序計數(shù)器 每個線程創(chuàng)建的時候會有一個獨立的程序計數(shù)器,可以看成是線程執(zhí)行的字節(jié)碼的行號指示器,不同線程之間的程序計數(shù)器互不影響,存儲于程序計數(shù)器所在的內(nèi)存中,這塊內(nèi)存很小。線程私有 2)java虛擬機棧 每個方法被執(zhí)行的時候虛擬機都會創(chuàng)建一個棧幀,用于存儲局部變量表, 操作數(shù)棧,動態(tài)鏈接,方法出口等信息,一個方法被調(diào)用至結(jié)束,就對應(yīng)著棧幀在虛擬機棧中從入棧到出棧的過程。線程私有 局部變量表存放了編譯器可知的各種基本數(shù)據(jù)類型:boolean,byte,char,short,int,float,long,double,對象引用類型和returnAddress類型。對象引用類型可以是一個指向?qū)ο笃鹗嫉刂返闹羔槪部梢允侵赶蛞粋€代表對象的句柄。returnAddress類型指向一條字節(jié)碼指令地址。 3)本地方法棧 調(diào)用native方法所需要用的棧 4)java堆 java堆是虛擬機所管理的內(nèi)存中最大的區(qū)域,在java虛擬機啟動的時候創(chuàng)建。幾乎所有的對象實例以及數(shù)組都在堆中分配。堆是垃圾收集器管理的主要對象,又稱“GC堆”,如果要細分的話,java堆還可以分為Eden,From Survivor,To Survivor等。線程共享 5)方法區(qū) 方法區(qū)用于存儲被虛擬機加載的類信息,常量,靜態(tài)變量,即時編譯器編譯的代碼等,方法區(qū)又稱“非堆”,用于與堆區(qū)分。線程共享,對于方法區(qū)的內(nèi)存回收主要是對常量池的回收以及對對象的卸載。運行時常量池是方法區(qū)的一部分,用于存放編譯期生成的各種字面量以及符號引用。 直接內(nèi)存(Direct Memory)并不是java虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是java虛擬機規(guī)范中定義的內(nèi)存區(qū)域。JDK1.4中新加入了NIO(New Input/Output)類,引入了一種基于通道與緩沖區(qū)的I/O方式,它可以使用native函數(shù)庫直接分配對外內(nèi)存,然后通過一個存儲在java堆里面的DirectByteBuffer對象作為這塊內(nèi)存的引用進行操作,避免了java堆與native堆中來回復(fù)制數(shù)據(jù)。 垃圾收集器的種類有:Serial收集器,ParNew收集器,Parallel Scavenge收集器,Serial Old收集器,Parallel Old收集器,CMS收集器,G1收集器 1)Serial收集器 單線程,工作時需要暫停用戶所有線程,可以和CMS配合工作 2)ParNew收集器 多線程,工作時需要用戶暫停所有線程,可以和CMS配合工作 3)Parallel Scavenge收集器 Parallel Scavenge收集器的主要目標是達到可控制吞吐量。吞吐量就是CPU運行用戶代碼的時間占CPU消耗時間的比值,即吞吐量=運行用戶代碼的時間/(運行用戶代碼的時間+垃圾收集的時間)。-XX:MaxGCPauseMillis=M設(shè)置垃圾收集時間盡量在M毫秒完成,減低M值不一定可以提高吞吐量;-XX:GCTimeRatio=N用于設(shè)置允許最大的垃圾收集時間占總時間的1/(1+N),默認值為99 4)Serial Old收集器 Serial Old是老年代版本,可以與Parallel Scavenge收集器配合使用,或者作為CMS的后備預(yù)案 5)Parallel Old收集器 Parallel Old收集器是Parallel Scavenge收集器的老年代版本,Parallel Scavenge收集器不可以與CMS收集器配合使用,如果使用Serial Old收集器,性能會被拖累,所以可以選擇Parallel Old收集器 6)CMS收集器 CMS收集器(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。CMS的工作步驟:初始標記,并發(fā)標記,重新標記,并發(fā)刪除。在初始標記以及重新標記的時候需要暫停用戶所有線程,但是耗時非常短。初始標記就是標記直接與GC ROOT關(guān)聯(lián)的對象,并發(fā)標記就是標記從GC ROOT可以連通的對象,重新標記就是在并發(fā)標記的過程中因為用戶線程繼續(xù)運行導(dǎo)致標記變更的對象的記錄,最后并發(fā)清除未被標記的對象。 CMS收集器的優(yōu)點:并發(fā)收集,低停頓 CMS收集器的缺點:CMS對CPU比較敏感,默認線程數(shù)為(CPU數(shù)量+3)/4,無法處理浮動垃圾(垃圾回收過程中用戶線程沒有停止導(dǎo)致),標記清楚算法會產(chǎn)生空間碎片 7)G1收集器 G1收集器采用標記整理算法,沒有碎片;可以精確控制停頓,指定M毫秒的時間片段內(nèi),消耗在垃圾搜集上的時間不能超過N毫秒;可以在不犧牲吞吐量的情況下完成低停頓的垃圾回收,原理:把堆分成多個大小固定的獨立區(qū)域,并跟蹤垃圾的堆積程度,在后臺維護一個優(yōu)先級列表 垃圾收集器采用的算法: 1)標記清除算法 會產(chǎn)生大量的空間碎片 2)復(fù)制算法 減小了內(nèi)存,把內(nèi)存分成2分,一份保持完整,當另一份存儲不下的時候啟用 3)標記整理算法 4)分代搜集 把堆分成新生代以及老年代,新生代使用復(fù)制算法,老年代使用標記整理算法 java虛擬機沒有使用引用計數(shù)算法,而是使用根搜索算法來查找不可用對象。 引用計數(shù)算法:當一個對象被引用,引用計數(shù)器加1,當引用失效的時候,引用計數(shù)器減1,當引用計數(shù)器為0的時候,被認為是可收回對象。 根搜索算法:通過一系列的”GC ROOTS“對象作為起始點,從這些點向下搜索,不能喝GC ROOTS聯(lián)通的對象便是可回收對象 java中可以作為“GC ROOTS”的對象包括:虛擬機棧(棧幀中的本地變量表)中的引用對象,方法區(qū)中的類靜態(tài)屬性引用的對象,方法區(qū)中的常量引用對象,本地方法棧中JNI(Native方法)的引用對象。 引用的類型: 強引用:只要強引用還存在,GC永遠不會回收內(nèi)存 軟引用:對于軟引用關(guān)聯(lián)著的對象,當要發(fā)生內(nèi)存溢出異常之前,把這些對象列入回收范圍并進行第二次回收 弱引用:對于弱引用關(guān)聯(lián)著的對象,能下一次GC工作的時候,不管內(nèi)存是否足夠,都會被回收 虛引用:虛引用不會影響關(guān)聯(lián)著的對象的生存時間,也無法通過虛引用來獲取對象的實例,虛引用的唯一目的就是這個對象被GC回收時收到一個系統(tǒng)通知 垃圾收集器的執(zhí)行邏輯: 1)用根搜索算法進行第一次標記 2)對標記的對象進行一次篩選,如果對象覆蓋了finalize方法,并且沒有執(zhí)行過,那么有可能在這個finalize方法中再次使用這個對象,這樣的話就把該對象從待刪除的對象隊列中刪除。finalize方法只能執(zhí)行一次,保證對象循環(huán)引用的時候不可能永遠存在。 3)java虛擬機創(chuàng)建一個低優(yōu)先級的Finalize線程去執(zhí)行對象覆蓋的finalize方法,垃圾收集器對待刪除隊列再次進行標記 4)垃圾收集器把兩次標記的對象回收了 =========================================================================== 下面我們來看下GC日志: ![]() 1 package com.froest.excel; 2 3 public class Test2 { 4 private static int _1M = 1024 * 1024; 5 6 /** 7 * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 8 * 9 * @param args 10 */ 11 public static void main(String[] args) { 12 // TODO Auto-generated method stub 13 byte[] alloc1, alloc2, alloc3, alloc4; 14 alloc1 = new byte[2 * _1M]; 15 alloc2 = new byte[2 * _1M]; 16 alloc3 = new byte[2 * _1M]; 17 // alloc4 = new byte[4 * _1M];//發(fā)生Minor GC 18 } 19 } 上面代碼運行以后得到下面的日志: -verbose:gc 打印垃圾收集器的執(zhí)行日志 -Xms20M 最小堆的大小 -Xmx20M 最大堆的大小 -Xmn10M 年輕代的大小 -XX:SurvivorRatio=8 設(shè)置Eden與Survivor的大小比例,默認8:1 以上參數(shù)得知該程序跑的虛擬機為堆的大小為20M,年輕代為10M,老年代10M,年輕代中Eden=8M,F(xiàn)rom Survivor = To Survivor = 1M, alloc1,alloc2,alloc3總共6M,Eden區(qū)域足夠容納,不觸犯Minor GC(年輕代垃圾回收)。 下面把alloc4注釋去掉運行: DefNew是指垃圾收集器的類型,6487K->148K指Minor GC前后年輕代的大小,9216為年輕代的總大小,6487K->6292K指Minor GC前后jvm堆的大小,19456指jvm堆的總大小,0.0041845 secs指Minor GC耗時,user=0.00指用戶耗時,sys=0.0指系統(tǒng)耗時,real=0.00指Minor GC實際耗時。def new generation total 9216K,used 4408K指年輕代堆的大小為9216K,使用了4408K;其中Eden區(qū)占了8192K,使用了52%(alloc4所占的內(nèi)存),from space(From Survivor,使用了14%)以及to space(to Survivor)各占1024K;tenured generation total 10240K,used 6144K指的是老年代的大小為10240K,用了6144K(alloc1,alloc2,alloc3),使用60%。 alloc1,alloc2,alloc3執(zhí)行以后Eden區(qū)就有6M,最大只有8M,當alloc4=4M想要放入的時候,Eden區(qū)大小不夠,所以觸發(fā)Minor GC,發(fā)現(xiàn)alloc1,alloc2,alloc3都是存活對象,有發(fā)現(xiàn)Survivor的空間只有1M,不夠大,所以利用空間分配擔保機制,把alloc1,alloc2,alloc3存放入老年代,老年代占用60%,alloc4就放入了Eden區(qū)。 下面的2張圖可以很好的解釋各個參數(shù)(原址:http://blog.csdn.net/alivetime/article/details/6895537): Minor GC: Full/Marjor GC: 配置參數(shù)-XX:PretenureSizeThreshold=3M,可以使大于3M的對象直接進入老年代 如果Survivor空間相同年齡的對象的大小總和大于Survivor空間的一半,那么大于等于該年齡的對象就直接進入老年代 空間分配擔保: 每次Minor GC執(zhí)行完以后,虛擬機會檢查之前晉升到老年代的平均大小是否大于老年代的剩余空間大小,如果大于,則發(fā)起一次Full/Major GC,如果小于,則查看HandlePromotionFailure值是否允許擔保失敗,如果允許,那只會進行一次Minor GC。如果不允許失敗,那么也要改為進行一次Full/Major GC 之前晉升老年代的平均大小大于老年代的剩余空間大小 ![]() 1 package com.froest.excel; 2 3 public class Test2 { 4 private static int _1M = 1024 * 1024; 5 6 /** 7 * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 8 * 9 * @param args 10 */ 11 public static void main(String[] args) { 12 // TODO Auto-generated method stub 13 byte[] alloc1, alloc2, alloc3, alloc4, alloc5; 14 alloc1 = new byte[2 * _1M]; 15 alloc2 = new byte[2 * _1M]; 16 alloc3 = new byte[2 * _1M]; 17 alloc4 = new byte[4 * _1M];// 發(fā)生Minor GC 18 alloc2 = null; 19 alloc3 = null; 20 alloc4 = null; 21 alloc5 = new byte[6 * _1M]; 22 } 23 } 日志如下: [GC [DefNew: 6487K->148K(9216K), 0.0049804 secs] 6487K->6292K(19456K), 0.0050137 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 這段代碼總共發(fā)生了2次GC,alloc4 = new byte[4 * _1M];這個時候發(fā)生一次,因為Eden區(qū)內(nèi)存不夠用了,這次內(nèi)存回收會把Eden區(qū)的所有的存活的對象轉(zhuǎn)移到老年代,并把alloc4存放入Eden區(qū),此時Eden區(qū)4M,Survivor區(qū)0M,老年區(qū)6M;第二次GC發(fā)生在最后一個alloc1 = new byte[6 * _1M],Eden區(qū)占有4M,不夠用,所以Eden區(qū)的存活對象要進入老年代,老年代之前晉升了6M,大于剩余空間2M,所以此時發(fā)生一次Full/Major GC,因為alloc2,alloc3,alloc4設(shè)置為null,所以在這次Full/Major GC中被清理了,老年代只剩下2M,新的alloc1=6M進入Eden區(qū)。 從日志中可以看到我的分析結(jié)果:Eden區(qū)占用了77%,大約6M,tenured generation占21%大約2M,為什么不是精確的相等,我的理解是垃圾收集器線程運行本身會耗費一定量的資源,所以不會嚴格的相等。 之前晉升老年代的平均大小小于老年代的剩余空間大小,并且關(guān)閉空間擔保機制-XX:-HandlePromotionFailure,但是我在機器上跑出來的日志顯示,java虛擬機并沒有進行一次Full/Major GC?。?!下面是我的代碼: ![]() 1 package com.froest.excel; 2 3 public class Test2 { 4 private static int _1M = 1024 * 1024; 5 6 /** 7 * -verbose:gc -Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8 -XX:+PrintGCDetails 8 * -XX:-HandlePromotionFailure 9 * 10 * @param args 11 */ 12 public static void main(String[] args) { 13 // TODO Auto-generated method stub 14 byte[] alloc1, alloc2, alloc3, alloc4, alloc5; 15 alloc1 = new byte[1 * _1M]; 16 alloc2 = new byte[1 * _1M]; 17 alloc3 = new byte[1 * _1M]; 18 alloc4 = new byte[6 * _1M];// 發(fā)生Minor GC 19 alloc2 = null; 20 alloc3 = null; 21 alloc1 = null; 22 alloc5 = new byte[5 * _1M]; 23 } 24 } GC日志: 代碼分析: alloc1,alloc2,alloc3 分別分配1M,當alloc4=6M的時候Eden區(qū)不夠大,并且Survivor區(qū)也不夠大,直接將alloc1,alloc2,alloc3分配入老年代,alloc4分配入Eden區(qū),alloc1,alloc2,alloc3設(shè)置為null,alloc5=5M的時候Eden區(qū)已經(jīng)存有6M的alloc4,所以不夠大,要存入老年代,首先判斷之前晉升入老年代的平均大小3M小于老年代的剩余空間7M,又因為我關(guān)閉了空間擔保機制,所以應(yīng)該會出現(xiàn)一次Full/Major GC,但是從日志來看并沒有出現(xiàn)Full/Major GC,還是只進行了一次Minor GC,這里不理解,哪位大牛知道請指導(dǎo)下,萬分感謝。繼續(xù)摸索??!看下面的2個代碼以及日志的比較,應(yīng)該就是問題的答案了。 看看下面代碼,我們是否查詢到一些端倪 ![]() 1 public class Test2 { 2 private static int _1M = 1024 * 1024; 3 4 /** 5 * -verbose:gc -Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8 -XX:+PrintGCDetails 6 * -XX:-HandlePromotionFailure 7 * 8 * @param args 9 */ 10 public static void main(String[] args) { 11 // TODO Auto-generated method stub 12 byte[] alloc1, alloc2, alloc3, alloc4, alloc5,alloc6; 13 alloc1 = new byte[1 * _1M]; 14 alloc2 = new byte[1 * _1M]; 15 alloc3 = new byte[1 * _1M]; 16 // alloc6 = new byte[1 * _1M]; 17 alloc4 = new byte[6 * _1M];// 發(fā)生Minor GC 18 alloc1 = null; 19 alloc2 = null; 20 alloc3 = null; 21 alloc4 = null; 22 alloc5 = new byte[5 * _1M]; 23 } 24 } gc日志: [GC [DefNew: 3415K->148K(9216K), 0.0025138 secs] 3415K->3220K(19456K), 0.0025390 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 把alloc6的注釋去掉,執(zhí)行以后的gc日志為: [GC [DefNew: 4439K->148K(9216K), 0.0032273 secs] 4439K->4244K(19456K), 0.0032624 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 通過以上2個代碼以及日志的比較是不是可以得出結(jié)論: 當之前的每次晉升到老年代的平均大小小于老年代的剩余空間大小的時候,如果設(shè)置不允許擔保失敗,并且老年代的剩余空間大小不足以存放本次晉升的對象的大小,那么會觸發(fā)一次Full/Major GC,如果老年代的剩余空間大小足以存放本次晉升的對象的大小,那么就會執(zhí)行Minor GC。 ![]() 1 package com.froest.excel; 2 3 public class Test2 { 4 private static int _1M = 1024 * 1024; 5 6 /** 7 * -verbose:gc -Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8 -XX:+PrintGCDetails 8 * -XX:-HandlePromotionFailure 9 * 10 * @param args 11 */ 12 public static void main(String[] args) { 13 // TODO Auto-generated method stub 14 byte[] alloc1, alloc2, alloc3, alloc4, alloc5; 15 alloc1 = new byte[2 * _1M]; 16 alloc2 = new byte[2 * _1M]; 17 alloc3 = new byte[2 * _1M]; 18 alloc4 = new byte[6 * _1M];// 發(fā)生Minor GC 19 alloc5 = new byte[2 * _1M]; 20 } 21 } 日志: 不管有沒有啟用擔保失敗機制,最后的2M內(nèi)存都會被分配入老年代。 想知道空間擔保機制做運作,所以想先把空間擔保機制關(guān)閉,如果之前晉升老年代的平均大小小于老年代的剩余空間大小,那么會直接發(fā)生Full/Major GC,如果把空間擔保機制開啟,那么就不會發(fā)生Full/Major GC,會直接執(zhí)行一次Minor GC。 如果有問題或者哪里寫的不對的地方,請各位大牛斧正,不勝感謝! jvm參數(shù)優(yōu)化參考:http://unixboy./blog/174173 jvm參數(shù)列表:http:///docs/java6-jvm-options-chinese-edition.htm
揚帆起航,生命正式從這里開始... 愛情終將消失于茫茫的時間洪流之中,沉淀于厚重的黃泥沙丘之下... |
|