1. 程式人生 > >深入理解JVM——JVM效能調優實戰

深入理解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的注意點

64位JDK支援更大的堆記憶體,但更大的堆記憶體會導致一次垃圾回收時間過長。

現階段,64位JDK的效能普遍比32位JDK低。

堆記憶體過大無法在發生記憶體溢位時生成記憶體快照

若將堆記憶體設為10G,那麼當堆記憶體溢位時就要生成10G的大檔案,這基本上是不可能的。

相同程式,64位JDK要比32位JDK消耗更大的記憶體

2. 使用32位JVM叢集

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

如何實現?

在一臺伺服器上執行多個伺服器程式,這些程式都執行在32位的JDK上。然後再執行個伺服器作為反向代理伺服器,由它來實現負載均衡。

由於32位JDK最多支援2G記憶體,因此每個虛擬結點的堆記憶體可以分配1.6G,一共執行10個虛擬結點的話,這臺物理伺服器可以擁有16G的堆記憶體。

有啥弊端?

多個虛擬節點競爭共享資源時容易出現問題

如多個虛擬節點共同競爭IO操作,很可能會引起IO異常。

很難高效地使用資源池

如果每個虛擬節點使用各自的資源池,那麼無法實現各個資源池的負載均衡。如果使用集中式資源池,那麼又存在競爭的問題。

每個虛擬節點最大記憶體為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. 儘量縮短大物件的有效時間

物件用完後儘快讓它失效,好讓垃圾收集器儘快將他回收,避免因在新生代呆的時間過長而進入老年代。

如果有正在學java的程式設計師,可來我們的java技術學習扣qun哦:72340,3928,小編花了近一個月整理了一份非常適合18年學習的java乾貨,加入就免費送java的視訊教程噢!而且我每天晚上都會在裡面直播講Java知識,從零基礎學習到有基礎進階,歡迎初學和進階中的小夥伴。