深入理解JVM(六)——JVM性能調優實戰

如何在高性能服務器上進行JVM調優?

爲了充分利用高性能服務器的硬件資源,有兩種JVM調優方案,它們都有各自的優缺點,需要根據具體的情況進行選擇。

1. 採用64位操作系統,併爲JVM分配大內存

我們知道,如果JVM中堆內存太小,那麼就會頻繁地發生垃圾回收,而垃圾回收都會伴隨不同程度的程序停頓,因此,如果擴大堆內存的話可以減少垃圾回收的頻率,從而避免程序的停頓。

因此,人們自然而然想到擴大內存容量。而32位操作系統理論上最大隻支持4G內存,64位操作系統最大能支持128G內存,因此我們可以使用64位操作系統,並使用64位JVM,併爲JVM分配更大的堆內存。但問題也隨之而來。

堆內存變大後,雖然垃圾收集的頻率減少了,但每次垃圾回收的時間變長。如果對內存爲14G,那麼每次Full GC將長達數十秒。如果Full GC頻繁發生,那麼對於一個網站來說是無法忍受的。

因此,對於使用大內存的程序來說,一定要減少Full GC的頻率,如果每天只有一兩次Full GC,而且發生在半夜, 那完全可以接受。

要減少Full GC的頻率,就要儘量避免太多對象進入老年代,可以有以下做法:

  • 確保對象都是“朝生夕死”的
    一個對象使用完後應儘快讓他失效,然後儘快在新生代中被Minor GC回收掉,儘量避免對象在新生代中停留太長時間。
  • 提高大對象直接進入老年代的門檻
    通過設置參數-XX:PretrnureSizeThreshold來提高大對象的門檻,儘量讓對象都先進入新生代,然後儘快被Minor GC回收掉,而不要直接進入老年代。

注意:使用64位JDK的注意點

  1. 64位JDK支持更大的堆內存,但更大的堆內存會導致一次垃圾回收時間過長。
  2. 現階段,64位JDK的性能普遍比32位JDK低。
  3. 堆內存過大無法在發生內存溢出時生成內存快照
    若將堆內存設爲10G,那麼當堆內存溢出時就要生成10G的大文件,這基本上是不可能的。
  4. 相同程序,64位JDK要比32位JDK消耗更大的內存

2. 使用32位JVM集羣

針對於64位JDK種種弊端,我們更多選擇使用32位JDK集羣來充分利用高性能機器的硬件資源。


如何實現?

在一臺服務器上運行多個服務器程序,這些程序都運行在32位的JDK上。然後再運行個服務器作爲反向代理服務器,由它來實現負載均衡。
由於32位JDK最多支持2G內存,因此每個虛擬結點的堆內存可以分配1.6G,一共運行10個虛擬結點的話,這臺物理服務器可以擁有16G的堆內存。

有啥弊端?

  1. 多個虛擬節點競爭共享資源時容易出現問題
    如多個虛擬節點共同競爭IO操作,很可能會引起IO異常。
  2. 很難高效地使用資源池
    如果每個虛擬節點使用各自的資源池,那麼無法實現各個資源池的負載均衡。如果使用集中式資源池,那麼又存在競爭的問題。
  3. 每個虛擬節點最大內存爲2G


別忘了直接內存也可能導致內存溢出!

問題描述

有個小型網站,使用32位JDK,堆1.6G。運行期間發現老是出現內存溢出。爲了判斷是否是堆內存溢出,在程序運行前添加參數:-XX:+HeapDumpOnOutOfMemeryError(添加這個參數後當堆內存溢出時就會輸出異常日至)。但當再次發生內存溢出時,沒有生成相關異常日誌。從而可以判定,不是堆內存發生溢出。

問題分析

我們可以發現,在32位JDK中,將1.6G分配給了堆,還有一部分分配給了JVM的其它內存,只有少於0.4G的內存爲非JVM內存。我們知道,如果使用了NIO,那麼JVM會在JVM內存之外分配內存空間,這部分內存也叫“直接內存”。因此,如果程序中使用了NIO,那麼就要小心“直接內存”不足時發生內存溢出異常了!

直接內存的垃圾回收過程

直接內存雖然不是JVM內存空間,但它的垃圾回收也有JVM負責。直接內存的垃圾回收發生在Full GC時,只有當老年代內存滿時,垃圾收集器纔會順便收集一下直接內存中的垃圾。
如果直接內存已滿,但老年代沒滿,這時直接內存先是拋出異常,相應的catch塊中調用System.gc()。由於System.gc()只是建議JVM回收,JVM可能不馬上回收內存,那麼這時直接內存就拋出內存溢出異常,使得程序終止。


JVM崩潰的原因

當內存溢出時,JVM僅僅會終止當前運行的程序,那麼什麼時候JVM會崩潰呢?

什麼是異步請求?

我們知道,Web服務器和客戶端採用HTTP通信,而HTTP底層採用TCP通信。異步通信就是當客戶端向服務器發送一個HTTP請求後,將這個請求的TCP連接委託給其它線程,然後它轉而做別的事,那條被委託的線程保持TCP連接,等待服務器的回信。當收到服務器回信後,再將收到的數據轉交給剛纔的線程。這個過程就是異步通信過程。

異步請求如何造成JVM崩潰?

如果一個Web應用使用了較多的異步請求(AJAX),每次主線程發送完請求後都將TCP連接交給一條新的線程去等待服務器回信,那麼如果網絡不流暢時,這些受委託的線程遲遲等不到服務器的回信,因此保持着TCP連接。當TCP連接過多時,超過JVM的承受能力,JVM就發生崩潰。


如何處理大對象?

大對象對於JVM來說是個噩耗。如果對象過大,當前新生代的剩餘空間裝不下它,那麼就需要使用分配擔保機制,將當前新生代的對象都複製到老年代中,給大對象騰出空間。分配擔保涉及到大量的複製,因此效率很低。

那麼,如果將大對象直接放入老年代,雖然避免了分配擔保過程,但該對象只有當Full GC時才能被回收,而Full GC的代價是高昂的。如果大對象過多時,老年代很快就裝滿了,這時就需要進行Full GC,如果Full GC頻率過高,程序就會變得很卡。

因此,對於大對象,有如下幾種處理方法:
1. 在寫程序的時候儘量避免大對象
從源頭降低大對象的出現,儘量選擇空間利用率較高的數據結構存儲。
2. 儘量縮短大對象的有效時間
對象用完後儘快讓它失效,好讓垃圾收集器儘快將他回收,避免因在新生代呆的時間過長而進入老年代。

發佈了40 篇原創文章 · 獲贊 75 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章