題目
- JVM的內(nèi)存區(qū)域是怎么劃分的?
- OOM可能發(fā)生在哪些區(qū)域上刺啦?
- 堆內(nèi)存結(jié)構(gòu)是怎么樣的留特?
- 常用的性能監(jiān)控與問題定位工具有哪些?
1.JVM的內(nèi)存區(qū)域是怎么劃分的玛瘸?
JVM的內(nèi)存劃分中蜕青,有部分區(qū)域是線程私有的,有部分是屬于整個(gè)JVM進(jìn)程糊渊;有些區(qū)域會(huì)拋出OOM異常右核,有些則不會(huì),了解JVM的內(nèi)存區(qū)域劃分以及特征渺绒,是定位線上內(nèi)存問題的基礎(chǔ)贺喝。那么JVM內(nèi)存區(qū)域是怎么劃分的呢菱鸥?
首先是程序計(jì)數(shù)器(Program Counter Register),在JVM規(guī)范中躏鱼,每個(gè)線程都有自己的程序計(jì)數(shù)器氮采。這是一塊比較小的內(nèi)存空間,存儲(chǔ)當(dāng)前線程正在執(zhí)行的Java方法的JVM指令地址挠他,即字節(jié)碼的行號(hào)扳抽。如果正在執(zhí)行Native方法,則這個(gè)計(jì)數(shù)器為空殖侵。該內(nèi)存區(qū)域是唯一一個(gè)在Java虛擬機(jī)規(guī)范中沒有規(guī)定任何OOM情況的內(nèi)存區(qū)域。
第二镰烧,Java虛擬機(jī)棧(Java Virtal Machine Stack)拢军,同樣也是屬于線程私有區(qū)域,每個(gè)線程在創(chuàng)建的時(shí)候都會(huì)創(chuàng)建一個(gè)虛擬機(jī)棧怔鳖,生命周期與線程一致茉唉,線程退出時(shí),線程的虛擬機(jī)棧也回收结执。虛擬機(jī)棧內(nèi)部保持一個(gè)個(gè)的棧幀度陆,每次方法調(diào)用都會(huì)進(jìn)行壓棧,JVM對(duì)棧幀的操作只有出棧和壓棧兩種献幔,方法調(diào)用結(jié)束時(shí)會(huì)進(jìn)行出棧操作懂傀。
該區(qū)域存儲(chǔ)著局部變量表,編譯時(shí)期可知的各種基本類型數(shù)據(jù)蜡感、對(duì)象引用蹬蚁、方法出口等信息。
第三郑兴,本地方法棧(Native Method Stack)與虛擬機(jī)棧類似犀斋,本地方法棧是在調(diào)用本地方法時(shí)使用的棧,每個(gè)線程都有一個(gè)本地方法棧情连。
第四叽粹,堆(Heap),幾乎所有創(chuàng)建的Java對(duì)象實(shí)例,都是被直接分配到堆上的却舀。堆被所有的線程所共享虫几,在堆上的區(qū)域,會(huì)被垃圾回收器做進(jìn)一步劃分禁筏,例如新生代持钉、老年代的劃分。Java虛擬機(jī)在啟動(dòng)的時(shí)候篱昔,可以使用“Xmx”之類的參數(shù)指定堆區(qū)域的大小每强。
第五始腾,方法區(qū)(Method Area)。方法區(qū)與堆一樣空执,也是所有的線程所共享浪箭,存儲(chǔ)被虛擬機(jī)加載的元(Meta)數(shù)據(jù),包括類信息辨绊、常量奶栖、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)门坷。這里需要注意的是運(yùn)行時(shí)常量池也在方法區(qū)中宣鄙。根據(jù)Java虛擬機(jī)規(guī)范的規(guī)定,當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時(shí)默蚌,將拋出OutOfMemoryError異常冻晤。由于早期HotSpot JVM的實(shí)現(xiàn),將CG分代收集拓展到了方法區(qū)绸吸,因此很多人會(huì)將方法區(qū)稱為永久代鼻弧。Oracle JDK8中已永久代移除永久代,同時(shí)增加了元數(shù)據(jù)區(qū)(Metaspace)锦茁。
第六攘轩,運(yùn)行時(shí)常量池(Run-Time Constant Pool),這是方法區(qū)的一部分码俩,受到方法區(qū)內(nèi)存的限制度帮,當(dāng)常量池?zé)o法再申請(qǐng)到內(nèi)存時(shí),會(huì)拋出OutOfMemoryError異常握玛。
在Class文件中够傍,除了有類的版本、方法挠铲、字段冕屯、接口等描述信息外,還有一項(xiàng)信息是常量池拂苹。每個(gè)Class文件的頭四個(gè)字節(jié)稱為Magic Number安聘,它的作用是確定這是否是一個(gè)可以被虛擬機(jī)接受的文件;接著的四個(gè)字節(jié)存儲(chǔ)的是Class文件的版本號(hào)瓢棒。緊挨著版本號(hào)之后的浴韭,就是常量池入口了。常量池主要存放兩大類常量:
字面量(Literal)脯宿,如文本字符串念颈、final常量值
符號(hào)引用,存放了與編譯相關(guān)的一些常量连霉,因?yàn)镴ava不像C++那樣有連接的過程榴芳,因此字段方法這些符號(hào)引用在運(yùn)行期就需要進(jìn)行轉(zhuǎn)換嗡靡,以便得到真正的內(nèi)存入口地址。
class文件中的常量池窟感,也稱為靜態(tài)常量池讨彼,JVM虛擬機(jī)完成類裝載操作后,會(huì)把靜態(tài)常量池加載到內(nèi)存中柿祈,存放在運(yùn)行時(shí)常量池哈误。
第七,直接內(nèi)存(Direct Memory)躏嚎,直接內(nèi)存并不屬于Java規(guī)范規(guī)定的屬于Java虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分蜜自。Java的NIO可以使用Native方法直接在java堆外分配內(nèi)存,使用DirectByteBuffer對(duì)象作為這個(gè)堆外內(nèi)存的引用卢佣。
下面這張圖袁辈,反映了運(yùn)行中的Java進(jìn)程內(nèi)存占用情況:
2.OOM可能發(fā)生在哪些區(qū)域上?
根據(jù)javadoc的描述珠漂,OOM是指JVM的內(nèi)存不夠用了,同時(shí)垃圾收集器也無法提供更多的內(nèi)存尾膊。從描述中可以看出媳危,在JVM拋出OutOfMemoryError之前,垃圾收集器一般會(huì)出馬先嘗試回收內(nèi)存冈敛。
從上面分析的Java數(shù)據(jù)區(qū)來看待笑,除了程序計(jì)數(shù)器不會(huì)發(fā)生OOM外,哪些區(qū)域會(huì)發(fā)生OOM的情況呢抓谴?
第一暮蹂,堆內(nèi)存。堆內(nèi)存不足是最常見的發(fā)送OOM的原因之一癌压,如果在堆中沒有內(nèi)存完成對(duì)象實(shí)例的分配仰泻,并且堆無法再擴(kuò)展時(shí),將拋出OutOfMemoryError異常滩届。當(dāng)前主流的JVM可以通過-Xmx和-Xms來控制堆內(nèi)存的大小集侯,發(fā)生堆上OOM的可能是存在內(nèi)存泄露,也可能是堆大小分配不合理帜消。
第二棠枉,Java虛擬機(jī)棧和本地方法棧,這兩個(gè)區(qū)域的區(qū)別不過是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法服務(wù)泡挺,而本地方法棧則為虛擬機(jī)使用到的Native方法服務(wù)辈讶,在內(nèi)存分配異常上是相同的。在JVM規(guī)范中娄猫,對(duì)Java虛擬機(jī)棧規(guī)定了兩種異常:1.如果線程請(qǐng)求的棧大于所分配的棧大小贱除,則拋出StackOverFlowError錯(cuò)誤生闲,比如進(jìn)行了一個(gè)不會(huì)停止的遞歸調(diào)用;2. 如果虛擬機(jī)棧是可以動(dòng)態(tài)拓展的勘伺,拓展時(shí)無法申請(qǐng)到足夠的內(nèi)存跪腹,則拋出OutOfMemoryError錯(cuò)誤。
第三飞醉,直接內(nèi)存冲茸。直接內(nèi)存雖然不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,但既然是內(nèi)存缅帘,就會(huì)受到物理內(nèi)存的限制轴术。在JDK1.4中引入的NIO使用Native函數(shù)庫在堆外內(nèi)存上直接分配內(nèi)存,但直接內(nèi)存不足時(shí)钦无,也會(huì)導(dǎo)致OOM逗栽。
第四,方法區(qū)失暂。隨著Metaspace元數(shù)據(jù)區(qū)的引入彼宠,方法區(qū)的OOM錯(cuò)誤信息也變成了“java.lang.OutOfMemoryError:Metaspace”。對(duì)于舊版本的Oracle JDK弟塞,由于永久代的大小有限凭峡,而JVM對(duì)永久代的垃圾回收并不積極,如果往永久代不斷寫入數(shù)據(jù)决记,例如String.Intern()的調(diào)用摧冀,在永久代占用太多空間導(dǎo)致內(nèi)存不足,也會(huì)出現(xiàn)OOM的問題系宫,對(duì)應(yīng)的錯(cuò)誤信為“java.lang.OutOfMemoryError:PermGen space”
3.堆內(nèi)存結(jié)構(gòu)是怎么樣的索昂?
可以借助一些工具來了解JVM的內(nèi)存內(nèi)容,具體到特定的內(nèi)存區(qū)域扩借,應(yīng)該用什么工具去定位呢椒惨?
- 圖形化工具。圖形化工具的優(yōu)點(diǎn)是直觀往枷,連接到Java進(jìn)程后框产,可以顯示堆內(nèi)存、堆外內(nèi)存的使用情況错洁,類似的工具有JConsole,VisualVm等秉宿。
- 命令行工具。這類工具可以在運(yùn)行時(shí)進(jìn)行查詢屯碴,包括jstat描睦,jmap等,可以對(duì)堆內(nèi)存导而、方法區(qū)等進(jìn)行查看忱叭。定位線上問題時(shí)也多會(huì)使用這些工具隔崎。jmap也可以生成堆轉(zhuǎn)儲(chǔ)文件(Heap Dump)文件,如果是在linux上韵丑,可以將堆轉(zhuǎn)儲(chǔ)文件拉到本地來爵卒,使用Eclipse MAT進(jìn)行分析,也可以使用jhap進(jìn)行分析撵彻。
關(guān)于內(nèi)存的監(jiān)控與診斷钓株,在后面會(huì)進(jìn)行深入了解。現(xiàn)在來看下一個(gè)問題:堆內(nèi)的結(jié)構(gòu)是怎么的呢陌僵?
站在垃圾收集器的角度來看轴合,可以把內(nèi)存分為新生代與老年代。內(nèi)存的分配規(guī)則取決于當(dāng)前使用的是哪種垃圾收集器的組合碗短,以及內(nèi)存相關(guān)的參數(shù)配置受葛。往大的方向說,對(duì)象優(yōu)先分配在新生代的Eden區(qū)域偎谁,而大對(duì)象直接進(jìn)入老年代总滩。
第一, 新生代的Eden區(qū)域,對(duì)象優(yōu)先分配在該區(qū)域巡雨,同時(shí)JVM可以為每個(gè)線程分配一個(gè)私有的緩存區(qū)域咳秉,稱為TLAB(Thread Local Allocation Buffer),避免多線程同時(shí)分配內(nèi)存時(shí)需要使用加鎖等機(jī)制而影響分配速度鸯隅。TLAB在堆上分配,位于Eden中向挖。TLAB的結(jié)構(gòu)如下:
// 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ì)上來說蝌以,TLAB的管理是依靠三個(gè)指針:start、end何之、top跟畅。start與end標(biāo)記了Eden中被該TLAB管理的區(qū)域,該區(qū)域不會(huì)被其他線程分配內(nèi)存所使用溶推,top是分配指針徊件,開始時(shí)指向start的位置,隨著內(nèi)存分配的進(jìn)行蒜危,慢慢向end靠近虱痕,當(dāng)撞上end時(shí)觸發(fā)TLAB refill。因此內(nèi)存中Eden的結(jié)構(gòu)大體為:
第二辐赞、新生代的Survivor區(qū)域部翘。當(dāng)Eden區(qū)域內(nèi)存不足時(shí)會(huì)觸發(fā)Minor GC,也稱為新生代GC响委,在Minor GC存活下來的對(duì)象新思,會(huì)被復(fù)制到Survivor區(qū)域中窖梁。我認(rèn)為Survivor區(qū)的作用在于避免過早觸發(fā)Full GC。如果沒有Survivor夹囚,Eden區(qū)每進(jìn)行一次Minor GC都把對(duì)象直接送到老年代纵刘,老年代很快便會(huì)內(nèi)存不足引發(fā)Full GC。新生代中有兩個(gè)Survivor區(qū)荸哟,我認(rèn)為兩個(gè)Survivor的作用在于提高性能假哎,避免內(nèi)存碎片的出現(xiàn)。在任何時(shí)候敲茄,總有一個(gè)Survivor是empty的位谋,在發(fā)生Minor GC時(shí),會(huì)將Eden及另一個(gè)的Survivor的存活對(duì)象拷貝到該empty Survivor中堰燎,從而避免內(nèi)存碎片的產(chǎn)生掏父。新生代的內(nèi)存結(jié)構(gòu)大體為:
第三、老年代秆剪。老年代放置長生命周期的對(duì)象赊淑,通常是從Survivor區(qū)域拷貝過來的對(duì)象,不過當(dāng)對(duì)象過大的時(shí)候仅讽,無法在新生代中用連續(xù)內(nèi)存的存放陶缺,那么這個(gè)大對(duì)象就會(huì)被直接分配在老年代上。一般來說洁灵,普通的對(duì)象都是分配在TLAB上饱岸,較大的對(duì)象,直接分配在Eden區(qū)上的其他內(nèi)存區(qū)域徽千,而過大的對(duì)象苫费,直接分配在老年代上。
第四双抽、永久代百框。如前面所說,在早起的Hotspot JVM中有老年代的概念牍汹,老年代用于存儲(chǔ)Java類的元數(shù)據(jù)铐维、常量池、Intern字符串等慎菲。在JDK8之后嫁蛇,就將老年代移除,而引入元數(shù)據(jù)區(qū)的概念露该。
第五棠众、Vritual空間。前面說過,可以使用Xms與Xmx來指定堆的最小與最大空間闸拿。如果Xms小于Xmx空盼,堆的大小不會(huì)直接擴(kuò)展到上限,而是留著一部分等待內(nèi)存需求不斷增長時(shí)新荤,再分配給新生代揽趾。Vritual空間便是這部分保留的內(nèi)存區(qū)域。
那么綜上所述苛骨,可以畫出Java堆內(nèi)的內(nèi)存結(jié)構(gòu)大體為:
通過一些參數(shù)篱瞎,可以來指定上述的堆內(nèi)存區(qū)域的大小:
- -Xmx value 指定最大的堆大小
- -Xms value 指定初始的最小堆大小
- -XX:NewSize = value 指定新生代的大小
- -XX:NewRatio = value 老年代與新生代的大小比例痒芝。默認(rèn)情況下俐筋,這個(gè)比例是2,也就是說老年代是新生代的2倍大严衬。老年代過大的時(shí)候澄者,F(xiàn)ull GC的時(shí)間會(huì)很長;老年代過小请琳,則很容易觸發(fā)Full GC粱挡,F(xiàn)ull GC頻率過高,這就是這個(gè)參數(shù)會(huì)造成的影響俄精。
- -XX:SurvivorRation = value . 設(shè)置Eden與Srivivor的大小比例询筏,如果該值為8,代表一個(gè)Survivor是Eden的1/8竖慧,是整個(gè)新生代的1/10嫌套。
4.常用的性能監(jiān)控與問題定位工具有哪些?
在系統(tǒng)的性能分析中圾旨,CPU灌危、內(nèi)存與IO是主要的關(guān)注項(xiàng)。很多時(shí)候服務(wù)出現(xiàn)問題碳胳,在這三者上會(huì)體現(xiàn)出現(xiàn),比如CPU飆升沫勿,內(nèi)存不足發(fā)生OOM等挨约,這時(shí)候需要使用對(duì)應(yīng)的工具,來對(duì)性能進(jìn)行監(jiān)控产雹,對(duì)問題進(jìn)行定位诫惭。
對(duì)于CPU的監(jiān)控,首先可以使用top命令來進(jìn)行查看蔓挖,下面是使用top查看負(fù)載的一個(gè)截圖:
load average 代表1分鐘夕土、5分鐘、15分鐘的系統(tǒng)平均負(fù)載,從這三個(gè)數(shù)字怨绣,可以判斷系統(tǒng)負(fù)荷是大還是小角溃。當(dāng)CPU完全空閑的時(shí)候,平均負(fù)荷為0篮撑;當(dāng)CPU工作量飽和的時(shí)候减细,平均負(fù)荷為1。因此 load average 這三個(gè)數(shù)值越低赢笨,代表系統(tǒng)負(fù)荷越小未蝌,那么什么時(shí)候能看出系統(tǒng)負(fù)荷比較重呢?這篇文章(Understanding Linux CPU Load – when should you be worried)里解釋得非常通俗茧妒。如果電腦里只有一個(gè)CPU萧吠,把CPU看成一條單行橋,橋上只有一個(gè)車道桐筏,所有的車都必須從這個(gè)橋上通過纸型。那么
系統(tǒng)負(fù)荷為0,代表橋上一輛車也沒有
系統(tǒng)負(fù)荷0.5九昧,意味著橋上一半路段上有車
系統(tǒng)負(fù)荷1绊袋,意味著橋上道路已經(jīng)被車占滿
系統(tǒng)負(fù)荷1.7,代表著在橋上車子已經(jīng)滿了(100%)铸鹰,同時(shí)還有70%的車子在等待從橋上通過:
從top命令的截圖中可以看到這三個(gè)值機(jī)器的load average非常低癌别。如果這三個(gè)值非常高,比如超過了50%或60%蹋笼,就應(yīng)當(dāng)引起注意展姐。從時(shí)間維度上來說,如果發(fā)現(xiàn)CPU負(fù)荷慢慢升高剖毯,也需要警惕圾笨。
其他的內(nèi)存、CPU等性能監(jiān)控工具的使用逊谋,以一張腦圖來展示:
具體的使用方式可以參考從一次線上故障思考Java問題定位思路擂达。
轉(zhuǎn)載自公眾號(hào)方志朋