在一些高併發的程序,或者一些大量使用內存來進行計算的程序,有時候經常會遇到一些這樣的問題:程序剛開始運行挺快的,後來就運行緩慢下來了,甚至於到了必定時間還會出現OOM或者StackOverFlow等錯誤。要理解這些錯誤產生的根源,就要了解JVM是何如劃分、管理、回收內存的,本篇博客將從博主對JVM的認識以及實際經驗角度出發,聊聊這些話題。算法
JVM內存結構安全
一旦涉足JVM內存結構,恐怕會冒出大量的術語:新生代?老生代?永久代?等等,咱們暫且拋開這些名稱,基於咱們的JAVA基礎,它應該是什麼樣子的呢?多線程
第一:程序是多線程的(單線程不過是多線程的一個極端而已),要知道CPU在多個線程之間來回切換,是保留有上下文信息的,那麼這些信息是應該被存儲的。說的簡單點,至少每一個線程都應該知道,若是CPU要執行本身,那麼應該從哪裏開始呢?所以JVM內存結構中,應該要有這樣的區域A,並且這個A區域應該是每一個線程的專屬區域。併發
第二:不管是咱們編寫的代碼,仍是利用的第三方的工具,都是須要類裝載器進行加載的,這說明應該有區域B專門用於存儲這些信息,好比編譯後的類,方法,常量池等。咱們是否曾經遇到Server啓動時,會拋出OOM的錯誤呢?可能就是由於Server啓動內存較小,而須要Load Class太多致使的。這塊區域B應該對每一個線程進行開放共享。ide
第三:從學習JAVA開始,咱們就知道了內存有堆和棧的概念,並且咱們都說new 出來的對象是存放在堆中的,要知道JAVA是面向對象的,因此說這一塊應該是佔用空間較大的一塊,也是內存管理、回收的一個核心點。堆,也是每一個線程均可以來訪問的,若是堆的空間不足了,卻仍需爲對象分配空間的話,就會OOM了。高併發
第四:既然上面說到了堆,那麼下面就得說下棧的概念了。在多線程中,咱們但願造成棧封閉,來達到線程安全的目的,好比ThreadLocal,使用局部變量代替成員變量等。以上其實說明,棧空間是每一個線程所私有的,棧中存放的是方法中的局部變量,方法入口信息等。每一次調用方法,都涉及到入棧和出棧,若是有一個遞歸方法調用了幾千次,甚至幾萬次,那麼可能會由於在棧中積壓了那麼多信息致使棧溢出的,從而拋出StackOverFlow。工具
綜合以上,其實,咱們就容易獲得下面的:性能
程序計數器,就是上面所說的A區域;方法區就是B區域。學習
根據上面的討論,程序計數器和棧跟隨線程的生命週期,而堆和方法區是由JVM的GC機制所管理的,那麼下面咱們來討論JVM中的分代垃圾回收機制。spa
分代垃圾回收機制
JVM把堆細分了2個代:新生代、老生代(老年代);而方法區叫作永久代。先來看一個圖:
新生代,即young genration,分爲3個部分,一個Eden,2個survior。所謂Eden,即伊甸園,顧名思義,其實就是新的生命,快樂;而survior表示倖存者。若是建立對象,會優先在Eden中分配,若是Eden不足了,會進行一次Minor GC,將Eden中能夠清空的清理掉,若是不能回收的,讓它進入一個survior區域,這樣倖存者的概念就出來了,其實本質上是複製回收算法。那麼爲何要搞2個survior呢?是由於若是Eden+1個survior進行Minor GC的時候,另外一個survior就發揮做用了,顯然2個survior中必然有一個是空閒的。那麼咱們應該讓Eden的空間大些,否則進行頻繁的Minor GC也會消耗資源的。
老生代,即old genration/tenured genration。若是young genration區域的空間不足了,發生了屢次Minor GC的話,那麼會把young genration中的一部分對象COPY TO 老生代區域。其實這裏涉及到一個對象年齡的問題。當old genration區域也不足時,就會進行Full GC。要知道Full GC,是很是耗時的,若是程序在執行的過程當中,JVM進行Full GC的話,就會嚴重影響性能,致使程序執行時間大大增長了。
永久代,即permant generation。通常不參與垃圾回收的。
經過上面的分析,咱們已經發現,新生代、老生代、永久代的空間大小其實在必定程度上可能會影響咱們的程序執行效果,由於他們的大小會影響JVM GC,所以咱們應該關注這些參數的設置。
JVM參數設置
-Xms : 堆的初始化大小
-Xmx : 堆的最大大小
-Xmn : young geration的大小
-Xss : 棧大小
-XX:PermSize : 永久代初始大小
-XX:MaxPermSize : 永久代最大大小
......
LINUX下如何監控程序的JVM情況?
在Linux下,要想監控程序的JVM內存使用,好比Eden,S0,S1,YGC,FULL GC等的狀況的話,該怎麼作呢?
首先來講,咱們要找到程序在LINUX中運行的PID,很簡單,咱們能夠經過top 或者 ps來查找。在這裏,咱們想一想,若是是多線程的程序,好比有10個線程啓動的話,是否在top或者ps中會有10個這樣的JAVA命令進程呢?那麼究竟是什麼個狀況呢?咱們先來看看:
top的-H選項其實已經很明白指出:會打印出此進程下面的全部線程信息。也就是說,一個程序,啓動了多個線程,在LINUX下一個線程將會對應一個進程!
此時,咱們能夠利用jstack進一步分析線程的DUMP文件:
咱們能夠垂手可得的獲取,這些線程的名稱、優先級、TID(JAVA中線程的惟一標示)、NID(將這個16進制的數字轉成10進制那麼就和top/ps中的PID對應上了),運行狀態信息等。
更進一步,咱們還能夠利用jstat來分析內存使用情況:
【關於jstat徹底能夠利用man jstat來獲取幫助信息】
經過jstat命令,咱們將一目瞭然的清楚程序新生代,老生代,永久代的佔比,各類GC的次數以及耗時等信息了。