天天看點

Java程式員也應該知道的系統知識系列之記憶體

作者:林昊

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了。