對于JAVA編程和很多類似C、C++語言有一個巨大區別就是內存不需要自己去free或者delete,而是由JVM垃圾回收機制去完成的。對于這個過程很多人一直比較茫然或者覺得很智能,使得在寫程序的過程不太考慮它的感受,其實知道一些內在的原理,幫助我們編寫更加優秀的代碼是非常有必要的。
本文從以下幾個方面進行闡述:
1、finalize()方法
2、System.gc()方法及一些實用方法
3、JAVA如何申請內存,和C、C++有何區別
4、JVM如何尋找到需要回收的內存
5、JVM如何回收內存的(回收算法分解詳述)
6、應用服務器部署及常用參數設置
7、擴展話題JIT(及時編譯技術)與lazy evaluation(惰性評估)
1、finalize()方法: 為了說明JVM回收,不得不先說明一個問題就是關于finalize()方法,所有實體對象都會有這個方法,因為這個Object類定義的,這個可能會被認為是垃圾回收的方法或者叫做析構函數,其實并非如此。finalize在JVM內存回收前會被調用(但并非絕對),而即使不調用它,JVM回收機制通過后面所述的一些算法就可以定位哪些是垃圾內存,那么這個拿來干什么用呢?
finalize()其實是要做一些特殊的內存回收操作,如果對JAVA研究稍微多一點,大家會發現JAVA中有一種JNI的機制,即:Java native interface,這種屬于JAVA本地接口調用,即調用本地的其他語言信息,JAVA虛擬機底層掉調用也是這樣實現的,這部分調用中可能存在一些對C、C++語言的操作,在C和C++內部通過new、malloc、realloc等關鍵詞創建的對象垃圾回收機制是無能為力的,因為這不是它要管理的范圍,而平時這些對象可能被JAVA對應的實體所調用,那么需要在對應JAVA對象放棄時(并不代表回收,只是程序中不使用它了)去調用對應的C、C++提供的本地接口去釋放這段內存信息,他們的釋放同樣需要通過free或delete去釋放,所以我們一般情況下不要濫用finalize(),可能你會聯想到另一類某些特殊引用對象的釋放,如層數引用太多,JAVA虛擬機有些時候不知道這一線的對象是否都可能被回收那么,你可以自己將finalize()重寫,并將內置對象的句柄先釋放掉,這樣也是沒有問題的,不過一般不要濫用而已。2、System.gc()或者Runtime.getRuntime().gc();
這個可以被認為是強制垃圾回收的一種機制,但是并非強制回收,只是向JVM建議可以進行垃圾回收,而且垃圾回收的地方和多少是不能像C語言一樣控制,這是JVM垃圾回收機去控制的。程序中盡量不要是去使用這些東西,除自己開發一些管理代碼除外,一般由JVM自己管理即可。
這里順便提及幾個查看當前JVM內存的幾個方法(在同一個集群下的多個SERVER,即使在同一個機器上通過下面方法只能查看到當前SERVER下的內存情況):
2.1.設置的最大內存:-Xmx等值:
(Runtime.getRuntime().maxMemory()/ (1024 * 1024)) + "MB"
2.2.當前JVM可使用的內存,這個值初始化和-Xms等值,若加載東西超過這個值,那么以下值會跟著變大,不過上限為-Xmx,由于變動過程中需要由虛擬機向操作系統申請新的內存,所以存在不連續內存以及影響開銷,很多時候應用軟件服務器提供商(如BEA)推薦是-Xms等價于-Xmx的值:
(Runtime.getRuntime().totalMemory()/ (1024 * 1024)) + "MB"
2.3.剩余內存,在當前可使用內存基礎上,剩余內存等價于其剪掉使用了的內存容量:
(Runtime.getRuntime().freeMemory()/ (1024 * 1024)) + "MB"
同理如果要查看使用了多少內存或者百分比??梢酝ㄟ^上述幾個參數進行運算查看到。。。。
順便在這里提供幾個實用方法和類,這部分可能和JVM回收關系不大,不過只是相關推敲,擴展知識面,而且也較為實用的東西:
2.4.獲取JAVA中的所有系統級屬性值(包含虛擬機版本、操作系統、字符集等等信息):
System.setProperty("AAA", "123445");
Properties properties = System.getProperties();
Enumeration<Object> e = properties.keys();
while (e.hasMoreElements()) {
String key = (String) e.nextElement();
System.out.println(key + " = " + properties.getProperty(key));
}
2.5.獲取系統中所有的環境變量信息:
Map<String, String> env = System.getenv();
for (Iterator<String> iterator = env.keySet().iterator(); iterator.hasNext();) {
String key = iterator.next();
System.out.println(key + " = " + env.get(key));
}
System.out.println(System.getenv("CLASSPATH"));
2.6.在Win環境下,打開一個記事本和一個WORD文檔:
try {
Runtime.getRuntime().exec("notepad");
Runtime.getRuntime().exec("cmd /c start Winword");
}catch(Exception e) {
e.printStackTrace();
}
2.7.查詢當前SERVER下所有的線程信息列表情況(這里需要提供兩個步驟,首先要根據任意一個線程獲取到頂級線程組的句柄(有關線程的說明,后面專門會有一篇文章說明),然后通過頂級線程組得到其存在線程信息,進行一份拷貝,給與遍歷):
2.7.1.這里通過當前線程得到頂級線程組信息:
public static ThreadGroup getHeadThreadGroup() {
Thread t = Thread.currentThread();
ThreadGroup group = t.getThreadGroup();
while(group.getParent() != null) {
group = group.getParent();
}
return group;
}
2.7.2.通過得到的頂級線程組,遍歷存在的子元素信息(僅僅遍歷常用屬性):
public static void disAllThread(ThreadGroup threadgroup) {
Thread list[] = new Thread[threadgroup.activeCount()];
threadgroup.enumerate(list);
for(Thread thread:list) {
System.out.println(thread.getId()+"\t"+thread.getName()
+"\t"+thread.getThreadGroup()+"\t"
+thread.getState()+"\t"+thread.isAlive());}
}
2.7.3.測試方法如:
類名.disAllThread(getHeadThreadGroup());即可完成,第一個方法帶有不斷向上查詢的過程,這個過程可能在一般情況下也不會太慢,不過我們最好將其記錄在一個地方,方便我們提供管理類來進行直接管理,而不需要每次去獲取,對外調用都是封裝的運行過程而已。
好,回到話題,繼續說明JVM垃圾回收機制的信息,下面開始說明JAVA申請內存、回收內存的機制了。
3、JAVA如何申請內存,和C、C++有何區別。
在上一次縮寫的關于JAVA集合類文章中其實已經有部分說明,可以大致看到JAVA內部是按照句柄指向實體的過程,不過這是從JAVA程序設計的角度去理解,如果我們需要更加細致的問一個問題是:JVM垃圾回收機制是如何知道哪些內存是垃圾內存的?JVM為什么不在平時就去回收內存,而是要等到內存不夠用的時候才會去回收內存?不得不讓我進一步去探討JAVA是如何細節的申請內存的。
從編程思想的角度來說,C、C++new申請的內存也是通過指針指向完成,不過你可以看成是一個地球板塊圖,在這些板塊中,他們去new的過程中,就是好比是找一個版塊,因為C、C++在申請內存的過程中,是不斷的free和delete操作,所以會產生很多內存的碎片操作,而JAVA不是,JAVA只有內存不夠用的時候才會去回收(回收細節講會在文章后面介紹),也就是說,可以保證內存在一定程度上是連續的。從某種意義上將,只要下一塊申請的內存不會到頭,就可以繼續在上一塊申請內存的后面緊跟著去申請內存,那么從某種意義上講,其申請的開銷可能可以和C++媲美。那么JAVA在回收內存后,內存還能是連續的嘛。。。。我們姑且這樣去理解,在第五節會說明。。繼續深入話題:
在啟動weblogic的時候,如果打開任務管理器,可以馬上發現,內存被占用了最少-Xms的大小,一個說明現象就是JVM首先將內存先占用了,然后再分配給其對象的,也就是說我們所謂的new可以理解為在堆上做了一個標記,所以在一定程度上做連續分配內存是可以實現的,只是你會發現若要真正實現連續,必然導致一定程度上的序列化,所以new的開銷一般還是蠻大的,即使在后面說的JVM會將內存分成幾個大塊來完成操作,但是也避免不了序列化的過程。
在這里一個小推敲就是,一個SERVER的管理內存范圍一般不要太大(一般在1~2G一個SERVER),推薦也不要太大,因數去考慮:
1、JAVA虛擬機回收內存是在不夠用的時候再去回收,這個不夠用何以說明,很多時候因為計算上的失誤導致內存溢出。
2、如果一個主機只有2G左右內存,很少的CPU,那么一個JVM也好,但是如果主機很好,如32G內存,那么這樣做未必有點過,第一發揮不出來,一個JVM管這么大塊內存好像有點過,還有內存不夠用去回收這么大塊內存(回收內存時一般需暫停服務),需要花時間,第二舉個很現實的例子,一個學校如果只有20~30人,一個人可以既當校長又當老師,如果一個學校有幾百上千人,我想這個人再大的能力忙死也管不過來,而且會出亂子,此時它要請班主任來管了。
3、對于大內存來說,使用多個SERVER完成負載均衡,一個暫停服務回收內存,另一個還可以運行嘛。
4、JVM如何尋找到需要回收的內存:
4.1、 引用計數算法:在早期的JAVA虛擬機中,他們采用思維上最為常用的算法,就是計數器,在對象被使用“=”給與句柄的過程中(在集合類內部雖然外部調用通過add或者put完成,不過內部仍然是這樣),它同時會告訴JAVA虛擬機,這個對象被增加一次引用(注意一個句柄如果已經存在指向的實體,此時指向另一個實體,它也會同時告訴JAVA虛擬機以前指向那個實體少了一個引用,不然這就亂了),當你使用句柄=null的時候,它會告訴JAVA虛擬機這個指向的實體少了一個引用,這個計數器并不是記錄在實體本省,而是被JVM私有化管理起來,這部分也是JVM垃圾回收機的基礎信息(私有化管理部分為JVM中的永久域,為所有靜態常量的管理池,如:public class的代碼段、static代碼段、static變量、String常量數組的一份拷貝等等),JVM回收內存的時候,就會去找計數器中位0的元素,將其回收(回收過程,在第五節說明),如果有級聯引用的,如果父親級別的引用被回收后,子對象的引用數會自動減少1。
問題出來了:
對于A對象有B對象的引用、B對象有A對象的引用,如果兩個引用都沒有程序員去手動去=null操作,那么實用引用計數算法,將永遠計算不出來他們是需要被回收的內存,這就是一種在使用引用計數器中的JAVA內存泄露問題(對于這類內存泄露屬于引用計數上,使用樹結構遍歷是沒有問題的,同樣在引用計數算法上,存在多層集合類級聯引用的問題可能不會被回收到;另一類是流的內存泄露(連接的內存泄露也屬于其中),對于這類對象,進行特殊的管理,內部有一個管理器,如連接數據有一個DriverManager,他們永遠都保存著對連接的引用,如果直接使用JDBC操作,使用完然后做close操作,相當于告訴連接管理器斷開并可以釋放連接對象,反過來說,如果不做Close操作,系統永遠不知道這塊資源信息是垃圾內存,永遠不會回收它,直到資源耗盡內存溢出為止,直接將句柄=null,對于連接對象是無效)。
這個引用計數需要遍歷整塊JVM才知道哪些需要回收,哪些不需要回收,太慢了。。。。
4.2.樹結構遍歷算法:其實為什么JAVA虛擬機可以管理其區域下的內存,因為我們是在JVM內部去創建內存,所以可以理解為打一個TAG,那么它必然有一個根引用(靜態區)通過分類的管理機制遍歷到所使用的每一個堆空間中,此時這棵很大的樹上,進行遍歷下來得到的全部是活著的結點。那么沒有被遍歷到的全部是沒有活著的結點,對于大量內存需要回收的情況(很多情況下是的,因為業務級的請求用完這塊內存就沒用了),我們很快可以知道哪些內存是活著的,在后面回收時對應其快速復制的過程,另一個區域就作為自動作為垃圾內存了,在JDK1.4.2后開始逐步提出并行回收算法(包含了并行遍歷算法,內部包含了后面說的需要將內存分塊管理,而并非完全是一個整體)。
現在的JVM根據實際情況會采用一種自適應的算法去尋找垃圾內存,它會按照上述兩種算法進行分別管理,當發現樹結構的開銷較大的時候(大部分是不需要回收的內存,由于樹的遍歷要么通過遞歸消耗代碼段并在時間開銷上很大以外或者利用一個緩沖區來遍歷,比順序遍歷要慢很多,這種情況其實很少,如果真是這樣,內存很容易溢出),所以此時它會自適應的去采用引用計數算法去找需要回收的內存部分。
5、JVM如何回收內存的(回收算法分解詳述):
首先了解幾個其他的概念:
5.1.平時所說的JDK,其實是JAVA開發工具的意思,安裝JAVA虛擬機會產生兩個JRE目錄,JRE目錄為JAVA運行時環境的意思,兩個JRE目錄的區別是其中在JDK所在的JRE目錄下沒有Server和Client文件夾(JDK1.5自動安裝包會自動將其復制到JDK下面一份),JRE為運行時環境,提供對JVM操作的API,JVM內部通過動態鏈接庫(就是配置PATH的路徑下),通過它作為主動態鏈接庫尋找到其它的動態鏈接庫,動態鏈接庫為何OS綁定的參數,即代碼最終要通過這些東西轉換為X86指令集進行運行,另一個核心工具為JIT(JAVA即時編譯工具),用于將代碼轉換為對應操作系統的運行指令集合的過程,不過其與惰性評估形成對比,后面會專門介紹。
5.1.JVM首先將大致分為:JVM指令集、JVM存儲器、JVM內存(堆棧區域部分)、JVM垃圾回收區域;JVM的堆部分又一般分為:新域、舊域、永久域(很多時候不會認為段是堆的一部分,因為它是永遠不會被回收的,它一般包含class的定義信息、static定義的方法、static匿名塊代碼段、常量信息(較為典型的就是String常量),不過這塊內存也是可以被配置的);新域內部又可以分為Eden和兩個救助區域,這幾個對象在JVM內部有一定的默認值,但是也是可以被設置的。
當新申請的對象的時候,會放入Eden區中(這個區域一般不會太大),當對象在一定時間內還在使用的時候,它會逐步的進入舊域(此時是一個內存復制的過程,舊區域按照順序,其引用的句柄也會被修改指向的位置),JVM回收中會先將Eden里面的內存和一個救助區域的內存就會被賦值到另一個救助區域,然后對這兩塊內存進行回收,同理,舊區域也有一個差不多大小的內存區域進行被復制,這個復制的過程肯定就會在一定程度上將內存連續的排列起來;另外可以想到JAVA提供內存復制最快的就是System.arrayCopy方法,那么這個肯定是按照內存數組進行拷貝(JVM起始就是一個大內存,本身就可以成是幾個大數組組成的,而這個拷貝方法,默認拷貝多長呢,其實數組最長可以達到多少,通過數組的length返回的是int類型數據就可以清楚發現,為int類型的上限1<<30的長度(理想情況,因為有可能因為操作系統的其他進程導致JVM內存本身就不是連續的),即在2G*單元內存長度,所以也在一定程度上說明我們的一個JVM設置內存不要太大,不然復制內存的過程開銷是很大的)。
其實上述描述的是一種停止-復制回收算法,在這個過程中形成了幾個大的內存來回倒,這必然是很惡心的事情,那么繼續將其切片為幾個大的板塊,有些大的對象會出現一兩個對象占用一個版塊的現象,這些大對象基本不會怎么移動(被回收就是另一回事,因為會清空這個版塊),板塊之間有一些對應關系,在回收時先將一些版塊的小對象,向另一個還未裝滿的大板塊內部轉移,復制的粒度變小了,另外管理上可以發揮多線程的優勢所在,好比是將一塊大的田地,分成很多小田地,每塊田地種植不同檔次的秧苗,將其劃分等級,我們假如秧苗經常會隨機的死掉一些(這塊是垃圾),在清理一些很普通的秧苗田地的時候,可能會將其中一塊或幾塊田地的(活著的秧苗)種植到另一塊田地中,但是他們不可以將高檔次的秧苗移植到低檔次的田地中,因為高檔次的秧苗造價太高(內存太大),移植過程中代價太大,需要使用非普通秧苗的手段要移動他們,所以基本不移動他們,除非豐收他們的時候(他們也成為垃圾內存的時候),才會被拔出,騰出田地來。在轉移秧苗的過程中,他們需要整理出順序便于管理,在很多書籍上把這個過程叫做壓縮,因為這樣使得保證在只要內存不溢出的情況下,申請的對象都有足夠的控件可以存放,不然零碎的空間中間的縫隙未必可以存放下一個較大的對象。將內存分塊管理就是另一個停止復制收集器的進一步升級:增量收集思想。
5.2.回收過程,垃圾回收機制一般分為:標記—清除、標記—壓縮、停止—復制、增量收集、分代收集、并發收集和并行收集。
標記清除收集器,一般依賴于第一種尋找垃圾的算法去尋找,當尋找到需要使用的內存,會打一個TAG,此時未標記的對象,就會被該收集器回收,一般先停止外部服務,并且是單線程的去尋找并清除。
標記壓縮收集器,和上述收集器唯一區別就是多一步壓縮操作,壓縮操作刪除前或刪除后去操作是將標記為正在使用的對象復制到一塊新的內存中,也就是大致上是按照順序排列的。
停止復制收集器、增量收集器上述描述的較多,這里就不再多說了,總之是來回倒這些內存,為了介于內存,在其基礎上按照一定規則進行內存分塊操作。
并發收集器,這里一定要明確并發和并行的概念不同之處,并發收集器是可以再內存回收的過程中不暫停服務,也就是不影響運行,類似上述的收集器,要進行壓縮空間等操作不得不暫停服務保證系統的正常運行;可以思考到它基本會建立在首先將內存分塊的基礎上,可能會更細,它獨立的運行并和應用程序同時運行,回收的方式也是和上面一樣,只是它復制的時候只是較小部分內存的復制,所以其他業務系統運行照常,粒度很小,最好的情況就是這塊內存99%是需要回收的,它正在回收這塊內存,關于剩下的1%的內存應用服務被暫停,其余其它塊的內存照常運行不受到影響。
并行收集器是使用上述某類收集方法,但是使用多線程算法進行回收,在服務器應用中,利用多CPU進行回收可以顯著提高性能,此時需要進行相關的配置,在第六節中有詳細的說明。
對于上述了解后,可能對于不同的應用服務器有不同的JVM垃圾查找算法和回收算法,但是大致不離其中,根據實際情況,進行服務器的相關調試就可以在一定程度上提高服務器的運行性能,第六節就詳細說明下。
6、應用服務器部署及常用參數設置:
說到JVM的配置,最常用的兩個配置就是:
-Xms512m –Xmx1024m
-Xms設置JVM的初始化內存大小,-Xmx為最大內存大小,當突破這個值,將會報內存溢出,導致的原因有很多,主要是虛擬機的回收問題以及程序設計上的內存泄露問題;由于在超過-Xms時會產生頁面申請的開銷,所以一般很多應用服務器會推薦-Xms和-Xmx是等值的;最大值一般不保持在主機內存的75%的內存左右(多個SERVER是加起來的內存),當JVM絕大部分時間處于回收狀態,并且內存長時間處于非常長少的狀態就會報:java.lang.OutOfMemoryError:Java heap space的錯誤。
上面提及到JVM很多的知識面,很顯然你想去設置一下其它的參數,其實對于JVM設置的參數有上百個,這里就說一些較為常用配置即可。
JVM內存配置分兩大類:
1、-X開頭的參數信息:一般每個版本變化不大。
2、-XX開頭的參數信息:版本升級變化較大,如果沒有太大必要保持默認即可。
3、另外還有一個特殊的選項就是-server還是-client,他們在默認配置內存上有一些細微的區別,直接用JDK運行程序默認是-client,應用服務器生產模式一般只會用-server。
這些命令其實就是在運行java命令或者javaw等相關命令后可以配置的參數,如果不配置,他們有相應的默認值配置。
1、-X開頭的常用配置信息:
-Xnoclassgc 禁用垃圾回收,一般不適用這個參數
-Xincgc 啟用增量垃圾回收
-Xmn1024K Eden區初始化JAVA堆的尺寸,默認值640K
-Xms512m JAVA堆初始化尺寸,默認是32M
-Xmx512m JAVA堆最大尺寸,默認64M,一般不超過2G,在64位機上,使用64位的JVM,需要操作系統進行unlimited方可設置到2G以上。
2、-XX開頭常用內存配置信息:
-XX:-DisableExplicitGC 將會忽略手動調用GC的代碼,如:System.gc(),將-DisableExplicitGC, 改成+DisableExplicitGC即為啟用,默認為啟用,什么也不寫,默認是加號,但是系統內部默認的并不是什么都啟用。
-XX:+UseParallelGC 將會自動啟用并行回收,多余多CPU主機有效,默認是不啟用。
-XX:+UseParNewGC 啟用并行收集(不是回收),也是多CPU有效。
-XX:NewSize=128m 新域的初始化尺寸。
-XX:MaxNewSize=128m 新創建的對象都是在Eden中,其屬于新域,在-client中默認為640K,而-server中默認是2M,為減少頻繁的對新域進行回收,可以適當調大這個值。
-XX:PerSize=64m 設置永久域的初始化大小,在WEBLOGIC中默認的尺寸應該是48M,一般夠用,可以根據實際情況作相應條調整。
-XX:MaxPerSize=64m 設置永久域的最大尺寸。
另外還可以設置按照區域的比例進行設置操作,以及設置線程、緩存、頁面大小等等操作。
3、-XX開頭的幾個監控信息:
-XX:+GITime 顯示有多少時間花在編譯代碼代碼上,這部分為運行時編譯為對應機器碼時間。
-XX:+PrintGC 打印垃圾回收的基本信息
-XX:+PrintGCTimeStamps 打印垃圾回收時間戳信息
-XX:+PrintGCDetails 打印垃圾回收的詳細信息
-XX:+TraceClassLoading 跟蹤類的加載
-XX:+TraceClassResolution 跟蹤常量池
-XX:+TraceClassUnLoading 跟蹤類卸載
等等。。。。。。
例子:
編寫一個簡單的JAVA類:
public class Hello {
public static void main(String []args) {
byte []a1 = new byte[4*1024*1024];
System.out.println("第一次申請");
byte []a2 = new byte[4*1024*1024];
System.out.println("第二次申請");
byte []a3 = new byte[4*1024*1024];
System.out.println("第三次申請");
byte []a4 = new byte[4*1024*1024];
System.out.println("第四次申請");
byte []a5 = new byte[4*1024*1024];
System.out.println("第五次申請");
byte []a6 = new byte[4*1024*1024];
System.out.println("第六次申請");
}
}
此時運行程序,這樣調試一下:
C:\>java -Xmn4m -Xms16m -Xmx16m Hello
第一次申請
第二次申請
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
此時內存溢出,因為這些空間都有釋放,16M空間正好一半8M就溢出了,和我們的理論較吻合的就是一半在使用一半的大小就會崩掉,顯示的使用值會成倍增加。
那么我們將程序修改一下再看效果:
public class Hello {
public static void main(String []args) {
byte []a1 = new byte[4*1024*1024];
System.out.println("第一次申請");
a1 = new byte[4*1024*1024];
System.out.println("第二次申請");
a1 = new byte[4*1024*1024];
System.out.println("第三次申請");
a1 = new byte[4*1024*1024];
System.out.println("第四次申請");
a1 = new byte[4*1024*1024];
System.out.println("第五次申請");
a1 = new byte[4*1024*1024];
System.out.println("第六次申請");
}
}
運行程序如下:
C:\>javac Hello.java
C:\>java -Xmn4m -Xms16m -Xmx16m Hello
第一次申請
第二次申請
第三次申請
第四次申請
第五次申請
第六次申請
程序正常下來了,說明中途進行了垃圾回收的動作,我們想看下垃圾回收的整個過程,如何看,把上面的參數搬下來:
E:\>java -Xmn4m -Xms16m -Xmx16m -XX:+PrintGCDetails Hello
第一次申請
第二次申請
[GC [DefNew: 189K->133K(3712K), 0.0014622 secs][Tenured: 8192K->4229K(12288K), 0.0089967 secs] 8381K->4229K(16000K), 0.0110011 secs]
第三次申請
[GC [DefNew: 0K->0K(3712K), 0.0004749 secs][Tenured: 8325K->4229K(12288K), 0.0083114 secs] 8325K->4229K(16000K), 0.0092936 secs]
第四次申請
[GC [DefNew: 0K->0K(3712K), 0.0003168 secs][Tenured: 8325K->4229K(12288K), 0.0081516 secs] 8325K->4229K(16000K), 0.0089735 secs]
第五次申請
[GC [DefNew: 0K->0K(3712K), 0.0003179 secs][Tenured: 8325K->4229K(12288K), 0.0080368 secs] 8325K->4229K(16000K), 0.0088335 secs]
第六次申請
從上面的結果中看到第一次回收是從兩個對象申請后,開始回收,后面是每申請一個就回收一次,那是因為,程序代碼中始終用一個句柄指向了一個空間,第一次回收的時候只回收了一個對象,還有一個對象沒有回收,而當后面申請對象過程中,沒申請一個對象以前那個對象就是垃圾,而且內存又滿了,所以內存就會在每申請一個對象的時候被回收。
把程序稍微修改一下,增加兩個申請得較大的內存,因為新區域的回收從上一個試驗中看不出來:
public class Hello {
public static void main(String []args) {
byte []a1 = new byte[4*1024*1024];
System.out.println("第一次申請");
a1 = new byte[4*1024*1024];
System.out.println("第二次申請");
a1 = new byte[4*1024*1024];
System.out.println("第三次申請");
a1 = new byte[4*1024*1024];
System.out.println("第四次申請");
a1 = new byte[4*1024*1024];
System.out.println("第五次申請");
a1 = new byte[4*1024*1024];
System.out.println("第六次申請");
a1 = new byte[12*1024*1024];
System.out.println("第七次申請(大三倍的內存)");
a1 = new byte[12*1024*1024];
System.out.println("第八次申請(大三倍的內存)");
}
}
E:\>javac Hello.java
--先將新區域設置為4m看下結果:
E:\>java -Xmn4m -Xms32m -Xmx32m -XX:+PrintGCDetails Hello
第一次申請
第二次申請
第三次申請
第四次申請
第五次申請
第六次申請
[GC [DefNew: 189K->133K(3712K), 0.0015527 secs][Tenured: 24576K->4229K(28672K), 0.0091076 secs] 24765K->4229K(32384K), 0.0111978 secs]
第七次申請(大三倍的內存)
[GC [DefNew: 0K->0K(3712K), 0.0005428 secs][Tenured: 16517K->12421K(28672K), 0.0131774 secs] 16517K->12421K(32384K), 0.0142610 secs]
第八次申請(大三倍的內存)
--再將新域的大小擴大一倍:
E:\>java -Xmn8m -Xms32m -Xmx32m -XX:+PrintGCDetails Hello
第一次申請
[GC [DefNew: 4362K->133K(7424K), 0.0049861 secs] 4362K->4229K(32000K), 0.0053091 secs]
第二次申請
[GC [DefNew: 4229K->133K(7424K), 0.0043679 secs] 8325K->8325K(32000K), 0.0047056 secs]
第三次申請
[GC [DefNew: 4229K->133K(7424K), 0.0044629 secs] 12421K->12421K(32000K), 0.0047791 secs]
第四次申請
[GC [DefNew: 4229K->133K(7424K), 0.0044263 secs] 16517K->16517K(32000K), 0.0047646 secs]
第五次申請
[GC [DefNew: 4229K->133K(7424K), 0.0045198 secs] 20613K->20613K(32000K), 0.0048372 secs]
第六次申請
[GC [DefNew: 4229K->4229K(7424K), 0.0001604 secs][Tenured: 20480K->4229K(24576K), 0.0092190 secs] 24709K->4229K(32000K), 0.0098943 secs]
第七次申請(大三倍的內存)
[GC [DefNew: 0K->0K(7424K), 0.0005219 secs][Tenured: 16517K->12421K(24576K), 0.0131838 secs] 16517K->12421K(32000K), 0.0142096 secs]
[Full GC [Tenured: 12421K->12420K(24576K), 0.0115076 secs] 12421K->12420K(32000K), [Perm : 338K->337K(8192K)], 0.0123320 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
盡然內存溢出了,怎么擴大了內存溢出了?難道新域不能設置得太大?那么為了論證,我們將新域的大小再放大一倍:
E:\>java -Xmn16m -Xms32m -Xmx32m -XX:+PrintGCDetails Hello
第一次申請
第二次申請
第三次申請
[GC [DefNew: 12551K->133K(14784K), 0.0052593 secs] 12551K->4229K(31168K), 0.0055915 secs]
第四次申請
第五次申請
第六次申請
[GC [DefNew: 12556K->12556K(14784K), 0.0001953 secs][Tenured: 4096K->4229K(16384K), 0.0091847 secs] 16652K->4229K(31168K), 0.0098906 secs]
第七次申請(大三倍的內存)
[GC [DefNew: 12288K->12288K(14784K), 0.0001972 secs][Tenured: 4229K->12421K(16384K), 0.0148896 secs] 16517K->12421K(31168K), 0.0155777 secs]
第八次申請(大三倍的內存)
為什么又不內存溢出了,所以這個設置和絕對是否大和絕對是否小幾乎無關,而是和實際情況的對象的關系,我們推算一下:
前面每次理想申請堆空間是4M(新域內部還包含救助區域,所以直接放不下了),這個申請直接就進入舊域才能放下,所以在新區域申請4m的過程中,舊區域空間又28m空間,前6次申請空間的時候,是4*6=24M接近位數,此時第七次申請空間時候就需要回收內存,可以看到回收的全部是舊域的內存,新域的內存沒有什么變化。
而當新域設置為8M的時候,舊域只有24M,第一個4M顯然能放下去,第二個4M不行了,就要將前面那個轉移到舊域內部,這個轉移過程并不會去做GC的操作,而是不斷的將信息轉移過去,而這些內存屬于垃圾,后面會不斷從新域向舊區域轉移數據,但是這個過程中不會回收信息,和上述一樣,不過當一個12M的數據下來的時候,此時前面會有最多兩個對象及8M的內容在新區域內部(但是通過試驗論證最多只能又一個在新域,因為新域雖然有8M但是實際拿給Eden用的肯定沒有這么多),此時也就是說舊域使用的內存應該是16M或者20M的空間,但是此時JVM認為還沒有到必須要回收內存的地步,如果進來一個4M以內的內存,JVM啟動內存馬上回收,即可將垃圾內存回收掉;而進來一個12M的內存,整個JVM新域+舊區域也放不下那么大的內存了,所以會報內存溢出。
同理可以分析設置為16M的情況。
在設置過程中并不是設置大或者小有好處,根據實際情況而定如何去控制,默認情況下,新域的大小為舊域的二分之一(即堆空間的三分之一),由參數:-XX:NewRatio的值來設置其比例關系,默認值為2。
7、擴展話題JIT(及時編譯技術)與lazy evaluation(惰性評估):