文章目錄
- 概念
- 案例分析
- 高性能硬件上的程序部署策略
- 情景再現1
- 問題分析1
- 關于Full GC
- 使用64位JDK來管理大內存可能遇到問題
- 建立邏輯集群
- 使用邏輯集群可能遇到的問題
- 最后解決方案
- 集群間同步導致的內存溢出
- 情景再現2
- 問題分析2
- 堆外內存導致的溢出錯誤
- 情景再現3
- 問題分析3
- 外部命令導致系統緩慢
- 情景再現4
- 問題分析4
- 服務器JVM進程崩潰
- 情景再現5
- 問題分析5
- 不恰當數據結構導致內存占用過大
- 情景再現6
- 問題分析6
- 由Windows虛擬內存導致的長時間停頓
- 情景再現7
- 問題分析7
- 實戰:Eclipse運行速度調優
- 調優前的程序運行狀態
- 簡單eclipse插件開發:eclipse啟動時間顯示器
- 升級JDK1.6的性能變化及兼容問題
- 編譯時間和類加載時間的優化
- Compile Time
- 調整內存設置控制垃圾收集頻率
- 選擇收集器降低延遲
概念
實踐是檢驗真理的唯一標準
案例分析
高性能硬件上的程序部署策略
情景再現1
一個15萬PV/天左右的在線文檔類型網站最近更換了硬件系統,新的硬件為4個CPU、16GB物理內存,操作系統為64位CentOS 5.4, Resin作為Web服務器。
整個服務器暫時沒有部署別的應用,所有硬件資源都可以提供給這訪問量并不算太大的網站使用。管理員為了盡量利用硬件資源選用了64位的JDK 1.5,并通過-Xmx和-Xms參數將Java堆固定在12GB。使用一段時間后發現使用效果并不理想,網站經常不定期出現長時間失去響應的情況。
問題分析1
監控服務器運行狀況后發現網站失去響應是由GC停頓導致的,虛擬機運行在Server模式 ,默認使用吞吐量優先收集器,回收12GB的堆 ,一次Full GC的停頓時間高達14秒。并且由于程序設計的關系,訪問文檔時要把文檔從磁盤提取到內存中,導致內存中出現很多由文檔序列化產生的大對象,這些大對象很多都進入了老年代,沒有在Minor GC中清理掉。
這種情況下即使有12GB的堆,內存也很快被消耗殆盡,由此導致每隔十幾分鐘出現十幾秒的停頓,令網站開發人員和管理員感到很沮喪。
這里先不延伸討論程序代碼問題,程序部署上的主要問題顯然是過大的堆內存進行回收時帶來的長時間的停頓。硬件升級前使用32位系統1.5GB的堆,用戶只感覺到使用網站比較緩慢,但不會發生十分明顯的停頓,因此才考慮升級硬件以提升程序效能,如果重新縮小給Java堆分配的內存,那么硬件上的投資就顯得很浪費。
在高性能硬件上部署程序,目前主要有兩種方式:
- 通過64位JDK來使用大內存。
- 使用若干個32位虛擬機建立邏輯集群來利用硬件資源。
關于Full GC
此案例中的管理員采用了第一種部署方式。對于用戶交互性強、對停頓時間敏感的系統,可以給Java虛擬機分配超大堆的前提是有把握把應用程序的Full GC頻率控制得足夠低, 至少要低到不會影響用戶使用,譬如十幾個小時乃至一天才出現一次Full GC,這樣可以通過在深夜執行定時任務的方式觸發Full GC甚至自動重啟應用服務器來保持內存可用空間在一個穩定的水平。
控制Full GC頻率的關鍵是看應用中絕大多數對象能否符合“朝生夕滅”的原則,即大多數對象的生存時間不應太長,尤其是不能有成批量的、長生存時間的大對象產生,這樣才能保障老年代空間的穩定。
在大多數網站形式的應用里,主要對象的生存周期都應該是請求級或者頁面級的,會話級和全局級的長生命對象相對很少。只要代碼寫得合理,應當都能實現在超大堆中正常使用而沒有Full GC ,這樣的話,使用超大堆內存時,網站響應速度才會比較有保證。
使用64位JDK來管理大內存可能遇到問題
除此之外, 如果讀者計劃使用64位JDK來管理大內存,還需要考慮下面可能面臨的問題:
- 內存回收導致的長時間停頓。
- 現階段 ,64位JDK的性能測試結果普遍低于32位JDK。
- 需要保證程序足夠穩定,因為這種應用要是產生堆溢出幾乎就無法產生堆轉儲快照(因為要產生十幾GB乃至更大的Dump文件 ),哪怕產生了快照也幾乎無法進行分析。
- 相同程序在64位JDK消耗的內存一般比32位JDK大 ,這是由于指針膨脹,以及數據類型對齊補白等因素導致的。
建立邏輯集群
上面的問題聽起來有點嚇人,所以現階段不少管理員還是選擇第二種方式:使用若干個32位虛擬機建立邏輯集群來利用硬件資源。具體做法是在一臺物理機器上啟動多個應用服務器進程 ,每個服務器進程分配不同端口 ,然后在前端搭建一個負載均衡器 ,以反向代理的方式來分配訪問請求。讀者不需要太過在意均衡器轉發所消耗的性能,即使使用64位JDK ,許多應用也不止有一臺服務器,因此在許多應用中前端的均衡器總是要存在的。
考慮到在一臺物理機器上建立邏輯集群的目的僅僅是為了盡可能利用硬件資源,并不需要關心狀態保留、熱轉移之類的高可用性需求,也不需要保證每個虛擬機進程有絕對準確的均衡負載,因此使用無Session復制的親合式集群是一個相當不錯的選擇。我們僅僅需要保障集群具備親合性,也就是均衡器按一定的規則算法(一般根據SessionID分配)將一個固定的用戶請求永遠分配到固定的一個集群節點進行處理即可,這樣程序開發階段就基本不用為集群環境做什么特別的考慮了。
使用邏輯集群可能遇到的問題
當然 ,很少有沒有缺點的方案,如果使用邏輯集群的方式來部署程序,可能會遇到下面一些問題:
- 盡量避免節點競爭全局的資源,最典型的就是磁盤競爭,各個節點如果同時訪問某個磁盤文件的話(尤其是并發寫操作容易出現問題),很容易導致IO異常。
- 很難最高效率地利用某些資源池,譬如連接池,一般都是在各個節點建立自己獨立的連接池 ,這樣有可能導致一些節點池滿了而另外一些節點仍有較多空余。盡管可以使用集中式的JNDI,但這個有一定復雜性并且可能帶來額外的性能開銷。
- 各個節點仍然不可避免地受到32位的內存限制,在32位Windows平臺中每個進程只能使用2GB的內存,考慮到堆以外的內存開銷,堆一般最多只能開到1.5GB。在某些Linux或UNIX系統(如Solaris)中 ,可以提升到3GB乃至接近4GB的內存,但32位中仍然受最高4GB(232)內存的限制。
- 大量使用本地緩存(如大量使用HashMap作為K/V緩存 )的應用 ,在邏輯集群中會造成較大的內存浪費,因為每個邏輯節點上都有一份緩存,這時候可以考慮把本地緩存改為集中式緩存。
最后解決方案
介紹完這兩種部署方式,再重新回到這個案例之中,最后的部署方案調整為
- 建立5個32位JDK的邏輯集群,每個進程按2GB內存計算(其中堆固定為1.5GB ),占用了10GB內存。
- 另外建立一個Apache服務作為前端均衡代理訪問門戶。
- 考慮到用戶對響應速度比較關心,并且文檔服務的主要壓力集中在磁盤和內存訪問,CPU資源敏感度較低,因此改為CMS收集器進行垃圾回收。
部署方式調整后,服務再沒有出現長時間停頓,速度比硬件升級前有較大提升。
集群間同步導致的內存溢出
情景再現2
有一個基于B/S的MIS系統,硬件為兩臺2個CPU、8GB內存的HP小型機,服務器是WebLogic 9.2 ,每臺機器啟動了3個WebLogic實例 ,構成一個6個節點的親合式集群。
由于是親合式集群,節點之間沒有進行Sessurn同步,但是有一些需求要實現部分數據在各個節點間共享。開始這些數據存放在數據庫中,但由于讀寫頻繁競爭很激烈,性能影響較大,后面使用JBossCache構建了 一個全局緩存。
全局緩存啟用后,服務正常使用了一段較長的時間, 但最近卻不定期地出現了多次的內存溢出問題。
問題分析2
在內存溢出異常不出現的時候,服務內存回收狀況一直正常,每次內存回收后都能恢復到一個穩定的可用空間,開始懷疑是程序某些不常用的代碼路徑中存在內存泄漏,但管理員反映最近程序并未更新、升級過,也沒有進行什么特別操作。只好讓服務帶著-XX : +HeapDumpOnOutOfMemoryError參數運行了一段時間。在最近一次溢出之后,管理員發回了 heapdump文件,發現里面存在著大量的org.jgroups.protocols.pbcast.NAKACK對象。
JBossCache是基于自家的JGroups進行集群間的數據通信,JGroups使用協議棧的方式來實現收發數據包的各種所需特性自由組合,數據包接收和發送時要經過每層協議棧的up()和down()方法,其中的NAKACK棧用于保障各個包的有效順序及重發。JBossCache協議棧如下圖所示。
由于信息有傳輸失敗需要重發的可能性,在確認所有注冊在GMS ( Group Membership Service ) 的節點都收到正確的信息前,發送的信息必須在內存中保留。而此MIS的服務端中有一個負責安全校驗的全局Filter , 每當接收到請求時,均會更新一次最后操作時間,并且將這個時間同步到所有的節點去,使得一個用戶在一段時間內不能在多臺機器上登錄。在服務使用過程中,往往一個頁面會產生數次乃至數十次的請求,因此這個過濾器導致集群各個節點之間網絡交互非常頻繁。當網絡情況不能滿足傳輸要求時,重發數據在內存中不斷堆積,很快就產生了內存溢出。
這個案例中的問題,既有JBossCache的缺陷,也有MIS系統實現方式上缺陷。 JBossCache官方的maillist中討論過很多次類似的內存溢出異常問題,據說后續版本也有了改進。而更重要的缺陷是這一類被集群共享的數據要使用類似JBossCache這種集群緩存來同步的話 ,可以允許讀操作頻繁,因為數據在本地內存有一份副本,讀取的動作不會耗費多少資源 ,但不應當有過于頻繁的寫操作,那樣會帶來很大的網絡同步的開銷。
堆外內存導致的溢出錯誤
情景再現3
一個學校的小型項目:基于B/S的電子考試系統,為了實現客戶端能實時地從服務器端接收考試數據 , 系統使用了逆向AJAX技術(也稱為Comet或者Server Side Push) ,選用CometD 1.1.1作為服務端推送框架,服務器是Jetty 7.1.4 ,硬件為一臺普通PC機 , Core i5 CPU , 4GB內存,運行32位Windows操作系統。
測試期間發現服務端不定時拋出內存溢出異常,服務器不一定每次都會出現異常,但假如正式考試時崩潰一次,那估計整場電子考試都會亂套,網站管理員嘗試過把堆開到最大, 而32位系統最多到1.6GB就基本無法再加大了,而且開大了基本沒效果,拋出內存溢出異常好像還更加頻繁了。加入-XX :+HeapDumpOnOutOfMemoryError,居然也沒有任何反應,拋出內存溢出異常時什么文件都沒有產生。無奈之下只好掛著jstat并一直緊盯屏幕,發現GC并不頻繁 ,Eden區、Survivor區、老年代以及永久代內存全部都表示“情緒穩定,壓力不大”, 但就是照樣不停地拋出內存溢出異常,管理員壓力很大。最后 ,在內存溢出后從系統日志中找到異常堆棧,如代碼下面所示。
問題分析3
操作系統對每個進程能管理的內存是有限制的,這臺服務器使用的32位 Windows平臺的限制是2GB ,其中劃了1.6GB給Java堆 ,而Direct Memory內存并不算入1.6GB的堆之內,因此它最大也只能在剩余的0.4GB空間中分出一部分。在此應用中導致溢出的關鍵是:垃圾收集進行時,虛擬機雖然會對Direct Memory進行回收,但是Direct Memory卻不能像新生代、老年代那樣,發現空間不足了就通知收集器進行垃圾回收,它只能等待老年代滿了后Full GC , 然后“順便地”幫它清理掉內存的廢棄對象。
否則它只能一直等到拋出內存溢出異常時,先catch掉 ,再在catch塊里面“大喊聲:“System.gc()! ”。要是虛擬機還是不聽 ( 譬如打開了-XX:+DisableExplicitGC開關),那就只能眼睜睜地看著堆中還有許多空閑內存 ,自己卻不得不拋出內存溢出異常了。而本案例中使用的CometD 1.1.1框架,正好有大量 的NIO操作需要使用到Direct Memory內存。
從實踐經驗的角度出發,除了Java堆和永久代之外,我們注意到下面這些區域還會占用較多的內存,這里所有的內存總和受到操作系統進程最大內存的限制。
- Direct Memory:可通過-XX:MaxDirectMemorySize調整大小,內存不足時拋出OutOfMemoryError或者OutOfMemoryError : Direct buffer memory。
- 線程堆棧:可通過-Xss調整大小,內存不足時拋出StackOverflowError (縱向無法分配, 即無法分配新的棧幀)或者OutOfMemoryError : unable to create new native thread (橫向無法分配 ,即無法建立新的線程)。
- Socket緩存區:每個Socket連接都Receive和Send兩個緩存區,分別占大約37KB和25KB內存,連接多的話這塊內存占用也比較可觀。如果無法分配,則可能會拋出IOException : Too many open files異常。
- JNI代碼 :如果代碼中使用JNI調用本地庫,那本地庫使用的內存也不在堆中。
- 虛擬機和GC:虛擬機、GC的代碼執行也要消耗一定的內存。
外部命令導致系統緩慢
情景再現4
一個數字校園應用系統,運行在一臺4個CPU的Solaris 10操作系統上,中間件為GlassFish服務器。系統在做大并發壓力測試的時候,發現請求響應時間比較慢 ,通過操作系統的mpstat工具發現CPU使用率很高 ,并且系統占用絕大多數的CPU資源的程序并不是應用系統本身。這是個不正常的現象,通常情況下用戶應用的CPU占用率應該占主要地位,才能說明系統是正常工作的。
通過Solaris 10的Dtrace腳本可以查看當前情況下哪些系統調用花費了最多的CPU資源 ,Dtrace運行后發現最消耗CPU資源的竟然是“fork”系統調用。眾所周知,“fork”系統調用 是Linux用來產生新進程的,在Java虛擬機中,用戶編寫的Java代碼最多只有線程的概念,不應當有進程的產生。
問題分析4
這是個非常異常的現象。通過本系統的開發人員,最終找到了答案:每個用戶請求的處 理都需要執行一個外部shell腳本來獲得系統的一些信息。執行這個shell腳本是通過Java的 Runtime.getRuntime().exec()方法來調用的。這種調用方式可以達到目的,但是它在Java 虛擬機中是非常消耗資源的操作,即使外部命令本身能很快執行完畢,頻繁調用時創建進程 的開銷也非常可觀。Java虛擬機執行這個命令的過程是:首先克隆一個和當前虛擬機擁有一樣環境變量的進程,再用這個新的進程去執行外部命令,最后再退出這個進程。如果頻繁執 行這個操作,系統的消耗會很大,不僅是CPU, 內存負擔也很重。
用戶根據建議去掉這個Shell腳本執行的語句,改為使用Java的API去獲取這些信息后, 系統很快恢復了正常。
服務器JVM進程崩潰
情景再現5
一個基于B/S的MIS系統,硬件為兩臺2個CRJ、8GB內存的HP系統,服務器是WebLogic 9.2 。正常運行一段時間后,最近發現在運行期間頻繁出現集群節點的虛擬機進程自動關閉的現象,留下了一個hs_err_pid###.log文件后 ,進程就消失了,兩臺物理機器里的每個節點都出現過進程崩潰的現象。
從系統日志中可以看出, 每個節點的虛擬機進程在崩潰前不久,都發生過大量相同的異常,見代碼如下
這是一個遠端斷開連接的異常,通過系統管理員了解到系統最近與一個OA門戶做了集成 ,在MIS系統工作流的待辦事項變化時,要通過Web服務通知0A門戶系統,把待辦事項的變化同步到OA門戶之中。通過SoapU測試了一下同步待辦事項的幾個Web服務,發現調用后竟然需要長達3分鐘才能返回,并且返回結果都是連接中斷。
問題分析5
由于MIS系統的用戶多,待辦事項變化很快,為了不被OA系統速度拖累,使用了異步的方式調用Web服務,但由于兩邊服務速度的完全不對等,時間越長就累積了越多Web服務沒有調用完成,導致在等待的線程和Socket連接越來越多,最終在超過虛擬機的承受能力后使得虛擬機進程崩潰。解決方法:通知OA門戶方修復無法使用的集成接口,并將異步調用改為生產者/消費者模式的消息隊列實現后,系統恢復正常。
不恰當數據結構導致內存占用過大
情景再現6
有一個后臺RPC服務器,使用64位虛擬機,內存配置為-Xms4g -Xmx8g-Xmn1g, 使用ParNew+CMS的收集器組合。平時對外服務的Minor GC時間約在30毫秒以內,完全可以接受。但業務上需要每10分鐘加載一個約80MB的數據文件到內存進行數據分析,這些數據會在內存中形成超過100萬個HashMap<Long,Long>Entry,在這段時間里面Minor GC就會造成超過500毫秒的停頓,對于這個停頓時間就接受不了了,具體情況如下面GC日志所示。
問題分析6
觀察這個案例,發現平時的Minor GC時間很短,原因是新生代的絕大部分對象都是可清除的, 在Minor GC之后Eden和Survivor基本上處于完全空閑的狀態。而在分析數據文件期間,800MB的Eden空間很快被填滿從而引發GC ,但Minor GC之后,新生代中絕大部分對象依然是存活的。我們知道ParNew收集器使用的是復制算法,這個算法的高效是建立在大部分對象都“朝生夕滅”的特性上的,如果存活對象過多,把這些對象復制到Survivor并維持這些對象引用的正確就成為一個沉重的負擔,因此導致GC暫停時間明顯變長。
如果不修改程序,僅從GC調優的角度去解決這個問題,可以考慮將Survivor空間去掉(加入參數-XX:SurvivorRatio=65536、 -XX:MaxTenuringThreshold=0或者-XX:+AlwaysTenure ) , 讓新生代中存活的對象在第一次Minor GC后立即進入老年代,等到Major GC的時候再清理它們。這種措施可以治標,但也有很大副作用,治本的方案需要修改程序,因為這里的問題產生的根本原因是用HashMap<Long,Long>結構來存儲數據文件空間效率太低。
下面具體分析一下空間效率。在HashMap<Long,Long>結構中,只有Key和Value所存放的兩個長整型數據是有效數據,共16B(2x8B)。這兩個長整型數據包裝成java.lang.Long對象之后,就分別具有8B的MarkWord、8B的Klass指針 ,在加8B存儲數據的long值。在這兩個Long對贏組成Map.Entry之后 ,又多了 16B的對象頭,然后一個8B的next字段和4B的int型的hash字段 ,為了對齊,還必須添加4B的空白填充,最后還有HashMap中對這個Entry的8B的引用 ,這樣增加兩個長整型數字,實際耗費的內存為 (Long(24B)x2)+Entry(32B)+HashMap Ref(8B)=88B,空間效率為16B/88B=18%,實在太低了。
由Windows虛擬內存導致的長時間停頓
情景再現7
有一個帶心跳檢測功能的GUI桌面程序,每15秒會發送一次心跳檢測信號,如果對方30秒以內都沒有信號返回,那就認為和對方程序的連接已經斷開。程序上線后發現心跳檢測有誤報的概率,查詢日志發現誤報的原因是程序會偶爾出現間隔約一分鐘左右的時間完全無日志輸出,處于停頓狀態。
因為是桌面程序,所需的內存并不大(-Xmx256m), 所以開始并沒有想到是GC導致的程序停頓,但是加入參數-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCDateStamps -Xloggc:gclog.log后 ,從GC日志文件中確認了停頓確實是由GC導致的,大部分GC時間都控制在100毫秒以內,但偶爾就會出現一次接近1分鐘的GC。
從GC日志中找到長時間停頓的具體日志信息(添加了-XX:+PrintReferenceGC參數), 找到的日志片段如下所示。從日志中可以看出,真正執行GC動作的時間不是很長,但從準備開始GC ,到真正開始GC之間所消耗的時間卻占了絕大部分。
問題分析7
除GC日志之外,還觀察到這個GUI程序內存變化的一個特點,當它最小化的時候,資源管理中顯示的占用內存大幅度減小,但是虛擬內存則沒有變化,因此懷疑程序在最小化時它的工作內存被自動交換到磁盤的頁面文件之中了,這樣發生GC時就有可能因為恢復頁面文件的操作而導致不正常的GC停頓。
在MSDN上查證后確認了這種猜想,因此,在Java的GUI程序中要避免這種現象,可以加入參數“-Dsun.awt.keepWorkingSetOnMinimize=true”來解決。這個參數在許多AWT的程序上都有應用,例如JDK自帶的Visual VM,用于保證程序在恢復最小化時能夠立即響應。在這個案例中加入該參數后,問題得到解決。
實戰:Eclipse運行速度調優
由于環境相差較大,所以不具體重現實驗,只記錄相關要點
調優前的程序運行狀態
VisualVM的VisualGC插件
簡單eclipse插件開發:eclipse啟動時間顯示器
Link
1.下載并安裝jdk和eclipse 這里強調一下: 需要下載Eclipse for RCP and RAP Developers, 否則無法新建Plug-in Development 項目.
2.新建項目 安裝好之后打開eclipse, 點擊 File->NewProject。選擇Plug-in Project,點擊Next。新建一個名為com.developer.showtime的項目,所有參數采用默認值.
3.在com.developer.showtime項目的src下新建一個類: ShowTime,代碼如下:
package com.developer.showtime;import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IStartup;public class ShowTime implements IStartup {public void earlyStartup() {Display.getDefault().syncExec(new Runnable() {public void run(){long eclipseStartTime = Long.parseLong(System.getProperty("eclipse.startTime"));long costTime = System.currentTimeMillis() - eclipseStartTime;Shell shell = Display.getDefault().getActiveShell();String message = "Eclipse start in " + costTime + "ms";MessageDialog.openInformation(shell, "Information", message);}});}
}
4.修改plugin.xml文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?><plugin><extension point="org.eclipse.ui.startup"><startup class="com.developer.showtime.ShowTime"/></extension>
</plugin>
5.試運行
右鍵點擊Run as -> Eclipse Application. 此時會運行一個eclipse, 啟動之后就能顯示啟動所需時間.
6.導出插件.
右鍵Export -> Deployable plug-ins and fragments. 在Directory中輸入需要導出的路徑, 點擊finish后會在該目錄下產生一個plugins的目錄, 里面就是插件包:com.developer.showTime_1.0.0.201110161216.jar. 把這個包復制到eclipse目錄下的plugin目錄下. 然后再啟動eclipse 便可以看到eclipse啟動所花的時間.
升級JDK1.6的性能變化及兼容問題
從JDK 1.5升級到1.6并不一定帶來性能的提升。
編譯時間和類加載時間的優化
-Xverify:none禁止掉字節碼驗證過程進行優化
Compile Time
編譯時間是指虛擬機的JIT編譯器(Just In Time Compiler)編譯熱點代碼(Hot Spot Code)的耗時。
我們知道Java語言為了實現跨平臺的特性,Java代碼編譯出來后形成的Class文件中存儲的是字節碼(ByteCode),虛擬機通過解釋方式執行字節碼命令,比起C/C++編譯成本地二進制代碼來說,速度要慢不少。
為了解決程序解釋執行的速度問題, JDK 1.2以后,虛擬機內置了兩個運行時編譯器1 ,如果一段Java方法被調用次數達到一定程度,就會被判定為熱代碼交給JIT編譯器即時編譯為本地代碼,提高運行速度(這就是HotSpot虛擬機名字的由來 )。
甚至有可能在運行期動態編譯比C/C++的編譯期靜態譯編出來的代碼更優秀,因為運行期可以收集很多編譯器無法知道的信息,甚至可以采用一些很激進的優化手段,在優化條件不成立的時候再逆優化退回來。所以Java程序只要代碼沒有問題(主要是泄漏問題,如內存泄漏、連接泄漏),隨著代碼被編譯得越來越徹底,運行速度應當是越運行越快的。
Java的運行期編譯最大的缺點就是它進行編譯需要消耗程序正常的運行時間,這也就是上面所說的“編譯時間”。
虛擬機提供了一個參數-Xint禁止編譯器運作,強制虛擬機對字節碼采用純解釋方式執行。
但這樣的優化效果中看不中用
與解釋執行相對應的另一方面,虛擬機還有力度更強的編譯器:當虛擬機運行在-client 模式的時候,使用的是一個代號為C1的輕量級編譯器,另外還有一個代號為C2的相對重量級的編譯器能提供更多的優化措施。
如果使用-server模夫的虛擬機啟動Eclipse將會使用到C2 編譯器 ,這時從VisualGC可以看到啟動過程中虛擬機使用了超過15秒的時間去進行代碼編譯。如果讀者的工作習慣是長時間不關閉Eclipse的話 ,C2編譯器所消耗的額外編譯時間最終還是會在運行速度的提升之中賺回來,這樣使用-server模式也是一個不錯的選擇。
調整內存設置控制垃圾收集頻率
可通過以下參數要求虛擬機生成GC日志:
- -XX:+PrintGCTimeStamps 打印GC停頓時間
- -XX:+PrintGCDetails 打印GC詳細信息
- -verbose:gc 打印GC信息,輸出內容已被前一個參數包括,可以不寫
- -Xloggc:gc.log
Full GC大多數是由于老年代容量擴展而導致的,由永久代空間擴展而導致的也有一部分
-Xms/-Xmx
-XX:PermSize/-XX:MaxPermSize
上面參數將老年代和永久代的容量固定下來,避免運行時自動擴展
-XX:+DisableExplicitGC屏蔽掉System.gc()顯示觸發的GC
選擇收集器降低延遲
一邊編譯,一邊繼續其它編碼工作
CMS是最符合這情景的收集器
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
(ParNew收集器是使用CMS收集器后的默認新生代收集器,寫上僅是為了配置更加清晰),要求虛擬機在新生代和老年代分別使用ParNew和CMS收集器進行垃圾回收。