1. Java 內(nèi)存區(qū)域
1.1. JVM 內(nèi)存布局 與 運(yùn)行時(shí)數(shù)據(jù)區(qū)
1.2. Heap 堆
它的唯一目的就是存放對(duì)象實(shí)例;幾乎所有對(duì)象實(shí)例和數(shù)組,分配內(nèi)存的區(qū)域。
堆內(nèi)存區(qū)域是線(xiàn)程共享區(qū)域,并發(fā)編程時(shí)需要考慮線(xiàn)程安全問(wèn)題。
-
可以通過(guò)
-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)存 堆分成:新生代和老年代兩大塊,如名字一樣,對(duì)象初生在新,有一例外是新生代無(wú)法接納的超大對(duì)象會(huì)在老年代創(chuàng)建
-
新生代:對(duì)象主要分配在新生代的Eden區(qū)域
如果在新生代分配失敗且對(duì)象是一個(gè)不含任何對(duì)象引用的大數(shù)組,可被直接分配到老年代。
-
可以設(shè)置分配在老年代大對(duì)象的閾值:
-XX:PretenureSizeThreshold
默認(rèn)為0不生效,意味著任何對(duì)象都會(huì)現(xiàn)在新生代分配內(nèi)存。
可以通過(guò)-Xmn256M 設(shè)置新生代區(qū)域大小為256M。此處的大小是(eden + 2 survivor space),
可以通過(guò)
-XX:ServivorRatio=8
決定eden與Survivor的內(nèi)存空間占比為8:1-
長(zhǎng)期存活的對(duì)象會(huì)進(jìn)入老年代:虛擬機(jī)給每個(gè)初生對(duì)象都設(shè)置了一個(gè)age,當(dāng)age>=15時(shí)就會(huì)晉升到老年代。
當(dāng)對(duì)象出現(xiàn)在Eden,經(jīng)過(guò)YGC而存活,被移到Servivor區(qū),此時(shí)年齡變?yōu)?。每次YGC過(guò)后,存活的對(duì)象age就會(huì)+1.直到被回收或者晉升老年代。
另外如果在YGC中,要移動(dòng)的對(duì)象大于Survivor的容量上限,則直接進(jìn)入老年代。
-
可以設(shè)置這個(gè)age的閾值:
-XX:MaxTenuringThreshold
,當(dāng)age達(dá)到這個(gè)值就會(huì)進(jìn)入到老年代。對(duì)象的年齡并不是必須達(dá)到了
MaxTenuringThreshold
才晉升老年代,如果在Survivor中相同年齡所有對(duì)象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對(duì)象就可以直接進(jìn)入老年代。 -
堆的
OutOfMemoryRrror
(簡(jiǎn)稱(chēng)OOM)如果一個(gè)新生對(duì)象或者在晉升的對(duì)象,分配的區(qū)域放不下了就會(huì)拋出OOM。當(dāng)一個(gè)新生對(duì)象分配給Eden時(shí),如果Eden不夠,則會(huì)觸發(fā)Minor GC。
當(dāng)一個(gè)對(duì)象在晉升的時(shí)候JVM發(fā)現(xiàn)內(nèi)存空間不夠,如果Survivor區(qū)中無(wú)法放下,或者是超大對(duì)象的閾值超過(guò)上限,則嘗試在老年代分配,如果老年代也無(wú)法分配,則觸發(fā)Full Garbage Collection(FGC),如果依然無(wú)法放下,則拋出OOM。
要分析OOM我們可以使用
-XX:+HeapDumpOnOutOfMemory
,讓JVM打印OOM信息。
1.2. 方法區(qū)Method Area(PermGen & Metaspace)
-
方法區(qū)主要用于存放:類(lèi)元信息、字段、靜態(tài)屬性、方法、常量、JIT編譯后的代碼等數(shù)據(jù)。
永久帶(PerGen)和元空間(Metaspace)分別方法區(qū)的具體實(shí)現(xiàn)。
-
PermGen是Hotspot中(<=JDK1.7)特有的區(qū)域,稱(chēng)為永久代。
在該區(qū)域,如果動(dòng)態(tài)加載過(guò)多的類(lèi),容易產(chǎn)生Perm的OOM。
java.lang.OutOfMemory: PermGen space
錯(cuò)誤。上述錯(cuò)誤可以通過(guò)設(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)存。而其他的包括類(lèi)元信息、字段、靜態(tài)屬性、方法、常量等移到了元空間。其實(shí)在1.7的某個(gè)版本就已經(jīng)把字符串常量移到了堆內(nèi)存中。
大部分類(lèi)元數(shù)據(jù)都在本地內(nèi)存中分配。用于描述類(lèi)元數(shù)據(jù)的“klasses”已經(jīng)被移除。默認(rèn)情況下,類(lèi)元數(shù)據(jù)只受可用的本地內(nèi)存限制。可以通過(guò)
-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可以通過(guò)
-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ū)域,它是線(xiàn)程私有的。每個(gè)方法從開(kāi)始調(diào)用到結(jié)束調(diào)用就是棧幀從入棧到出棧的結(jié)果。
活動(dòng)線(xiàn)程中,只有棧頂?shù)臈攀怯行У模Q(chēng)為當(dāng)前棧幀。正在執(zhí)行的方法稱(chēng)為當(dāng)前方法,棧幀是方法運(yùn)行的基本結(jié)構(gòu)。在執(zhí)行引擎運(yùn)行時(shí),所有指令都只能針對(duì)當(dāng)前棧幀操作。
-
棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作棧、動(dòng)態(tài)鏈接、方法返回地址等信息。
局部變量表:存放方法參數(shù),編譯期可知的基本數(shù)據(jù)類(lèi)型、對(duì)象引用類(lèi)型(reference)和returnAddress類(lèi)型(指向一條字節(jié)碼指令地址)。局部變量表所需的內(nèi)存空間是在編譯期確定,方法在局部變量表中分配多少空間是完全確定的。在運(yùn)行期間不會(huì)改變局部變量表的大小。局部變量沒(méi)有準(zhǔn)備階段,必須顯示初始化。
操作棧是一個(gè)初始狀態(tài)為空的桶式結(jié)構(gòu)棧。方法執(zhí)行過(guò)程中,會(huì)有各種指令往棧寫(xiě)入和提取信息。JVM的執(zhí)行引擎就是基于操作棧的執(zhí)行引擎。
動(dòng)態(tài)連接: 在Class文件中的常量持中存有大量的符號(hào)引用。字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號(hào)引用作為參數(shù)。這些符號(hào)引用一部分在類(lèi)的加載階段或第一次使用的時(shí)候就轉(zhuǎn)化為了直接引用,稱(chēng)為靜態(tài)鏈接。而相反的,另一部分在運(yùn)行期間轉(zhuǎn)化為直接引用,就稱(chēng)為動(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)棧深度超過(guò)虛擬機(jī)分配給線(xiàn)程的棧大小時(shí)就會(huì)出現(xiàn)此error。最常見(jiàn)的就是遞歸深度超出了限定,然后拋出這個(gè)錯(cuò)誤
-
OutOfMemoryError
:虛擬機(jī)擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存空間,多線(xiàn)程下的內(nèi)存溢出,與棧空間是否足夠大并不存在任何聯(lián)系。為每個(gè)線(xiàn)程的棧分配的內(nèi)存越大(參數(shù)
-Xss
),那么可以建立的線(xiàn)程數(shù)量就越少,建立線(xiàn)程時(shí)就越容易把剩下的內(nèi)存耗盡,越容易內(nèi)存溢出。 -
可以通過(guò)
-Xss2m
設(shè)置棧內(nèi)存大小,設(shè)置每個(gè)線(xiàn)程的棧內(nèi)存,默認(rèn)1M,一般來(lái)說(shuō)是不需要改的。-XX:ThreadStackSize
線(xiàn)程堆棧大小如果把
-Xss
或者-XX:ThreadStackSize
設(shè)為0,就是使用“系統(tǒng)默認(rèn)值”。而在Linux x64上HotSpot VM給Java棧定義的“系統(tǒng)默認(rèn)”大小也是1MB。JDK1.6以前,誰(shuí)設(shè)置在后面,誰(shuí)就生效;JDK1.6以后,
-Xss
設(shè)置在后面,則以-Xss
為準(zhǔn),-XXThreadStackSize
設(shè)置在后面,則主線(xiàn)程以-Xss
為準(zhǔn),其它線(xiàn)程以-XX:ThreadStackSize
為準(zhǔn)。
1.4. 本地方法棧 Native Method Stacks
本地方法棧為Native方法服務(wù)
本地方法通過(guò)JNI(Java Native Interface)來(lái)訪(fǎng)問(wèn)虛擬機(jī)運(yùn)行時(shí)的數(shù)據(jù)區(qū),甚至是調(diào)用寄存器,具有和JVM相同的能力和權(quán)限。
本地方法棧也會(huì)拋出:OutOfMemoryError和StackOverflowError
-
JNI
JNI深度使用操作系統(tǒng)的特性功能。復(fù)用非Java代碼。如果大量使用其他語(yǔ)言來(lái)實(shí)現(xiàn)JNI,會(huì)失去跨平臺(tái)特性。
如果對(duì)執(zhí)行效率要求高,偏底層的跨進(jìn)程的操作等,可以考慮設(shè)計(jì)為JNI調(diào)用方式。
1.5. 程序計(jì)數(shù)器 Program Counter Register
- 每個(gè)線(xiàn)程創(chuàng)建后都會(huì)產(chǎn)生自己的程序計(jì)數(shù)器和棧幀,程序計(jì)數(shù)器用來(lái)存放執(zhí)行指令的偏移量和行號(hào)指示器等,線(xiàn)程執(zhí)行或恢復(fù)都依賴(lài)程序計(jì)數(shù)器。
- 程序計(jì)數(shù)器是線(xiàn)程獨(dú)占,在各個(gè)線(xiàn)程直接互不影響,在此區(qū)域也不會(huì)有內(nèi)存溢出異常。
- 線(xiàn)程如果在執(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
來(lái)限制使用內(nèi)存空間的最大值最大值-
DirectByteBuffer
可以直接操作DirectMemory,它通過(guò)JNI調(diào)用native方法直接分配堆外內(nèi)存,通過(guò)DirectByteBuffer
對(duì)象對(duì)這塊內(nèi)存對(duì)象進(jìn)行操作這個(gè)調(diào)用,實(shí)際上是從系統(tǒng)的用戶(hù)態(tài)切換到了內(nèi)核態(tài)使用系統(tǒng)調(diào)用來(lái)完成這個(gè)操作。
為什么要切換到內(nèi)核態(tài)?用戶(hù)態(tài)沒(méi)有權(quán)限去操作內(nèi)核態(tài)的資源,它只能通過(guò)系統(tǒng)調(diào)用外完成用戶(hù)態(tài)到內(nèi)核態(tài)的切換,然后在完成相關(guān)操作后再有內(nèi)核態(tài)切換回用戶(hù)態(tài)。
DirectByteBuffer
該類(lèi)本身還是位于Java內(nèi)存模型的堆中。堆內(nèi)內(nèi)存是JVM可以直接管控、操縱。由于
DirectByteBuffer
的權(quán)限修飾符是空的也就是默認(rèn)的,所以在我們編程中是無(wú)法直接new,只允許同包創(chuàng)建,我們可以通過(guò)ByteBuffer
中的靜態(tài)方法allocateDirect(int)
方法來(lái)創(chuàng)建對(duì)象。
java public static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity); }
> 而 DirectByteBuffer 類(lèi)中調(diào)用了native的unsafe.allocateMemory(size)來(lái)分配空間,實(shí)際上是使用了c語(yǔ)言的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)!!!掉黑板 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;
}
// 記錄分分配空間信息的類(lèi) private Deallocator(long address, long size, int capacity) { assert (address != 0); this.address = address; this.size = size; this.capacity = capacity; }
2. 對(duì)象創(chuàng)建與內(nèi)存分配
2.1 對(duì)象創(chuàng)建
- 對(duì)象使用new創(chuàng)建的簡(jiǎn)單過(guò)程
-
指針碰撞:
假設(shè)Java堆中內(nèi)存是絕對(duì)規(guī)整的,所有用過(guò)的內(nèi)存都被放在一邊,空閑的內(nèi)存被放在另一邊,中間放著一個(gè)指針作 為分界點(diǎn)的指示器,那所分配內(nèi)存就僅僅是把那個(gè)指針向空閑空間那邊挪動(dòng)一段與對(duì)象大小相等的距離,這種分配方式稱(chēng)為“指針碰撞”(Bump The Pointer)
【帶Compact過(guò)程的Serial、ParNew等采用指針碰撞。】 -
空閑列表
如果Java堆中的內(nèi)存并不是規(guī)整的,已使用的和空閑的內(nèi)存相互交錯(cuò),就無(wú)法進(jìn)行指針碰撞了,JVM就必須維護(hù)一個(gè)列表,記錄可用內(nèi)存區(qū)域,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例,并更新列表上的記錄,這種分配方式稱(chēng)為“空閑列表”(Free List)
【CMS這種基于Mark-Sweep算法的使用空閑列表】 -
不難想到,分配內(nèi)存時(shí)如果多個(gè)線(xiàn)程同時(shí)創(chuàng)建對(duì)象,就會(huì)出現(xiàn)并發(fā)問(wèn)題。JVM實(shí)際采用:
一種是CAS(Compare And Swap)加上失敗重試機(jī)制來(lái)保證更新操作的原子性;
另一種是本地線(xiàn)程緩沖(TLAB,Thread Local Allocation Buffer.),即把內(nèi)存分配的動(dòng)作按照線(xiàn)程劃分在不同的空間之中進(jìn)行,每個(gè)線(xiàn)程都預(yù)先分配一小塊內(nèi)存。線(xiàn)程在自己的TLAB中分配,只有TLAB用完才需要同步加鎖。虛擬機(jī)是否用TLAB,可以通過(guò)
-XX:+/-UseTLAB
參數(shù)設(shè)定。
2.2 對(duì)象內(nèi)存
-
對(duì)象頭(Header)包含兩部分:一是自身運(yùn)行時(shí)數(shù)據(jù);二是類(lèi)型指針
運(yùn)行時(shí)數(shù)據(jù): 32位和64位JVM分別對(duì)應(yīng)32位和64位長(zhǎng)度(未開(kāi)啟指正壓縮),存儲(chǔ)包括:哈希碼、GC分帶年齡、鎖狀態(tài)標(biāo)志、線(xiàn)程池持有鎖、偏向鎖ID、偏向時(shí)間戳等。(Mark Word)。
類(lèi)型指針: 即對(duì)象指向它的類(lèi)元數(shù)據(jù)的指針,虛擬機(jī)通過(guò)這個(gè)指針確定是哪個(gè)對(duì)象的實(shí)例。查找對(duì)象的元數(shù)據(jù)信息不一定要經(jīng)過(guò)對(duì)象本身。對(duì)象是Java數(shù)組,則對(duì)象頭中則會(huì)有一塊記錄數(shù)組長(zhǎng)度的數(shù)據(jù);普通Java類(lèi)可以通過(guò)元數(shù)據(jù)信息確定Java類(lèi)大小,但數(shù)組還需要需要對(duì)象頭中的長(zhǎng)度數(shù)據(jù)才能確定。
-
實(shí)例數(shù)據(jù)(Instance Data)
就是對(duì)象存儲(chǔ)的真正的有效信息,也就是程序代碼中定義的所有字段內(nèi)容。
-
對(duì)齊填充(Padding)
因?yàn)镠otSpot VM的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍,對(duì)象頭部分正好是8字節(jié)的倍數(shù),當(dāng)對(duì)象實(shí)例數(shù)據(jù)部分沒(méi)有對(duì)齊時(shí),就需要通過(guò)對(duì)齊填充來(lái)補(bǔ)全。
2.3 對(duì)象訪(fǎng)問(wèn)
-
Java通過(guò)棧上的reference數(shù)據(jù)來(lái)操作堆上的具體對(duì)象,而reference是一個(gè)指向?qū)ο蟮囊茫ㄟ^(guò)reference去定位和訪(fǎng)問(wèn)對(duì)象,目前主流的使用兩種方式:一是使用句柄,二是使用直接指針
句柄: JVM堆會(huì)專(zhuān)門(mén)劃分內(nèi)存作為句柄池,而reference中存的就是對(duì)象的句柄地址;句柄中包含了對(duì)象實(shí)例數(shù)據(jù)與類(lèi)型數(shù)據(jù)各自的具體地址。
句柄直接指針: 如果是直接指針,Java堆中就會(huì)防止訪(fǎng)問(wèn)類(lèi)型數(shù)據(jù)相關(guān)的信息。而reference中存儲(chǔ)的直接就是對(duì)象地址。
直接指針
關(guān)于我
- 坐標(biāo)杭州,普通本科在讀,計(jì)算機(jī)科學(xué)與技術(shù)專(zhuān)業(yè),20年畢業(yè),目前處于實(shí)習(xí)階段。
- 主要做Java開(kāi)發(fā),會(huì)寫(xiě)點(diǎn)Golang、Shell。對(duì)微服務(wù)、大數(shù)據(jù)比較感興趣,預(yù)備做這個(gè)方向。
- 目前處于菜鳥(niǎo)階段,各位大佬輕噴,小弟正在瘋狂學(xué)習(xí)。
- 歡迎大家和我交流鴨!!!