做者:melonstreet
https://www.cnblogs.com/QG-wh...
下面總結了 JVM 的 4 個問題,看你能頂住麼?html
二、OOM可能發生在哪些區域上?linux
一、JVM的內存區域是怎麼劃分的?緩存
JVM的內存劃分中,有部分區域是線程私有的,有部分是屬於整個JVM進程;有些區域會拋出OOM異常,有些則不會,瞭解JVM的內存區域劃分以及特徵,是定位線上內存問題的基礎。那麼JVM內存區域是怎麼劃分的呢?微信
首先是程序計數器(Program Counter Register),在JVM規範中,每一個線程都有本身的程序計數器。這是一塊比較小的內存空間,存儲當前線程正在執行的Java方法的JVM指令地址,即字節碼的行號。若是正在執行Native方法,則這個計數器爲空。該內存區域是惟一一個在Java虛擬機規範中沒有規定任何OOM狀況的內存區域。多線程
第二,Java虛擬機棧(Java Virtal Machine Stack),一樣也是屬於線程私有區域,每一個線程在建立的時候都會建立一個虛擬機棧,生命週期與線程一致,線程退出時,線程的虛擬機棧也回收。虛擬機棧內部保持一個個的棧幀,每次方法調用都會進行壓棧,JVM對棧幀的操做只有出棧和壓棧兩種,方法調用結束時會進行出棧操做。架構
該區域存儲着局部變量表,編譯時期可知的各類基本類型數據、對象引用、方法出口等信息。oracle
第三,本地方法棧(Native Method Stack)與虛擬機棧相似,本地方法棧是在調用本地方法時使用的棧,每一個線程都有一個本地方法棧。
第四,堆(Heap),幾乎全部建立的Java對象實例,都是被直接分配到堆上的。堆被全部的線程所共享,在堆上的區域,會被垃圾回收器作進一步劃分,例如新生代、老年代的劃分。Java虛擬機在啓動的時候,可使用「Xmx」之類的參數指定堆區域的大小。
第五,方法區(Method Area)。方法區與堆同樣,也是全部的線程所共享,存儲被虛擬機加載的元(Meta)數據,包括類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。這裏須要注意的是運行時常量池也在方法區中。
根據Java虛擬機規範的規定,當方法區沒法知足內存分配需求時,將拋出OutOfMemoryError異常。因爲早期HotSpot JVM的實現,將CG分代收集拓展到了方法區,所以不少人會將方法區稱爲永久代。Oracle JDK8中已永久代移除永久代,同時增長了元數據區(Metaspace)。
第六,運行時常量池(Run-Time Constant Pool),這是方法區的一部分,受到方法區內存的限制,當常量池沒法再申請到內存時,會拋出OutOfMemoryError異常。
在Class文件中,除了有類的版本、方法、字段、接口等描述信息外,還有一項信息是常量池。每一個Class文件的頭四個字節稱爲Magic Number,它的做用是肯定這是不是一個能夠被虛擬機接受的文件;接着的四個字節存儲的是Class文件的版本號。緊挨着版本號以後的,就是常量池入口了。常量池主要存放兩大類常量:
class文件中的常量池,也稱爲靜態常量池,JVM虛擬機完成類裝載操做後,會把靜態常量池加載到內存中,存放在運行時常量池。
第七,直接內存(Direct Memory),直接內存並不屬於Java規範規定的屬於Java虛擬機運行時數據區的一部分。Java的NIO可使用Native方法直接在java堆外分配內存,使用DirectByteBuffer對象做爲這個堆外內存的引用。
下面這張圖,反映了運行中的Java進程內存佔用狀況:
二、OOM可能發生在哪些區域上?
根據javadoc的描述,OOM是指JVM的內存不夠用了,同時垃圾收集器也沒法提供更多的內存。從描述中能夠看出,在JVM拋出OutOfMemoryError以前,垃圾收集器通常會出馬先嚐試回收內存。
從上面分析的Java數據區來看,除了程序計數器不會發生OOM外,哪些區域會發生OOM的狀況呢?
第一,堆內存。堆內存不足是最多見的發送OOM的緣由之一,若是在堆中沒有內存完成對象實例的分配,而且堆沒法再擴展時,將拋出OutOfMemoryError異常。當前主流的JVM能夠經過-Xmx和-Xms來控制堆內存的大小,發生堆上OOM的多是存在內存泄露,也多是堆大小分配不合理。
第二,Java虛擬機棧和本地方法棧,這兩個區域的區別不過是虛擬機棧爲虛擬機執行Java方法服務,而本地方法棧則爲虛擬機使用到的Native方法服務,在內存分配異常上是相同的。
在JVM規範中,對Java虛擬機棧規定了兩種異常:1.若是線程請求的棧大於所分配的棧大小,則拋出StackOverFlowError錯誤,好比進行了一個不會中止的遞歸調用;2. 若是虛擬機棧是能夠動態拓展的,拓展時沒法申請到足夠的內存,則拋出OutOfMemoryError錯誤。
第三,直接內存。直接內存雖然不是虛擬機運行時數據區的一部分,但既然是內存,就會受到物理內存的限制。在JDK1.4中引入的NIO使用Native函數庫在堆外內存上直接分配內存,但直接內存不足時,也會致使OOM。
第四,方法區。隨着Metaspace元數據區的引入,方法區的OOM錯誤信息也變成了「java.lang.OutOfMemoryError:Metaspace」。對於舊版本的Oracle JDK,因爲永久代的大小有限,而JVM對永久代的垃圾回收並不積極,若是往永久代不斷寫入數據,例如String.Intern()的調用,在永久代佔用太多空間致使內存不足,也會出現OOM的問題,對應的錯誤信爲「java.lang.OutOfMemoryError:PermGen space」
三、堆內存結構是怎麼樣的?
能夠藉助一些工具來了解JVM的內存內容,具體到特定的內存區域,應該用什麼工具去定位呢?
關於內存的監控與診斷,在後面會進行深刻了解。如今來看下一個問題:堆內的結構是怎麼的呢?深刻淺出 Java 中 JVM 內存管理,這篇推薦看下。
站在垃圾收集器的角度來看,能夠把內存分爲新生代與老年代。內存的分配規則取決於當前使用的是哪一種垃圾收集器的組合,以及內存相關的參數配置。往大的方向說,對象優先分配在新生代的Eden區域,而大對象直接進入老年代。
第一, 新生代的Eden區域,對象優先分配在該區域,同時JVM能夠爲每一個線程分配一個私有的緩存區域,稱爲TLAB(Thread Local Allocation Buffer),避免多線程同時分配內存時須要使用加鎖等機制而影響分配速度。TLAB在堆上分配,位於Eden中。TLAB的結構以下:
// ThreadLocalAllocBuffer: a descriptor for thread-local storage used by // the threads for allocation. // It is thread-private at any time, but maybe multiplexed over // time across multiple threads. The park()/unpark() pair is // used to make it avaiable for such multiplexing. class ThreadLocalAllocBuffer: public CHeapObj<mtThread> { friend class VMStructs; private: HeapWord* _start; // address of TLAB HeapWord* _top; // address after last allocation HeapWord* _pf_top; // allocation prefetch watermark HeapWord* _end; // allocation end (excluding alignment_reserve) size_t _desired_size; // desired size (including alignment_reserve) size_t _refill_waste_limit; // hold onto tlab if free() is larger than this
從本質上來講,TLAB的管理是依靠三個指針:start、end、top。start與end標記了Eden中被該TLAB管理的區域,該區域不會被其餘線程分配內存所使用,top是分配指針,開始時指向start的位置,隨着內存分配的進行,慢慢向end靠近,當撞上end時觸發TLAB refill。
所以內存中Eden的結構大致爲:
第2、新生代的Survivor區域。當Eden區域內存不足時會觸發Minor GC,也稱爲新生代GC,在Minor GC存活下來的對象,會被複制到Survivor區域中。我認爲Survivor區的做用在於避免過早觸發Full GC。若是沒有Survivor,Eden區每進行一次Minor GC都把對象直接送到老年代,老年代很快便會內存不足引起Full GC。
新生代中有兩個Survivor區,我認爲兩個Survivor的做用在於提升性能,避免內存碎片的出現。在任什麼時候候,總有一個Survivor是empty的,在發生Minor GC時,會將Eden及另外一個的Survivor的存活對象拷貝到該empty Survivor中,從而避免內存碎片的產生。新生代的內存結構大致爲:
第3、老年代。老年代放置長生命週期的對象,一般是從Survivor區域拷貝過來的對象,不過當對象過大的時候,沒法在新生代中用連續內存的存放,那麼這個大對象就會被直接分配在老年代上。通常來講,普通的對象都是分配在TLAB上,較大的對象,直接分配在Eden區上的其餘內存區域,而過大的對象,直接分配在老年代上。
第4、永久代。如前面所說,在早起的Hotspot JVM中有老年代的概念,老年代用於存儲Java類的元數據、常量池、Intern字符串等。在JDK8以後,就將老年代移除,而引入元數據區的概念。
第5、Vritual空間。前面說過,可使用Xms與Xmx來指定堆的最小與最大空間。若是Xms小於Xmx,堆的大小不會直接擴展到上限,而是留着一部分等待內存需求不斷增加時,再分配給新生代。Vritual空間即是這部分保留的內存區域。
關注微信公衆號:Java技術棧,在後臺回覆:JVM,能夠獲取我整理的 N 篇最新JVM 教程,都是乾貨。
那麼綜上所述,能夠畫出Java堆內的內存結構大致爲:
經過一些參數,能夠來指定上述的堆內存區域的大小:
四、經常使用的性能監控與問題定位工具備哪些?
在系統的性能分析中,CPU、內存與IO是主要的關注項。不少時候服務出現問題,在這三者上會體現出現,好比CPU飆升,內存不足發生OOM等,這時候須要使用對應的工具,來對性能進行監控,對問題進行定位。
對於CPU的監控,首先可使用top命令來進行查看,下面是使用top查看負載的一個截圖:
load average 表明1分鐘、5分鐘、15分鐘的系統平均負載,從這三個數字,能夠判斷系統負荷是大仍是小。當CPU徹底空閒的時候,平均負荷爲0;當CPU工做量飽和的時候,平均負荷爲1。
所以 load average 這三個數值越低,表明系統負荷越小,那麼何時能看出系統負荷比較重呢?這篇文章(Understanding Linux CPU Load – when should you be worried)裏解釋得很是通俗。若是電腦裏只有一個CPU,把CPU當作一條單行橋,橋上只有一個車道,全部的車都必須從這個橋上經過。那麼
系統負荷爲0,表明橋上一輛車也沒有
系統負荷0.5,意味着橋上一半路段上有車
系統負荷1,意味着橋上道路已經被車佔滿
系統負荷1.7,表明着在橋上車子已經滿了(100%),同時還有70%的車子在等待從橋上經過:
從top命令的截圖中能夠看到這三個值機器的load average很是低。若是這三個值很是高,好比超過了50%或60%,就應當引發注意。從時間維度上來講,若是發現CPU負荷慢慢升高,也須要警戒。
其餘的內存、CPU等性能監控工具的使用,以一張腦圖來展現:
具體的使用方式能夠參考從一次線上故障思考Java問題定位思路。
參考
https://www.cnblogs.com/dream...
https://docs.oracle.com/javas...
https://www.cnblogs.com/Kidez...
https://www.cnblogs.com/baihu...
https://www.jianshu.com/p/cd8...
http://www.ruanyifeng.com/blo..._load_average_explained.html
推薦去個人博客閱讀更多:
2.Spring MVC、Spring Boot、Spring Cloud 系列教程
3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程
生活很美好,明天見~