Java程序員也應該知道的係統知識係列之內存
作者:林昊
上篇說到了Java程序和CPU的關係,對於多數實現的較好的Java應用程序而言,基本上隨著CPU的核數增加或能力提升,係統能夠支撐的並發量就可以穩步上升,但對於內存而言,是否也是這樣呢,這篇我們就來看看Java程序和內存的關係。
和CPU一樣,我們首先要知道機器上的內存的硬件狀況,在linux下,可以通過dmidecode | grep -A16 “Memory Device$”命令來查看機器插了多少根內存條,以及每根內存條的具體型號,內存條的具體型號對Java應用的運行性能也會有些影響,但一般來說不會有CPU那麼明顯。
要查看機器上內存的使用狀況,可通過free -m來查看,這個時候常見的第一個問題是看到free值很小,就認為內存不夠用了,但其實真正可用的內存是free+buffers+cached,os為了提升運行性能,會利用一些內存來做cache,以提升諸如讀寫文件的速度等。
當free不夠的時候,os會根據一個係統值來決定是釋放buffers/cached還是使用swap,如果swap沒開啟就不用判斷了,如果swap開啟了,那麼vm.swappiness這個值就非常關鍵了,這個值是一個傾向值的意思,值越大表示越傾向於使用swap,越小表示越傾向於釋放buffers/cached,對於響應時間敏感的應用而言,隻要用到swap了,通常對響應時間的影響都會很明顯,而且swappiness默認是60,意味著默認其實是傾向於使用swap的,因此對於這類係統建議最好是關閉swap,畢竟對於集群型的應用來說,通常都是寧可接受內存不夠用的情況下機器掛掉,也不能接受響應時間變慢。
對於cached的內存區域,可以執行echo 3 > /proc/sys/vm/drop_caches來強製釋放,這種在某些情況下可能會需要用,例如希望把還在cache裏的文件內容刷到磁盤。
對於swap區域,可以通過執行swapoff -a來強製刷掉,如果需要再開啟,可以執行swapon -a。
除了os利用內存來提升運行性能外,cpu也同樣借助它的各級cache來提升運行速度,多核之後,UMA的方式導致係統總線帶寬會比較吃緊,而NUMA是解決這個的一種好的方式,關於NUMA具體是什麼就不在這裏講了,需要知道下的是默認通常是不打開NUMA的,從我們的一些測試來看,有些CPU型號在是否打開NUMA的情況下應用的性能會相差一倍,不過大部分的CPU型號裏打開NUMA的提升大概會在20%–30%左右,如果OS沒打開NUMA,其實在Java啟動參數上設置了-XX:+UseNuma也是沒什麼用的,可以用numactl -H來查看NUMA是否打開,但由於打開NUMA的話對應用跑在同一個NUMA Node上要求還是比較高的,因此在虛擬機類的場景中為了追求CPU搭配的靈活性以及維護的簡便性,通常就隻能放棄NUMA了。
要看運行的Java進程消耗的內存,可以用ps aux | grep java或具體的pid、或top -p [pid]也可以看,可以看到的是有兩列內存的信息,一列是VIRT,一列是RES。
VIRT表示的是此進程占用的地址空間的大小,地址空間在32bit的os上的上限是3G,在64bit可以認為是無限大,當地址空間不夠用的時候,Java進程會直接crash,在crash的log裏會有java.lang.OutOfMemoryError: Out of swap space的信息,Java進程在啟動時會根據-Xms + -XX:PermSize先申請好相應大小的地址空間,在創建線程等的時候也會直接申請好-Xss對應大小的地址空間,所以創建了很多線程的情況下可以看到VIRT會很高,
RES表示的是此進程具體占用的內存的大小,這個地方很容易產生兩個疑問:
1. 為什麼看到的RES值大於或小於了-Xmx的設置;
Java應用在剛啟動,或者說還沒有到觸發Full GC之前,隻有當真正需要使用內存才會去占用實際的內存,否則隻是占據了地址空間,因此看到的RES值有可能會小於-Xmx的值;
而對於一個運行了一段時間且觸發過CMS GC/Full GC的Java應用而言,則很有可能看到的RES大於了-Xmx的值,原因在於Java除了-Xmx會占用相應的內存外,Perm Gen、C Heap(CodeCache、Direct Memory、線程、對象結構、GC等)也要占據一些內存,所以看到的RES大於-Xmx也很正常。
2. 為什麼GC後RES的值沒下降相應的數值;
這個的原因在於GC後JVM並不會把內存釋放給OS,而是會占著繼續用。
Java程序在運行中過程,除了Direct Memory、直接用Unsafe操作、或間接的使用Deflater等的會涉及到C Heap,更多的是去JVM Heap中申請內存,並且由於JVM包裝掉了,所以Java程序員在寫代碼的時候很容易由於錯誤的使用API或數據結構導致內存的浪費,這通常是為什麼很多C的高手(注意:這裏說的是C的高手)寫的代碼效率會比普通的Java程序員寫的高不少的一個原因之一,而回收也由JVM來控製,這個係列的文章主要是科普下係統方麵的知識,JVM的一些就不在這裏寫了,在之前的一些PPT或文章裏也寫過很多次關於JVM的內存管理,同樣關於怎麼去查Java程序在JVM Heap和C Heap裏的消耗,之前也寫過不少的文章,就不在這裏寫了,畢竟這些多數和係統關係就不算大了。
關於內存資源這塊,Java程序倒不一定是越多越好,內存越大,通常也就意味著GC的負擔越重,而GC的時候通常應用是全暫停的(除了CMS是Almost Concurrently外),但也不能太小,太小的話運行時會比較明顯的暴露出來,因為會導致非常頻繁的GC(到底多頻繁算頻繁呢,從目前的經驗來看,ygc盡可能能在3s+一次,fgc或cms gc的話最好在10分鍾以上),而太頻繁的GC會導致CPU大部分時候都耗了執行GC上,應用能夠支撐的並發量自然就會不夠,夠用就OK,在排除內存泄露等因素外,可以看看在Full GC後實際需要占用的內存大小,一般來說隻要確保給Java進程留有的空間比這個需要常駐的大小大一定比例就OK(不過到底大多少還真不好說,憑經驗吧),不要因為機器內存有多(相對而言,現在多數機器在內存這塊都是比較夠的),就給Java分配更多的內存,否則一次較長時間的暫停搞不好就回導致極大的杯具,所以內存資源這塊和CPU不太一樣,我的觀點一向是夠用並留有一定空間就OK,而不用去追求用滿,當然如果能充分有效的利用多餘的內存提升性能當然是OK的,例如cache什麼的。
從內存資源的狀況可以看到,隨著硬件的不斷發展,將來對Java應用而言,會有個悲催的現象是,CPU用的比較滿,但機器的內存資源浪費的比較嚴重,針對這個問題,看來後麵必須專門寫一篇來講講虛擬化。
說到這了,順帶說下上篇文章留下的一個話題,就是GC這種線程在執行的時候是怎麼確保占有足夠的時間片,這個的原因是GC在執行的時候其他的線程其實都是處於暫停狀態(其實這話不太準確),GC要執行前,JVM會先將一個內存頁設為隻讀,而在所有有引用關係賦值的地方,JVM在編譯代碼時都會先插入一個檢查某個內存頁的狀態的代碼,而因為之前GC已經把這個內存頁狀態設為了隻讀,所以當其他線程的代碼走到這個地方的時候,會拋出異常,從而導致線程進入一個blocked的狀態,就不會來搶占GC線程需要的CPU了。
最後更新:2017-04-03 07:57:07