可能很多人都知道Java程序上生產后,運維人員都會設定好JVM的堆大小,而且還是把最大最小設置成一樣的值。那究竟是為什么呢?一般而言,Java程序如果你不顯示設定該值得話,會自動進行初始化設定。
-Xmx 的默認值為你當前機器最大內存的 1/4
-Xms 的默認值為你當前機器最大內存的 1/64
顯然這樣配置的意義是希望JVM可以根據當前運行的環境,動態伸縮堆內存大小。之所以生產上設置成固定大小,網上也是說法不一,很多時候都是使用“防止內存抖動”這樣的模糊詞語給出解釋。但是我相信各位讀者也很懵,不知道這個詞具體表達什么含義。
所以接下來我打算用這篇文章來着重解釋一下這其中的門道。帶大家徹底弄懂設置固定大小堆的底層原理和好處。為了能順利看懂本文,我假設你們已經具備了一定的操作系統基礎知識。
最大堆或最小堆,從字面上理解就是JVM在運行Java程序時,為其分配堆內存空間的上限和下限值。我們把最大和最小堆設置成相同值那意思就是分配了固定大小的內存唄。這樣不就省去了動態調整內存(申請和釋放)以及頻繁的用戶態和內核態的切換帶來的開銷嗎?。如下圖所示。


看上去就是這么回事,簡單明了。然而當我們嘗試去做個模擬實驗,事實卻並非如此。比如,隨便寫個Java程序,使用如下命令啟動之。並設置好固定大小堆為1G。
java -Xmx1024m -Xms1024m -jar demo.jar
然后我們通過查看進程的內存占用時,發現程序並沒有占用1G的空間,而是很小的占用。這個實驗結果和我們預期的完全不一致。究竟是什么原因呢?
問題其實出在我們對內存模型的理解上有問題。很多人可能都是像上面圖中那樣理解程序分配內存的。實際上是不對的,且也更復雜。首先我們要理解一個重要概念,那就是“進程的虛擬地址空間”,我們用戶程序通過malloc這個系統調用申請內存,實際上就是申請了一個虛擬的內存,並不是真正的物理內存。大家要注意,這個虛擬的內存就是指“進程的虛擬地址空間”,而不是我們通常理解的Windows下的虛擬內存或Linux下的swap(分區交換)。如下圖所示。


用戶程序申請的虛擬內存(虛擬地址空間),也就是通過malloc系統調用,本質就是在進程的虛擬地址空間里分配了一塊地址范圍而已。32位系統理論上最大4G,每個進程都有自己的虛擬地址空間,都能申請到最大4G內存。但是申請了的內存,如果沒有實際使用(寫入數據),則操作系統不會給這塊虛擬空間分配實際的物理內存。其實原因很簡單,物理內存一直屬於緊缺資源,所以現代操作系統都設計為由內核程序統一管理,用戶程序無權直接干涉。不是說你申請多少就真的給你多少,而是你實際使用多少才會給你多少。
回到上面那個小實驗,你發現啟動后程序內存占用很小就是這個原因。盡管JVM已經在你啟動時向系統申請了1G的固定堆大小空間。但是由於你這個程序只是一個簡單的測試,里面並沒有實際的代碼操作業務。所以你實際上只用到了很小的物理內存空間。但是如果你的程序真有業務邏輯,隨着系統的運行,實際占用物理內存就會越來越多,直到達到申請的上限值1G。運行期間,你的程序同時也會釋放一些對象(通過GC),並在適當的時機歸還一些物理內存給操作系統。所以占用的物理內存大小,也會動態有所調整。這樣操作系統就可以給其他程序使用,提高了內存利用效率。這樣的設計也沒什么不好的。
如上圖所示,操作系統對內存管理是以頁為基本單位的,一個頁代表了一個固定大小的地址范圍。用戶程序給某個變量比如byte[]賦值時,此時該變量對應的進程虛擬地址空間所在的頁在物理內存上找不到對應的頁映射時,就會觸發了一個缺頁中斷異常,操作系統就會重新將虛擬地址的頁映射到物理內存中的頁,此時才是真正實現了內存分配,會占用實際的物理內存空間。假如Java程序的GC把這個byte[]變量收回了,也就是不需要占用內存空間了,用戶進程的堆管理器會適當的歸還一些物理內存給操作系統,以便下次可以給其他任何程序使用。需要注意的是用戶程序調用的malloc和free兩個系統調用,都是針對用戶進程的虛擬地址空間而言的,並不是實際操作物理內存。只有操作系統才擁有對實際物理內存的管理權限。操作系統可以使用有效的各種算法,來獨立高效的管理物理內存。這里面的細節,我這里不詳細說了,有興趣的可以去看些操作系統的資料深入了解下。
然而我們實際的Java程序,配置成固定堆大小后,你會發現,內存占用一旦上去了就下不來了。即使當前程序處於比較空閑的狀態下。這又是為什么呢?難道Java的GC沒有回收內存?
其實並不是GC沒有回收內存,而是我們這里存在理解問題。GC回收內存並不是指物理內存,而是指當前進程的虛擬內存(虛擬地址空間)。一般而言,回收的虛擬內存並不會立即歸還給操作系統,從而操作系統也就無法回收它了。至於何時歸還物理內存,這取決於一個叫glibc的堆管理器。它根據一定的策略和算法適當的釋放真實的物理內存。否則即便Java程序GC了對象,該對象占用的物理內存也不會立即釋放出來。由於這里我們是設置了固定大小的堆空間,實際上GC回收的虛擬內存,也不會被釋放歸還給操作系統。故Java進程內存占用一旦增長,內存占用幾乎都不會再下降了,這樣也是出於對象再分配的效率考慮的。這樣顯然可以避免操作系統反復把進程的虛擬地址頁復映射物理內存頁(缺頁中斷異常)操作,導致頻繁的用戶態和內核態切換造成的性能問題。