1. Java 內(nèi)存區(qū)域
1.1. JVM 內(nèi)存布局 與 運(yùn)行時(shí)數(shù)據(jù)區(qū)
1.2. Heap 堆
它的唯一目的就是存放對象實(shí)例器罐;幾乎所有對象實(shí)例和數(shù)組梢为,分配內(nèi)存的區(qū)域。
堆內(nèi)存區(qū)域是線程共享區(qū)域轰坊,并發(fā)編程時(shí)需要考慮線程安全問題铸董。
-
可以通過
-Xms256M -Xmx1024M
設(shè)置堆內(nèi)存大小。注意: Java程序在運(yùn)行中衰倦,堆空間會(huì)不斷擴(kuò)容與減少袒炉,會(huì)造成系統(tǒng)壓力旁理,所以一般設(shè)置為同樣大小
-X
: 表示運(yùn)行參數(shù)ms
: 表示memory start樊零,即起始大小mx
: 表示memory max ,即最大內(nèi)存 堆分成:新生代和老年代兩大塊,如名字一樣驻襟,對象初生在新夺艰,有一例外是新生代無法接納的超大對象會(huì)在老年代創(chuàng)建
-
新生代:對象主要分配在新生代的Eden區(qū)域
如果在新生代分配失敗且對象是一個(gè)不含任何對象引用的大數(shù)組则涯,可被直接分配到老年代月而。
-
可以設(shè)置分配在老年代大對象的閾值:
-XX:PretenureSizeThreshold
默認(rèn)為0不生效,意味著任何對象都會(huì)現(xiàn)在新生代分配內(nèi)存页徐。
可以通過-Xmn256M 設(shè)置新生代區(qū)域大小為256M豌习。此處的大小是(eden + 2 survivor space)存谎,
可以通過
-XX:ServivorRatio=8
決定eden與Survivor的內(nèi)存空間占比為8:1-
長期存活的對象會(huì)進(jìn)入老年代:虛擬機(jī)給每個(gè)初生對象都設(shè)置了一個(gè)age,當(dāng)age>=15時(shí)就會(huì)晉升到老年代肥隆。
當(dāng)對象出現(xiàn)在Eden既荚,經(jīng)過YGC而存活,被移到Servivor區(qū)栋艳,此時(shí)年齡變?yōu)?。每次YGC過后吸占,存活的對象age就會(huì)+1.直到被回收或者晉升老年代晴叨。
另外如果在YGC中,要移動(dòng)的對象大于Survivor的容量上限矾屯,則直接進(jìn)入老年代兼蕊。
-
可以設(shè)置這個(gè)age的閾值:
-XX:MaxTenuringThreshold
,當(dāng)age達(dá)到這個(gè)值就會(huì)進(jìn)入到老年代件蚕。對象的年齡并不是必須達(dá)到了
MaxTenuringThreshold
才晉升老年代遍略,如果在Survivor中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進(jìn)入老年代骤坐。 -
堆的
OutOfMemoryRrror
(簡稱OOM)如果一個(gè)新生對象或者在晉升的對象绪杏,分配的區(qū)域放不下了就會(huì)拋出OOM。當(dāng)一個(gè)新生對象分配給Eden時(shí)纽绍,如果Eden不夠蕾久,則會(huì)觸發(fā)Minor GC。
當(dāng)一個(gè)對象在晉升的時(shí)候JVM發(fā)現(xiàn)內(nèi)存空間不夠拌夏,如果Survivor區(qū)中無法放下僧著,或者是超大對象的閾值超過上限,則嘗試在老年代分配障簿,如果老年代也無法分配盹愚,則觸發(fā)Full Garbage Collection(FGC),如果依然無法放下站故,則拋出OOM皆怕。
要分析OOM我們可以使用
-XX:+HeapDumpOnOutOfMemory
,讓JVM打印OOM信息毅舆。
1.2. 方法區(qū)Method Area(PermGen & Metaspace)
-
方法區(qū)主要用于存放:類元信息、字段愈腾、靜態(tài)屬性憋活、方法、常量虱黄、JIT編譯后的代碼等數(shù)據(jù)悦即。
永久帶(PerGen)和元空間(Metaspace)分別方法區(qū)的具體實(shí)現(xiàn)。
-
PermGen是Hotspot中(<=JDK1.7)特有的區(qū)域橱乱,稱為永久代辜梳。
在該區(qū)域,如果動(dòng)態(tài)加載過多的類泳叠,容易產(chǎn)生Perm的OOM冗美。
java.lang.OutOfMemory: PermGen space
錯(cuò)誤。上述錯(cuò)誤可以通過設(shè)置
-XX:PermSize=1024M
解決析二。另外還可以設(shè)置
-XX:MaxPermSize=1024m
最大永久代大小粉洼。 默認(rèn)是64M但是JDK8及以后,由于用元空間替換了PermGen所以在JDK8及以后的版本中HotSpot會(huì)提示:Java Hotspot 64Bit Server VM warning ignoring option MaxPermSize=1024M; support was removed in 8.0叶摄。
-
Metaspace是為了解決永久帶的缺陷而優(yōu)化設(shè)計(jì)的新實(shí)現(xiàn)属韧,它分配內(nèi)存在本地內(nèi)存,并且它把以前Perm中的字符串常量全部移到了堆內(nèi)存蛤吓。而其他的包括類元信息宵喂、字段、靜態(tài)屬性会傲、方法锅棕、常量等移到了元空間。其實(shí)在1.7的某個(gè)版本就已經(jīng)把字符串常量移到了堆內(nèi)存中淌山。
大部分類元數(shù)據(jù)都在本地內(nèi)存中分配裸燎。用于描述類元數(shù)據(jù)的“klasses”已經(jīng)被移除。默認(rèn)情況下泼疑,類元數(shù)據(jù)只受可用的本地內(nèi)存限制德绿。可以通過
-XX:MaxDirectMemorySize=50m
設(shè)置直接內(nèi)存退渗。因?yàn)槭潜镜貎?nèi)存中存儲(chǔ)移稳,所以如果程序存在內(nèi)存泄露,不停的擴(kuò)展Metaspace的空間会油,會(huì)導(dǎo)致機(jī)器的內(nèi)存不足个粱,所以還是要有必要的調(diào)試和監(jiān)控。
Metaspace可以通過
-XX:MetaspaceSize=10m
和-XX:MaxMetaspaceSize=50m
設(shè)置初始空間大小和最大空間
1.3. 虛擬機(jī)棧 JVM Stack
Stack 是一個(gè)先進(jìn)后出的數(shù)據(jù)結(jié)構(gòu)翻翩。JVM中的棧是描述Java方法執(zhí)行的內(nèi)存區(qū)域都许,它是線程私有的稻薇。每個(gè)方法從開始調(diào)用到結(jié)束調(diào)用就是棧幀從入棧到出棧的結(jié)果。
活動(dòng)線程中梭稚,只有棧頂?shù)臈攀怯行У模Q為當(dāng)前棧幀絮吵。正在執(zhí)行的方法稱為當(dāng)前方法弧烤,棧幀是方法運(yùn)行的基本結(jié)構(gòu)。在執(zhí)行引擎運(yùn)行時(shí)蹬敲,所有指令都只能針對當(dāng)前棧幀操作暇昂。
-
棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作棧伴嗡、動(dòng)態(tài)鏈接急波、方法返回地址等信息。
局部變量表:存放方法參數(shù)瘪校,編譯期可知的基本數(shù)據(jù)類型澄暮、對象引用類型(reference)和returnAddress類型(指向一條字節(jié)碼指令地址)。局部變量表所需的內(nèi)存空間是在編譯期確定阱扬,方法在局部變量表中分配多少空間是完全確定的泣懊。在運(yùn)行期間不會(huì)改變局部變量表的大小。局部變量沒有準(zhǔn)備階段麻惶,必須顯示初始化馍刮。
操作棧是一個(gè)初始狀態(tài)為空的桶式結(jié)構(gòu)棧。方法執(zhí)行過程中窃蹋,會(huì)有各種指令往棧寫入和提取信息卡啰。JVM的執(zhí)行引擎就是基于操作棧的執(zhí)行引擎。
動(dòng)態(tài)連接: 在Class文件中的常量持中存有大量的符號引用警没。字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號引用作為參數(shù)匈辱。這些符號引用一部分在類的加載階段或第一次使用的時(shí)候就轉(zhuǎn)化為了直接引用,稱為靜態(tài)鏈接杀迹。而相反的梅誓,另一部分在運(yùn)行期間轉(zhuǎn)化為直接引用,就稱為動(dòng)態(tài)鏈接佛南。
方法返回地址:方法執(zhí)行時(shí)有兩種退出情況:一是正常退出梗掰,正常執(zhí)行到方法的返回字節(jié)碼指令;二是異常退出嗅回。兩種退出都會(huì)返回當(dāng)前被調(diào)用的位置及穗。方法退出相當(dāng)于彈出當(dāng)前棧幀,退出的方式有三種:
1.
返回值壓入上層調(diào)用棧幀绵载。2.
異常信息拋給能夠處理的棧幀埂陆。3.
PC計(jì)數(shù)器指向方法調(diào)用后的下一條指令苛白。 -
StackOverflowError
:當(dāng)棧深度超過虛擬機(jī)分配給線程的棧大小時(shí)就會(huì)出現(xiàn)此error。最常見的就是遞歸深度超出了限定焚虱,然后拋出這個(gè)錯(cuò)誤
-
OutOfMemoryError
:虛擬機(jī)擴(kuò)展時(shí)無法申請到足夠的內(nèi)存空間购裙,多線程下的內(nèi)存溢出,與椌樵裕空間是否足夠大并不存在任何聯(lián)系躏率。為每個(gè)線程的棧分配的內(nèi)存越大(參數(shù)
-Xss
),那么可以建立的線程數(shù)量就越少民鼓,建立線程時(shí)就越容易把剩下的內(nèi)存耗盡薇芝,越容易內(nèi)存溢出。 -
可以通過
-Xss2m
設(shè)置棧內(nèi)存大小,設(shè)置每個(gè)線程的棧內(nèi)存丰嘉,默認(rèn)1M夯到,一般來說是不需要改的。-XX:ThreadStackSize
線程堆棧大小如果把
-Xss
或者-XX:ThreadStackSize
設(shè)為0饮亏,就是使用“系統(tǒng)默認(rèn)值”耍贾。而在Linux x64上HotSpot VM給Java棧定義的“系統(tǒng)默認(rèn)”大小也是1MB。JDK1.6以前路幸,誰設(shè)置在后面逼争,誰就生效;JDK1.6以后劝赔,
-Xss
設(shè)置在后面誓焦,則以-Xss
為準(zhǔn),-XXThreadStackSize
設(shè)置在后面着帽,則主線程以-Xss
為準(zhǔn)杂伟,其它線程以-XX:ThreadStackSize
為準(zhǔn)。
1.4. 本地方法棧 Native Method Stacks
本地方法棧為Native方法服務(wù)
本地方法通過JNI(Java Native Interface)來訪問虛擬機(jī)運(yùn)行時(shí)的數(shù)據(jù)區(qū)仍翰,甚至是調(diào)用寄存器赫粥,具有和JVM相同的能力和權(quán)限。
本地方法棧也會(huì)拋出:OutOfMemoryError和StackOverflowError
-
JNI
JNI深度使用操作系統(tǒng)的特性功能予借。復(fù)用非Java代碼越平。如果大量使用其他語言來實(shí)現(xiàn)JNI,會(huì)失去跨平臺(tái)特性灵迫。
如果對執(zhí)行效率要求高秦叛,偏底層的跨進(jìn)程的操作等,可以考慮設(shè)計(jì)為JNI調(diào)用方式瀑粥。
1.5. 程序計(jì)數(shù)器 Program Counter Register
- 每個(gè)線程創(chuàng)建后都會(huì)產(chǎn)生自己的程序計(jì)數(shù)器和棧幀挣跋,程序計(jì)數(shù)器用來存放執(zhí)行指令的偏移量和行號指示器等,線程執(zhí)行或恢復(fù)都依賴程序計(jì)數(shù)器狞换。
- 程序計(jì)數(shù)器是線程獨(dú)占避咆,在各個(gè)線程直接互不影響舟肉,在此區(qū)域也不會(huì)有內(nèi)存溢出異常。
- 線程如果在執(zhí)行一個(gè)Java方法則記錄虛擬機(jī)字節(jié)碼指令的地址查库,如果代碼執(zhí)行到了Native方法計(jì)數(shù)器就為undefined路媚。
1.6. 直接內(nèi)存 Direct Memory
直接內(nèi)存,即本機(jī)使用的堆外的系統(tǒng)內(nèi)存樊销。該部分內(nèi)存可被JVM使用整慎,不會(huì)被JVM堆內(nèi)存限制,但是動(dòng)態(tài)拓展時(shí)也會(huì)出現(xiàn)
OutOfMemory
现柠,可用-XX:MaxDirectMemorySize=50m
來限制使用內(nèi)存空間的最大值最大值-
DirectByteBuffer
可以直接操作DirectMemory院领,它通過JNI調(diào)用native方法直接分配堆外內(nèi)存弛矛,通過DirectByteBuffer
對象對這塊內(nèi)存對象進(jìn)行操作這個(gè)調(diào)用够吩,實(shí)際上是從系統(tǒng)的用戶態(tài)切換到了內(nèi)核態(tài)使用系統(tǒng)調(diào)用來完成這個(gè)操作。
為什么要切換到內(nèi)核態(tài)丈氓?用戶態(tài)沒有權(quán)限去操作內(nèi)核態(tài)的資源周循,它只能通過系統(tǒng)調(diào)用外完成用戶態(tài)到內(nèi)核態(tài)的切換,然后在完成相關(guān)操作后再有內(nèi)核態(tài)切換回用戶態(tài)万俗。
DirectByteBuffer
該類本身還是位于Java內(nèi)存模型的堆中湾笛。堆內(nèi)內(nèi)存是JVM可以直接管控、操縱闰歪。由于
DirectByteBuffer
的權(quán)限修飾符是空的也就是默認(rèn)的嚎研,所以在我們編程中是無法直接new,只允許同包創(chuàng)建库倘,我們可以通過ByteBuffer
中的靜態(tài)方法allocateDirect(int)
方法來創(chuàng)建對象临扮。
java public static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity); }
> 而 DirectByteBuffer 類中調(diào)用了native的unsafe.allocateMemory(size)來分配空間,實(shí)際上是使用了c語言的malloc方法教翩。
```java
// Primary constructor
//
DirectByteBuffer(int cap) { // package-privatesuper(-1, 0, cap, cap); boolean pa = VM.isDirectMemoryPageAligned(); int ps = Bits.pageSize(); long size = Math.max(1L, (long)cap + (pa ? ps : 0)); Bits.reserveMemory(size, cap); long base = 0; try { // 這里是重點(diǎn)8擞隆!饱亿!掉黑板 base = unsafe.allocateMemory(size); } catch (OutOfMemoryError x) { Bits.unreserveMemory(size, cap); throw x; } unsafe.setMemory(base, size, (byte) 0); if (pa && (base % ps != 0)) { // Round up to page boundary address = base + ps - (base & (ps - 1)); } else { address = base; } // 這里記錄分配空間的信息蚜退。 cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); att = null;
}
// 記錄分分配空間信息的類 private Deallocator(long address, long size, int capacity) { assert (address != 0); this.address = address; this.size = size; this.capacity = capacity; }
2. 對象創(chuàng)建與內(nèi)存分配
2.1 對象創(chuàng)建
- 對象使用new創(chuàng)建的簡單過程
-
指針碰撞:
假設(shè)Java堆中內(nèi)存是絕對規(guī)整的,所有用過的內(nèi)存都被放在一邊彪笼,空閑的內(nèi)存被放在另一邊钻注,中間放著一個(gè)指針作 為分界點(diǎn)的指示器,那所分配內(nèi)存就僅僅是把那個(gè)指針向空閑空間那邊挪動(dòng)一段與對象大小相等的距離配猫,這種分配方式稱為“指針碰撞”(Bump The Pointer)
【帶Compact過程的Serial队寇、ParNew等采用指針碰撞≌滦眨】 -
空閑列表
如果Java堆中的內(nèi)存并不是規(guī)整的佳遣,已使用的和空閑的內(nèi)存相互交錯(cuò)识埋,就無法進(jìn)行指針碰撞了,JVM就必須維護(hù)一個(gè)列表零渐,記錄可用內(nèi)存區(qū)域窒舟,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對象實(shí)例,并更新列表上的記錄诵盼,這種分配方式稱為“空閑列表”(Free List)
【CMS這種基于Mark-Sweep算法的使用空閑列表】 -
不難想到惠豺,分配內(nèi)存時(shí)如果多個(gè)線程同時(shí)創(chuàng)建對象,就會(huì)出現(xiàn)并發(fā)問題风宁。JVM實(shí)際采用:
一種是CAS(Compare And Swap)加上失敗重試機(jī)制來保證更新操作的原子性洁墙;
另一種是本地線程緩沖(TLAB,Thread Local Allocation Buffer.)戒财,即把內(nèi)存分配的動(dòng)作按照線程劃分在不同的空間之中進(jìn)行热监,每個(gè)線程都預(yù)先分配一小塊內(nèi)存。線程在自己的TLAB中分配饮寞,只有TLAB用完才需要同步加鎖孝扛。虛擬機(jī)是否用TLAB,可以通過
-XX:+/-UseTLAB
參數(shù)設(shè)定幽崩。
2.2 對象內(nèi)存
-
對象頭(Header)包含兩部分:一是自身運(yùn)行時(shí)數(shù)據(jù)苦始;二是類型指針
運(yùn)行時(shí)數(shù)據(jù): 32位和64位JVM分別對應(yīng)32位和64位長度(未開啟指正壓縮),存儲(chǔ)包括:哈希碼慌申、GC分帶年齡陌选、鎖狀態(tài)標(biāo)志、線程池持有鎖蹄溉、偏向鎖ID咨油、偏向時(shí)間戳等。(Mark Word)类缤。
類型指針: 即對象指向它的類元數(shù)據(jù)的指針臼勉,虛擬機(jī)通過這個(gè)指針確定是哪個(gè)對象的實(shí)例。查找對象的元數(shù)據(jù)信息不一定要經(jīng)過對象本身餐弱。對象是Java數(shù)組宴霸,則對象頭中則會(huì)有一塊記錄數(shù)組長度的數(shù)據(jù);普通Java類可以通過元數(shù)據(jù)信息確定Java類大小膏蚓,但數(shù)組還需要需要對象頭中的長度數(shù)據(jù)才能確定瓢谢。
-
實(shí)例數(shù)據(jù)(Instance Data)
就是對象存儲(chǔ)的真正的有效信息,也就是程序代碼中定義的所有字段內(nèi)容驮瞧。
-
對齊填充(Padding)
因?yàn)镠otSpot VM的自動(dòng)內(nèi)存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數(shù)倍氓扛,對象頭部分正好是8字節(jié)的倍數(shù),當(dāng)對象實(shí)例數(shù)據(jù)部分沒有對齊時(shí),就需要通過對齊填充來補(bǔ)全采郎。
2.3 對象訪問
-
Java通過棧上的reference數(shù)據(jù)來操作堆上的具體對象千所,而reference是一個(gè)指向?qū)ο蟮囊茫ㄟ^reference去定位和訪問對象蒜埋,目前主流的使用兩種方式:一是使用句柄淫痰,二是使用直接指針
句柄: JVM堆會(huì)專門劃分內(nèi)存作為句柄池,而reference中存的就是對象的句柄地址整份;句柄中包含了對象實(shí)例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址待错。
直接指針: 如果是直接指針,Java堆中就會(huì)防止訪問類型數(shù)據(jù)相關(guān)的信息烈评。而reference中存儲(chǔ)的直接就是對象地址火俄。
關(guān)于我
- 坐標(biāo)杭州,普通本科在讀讲冠,計(jì)算機(jī)科學(xué)與技術(shù)專業(yè)瓜客,20年畢業(yè),目前處于實(shí)習(xí)階段沟启。
- 主要做Java開發(fā)忆家,會(huì)寫點(diǎn)Golang犹菇、Shell德迹。對微服務(wù)、大數(shù)據(jù)比較感興趣揭芍,預(yù)備做這個(gè)方向胳搞。
- 目前處于菜鳥階段,各位大佬輕噴称杨,小弟正在瘋狂學(xué)習(xí)肌毅。
- 歡迎大家和我交流鴨!9迷悬而!