來(lái)源:http://t.cn/EX1dxJu
JVM的內存區域是怎么劃分的?
OOM可能發(fā)生在哪些區域上?
堆內存結構是怎么樣的?
常用的性能監控與問(wèn)題定位工具有哪些?
參考
JVM的內存劃分中,有部分區域是線(xiàn)程私有的,有部分是屬于整個(gè)JVM進(jìn)程;有些區域會(huì )拋出OOM異常,有些則不會(huì ),了解JVM的內存區域劃分以及特征,是定位線(xiàn)上內存問(wèn)題的基礎。那么JVM內存區域是怎么劃分的呢?
首先是程序計數器(Program Counter Register),在JVM規范中,每個(gè)線(xiàn)程都有自己的程序計數器。這是一塊比較小的內存空間,存儲當前線(xiàn)程正在執行的Java方法的JVM指令地址,即字節碼的行號。如果正在執行Native方法,則這個(gè)計數器為空。該內存區域是唯一一個(gè)在Java虛擬機規范中沒(méi)有規定任何OOM情況的內存區域。
第二,Java虛擬機棧(Java Virtal Machine Stack),同樣也是屬于線(xiàn)程私有區域,每個(gè)線(xiàn)程在創(chuàng )建的時(shí)候都會(huì )創(chuàng )建一個(gè)虛擬機棧,生命周期與線(xiàn)程一致,線(xiàn)程退出時(shí),線(xiàn)程的虛擬機棧也回收。虛擬機棧內部保持一個(gè)個(gè)的棧幀,每次方法調用都會(huì )進(jìn)行壓棧,JVM對棧幀的操作只有出棧和壓棧兩種,方法調用結束時(shí)會(huì )進(jìn)行出棧操作。
該區域存儲著(zhù)局部變量表,編譯時(shí)期可知的各種基本類(lèi)型數據、對象引用、方法出口等信息。
第三,本地方法棧(Native Method Stack)與虛擬機棧類(lèi)似,本地方法棧是在調用本地方法時(shí)使用的棧,每個(gè)線(xiàn)程都有一個(gè)本地方法棧。
第四,堆(Heap),幾乎所有創(chuàng )建的Java對象實(shí)例,都是被直接分配到堆上的。堆被所有的線(xiàn)程所共享,在堆上的區域,會(huì )被垃圾回收器做進(jìn)一步劃分,例如新生代、老年代的劃分。Java虛擬機在啟動(dòng)的時(shí)候,可以使用“Xmx”之類(lèi)的參數指定堆區域的大小。
第五,方法區(Method Area)。方法區與堆一樣,也是所有的線(xiàn)程所共享,存儲被虛擬機加載的元(Meta)數據,包括類(lèi)信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數據。這里需要注意的是運行時(shí)常量池也在方法區中。根據Java虛擬機規范的規定,當方法區無(wú)法滿(mǎn)足內存分配需求時(shí),將拋出OutOfMemoryError異常。由于早期HotSpot JVM的實(shí)現,將CG分代收集拓展到了方法區,因此很多人會(huì )將方法區稱(chēng)為永久代。Oracle JDK8中已永久代移除永久代,同時(shí)增加了元數據區(Metaspace)。
第六,運行時(shí)常量池(Run-Time Constant Pool),這是方法區的一部分,受到方法區內存的限制,當常量池無(wú)法再申請到內存時(shí),會(huì )拋出OutOfMemoryError異常。
在Class文件中,除了有類(lèi)的版本、方法、字段、接口等描述信息外,還有一項信息是常量池。每個(gè)Class文件的頭四個(gè)字節稱(chēng)為Magic Number,它的作用是確定這是否是一個(gè)可以被虛擬機接受的文件;接著(zhù)的四個(gè)字節存儲的是Class文件的版本號。緊挨著(zhù)版本號之后的,就是常量池入口了。常量池主要存放兩大類(lèi)常量:
字面量(Literal),如文本字符串、final常量值
符號引用,存放了與編譯相關(guān)的一些常量,因為Java不像C 那樣有連接的過(guò)程,因此字段方法這些符號引用在運行期就需要進(jìn)行轉換,以便得到真正的內存入口地址。
class文件中的常量池,也稱(chēng)為靜態(tài)常量池,JVM虛擬機完成類(lèi)裝載操作后,會(huì )把靜態(tài)常量池加載到內存中,存放在運行時(shí)常量池。
第七,直接內存(Direct Memory),直接內存并不屬于Java規范規定的屬于Java虛擬機運行時(shí)數據區的一部分。Java的NIO可以使用Native方法直接在java堆外分配內存,使用DirectByteBuffer對象作為這個(gè)堆外內存的引用。
下面這張圖,反映了運行中的Java進(jìn)程內存占用情況:
根據javadoc的描述,OOM是指JVM的內存不夠用了,同時(shí)垃圾收集器也無(wú)法提供更多的內存。從描述中可以看出,在JVM拋出OutOfMemoryError之前,垃圾收集器一般會(huì )出馬先嘗試回收內存。
從上面分析的Java數據區來(lái)看,除了程序計數器不會(huì )發(fā)生OOM外,哪些區域會(huì )發(fā)生OOM的情況呢?
第一,堆內存。堆內存不足是最常見(jiàn)的發(fā)送OOM的原因之一,如果在堆中沒(méi)有內存完成對象實(shí)例的分配,并且堆無(wú)法再擴展時(shí),將拋出OutOfMemoryError異常。當前主流的JVM可以通過(guò)-Xmx和-Xms來(lái)控制堆內存的大小,發(fā)生堆上OOM的可能是存在內存泄露,也可能是堆大小分配不合理。
第二,Java虛擬機棧和本地方法棧,這兩個(gè)區域的區別不過(guò)是虛擬機棧為虛擬機執行Java方法服務(wù),而本地方法棧則為虛擬機使用到的Native方法服務(wù),在內存分配異常上是相同的。在JVM規范中,對Java虛擬機棧規定了兩種異常:1.如果線(xiàn)程請求的棧大于所分配的棧大小,則拋出StackOverFlowError錯誤,比如進(jìn)行了一個(gè)不會(huì )停止的遞歸調用;2. 如果虛擬機棧是可以動(dòng)態(tài)拓展的,拓展時(shí)無(wú)法申請到足夠的內存,則拋出OutOfMemoryError錯誤。
第三,直接內存。直接內存雖然不是虛擬機運行時(shí)數據區的一部分,但既然是內存,就會(huì )受到物理內存的限制。在JDK1.4中引入的NIO使用Native函數庫在堆外內存上直接分配內存,但直接內存不足時(shí),也會(huì )導致OOM。
第四,方法區。隨著(zhù)Metaspace元數據區的引入,方法區的OOM錯誤信息也變成了“java.lang.OutOfMemoryError:Metaspace”。對于舊版本的Oracle JDK,由于永久代的大小有限,而JVM對永久代的垃圾回收并不積極,如果往永久代不斷寫(xiě)入數據,例如String.Intern()的調用,在永久代占用太多空間導致內存不足,也會(huì )出現OOM的問(wèn)題,對應的錯誤信為“java.lang.OutOfMemoryError:PermGen space”
可以借助一些工具來(lái)了解JVM的內存內容,具體到特定的內存區域,應該用什么工具去定位呢?
圖形化工具。圖形化工具的優(yōu)點(diǎn)是直觀(guān),連接到Java進(jìn)程后,可以顯示堆內存、堆外內存的使用情況,類(lèi)似的工具有JConsole,VisualVm等。
命令行工具。這類(lèi)工具可以在運行時(shí)進(jìn)行查詢(xún),包括jstat,jmap等,可以對堆內存、方法區等進(jìn)行查看。定位線(xiàn)上問(wèn)題時(shí)也多會(huì )使用這些工具。jmap也可以生成堆轉儲文件(Heap Dump)文件,如果是在linux上,可以將堆轉儲文件拉到本地來(lái),使用Eclipse MAT進(jìn)行分析,也可以使用jhap進(jìn)行分析。
關(guān)于內存的監控與診斷,在后面會(huì )進(jìn)行深入了解?,F在來(lái)看下一個(gè)問(wèn)題:堆內的結構是怎么的呢?
站在垃圾收集器的角度來(lái)看,可以把內存分為新生代與老年代。內存的分配規則取決于當前使用的是哪種垃圾收集器的組合,以及內存相關(guān)的參數配置。往大的方向說(shuō),對象優(yōu)先分配在新生代的Eden區域,而大對象直接進(jìn)入老年代。
第一, 新生代的Eden區域,對象優(yōu)先分配在該區域,同時(shí)JVM可以為每個(gè)線(xiàn)程分配一個(gè)私有的緩存區域,稱(chēng)為T(mén)LAB(Thread Local Allocation Buffer),避免多線(xiàn)程同時(shí)分配內存時(shí)需要使用加鎖等機制而影響分配速度。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
從本質(zhì)上來(lái)說(shuō),TLAB的管理是依靠三個(gè)指針:start、end、top。start與end標記了Eden中被該TLAB管理的區域,該區域不會(huì )被其他線(xiàn)程分配內存所使用,top是分配指針,開(kāi)始時(shí)指向start的位置,隨著(zhù)內存分配的進(jìn)行,慢慢向end靠近,當撞上end時(shí)觸發(fā)TLAB refill。因此內存中Eden的結構大體為:
第二、新生代的Survivor區域。當Eden區域內存不足時(shí)會(huì )觸發(fā)Minor GC,也稱(chēng)為新生代GC,在Minor GC存活下來(lái)的對象,會(huì )被復制到Survivor區域中。我認為Survivor區的作用在于避免過(guò)早觸發(fā)Full GC。如果沒(méi)有Survivor,Eden區每進(jìn)行一次Minor GC都把對象直接送到老年代,老年代很快便會(huì )內存不足引發(fā)Full GC。新生代中有兩個(gè)Survivor區,我認為兩個(gè)Survivor的作用在于提高性能,避免內存碎片的出現。在任何時(shí)候,總有一個(gè)Survivor是empty的,在發(fā)生Minor GC時(shí),會(huì )將Eden及另一個(gè)的Survivor的存活對象拷貝到該empty Survivor中,從而避免內存碎片的產(chǎn)生。新生代的內存結構大體為:
第三、老年代。老年代放置長(cháng)生命周期的對象,通常是從Survivor區域拷貝過(guò)來(lái)的對象,不過(guò)當對象過(guò)大的時(shí)候,無(wú)法在新生代中用連續內存的存放,那么這個(gè)大對象就會(huì )被直接分配在老年代上。一般來(lái)說(shuō),普通的對象都是分配在TLAB上,較大的對象,直接分配在Eden區上的其他內存區域,而過(guò)大的對象,直接分配在老年代上。
第四、永久代。如前面所說(shuō),在早起的Hotspot JVM中有老年代的概念,老年代用于存儲Java類(lèi)的元數據、常量池、Intern字符串等。在JDK8之后,就將老年代移除,而引入元數據區的概念。
第五、Vritual空間。前面說(shuō)過(guò),可以使用Xms與Xmx來(lái)指定堆的最小與最大空間。如果Xms小于Xmx,堆的大小不會(huì )直接擴展到上限,而是留著(zhù)一部分等待內存需求不斷增長(cháng)時(shí),再分配給新生代。Vritual空間便是這部分保留的內存區域。
那么綜上所述,可以畫(huà)出Java堆內的內存結構大體為:
通過(guò)一些參數,可以來(lái)指定上述的堆內存區域的大?。?/p>
-Xmx value 指定最大的堆大小
-Xms value 指定初始的最小堆大小
-XX:NewSize = value 指定新生代的大小
-XX:NewRatio = value 老年代與新生代的大小比例。默認情況下,這個(gè)比例是2,也就是說(shuō)老年代是新生代的2倍大。老年代過(guò)大的時(shí)候,Full GC的時(shí)間會(huì )很長(cháng);老年代過(guò)小,則很容易觸發(fā)Full GC,Full GC頻率過(guò)高,這就是這個(gè)參數會(huì )造成的影響。
-XX:SurvivorRation = value . 設置Eden與Srivivor的大小比例,如果該值為8,代表一個(gè)Survivor是Eden的1/8,是整個(gè)新生代的1/10。
在系統的性能分析中,CPU、內存與IO是主要的關(guān)注項。很多時(shí)候服務(wù)出現問(wèn)題,在這三者上會(huì )體現出現,比如CPU飆升,內存不足發(fā)生OOM等,這時(shí)候需要使用對應的工具,來(lái)對性能進(jìn)行監控,對問(wèn)題進(jìn)行定位。
對于CPU的監控,首先可以使用top命令來(lái)進(jìn)行查看,下面是使用top查看負載的一個(gè)截圖:

load average 代表1分鐘、5分鐘、15分鐘的系統平均負載,從這三個(gè)數字,可以判斷系統負荷是大還是小。當CPU完全空閑的時(shí)候,平均負荷為0;當CPU工作量飽和的時(shí)候,平均負荷為1。因此 load average 這三個(gè)數值越低,代表系統負荷越小,那么什么時(shí)候能看出系統負荷比較重呢?這篇文章(Understanding Linux CPU Load – when should you be worried)里解釋得非常通俗。如果電腦里只有一個(gè)CPU,把CPU看成一條單行橋,橋上只有一個(gè)車(chē)道,所有的車(chē)都必須從這個(gè)橋上通過(guò)。那么
系統負荷為0,代表橋上一輛車(chē)也沒(méi)有

系統負荷0.5,意味著(zhù)橋上一半路段上有車(chē)

系統負荷1,意味著(zhù)橋上道路已經(jīng)被車(chē)占滿(mǎn)

系統負荷1.7,代表著(zhù)在橋上車(chē)子已經(jīng)滿(mǎn)了(100%),同時(shí)還有70%的車(chē)子在等待從橋上通過(guò):

從top命令的截圖中可以看到這三個(gè)值機器的load average非常低。如果這三個(gè)值非常高,比如超過(guò)了50%或60%,就應當引起注意。從時(shí)間維度上來(lái)說(shuō),如果發(fā)現CPU負荷慢慢升高,也需要警惕。
其他的內存、CPU等性能監控工具的使用,以一張腦圖來(lái)展示:

具體的使用方式可以參考從一次線(xiàn)上故障思考Java問(wèn)題定位思路。
聯(lián)系客服