關(guān)于 JVM 內(nèi)存的 N 個問題和定位工具

JVM的內(nèi)存區(qū)域是怎么劃分的?

JVM的內(nèi)存劃分中柴淘,有部分區(qū)域是線程私有的仿便,有部分是屬于整個JVM進程夕膀;有些區(qū)域會拋出OOM異常恼除,有些則不會咧擂,了解JVM的內(nèi)存區(qū)域劃分以及特征宫患,是定位線上內(nèi)存問題的基礎(chǔ)麦到。那么JVM內(nèi)存區(qū)域是怎么劃分的呢振惰?

首先是程序計數(shù)器(Program Counter Register)歌溉,在JVM規(guī)范中,每個線程都有自己的程序計數(shù)器报账。這是一塊比較小的內(nèi)存空間研底,存儲當前線程正在執(zhí)行的Java方法的JVM指令地址,即字節(jié)碼的行號透罢。如果正在執(zhí)行Native方法榜晦,則這個計數(shù)器為空。該內(nèi)存區(qū)域是唯一一個在Java虛擬機規(guī)范中沒有規(guī)定任何OOM情況的內(nèi)存區(qū)域羽圃。

第二乾胶,Java虛擬機棧(Java Virtal Machine Stack),同樣也是屬于線程私有區(qū)域朽寞,每個線程在創(chuàng)建的時候都會創(chuàng)建一個虛擬機棧识窿,生命周期與線程一致,線程退出時脑融,線程的虛擬機棧也回收喻频。虛擬機棧內(nèi)部保持一個個的棧幀,每次方法調(diào)用都會進行壓棧肘迎,JVM對棧幀的操作只有出棧和壓棧兩種甥温,方法調(diào)用結(jié)束時會進行出棧操作。

該區(qū)域存儲著局部變量表妓布,編譯時期可知的各種基本類型數(shù)據(jù)姻蚓、對象引用、方法出口等信息匣沼。

第三狰挡,本地方法棧(Native Method Stack)與虛擬機棧類似,本地方法棧是在調(diào)用本地方法時使用的棧,每個線程都有一個本地方法棧加叁。

第四倦沧,堆(Heap),幾乎所有創(chuàng)建的Java對象實例,都是被直接分配到堆上的它匕。堆被所有的線程所共享刀脏,在堆上的區(qū)域,會被垃圾回收器做進一步劃分超凳,例如新生代、老年代的劃分耀态。Java虛擬機在啟動的時候轮傍,可以使用“Xmx”之類的參數(shù)指定堆區(qū)域的大小。

第五首装,方法區(qū)(Method Area)创夜。方法區(qū)與堆一樣,也是所有的線程所共享仙逻,存儲被虛擬機加載的元(Meta)數(shù)據(jù)驰吓,包括類信息、常量系奉、靜態(tài)變量檬贰、即時編譯器編譯后的代碼等數(shù)據(jù)。這里需要注意的是運行時常量池也在方法區(qū)中缺亮。根據(jù)Java虛擬機規(guī)范的規(guī)定翁涤,當方法區(qū)無法滿足內(nèi)存分配需求時,將拋出OutOfMemoryError異常萌踱。由于早期HotSpot JVM的實現(xiàn)葵礼,將CG分代收集拓展到了方法區(qū),因此很多人會將方法區(qū)稱為永久代并鸵。Oracle JDK8中已永久代移除永久代鸳粉,同時增加了元數(shù)據(jù)區(qū)(Metaspace)。

第六园担,運行時常量池(Run-Time Constant Pool)届谈,這是方法區(qū)的一部分,受到方法區(qū)內(nèi)存的限制粉铐,當常量池無法再申請到內(nèi)存時疼约,會拋出OutOfMemoryError異常。

在Class文件中蝙泼,除了有類的版本程剥、方法、字段、接口等描述信息外织鲸,還有一項信息是常量池舔腾。每個Class文件的頭四個字節(jié)稱為Magic Number,它的作用是確定這是否是一個可以被虛擬機接受的文件搂擦;接著的四個字節(jié)存儲的是Class文件的版本號稳诚。緊挨著版本號之后的,就是常量池入口了瀑踢。常量池主要存放兩大類常量:

1.? 字面量(Literal)扳还,如文本字符串、final常量值

2.? 符號引用橱夭,存放了與編譯相關(guān)的一些常量氨距,因為Java不像C++那樣有連接的過程,因此字段方法這些符號引用在運行期就需要進行轉(zhuǎn)換棘劣,以便得到真正的內(nèi)存入口地址俏让。

class文件中的常量池,也稱為靜態(tài)常量池茬暇,JVM虛擬機完成類裝載操作后首昔,會把靜態(tài)常量池加載到內(nèi)存中,存放在運行時常量池糙俗。

第七勒奇,直接內(nèi)存(Direct Memory),直接內(nèi)存并不屬于Java規(guī)范規(guī)定的屬于Java虛擬機運行時數(shù)據(jù)區(qū)的一部分臼节。Java的NIO可以使用Native方法直接在java堆外分配內(nèi)存撬陵,使用DirectByteBuffer對象作為這個堆外內(nèi)存的引用。

下面這張圖网缝,反映了運行中的Java進程內(nèi)存占用情況:

OOM可能發(fā)生在哪些區(qū)域上巨税?

根據(jù)javadoc的描述,OOM是指JVM的內(nèi)存不夠用了粉臊,同時垃圾收集器也無法提供更多的內(nèi)存草添。從描述中可以看出,在JVM拋出OutOfMemoryError之前扼仲,垃圾收集器一般會出馬先嘗試回收內(nèi)存远寸。

從上面分析的Java數(shù)據(jù)區(qū)來看,除了程序計數(shù)器不會發(fā)生OOM外屠凶,哪些區(qū)域會發(fā)生OOM的情況呢驰后?

第一,堆內(nèi)存矗愧。堆內(nèi)存不足是最常見的發(fā)送OOM的原因之一灶芝,如果在堆中沒有內(nèi)存完成對象實例的分配,并且堆無法再擴展時,將拋出OutOfMemoryError異常夜涕。當前主流的JVM可以通過-Xmx和-Xms來控制堆內(nèi)存的大小犯犁,發(fā)生堆上OOM的可能是存在內(nèi)存泄露,也可能是堆大小分配不合理女器。

第二酸役,Java虛擬機棧和本地方法棧,這兩個區(qū)域的區(qū)別不過是虛擬機棧為虛擬機執(zhí)行Java方法服務(wù)驾胆,而本地方法棧則為虛擬機使用到的Native方法服務(wù)涣澡,在內(nèi)存分配異常上是相同的。在JVM規(guī)范中丧诺,對Java虛擬機棧規(guī)定了兩種異常:1.如果線程請求的棧大于所分配的棧大小暑塑,則拋出StackOverFlowError錯誤,比如進行了一個不會停止的遞歸調(diào)用锅必;2. 如果虛擬機棧是可以動態(tài)拓展的,拓展時無法申請到足夠的內(nèi)存惕艳,則拋出OutOfMemoryError錯誤搞隐。

第三,直接內(nèi)存远搪。直接內(nèi)存雖然不是虛擬機運行時數(shù)據(jù)區(qū)的一部分劣纲,但既然是內(nèi)存,就會受到物理內(nèi)存的限制谁鳍。在JDK1.4中引入的NIO使用Native函數(shù)庫在堆外內(nèi)存上直接分配內(nèi)存癞季,但直接內(nèi)存不足時,也會導(dǎo)致OOM倘潜。

第四绷柒,方法區(qū)。隨著Metaspace元數(shù)據(jù)區(qū)的引入涮因,方法區(qū)的OOM錯誤信息也變成了“java.lang.OutOfMemoryError:Metaspace”废睦。對于舊版本的Oracle JDK,由于永久代的大小有限养泡,而JVM對永久代的垃圾回收并不積極嗜湃,如果往永久代不斷寫入數(shù)據(jù),例如String.Intern()的調(diào)用澜掩,在永久代占用太多空間導(dǎo)致內(nèi)存不足购披,也會出現(xiàn)OOM的問題,對應(yīng)的錯誤信為“java.lang.OutOfMemoryError:PermGen space”

堆內(nèi)存結(jié)構(gòu)是怎么樣的肩榕?

可以借助一些工具來了解JVM的內(nèi)存內(nèi)容刚陡,具體到特定的內(nèi)存區(qū)域,應(yīng)該用什么工具去定位呢?

圖形化工具橘荠。圖形化工具的優(yōu)點是直觀屿附,連接到Java進程后,可以顯示堆內(nèi)存哥童、堆外內(nèi)存的使用情況挺份,類似的工具有JConsole,VisualVm等。

命令行工具贮懈。這類工具可以在運行時進行查詢匀泊,包括jstat,jmap等朵你,可以對堆內(nèi)存各聘、方法區(qū)等進行查看。定位線上問題時也多會使用這些工具抡医。jmap也可以生成堆轉(zhuǎn)儲文件(Heap Dump)文件躲因,如果是在linux上,可以將堆轉(zhuǎn)儲文件拉到本地來忌傻,使用Eclipse MAT進行分析大脉,也可以使用jhap進行分析。

關(guān)于內(nèi)存的監(jiān)控與診斷水孩,在后面會進行深入了解×螅現(xiàn)在來看下一個問題:堆內(nèi)的結(jié)構(gòu)是怎么的呢?

站在垃圾收集器的角度來看俘种,可以把內(nèi)存分為新生代與老年代秤标。內(nèi)存的分配規(guī)則取決于當前使用的是哪種垃圾收集器的組合,以及內(nèi)存相關(guān)的參數(shù)配置宙刘。往大的方向說苍姜,對象優(yōu)先分配在新生代的Eden區(qū)域,而大對象直接進入老年代悬包。

第一, 新生代的Eden區(qū)域怖现,對象優(yōu)先分配在該區(qū)域,同時JVM可以為每個線程分配一個私有的緩存區(qū)域玉罐,稱為TLAB(Thread Local Allocation Buffer)屈嗤,避免多線程同時分配內(nèi)存時需要使用加鎖等機制而影響分配速度。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的管理是依靠三個指針:start季蚂、end茫船、top琅束。start與end標記了Eden中被該TLAB管理的區(qū)域,該區(qū)域不會被其他線程分配內(nèi)存所使用算谈,top是分配指針涩禀,開始時指向start的位置,隨著內(nèi)存分配的進行然眼,慢慢向end靠近艾船,當撞上end時觸發(fā)TLAB refill。因此內(nèi)存中Eden的結(jié)構(gòu)大體為:

第二 高每、新生代的Survivor區(qū)域屿岂。當Eden區(qū)域內(nèi)存不足時會觸發(fā)Minor GC,也稱為新生代GC鲸匿,在Minor GC存活下來的對象爷怀,會被復(fù)制到Survivor區(qū)域中。我認為Survivor區(qū)的作用在于避免過早觸發(fā)Full GC带欢。如果沒有Survivor运授,Eden區(qū)每進行一次Minor GC都把對象直接送到老年代,老年代很快便會內(nèi)存不足引發(fā)Full GC乔煞。新生代中有兩個Survivor區(qū)徒坡,我認為兩個Survivor的作用在于提高性能,避免內(nèi)存碎片的出現(xiàn)瘤缩。在任何時候,總有一個Survivor是empty的伦泥,在發(fā)生Minor GC時剥啤,會將Eden及另一個的Survivor的存活對象拷貝到該empty Survivor中,從而避免內(nèi)存碎片的產(chǎn)生不脯。新生代的內(nèi)存結(jié)構(gòu)大體為:

第三府怯、老年代。老年代放置長生命周期的對象防楷,通常是從Survivor區(qū)域拷貝過來的對象牺丙,不過當對象過大的時候,無法在新生代中用連續(xù)內(nèi)存的存放复局,那么這個大對象就會被直接分配在老年代上冲簿。一般來說,普通的對象都是分配在TLAB上亿昏,較大的對象峦剔,直接分配在Eden區(qū)上的其他內(nèi)存區(qū)域,而過大的對象角钩,直接分配在老年代上吝沫。

第四呻澜、永久代。如前面所說惨险,在早起的Hotspot JVM中有老年代的概念羹幸,老年代用于存儲Java類的元數(shù)據(jù)、常量池辫愉、Intern字符串等栅受。在JDK8之后,就將老年代移除一屋,而引入元數(shù)據(jù)區(qū)的概念窘疮。

第五、Vritual空間冀墨。前面說過闸衫,可以使用Xms與Xmx來指定堆的最小與最大空間。如果Xms小于Xmx诽嘉,堆的大小不會直接擴展到上限蔚出,而是留著一部分等待內(nèi)存需求不斷增長時,再分配給新生代虫腋。Vritual空間便是這部分保留的內(nèi)存區(qū)域骄酗。

那么綜上所述,可以畫出Java堆內(nèi)的內(nèi)存結(jié)構(gòu)大體為:

通過一些參數(shù)悦冀,可以來指定上述的堆內(nèi)存區(qū)域的大星鞣:

1.? -Xmx value 指定最大的堆大小

2.? -Xms value 指定初始的最小堆大小

3.? -XX:NewSize = value 指定新生代的大小

4.? -XX:NewRatio = value 老年代與新生代的大小比例。默認情況下盒蟆,這個比例是2踏烙,也就是說老年代是新生代的2倍大。老年代過大的時候历等,F(xiàn)ull GC的時間會很長讨惩;老年代過小,則很容易觸發(fā)Full GC寒屯,F(xiàn)ull GC頻率過高荐捻,這就是這個參數(shù)會造成的影響。

5.? -XX:SurvivorRation = value . 設(shè)置Eden與Srivivor的大小比例寡夹,如果該值為8处面,代表一個Survivor是Eden的1/8,是整個新生代的1/10菩掏。

常用的性能監(jiān)控與問題定位工具有哪些鸳君?

在系統(tǒng)的性能分析中,CPU患蹂、內(nèi)存與IO是主要的關(guān)注項或颊。很多時候服務(wù)出現(xiàn)問題砸紊,在這三者上會體現(xiàn)出現(xiàn),比如CPU飆升囱挑,內(nèi)存不足發(fā)生OOM等醉顽,這時候需要使用對應(yīng)的工具,來對性能進行監(jiān)控平挑,對問題進行定位游添。

對于CPU的監(jiān)控,首先可以使用top命令來進行查看通熄,下面是使用top查看負載的一個截圖:

load average 代表1分鐘唆涝、5分鐘、15分鐘的系統(tǒng)平均負載唇辨,從這三個數(shù)字廊酣,可以判斷系統(tǒng)負荷是大還是小。當CPU完全空閑的時候赏枚,平均負荷為0亡驰;當CPU工作量飽和的時候,平均負荷為1饿幅。因此 load average 這三個數(shù)值越低凡辱,代表系統(tǒng)負荷越小,那么什么時候能看出系統(tǒng)負荷比較重呢栗恩?這篇文章(Understanding Linux CPU Load – when should you be worried)里解釋得非常通俗透乾。如果電腦里只有一個CPU,把CPU看成一條單行橋磕秤,橋上只有一個車道乳乌,所有的車都必須從這個橋上通過。那么

系統(tǒng)負荷為0亲澡,代表橋上一輛車也沒有

系統(tǒng)負荷0.5,意味著橋上一半路段上有車

系統(tǒng)負荷1纫版,意味著橋上道路已經(jīng)被車占滿

系統(tǒng)負荷1.7床绪,代表著在橋上車子已經(jīng)滿了(100%),同時還有70%的車子在等待從橋上通過:

從top命令的截圖中可以看到這三個值機器的load average非常低其弊。如果這三個值非常高癞己,比如超過了50%或60%,就應(yīng)當引起注意梭伐。從時間維度上來說痹雅,如果發(fā)現(xiàn)CPU負荷慢慢升高,也需要警惕糊识。

每天都會有更新看過的朋友可以點波關(guān)注绩社,Java學習路線和優(yōu)質(zhì)資源評論或后臺回復(fù)“Java”獲取摔蓝。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市愉耙,隨后出現(xiàn)的幾起案子贮尉,更是在濱河造成了極大的恐慌,老刑警劉巖朴沿,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異赌渣,居然都是意外死亡魏铅,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門坚芜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來览芳,“玉大人,你說我怎么就攤上這事货岭÷凡伲” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵千贯,是天一觀的道長屯仗。 經(jīng)常有香客問我,道長搔谴,這世上最難降的妖魔是什么魁袜? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮敦第,結(jié)果婚禮上峰弹,老公的妹妹穿的比我還像新娘。我一直安慰自己芜果,他們只是感情好鞠呈,可當我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著右钾,像睡著了一般蚁吝。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上舀射,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天窘茁,我揣著相機與錄音,去河邊找鬼脆烟。 笑死山林,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的邢羔。 我是一名探鬼主播驼抹,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼桑孩,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了砂蔽?” 一聲冷哼從身側(cè)響起洼怔,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎左驾,沒想到半個月后镣隶,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡诡右,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年安岂,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片帆吻。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡域那,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出猜煮,到底是詐尸還是另有隱情次员,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布王带,位于F島的核電站淑蔚,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏愕撰。R本人自食惡果不足惜刹衫,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望搞挣。 院中可真熱鬧带迟,春花似錦具帮、人聲如沸霸褒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽舍肠。三九已至搀继,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間貌夕,已是汗流浹背律歼。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工民镜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留啡专,地道東北人。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓制圈,卻偏偏與公主長得像们童,于是被迫代替她去往敵國和親畔况。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容