析JAVA之垃圾回收機製
本文為2010年編寫,所以有很多看法不是很準確,有一定的參考價值,如需要更加深入細節,請參看,2012年編寫的關於JVM的文章:
相繼的還會有更多的java深入的知識和機製。
對於JAVA編程和很多類似C、C++語言有一個巨大區別就是內存不需要自己去free或者delete,而是由JVM垃圾回收機製去完成的。對於這個過程很多人一直比較茫然或者覺得很智能,使得在寫程序的過程不太考慮它的感受,其實知道一些內在的原理,幫助我們編寫更加優秀的代碼是非常有必要的;本文介紹一些JVM垃圾回收的基本知識,後續的文章中會深入探討JVM的內在;首先在看文章之前大家需要知道為什麼要寫JVM垃圾回收,在Java發展以來,由於需要麵向對象,而屏蔽掉程序員對於底層的關心,所以在性能上存在很多的缺陷,而通過不斷改良,很多缺陷已經逐漸的取消掉了,不過還是依然存在很多的問題,其中最大的一塊問題就是JVM的垃圾回收機製,一直以來Java在設計實時係統上都被罵聲重重,就是因為垃圾回收存在非常多的問題,世界上目前還沒有任何一個垃圾回收機製可以做到無暫停,而隻是某些係統可以做到非常少的暫停;本文還不會討論那麼深入的隻是,就簡單的內部認識做一些概要性的介紹。
本文從以下幾個方麵進行闡述:
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(),個人建議是最好不要用,所有非同類語言的調用不一定非要通過JNI來完成的,或者調用完就直接釋放掉相應的內容,而不要寄希望於finalize這個方法,因為JVM不保證什麼時候會調用這個方法。
2、System.gc()或者Runtime.getRuntime().gc();
這個可以被認為是強製垃圾回收的一種機製,但是並非強製回收,隻是向JVM建議可以進行垃圾回收,而且垃圾回收的地方和多少是不能像C語言一樣控製,這是JVM垃圾回收機去控製的。程序中盡量不要是去使用這些東西,除自己開發一些管理代碼除外,一般由JVM自己管理即可。
這裏順便提及幾個查看當前JVM內存的幾個簡單代碼方法(在JVM監控下有很多的工具,而且不同的廠商也有自己不同的工具,不過後續大部分關於java的文章都是隻提及到:Hotspot VM的版本,其他的版本可能隻是略微說明下):
2.1.設置的最大內存:-Xmx等值:
(Runtime.getRuntime().maxMemory()/ (1024 * 1024)) + "MB"
2.2.當前JVM可使用的內存,這個值初始化和-Xms等值,若加載東西超過這個值,那麼以下值會跟著變大,不過上限為-Xmx,由於變動過程中需要將虛擬內存做不斷的伸縮過程,所以我們推薦服務器:是-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完成負載均衡,一個暫停服務回收內存,另一個還可以運行嘛。
但是JVM是不是真的就不支持大內存了呢?現在你可以這樣理解,因為到目前為止可以這樣認為,因為世界上所有的java虛擬機,沒有不暫停的,而內存越大,回收的時間是必然越長的,不論有多麼優秀的算法還做不到“不暫停”的這一點,所以我們的目標是盡量少的暫停,現在的CMS GC已經讓我們看到了希望,不過還存在很多的缺陷,我們期待G1的成熟版本的出現,G1的論文很清晰,不過現在還沒有一個成熟的版本,所以很期待。
4、JVM如何尋找到需要回收的內存:
要回收垃圾,那麼首先要知道哪些內存是垃圾,或者反過來哪些不是垃圾,這個過程我們一般稱為:Mark的過程,Mark過程世界上沒有任何一門虛擬機不進行對外暫停。
4.1、引用計數算法:引用計數這裏簡單說明下,就是當一個引用被賦值的時候,虛擬機將會被知道(部分虛擬機通過寫屏障實現),多一個引用,對象的計數增加1,少一個減少1,回收時,隻回收等於0的,好處是算法非常簡單,而且這種算法由於回收過程中隻是看那些沒有被引用,所以在一般情況下無需暫停,不過由於它在計數的過程中需要一個鎖的機製,而且遍曆內存的過程十分漫長,所以現在已經沒有這個東西的存在了;另外一個問題出來了:
問題出來了:
循環引用,以及被這些對象引用的對象都講永遠回收不掉,因為循環引用中的對象引用計數永遠大於等於1,那麼這個資源在循環引用中,其實不是虛擬機算不出來,而且為了這個非常低的代價,虛擬機的算法將會複雜非常多。
其次這種分配方法在分配回收的過程中因為需要記錄哪些內存是垃圾,哪些不是垃圾,所以一般需要維護一個freelist的區域。
4.2.引用樹遍曆算法:首先,每個內存都有原始的引用根,這些根部一般來源於當前線程的棧針、靜態引用、JNI的句柄等,從這裏開始mark,將可達的對象標記為活著的對象,其餘的就認為不是活著的對象,至於找到這些對象如何處理也就是回收的算法所決定的。
5、JVM如何回收內存的(回收算法分解詳述):
首先了解幾個其他的概念:
5.1.平時所說的JDK,其實是JAVA開發工具的意思,安裝JAVA虛擬機會產生兩個JRE目錄,JRE目錄為JAVA運行時環境的意思,兩個JRE目錄的區別是其中在JDK所在的JRE目錄下沒有Server和Client文件夾(JDK1.5自動安裝包會自動將其複製到JDK下麵一份),JRE為運行時環境,提供對JVM操作的API,JVM內部通過動態鏈接庫(就是配置PATH的路徑下),通過它作為主動態鏈接庫尋找到其它的動態鏈接庫,動態鏈接庫為何OS綁定的參數,即代碼最終要通過這些東西轉換為操作係統指令集進行運行,另一個核心工具為JIT(JAVA即時編譯工具),用於將代碼轉換為對應操作係統的運行指令集合的過程,不過其與惰性評估形成對比,後麵會專門介紹。
5.1.JVM首先將大致分為:JVM指令集、JVM存儲器、JVM內存(堆棧區域部分)、JVM垃圾回收區域;JVM的堆部分又一般分為:新域、舊域、永久域(很多時候不會認為永久域是堆的一部分,因為它是永遠不會被回收的,它一般包含class的定義信息、static定義的方法、static匿名塊代碼段、常量信息(較為典型的就是String常量),不過這塊內存也是可以被配置的);新域內部又可以分為Eden和兩個救助區域,這幾個對象在JVM內部有一定的默認值,但是也是可以被設置的。
當新申請的對象的時候,會放入Eden區中(這個區域一般不會太大,默認為新域的3/4, 還有1/4一般會被切成兩塊,成為救助域),當對象在一定時間內還在使用的時候,它會逐步的進入舊域(此時是一個內存複製的過程,舊區域按照順序,其引用的句柄也會被修改指向的位置),JVM回收中會先將Eden裏麵的內存和一個救助區域的內存就會被賦值到另一個救助區域,然後對這兩塊內存進行回收,同理,舊區域也有一個差不多大小的內存區域進行被複製,這個複製的過程肯定就會在一定程度上將內存連續的排列起來;另外可以想到JAVA提供內存複製最快的就是System.arrayCopy方法,那麼這個肯定是按照內存數組進行拷貝(JVM起始就是一個大內存,本身就可以成是幾個大數組組成的,而這個拷貝方法,默認拷貝多長呢,其實數組最長可以達到多少,通過數組的length返回的是int類型數據就可以清楚發現,為int類型的上限1<<31 - 1的長度(理想情況,因為有可能因為操作係統的其他進程導致JVM內存本身就不是連續的),即在(2G-1)*單元內存長度,所以也在一定程度上說明我們的一個JVM設置內存不要太大,不然複製內存的過程開銷是很大的)。
其實上述描述的是一種停止-複製回收算法,在這個過程中形成了幾個大的內存來回倒,這必然是很惡心的事情,那麼繼續將其切片為幾個大的板塊,有些大的對象會出現一兩個對象占用一個版塊的現象,這些大對象基本不會怎麼移動(被回收就是另一回事,因為會清空這個版塊),板塊之間有一些對應關係,在回收時先將一些版塊的小對象,向另一個還未裝滿的大板塊內部轉移,複製的粒度變小了,另外管理上可以發揮多線程的優勢所在,好比是將一塊大的田地,分成很多小田地,每塊田地種植不同檔次的秧苗,將其劃分等級,我們假如秧苗經常會隨機的死掉一些(這塊是垃圾),在清理一些很普通的秧苗田地的時候,可能會將其中一塊或幾塊田地的(活著的秧苗)種植到另一塊田地中,但是他們不可以將高檔次的秧苗移植到低檔次的田地中,因為高檔次的秧苗造價太高(內存太大),移植過程中代價太大,需要使用非普通秧苗的手段要移動他們,所以基本不移動他們,除非豐收他們的時候(他們也成為垃圾內存的時候),才會被拔出,騰出田地來。在轉移秧苗的過程中,他們需要整理出順序便於管理,在很多書籍上把這個過程叫做壓縮,因為這樣使得保證在隻要內存不溢出的情況下,申請的對象都有足夠的控件可以存放,不然零碎的空間中間的縫隙未必可以存放下一個較大的對象。將內存分塊管理就是另一個停止複製收集器的進一步升級:增量收集思想。
5.2.一般在hotspot的回收過程有以下一些曆史:標記—清除、標記—壓縮、停止—複製、增量收集、分代收集、並行收集、並發收集。
這些收集器各有優缺點,某些收集器可能在早期硬件設備上顯示不出優勢,不過後來的收集器顯示出優勢出來,這也是為什麼技術沒有長短之分,隻有時機和場合的問題,不過不論那一種收集器都需要暫停,細節後續來討論,總的來說hotspot版本的虛擬機經曆了串行收集、到早期的並行收集、到新的並行收集算法、再到並發收集(CMS
GC)的過程,不過一直以來還是不能滿足很多高可用的需求,尤其是麵對大內存時,回收會顯得非常緩慢,很多時候不得不將其拆分為多個JVM來處理;一直以來我們都期待有一塊自己管理的區域出現,或者幾乎不受到JVM的幹涉,或者有多快這樣的區域,也就是目前來說一般JVM的Heap是短命、長命的兩個大區域,不過很多時候我們的對象是半長命得,很難讓我們控製起來,比如我們的pageCache,既不是很長的命,但是命也不短,但是又不想影響Young區域的正常對象申請,又不想去導致Old區域的大量回收和compaction,所以我們希望有這樣的區域出現,不過可惜的事情是現在不是SUN說了算,而是Oracle,這個夢想我想在開源的Hotspot上很難實現了,不過G1倒是讓我們看到一些希望,期望G1成熟版本的誕生。
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
分析下為什麼會這樣,Heap總大小為16M,而Young的大小為4M,一般情況下的默認值Eden為Young的80%,所以Eden肯定不到4M,其實初始化直接申請4M空間Enden肯定放不下(拋開對象頭部本身的區域也有4M),此時直接放入Old區域,Old區域本身自有剩下12M,第二次也是一樣,當嚐試第三次放入4M時,JVM檢查空間已經不夠了,並且以前的空間釋放不掉,所以就直接拋出異常了,而不是先將內存放下去,這樣引起的是類似於其他語言類似的OS級別的錯誤,導致的問題就是操作係統直接將進城Crash掉。
那麼我們將程序修改一下再看效果:
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]
第六次申請
上麵可以看到,DefNew一直就沒有怎麼回收過,其實剛開始看到的189K隻是一些引用空間本身內部的一些開銷,而Tenured也就是我們說的老年代的每次GC的變法,而括號中的部分代表該區域實際運行中的最大尺寸,後麵會給出GC的延遲時間,順便說明下,這是默認-client情況下是串行回收,當你使用並行回收的時候看到的提示會有所變化,原因是因為他們完全是兩套程序控製,所謂DefNew沒什麼就是它的程序名稱叫做這個,Tenured也是這個意思。
對於內存回收部分的內容,這裏不想說得太深入,隻是讓大家有一個大致的了解,後續有空專門寫幾篇文章為大家分享,下麵分享一點點雕蟲小技。
7、擴展話題JIT(即時編譯技術)與lazy evaluation(惰性評估),如何在應用服務器中控製一些必要的信息:
7.1.JIT為即時編譯技術,虛擬機有兩種方案:一種是在啟動時將對應的class信息編譯對應的機器指令集合,但是這樣會導致的問題是裝在時間很長,另一個是機器指令碼比字節碼要長很多,裝在的時間頁麵操作非常大,此時JAVA提出惰性評估方案,即啟動時對於CLASS的字節碼並不翻譯,當需要調用其代碼段了,再去編譯(注意代碼段若裝載後,實例存在其對應代碼段是不會注銷的,單例程序的代碼段也是單例的)。
7.2.如何在應用服務中控製信息:其實通過上述控製已經發現一些控製原理,當內存在某些特殊的情況下就會內存溢出,尤其在進行一些大批量導出數據的情況下,此時可能會同時導出幾萬條數據,如果在前端去控製隻能到處幾百天或者幾千條可能客戶不答應,因為這太少了;假如我們的控製方式是要在1G內存將各類導出內存數據進行分類:業務類別、平均一百行占用內存多少M。進行計算,然後對於一個SERVER下允許同時在線導出多少個線程進行配置化,按照提交的業務類別,在抽象頂層進行控製,若為導出某類業務將其進行校驗,若未通過校驗,線程wait(),即釋放臨界資源,進入等待池,當下載完畢一個時,調用管理器進行對應對象的notify操作,並使得計數器減少。大致原理可以基於以下方式(不過實際應用須稍微修改下):
//代碼段1:設置共享信息,該類
class WaitObj {//該類所在對象須申明為單例,才可以達到效果。 private volatile int index = 0; private int maxMutile = 20;//假如最多運行20個同時導出 synchronized public void checkInfo() {// while(index >= maxMutile) { try { this.wait();//超過數量等待激活,激活後還要判定 } catch (InterruptedException e) { e.printStackTrace(); } } index++;//得到申請可以導出時,將在線計數器增加1 } synchronized public void notifyInfo() {//做完事情,激活一個 index--; this.notify(); } public void setMaxMutile(String maxMutilePara) {//手工設置最大值 maxMutile = (maxMutilePara == null)? 20 : Integer.valueOf(maxMutilePara).intValue(); } } //同文件中代碼段2:設置管理器,設置控製簡單單例,並提供管理規則 public class TestManager { private final static waitObj = new waitObj();//隻有一個實例 public static void checkInfo() { waitObj.checkInfo(); } public static void notifyInfo() { waitObj.notifyInfo(); } } //外部代碼段調用:假如導出部分代碼上層有公共調用部分去調用導出代碼,那麼在公共代碼部分這樣寫: TestManager.checkInfo();//這裏調用了檢測部分 try { export……調用部分。。。。根據實際情況而定 }cache(Exception e) { 異常處理 }finally { TestManager.notifyInfo();//執行完畢後,釋放一個資源 }
為了驗證程序的正確性,可以從幾個角度去測試:
1、 在本地模擬一個多線程,利用多個線程同時去訪問一段代碼,這段嗲嗎如上,,在執行前通過TestManager.checkInfo()序列化操作,在finally中去TestManager.notifyInfo()操作。
2、 多線程取一個名字,然後輸出名字即可,這裏就不提供模擬程序了(因為怕誤認為下麵的程序為實際的運行程序,下麵隻是為了模擬情況而已),提供了001~007之間七個線程去訪問,而最大同時在線導出人數為5個,以打印信息表示動作已經執行,此時運行結果如下:
001執行了,時間:1274763253343
002執行了,時間:1274763253359
004執行了,時間:1274763253359
003執行了,時間:1274763253359
006執行了,時間:1274763253359
007執行了,時間:1274763253359
005執行了,時間:1274763253359
3、 此時發現,幾乎同時執行,為什麼,因為程序運行太快,前麵執行完後,就直接釋放掉信息了,所以看不出什麼區別,為了驗證先執行完的程序暫時不釋放,我們讓每個線程執行完(輸出信息後)以後,等待兩秒再去執行,那麼輸出結果如下所示:
001執行了,時間:1274763842140
002執行了,時間:1274763842140
004執行了,時間:1274763842140
003執行了,時間:1274763842140
005執行了,時間:1274763842140
007執行了,時間:1274763844140
006執行了,時間:1274763844140
你會發現,後麵兩個線程幾乎同時執行,前麵的線程幾乎同時執行,是不是後麵兩個線程同時運行的呢?這樣會不會有問題,如現在堵塞了10個線程在這裏,那麼一旦釋放,那麼計數器的值是否會錯誤,此時的確前麵5個線程同時幾乎同時釋放了(雖然都睡了兩秒),為了驗證後麵兩個線程對於計數器的操作是否為順序的或者互斥的,一種首先在自定義線程中,定義一個自定義的time,初始化的時候,設置不同的值,讓他們睡不同的時間來激活。另一個模擬就是在每一個線程進行checkInfo()內部,跳出循環的時候,也睡兩秒,此時若程序剩下兩個線程能夠以相差2秒左右的時間下來就是理想結果,如果是同時下來,那麼多線程在這裏釋放的過程中就沒有控製到,代碼稍微修改下(測試代碼):
boolean i = false; while(index >= maxMutile) { try { i = true; this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } if(i) { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } }
index ++;
此時運行結果如下:
001執行了,時間:1274764270203
003執行了,時間:1274764270203
005執行了,時間:1274764270203
002執行了,時間:1274764270203
007執行了,時間:1274764270203
004執行了,時間:1274764274203
006執行了,時間:1274764276203
注意:可能你會發現,上麵的順序怎麼在變化,因為它是多線程,誰先獲取到鎖這是隨即的,雖然也有順序,不過在很短的時間內,不一定誰先獲取到鎖。
另外上述為共享鎖機製,一般不允許外部代碼所直接調用,可以將其作為管理器內部的private static class 的內部類,隻有其管理類才可以創建它的實例並直接操作它。
這的確是我們要要的結果,那麼在一定程度決定了它的正確性,穩定性由程序完成控製過程,並通過管理器控製外部調用,若可以的話,可以將這部分代碼進行AOP切入方式控製到程序中,對於AOP切入方式的原理和核心說明,後麵再介紹反射中給予詳細例子和說明。
這種應該說在絕大部分概率下問題不大,因為finally是SUN公司承諾無論發生任何事情,是肯定會執行,唯一可能出現漏洞就是TestManager.notifyInfo()出現了異常,不過這個概率非常低。
同理如果要完善自己的一些內存數據的管理,進一步分類管理:
1、信息類別:頁麵流、文章、圖片等
2、業務分類
3、初始化內存行數
4、最大數據行數
5、置換算法(這個可以配置也可以寫死,置換算法比較經典就是LRU最近最久未使用算法,不過寫得不好的話,會很慢,還不如直接從數據庫裏麵讀,不過細細讀每一行意義,總體把握性能,量化評估算法,問題不會太大。)
最後更新:2017-04-02 06:51:17