當(dāng)前位置:首頁(yè) > 芯聞號(hào) > 充電吧
[導(dǎo)讀]對(duì)象的創(chuàng)建虛擬機(jī)遇到一條new指令時(shí),首先檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類的符號(hào)引用,并且檢查這個(gè)符號(hào)引用代表的類是否已被加載、解析和初始化過(guò)。如果沒(méi)有,那必須先執(zhí)行相應(yīng)的類加載過(guò)程。在

對(duì)象的創(chuàng)建

虛擬機(jī)遇到一條new指令時(shí),首先檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類的符號(hào)引用,并且檢查這個(gè)符號(hào)引用代表的類是否已被加載、解析和初始化過(guò)。如果沒(méi)有,那必須先執(zhí)行相應(yīng)的類加載過(guò)程。


在類加載檢查通過(guò)后,虛擬機(jī)將為新生對(duì)象分配內(nèi)存。對(duì)象所需內(nèi)存的大小在類加載完成后便可完全確定,為對(duì)象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來(lái)。假設(shè)Java堆中內(nèi)存是絕對(duì)規(guī)整的,所有用過(guò)的內(nèi)存都放在一邊,空閑的內(nèi)存放在另一邊,中間放著一個(gè)指針作為分界點(diǎn)的指示器,那所分配內(nèi)存就僅僅是把那個(gè)指針向空閑空間那邊挪動(dòng)一段與對(duì)象大小相等的距離,這種分配方式稱為“指針碰撞”(Bump the Pointer)。如果Java堆中的內(nèi)存并不是規(guī)整的,已使用的內(nèi)存和空閑的內(nèi)存相互交錯(cuò),那就沒(méi)有辦法簡(jiǎn)單地進(jìn)行指針碰撞了,虛擬機(jī)就必須維護(hù)一個(gè)列表,記錄上哪些內(nèi)存塊是可用的,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例,并更新列表上的記錄,這種分配方式稱為“空閑列表”(Free List)。選擇哪種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。因此,在使用Serial、ParNew等帶Compact過(guò)程的收集器時(shí),系統(tǒng)采用的分配算法是指針碰撞,而使用CMS這種基于Mark-Sweep算法的收集器時(shí),通常采用空閑列表。


除如何劃分可用空間之外,還有另外一個(gè)需要考慮的問(wèn)題是對(duì)象創(chuàng)建在虛擬機(jī)中是非常頻繁的行為,即使是僅僅修改一個(gè)指針?biāo)赶虻奈恢?,在并發(fā)情況下也并不是線程安全的,可能出現(xiàn)正在給對(duì)象A分配內(nèi)存,指針還沒(méi)來(lái)得及修改,對(duì)象B又同時(shí)使用了原來(lái)的指針來(lái)分配內(nèi)存的情況。解決這個(gè)問(wèn)題有兩種方案,一種是對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行同步處理——實(shí)際上虛擬機(jī)采用CAS配上失敗重試的方式保證更新操作的原子性;另一種是把內(nèi)存分配的動(dòng)作按照線程劃分在不同的空間之中進(jìn)行,即每個(gè)線程在Java堆中預(yù)先分配一小塊內(nèi)存,稱為本地線程分配緩沖(Thread Local Allocation Buffer,TLAB)。哪個(gè)線程要分配內(nèi)存,就在哪個(gè)線程的TLAB上分配,只有TLAB用完并分配新的TLAB時(shí),才需要同步鎖定。虛擬機(jī)是否使用TLAB,可以通過(guò)-XX:+/-UseTLAB參數(shù)來(lái)設(shè)定。


內(nèi)存分配完成后,虛擬機(jī)需要將分配到的內(nèi)存空間都初始化為零值(不包括對(duì)象頭),如果使用TLAB,這一工作過(guò)程也可以提前至TLAB分配時(shí)進(jìn)行。這一步操作保證了對(duì)象的實(shí)例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問(wèn)到這些字段的數(shù)據(jù)類型所對(duì)應(yīng)的零值。


接下來(lái),虛擬機(jī)要對(duì)對(duì)象進(jìn)行必要的設(shè)置,例如這個(gè)對(duì)象是哪個(gè)類的實(shí)例、如何才能找到類的元數(shù)據(jù)信息、對(duì)象的哈希碼、對(duì)象的GC分代年齡等信息。這些信息存放在對(duì)象的對(duì)象頭(Object Header)之中。根據(jù)虛擬機(jī)當(dāng)前的運(yùn)行狀態(tài)的不同,如是否啟用偏向鎖等,對(duì)象頭會(huì)有不同的設(shè)置方式。


在上面工作都完成之后,從虛擬機(jī)的視角來(lái)看,一個(gè)新的對(duì)象已經(jīng)產(chǎn)生了,但從Java程序的視角來(lái)看,對(duì)象創(chuàng)建才剛剛開(kāi)始——<init>方法還沒(méi)有執(zhí)行,所有的字段都還為零。所以,一般來(lái)說(shuō)(由字節(jié)碼中是否跟隨invokespecial指令所決定),執(zhí)行new指令之后會(huì)接著執(zhí)行<init>方法,把對(duì)象按照程序員的意愿進(jìn)行初始化,這樣一個(gè)真正可用的對(duì)象才算完全產(chǎn)生出來(lái)。


對(duì)象的內(nèi)存布局 在HotSpot虛擬機(jī)中,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為3塊區(qū)域:對(duì)象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)和對(duì)齊填充(Padding)。
HotSpot虛擬機(jī)的對(duì)象頭包括兩部分信息,第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時(shí)間戳等,這部分?jǐn)?shù)據(jù)的長(zhǎng)度在32位和64位的虛擬機(jī)(未開(kāi)啟壓縮指針)中分別為32bit和64bit,官方稱它為“Mark Word”。對(duì)象需要存儲(chǔ)的運(yùn)行時(shí)數(shù)據(jù)很多,其實(shí)已經(jīng)超出了32位、64位Bitmap結(jié)構(gòu)所能記錄的限度,但是對(duì)象頭信息是與對(duì)象自身定義的數(shù)據(jù)無(wú)關(guān)的額外存儲(chǔ)成本,考慮到虛擬機(jī)的空間效率,Mark Word被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存儲(chǔ)盡量多的信息,它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間。


對(duì)象頭的另外一部分是類型指針,即對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。并不是所有的虛擬機(jī)實(shí)現(xiàn)都必須在對(duì)象數(shù)據(jù)上保留類型指針,換句話說(shuō),查找對(duì)象的元數(shù)據(jù)信息并不一定要經(jīng)過(guò)對(duì)象本身,另外,如果對(duì)象是一個(gè)Java數(shù)組,那在對(duì)象頭中還必須有一塊用于記錄數(shù)組長(zhǎng)度的數(shù)據(jù),因?yàn)樘摂M機(jī)可以通過(guò)普通Java對(duì)象的元數(shù)據(jù)信息確定Java對(duì)象的大小,但是從數(shù)組的元數(shù)據(jù)中卻無(wú)法確定數(shù)組的大小。

實(shí)例數(shù)據(jù)部分是對(duì)象真正存儲(chǔ)的有效信息,也是在程序代碼中所定義的各種類型的字段內(nèi)容。無(wú)論是從父類繼承下來(lái)的,還是在子類中定義的,都需要記錄起來(lái)。這部分的存儲(chǔ)順序會(huì)受到虛擬機(jī)分配策略參數(shù)(FieldsAllocationStyle)和字段在Java源碼中定義順序的影響。HotSpot虛擬機(jī)默認(rèn)的分配策略為longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),從分配策略中可以看出,相同寬度的字段總是被分配到一起。在滿足這個(gè)前提條件的情況下,在父類中定義的變量會(huì)出現(xiàn)在子類之前。如果CompactFields參數(shù)值為true(默認(rèn)為true),那么子類之中較窄的變量也可能會(huì)插入到父類變量的空隙之中。

第三部分對(duì)齊填充并不是必然存在的,也沒(méi)有特別的含義,它僅僅起著占位符的作用。由于HotSpot VM的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍,換句話說(shuō),就是對(duì)象的大小必須是8字節(jié)的整數(shù)倍。而對(duì)象頭部分正好是8字節(jié)的倍數(shù)(1倍或者2倍),因此,當(dāng)對(duì)象實(shí)例數(shù)據(jù)部分沒(méi)有對(duì)齊時(shí),就需要通過(guò)對(duì)齊填充來(lái)補(bǔ)全。

對(duì)象的訪問(wèn)定位 建立對(duì)象是為了使用對(duì)象,我們的Java程序需要通過(guò)棧上的reference數(shù)據(jù)來(lái)操作堆上的具體對(duì)象。由于reference類型在Java虛擬機(jī)規(guī)范中只規(guī)定了一個(gè)指向?qū)ο蟮囊?,并沒(méi)有定義這個(gè)引用應(yīng)該通過(guò)何種方式去定位、訪問(wèn)堆中的對(duì)象的具體位置,所以對(duì)象訪問(wèn)方式也是取決于虛擬機(jī)實(shí)現(xiàn)而定的。目前主流的訪問(wèn)方式有使用句柄和直接指針兩種。
?如果使用句柄訪問(wèn)的話,那么Java堆中將會(huì)劃分出一塊內(nèi)存來(lái)作為句柄池,reference中存儲(chǔ)的就是對(duì)象的句柄地址,而句柄中包含了對(duì)象實(shí)例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址信息,


如果使用直接指針訪問(wèn),那么Java堆對(duì)象的布局中就必須考慮如何放置訪問(wèn)類型數(shù)據(jù)的相關(guān)信息,而reference中存儲(chǔ)的直接就是對(duì)象地址,


使用句柄來(lái)訪問(wèn) 好處 是 reference中存儲(chǔ)的是穩(wěn)定的句柄地址,在對(duì)象被移動(dòng)(垃圾收集時(shí))時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,而reference本身不需要修改。

使用直接指針訪問(wèn) 好處速度更快,它節(jié)省了一次指針定位的時(shí)間開(kāi)銷。

OutOfMemoryError異常 在Java虛擬機(jī)規(guī)范的描述中,除了程序計(jì)數(shù)器外,虛擬機(jī)內(nèi)存的其他幾個(gè)運(yùn)行時(shí)區(qū)域都有發(fā)生OutOfMemoryError(OOM)異常的可能。

Java堆溢出 Java堆用于存儲(chǔ)對(duì)象實(shí)例,只要不斷地創(chuàng)建對(duì)象,并且保證GC Roots到對(duì)象之間有可達(dá)路徑來(lái)避免垃圾回收機(jī)制清除這些對(duì)象,那么在對(duì)象數(shù)量到達(dá)最大堆的容量限制后就會(huì)產(chǎn)生內(nèi)存溢出異常。

如下代碼限制Java堆的大小為20MB,不可擴(kuò)展(將堆的最小值-Xms參數(shù)與最大值-Xmx參數(shù)設(shè)置為一樣即可避免堆自動(dòng)擴(kuò)展),通過(guò)參數(shù)-XX:+HeapDumpOnOutOfMemoryError可以讓虛擬機(jī)在出現(xiàn)內(nèi)存溢出異常時(shí)Dump出當(dāng)前的內(nèi)存堆轉(zhuǎn)儲(chǔ)快照以便事后進(jìn)行分析。
VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError?

public?class?HeapOOM{
static?class?OOMObject{?}
public?static?void?main(String[]args){
?List<OOMObject>list=new?ArrayList<OOMObject>();
?while(true){
list.add(new?OOMObject());
}
}
}


運(yùn)行結(jié)果:

java.lang.OutOfMemoryError:
Java?heap?space?Dumping?heap?to?java_pid3404.hprof……
?Heap?dump?file?created[22045981?bytes?in?0.663?secs]


Java堆內(nèi)存的OOM異常是實(shí)際應(yīng)用中常見(jiàn)的內(nèi)存溢出異常情況。 當(dāng)出現(xiàn)Java堆內(nèi)存溢出時(shí),異常堆棧信息“java.lang.OutOfMemoryError”會(huì)跟著進(jìn)一步提示“Java heap space”。

要解決這個(gè)區(qū)域的異常,一般的手段是先通過(guò)內(nèi)存映像分析工具 對(duì)Dump出來(lái)的堆轉(zhuǎn)儲(chǔ)快照進(jìn)行分析,重點(diǎn)是確認(rèn)內(nèi)存中的對(duì)象是否是必要的,也就是要先分清楚到底是出現(xiàn)了內(nèi)存泄漏(Memory Leak)還是內(nèi)存溢出(Memory Overflow)

要解決這個(gè)區(qū)域的異常,一般的手段是先通過(guò)內(nèi)存映像分析工具(如Eclipse Memory Analyzer)對(duì)Dump出來(lái)的堆轉(zhuǎn)儲(chǔ)快照進(jìn)行分析,重點(diǎn)是確認(rèn)內(nèi)存中的對(duì)象是否是必要的,也就是要先分清楚到底是出現(xiàn)了內(nèi)存泄漏(Memory Leak)還是內(nèi)存溢出(Memory Overflow)

由于在HotSpot虛擬機(jī)中并不區(qū)分虛擬機(jī)棧和本地方法棧,因此,對(duì)于HotSpot來(lái)說(shuō),雖然-Xoss參數(shù)(設(shè)置本地方法棧大?。┐嬖?,但實(shí)際上是無(wú)效的,棧容量只由-Xss參數(shù)設(shè)定。關(guān)于虛擬機(jī)棧和本地方法棧,在Java虛擬機(jī)規(guī)范中描述了兩種異常:
如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的最大深度,將拋出StackOverflowError異常。 如果虛擬機(jī)在擴(kuò)展棧時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存空間,則拋出OutOfMemoryError異常。

這里把異常分成兩種情況,實(shí)際存在著一些互相重疊的地方:當(dāng)??臻g無(wú)法繼續(xù)分配時(shí),到底是內(nèi)存太小,還是已使用的??臻g太大,其本質(zhì)上只是對(duì)同一件事情的兩種描述而已。

定義了大量的本地變量,增大此方法幀中本地變量表的長(zhǎng)度。結(jié)果:拋出StackOverflowError異常時(shí)輸出的堆棧深度相應(yīng)縮小。
使用-Xss參數(shù)減少棧內(nèi)存容量,VM Args:-Xss128k

public?class?JavaVMStackSOF{?
private?int?stackLength=1;?
public?void?stackLeak(){?
stackLength++;?
stackLeak();
?}?
public?static?void?main(String[]args)throws?Throwable{?JavaVMStackSOF?oom=new?JavaVMStackSOF();
try{?oom.stackLeak();?
}catch(Throwable?e){?
System.out.println("stack?length:"+oom.stackLength);
?throw?e;?}?}


運(yùn)行結(jié)果:

stack?length:2402
Exception?in?thread"main"java.lang.StackOverflowError
at?org.fenixsoft.oom.VMStackSOF.leak(VMStackSOF.java:20)
at?org.fenixsoft.oom.VMStackSOF.leak(VMStackSOF.java:21)
at?org.fenixsoft.oom.VMStackSOF.leak(VMStackSOF.java:21)
……后續(xù)異常堆棧信息省略


實(shí)驗(yàn)結(jié)果表明:在單個(gè)線程下,無(wú)論是由于棧幀太大還是虛擬機(jī)棧容量太小,當(dāng)內(nèi)存無(wú)法分配的時(shí)候,虛擬機(jī)拋出的都是StackOverflowError異常。
如果測(cè)試時(shí)不限于單線程,通過(guò)不斷地建立線程的方式倒是可以產(chǎn)生內(nèi)存溢出異常。但是這樣產(chǎn)生的內(nèi)存溢出異常與??臻g是否足夠大并不存在任何聯(lián)系,或者準(zhǔn)確地說(shuō),在這種情況下,為每個(gè)線程的棧分配的內(nèi)存越大,反而越容易產(chǎn)生內(nèi)存溢出異常。
操作系統(tǒng)分配給每個(gè)進(jìn)程的內(nèi)存是有限制的,譬如32位的Windows限制為2GB。虛擬機(jī)提供了參數(shù)來(lái)控制Java堆和方法區(qū)的這兩部分內(nèi)存的最大值。剩余的內(nèi)存為2GB(操作系統(tǒng)限制)減去Xmx(最大堆容量),再減去MaxPermSize(最大方法區(qū)容量),程序計(jì)數(shù)器消耗內(nèi)存很小,可以忽略掉。如果虛擬機(jī)進(jìn)程本身耗費(fèi)的內(nèi)存不計(jì)算在內(nèi),剩下的內(nèi)存就由虛擬機(jī)棧和本地方法?!肮戏帧绷?。每個(gè)線程分配到的棧容量越大,可以建立的線程數(shù)量自然就越少,建立線程時(shí)就越容易把剩下的內(nèi)存耗盡。

這一點(diǎn)讀者需要在開(kāi)發(fā)多線程的應(yīng)用時(shí)特別注意,出現(xiàn)StackOverflowError異常時(shí)有錯(cuò)誤堆??梢蚤喿x,相對(duì)來(lái)說(shuō),比較容易找到問(wèn)題的所在。而且,如果使用虛擬機(jī)默認(rèn)參數(shù),棧深度在大多數(shù)情況下(因?yàn)槊總€(gè)方法壓入棧的幀大小并不是一樣的,所以只能說(shuō)在大多數(shù)情況下)達(dá)到1000~2000完全沒(méi)有問(wèn)題,對(duì)于正常的方法調(diào)用(包括遞歸),這個(gè)深度應(yīng)該完全夠用了。但是,如果是建立過(guò)多線程導(dǎo)致的內(nèi)存溢出,在不能減少線程數(shù)或者更換64位虛擬機(jī)的情況下,就只能通過(guò)減少最大堆和減少棧容量來(lái)?yè)Q取更多的線程。如果沒(méi)有這方面的處理經(jīng)驗(yàn),這種通過(guò)“減少內(nèi)存”的手段來(lái)解決內(nèi)存溢出的方式會(huì)比較難以想到。

VM Args:-Xss2M

public?class?JavaVMStackOOM{?
private?void?dontStop(){?
while(true){?}
?}?
public?void?stackLeakByThread(){?
while(true){?
Thread?thread=new?Thread(new?Runnable(){
?@Override
?public?void?run(){
?dontStop();
?}?});
?thread.start();?}?}
?public?static?void?main(String[]args)throws?Throwable{?
JavaVMStackOOM?oom=new?JavaVMStackOOM();
?oom.stackLeakByThread();
?}?}


運(yùn)行結(jié)果:

Exception?in?thread"main"java.lang.OutOfMemoryError:unable?to?create?new?native?thread


方法區(qū)和運(yùn)行時(shí)常量池溢出 String.intern()是一個(gè)Native方法,它的作用是:如果字符串常量池中已經(jīng)包含一個(gè)等于此String對(duì)象的字符串,則返回代表池中這個(gè)字符串的String對(duì)象;否則,將此String對(duì)象包含的字符串添加到常量池中,并且返回此String對(duì)象的引用。 在JDK 1.6及之前的版本中,由于常量池分配在永久代內(nèi),我們可以通過(guò)-XX:PermSize和-XX:MaxPermSize限制方法區(qū)大小,從而間接限制其中常量池的容量。
VM Args:-XX:PermSize=10M-XX:MaxPermSize=10M

public?class?RuntimeConstantPoolOOM{?
public?static?void?main(String[]args){
?//使用List保持著常量池引用,避免Full?GC回收常量池行為
?List<String>list=new?ArrayList<String>();?
//10MB的PermSize在integer范圍內(nèi)足夠產(chǎn)生OOM了
?int?i=0;?while(true){?
list.add(String.valueOf(i++).intern());?}?}
?}


運(yùn)行結(jié)果:

Exception?in?thread"main"java.lang.OutOfMemoryError:
PermGen?space?at?java.lang.String.intern(Native?Method)
at?org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18)


從運(yùn)行結(jié)果中可以看到,運(yùn)行時(shí)常量池溢出,在OutOfMemoryError后面跟隨的提示信息是“PermGen space”,說(shuō)明運(yùn)行時(shí)常量池屬于方法區(qū)(HotSpot虛擬機(jī)中的永久代)的一部分。

而使用JDK 1.7運(yùn)行這段程序就不會(huì)得到相同的結(jié)果,while循環(huán)將一直進(jìn)行下去。關(guān)于這個(gè)字符串常量池的實(shí)現(xiàn)問(wèn)題,還可以引申出一個(gè)更有意思的影響。

public?class?RuntimeConstantPoolOOM{
?public?static?void?main(String[]args){
?String?str1=new?StringBuilder("計(jì)算機(jī)").append("軟件").toString();?
System.out.println(str1.intern()==str1);
?String?str2=new?StringBuilder("ja").append("va").toString();
?System.out.println(str2.intern()==str2);
?}?}?}

這段代碼在JDK 1.6中運(yùn)行,會(huì)得到兩個(gè)false,而在JDK 1.7中運(yùn)行,會(huì)得到一個(gè)true和一個(gè)false。產(chǎn)生差異的原因是:在JDK 1.6中,intern()方法會(huì)把首次遇到的字符串實(shí)例復(fù)制到永久代中,返回的也是永久代中這個(gè)字符串實(shí)例的引用,而由StringBuilder創(chuàng)建的字符串實(shí)例在Java堆上,所以必然不是同一個(gè)引用,將返回false。而JDK 1.7(以及部分其他虛擬機(jī),例如JRockit)的intern()實(shí)現(xiàn)不會(huì)再?gòu)?fù)制實(shí)例,只是在常量池中記錄首次出現(xiàn)的實(shí)例引用,因此intern()返回的引用和由StringBuilder創(chuàng)建的那個(gè)字符串實(shí)例是同一個(gè)。對(duì)str2比較返回false是因?yàn)椤癹ava”這個(gè)字符串在執(zhí)行StringBuilder.toString()之前已經(jīng)出現(xiàn)過(guò),字符串常量池中已經(jīng)有它的引用了,不符合“首次出現(xiàn)”的原則,而“計(jì)算機(jī)軟件”這個(gè)字符串則是首次出現(xiàn)的,因此返回true。
方法區(qū)用于存放Class的相關(guān)信息,如類名、訪問(wèn)修飾符、常量池、字段描述、方法描述等。對(duì)于這些區(qū)域的測(cè)試,基本的思路是運(yùn)行時(shí)產(chǎn)生大量的類去填滿方法區(qū),直到溢出。雖然直接使用Java SE API也可以動(dòng)態(tài)產(chǎn)生類(如反射時(shí)的GeneratedConstructorAccessor和動(dòng)態(tài)代理等)。借助CGLib[1]直接操作字節(jié)碼運(yùn)行時(shí)生成了大量的動(dòng)態(tài)類。
這樣的應(yīng)用經(jīng)常會(huì)出現(xiàn)在實(shí)際應(yīng)用中:當(dāng)前的很多主流框架,如Spring、Hibernate,在對(duì)類進(jìn)行增強(qiáng)時(shí),都會(huì)使用到CGLib這類字節(jié)碼技術(shù),增強(qiáng)的類越多,就需要越大的方法區(qū)來(lái)保證動(dòng)態(tài)生成的Class可以加載入內(nèi)存。另外,JVM上的動(dòng)態(tài)語(yǔ)言(例如Groovy等)通常都會(huì)持續(xù)創(chuàng)建類來(lái)實(shí)現(xiàn)語(yǔ)言的動(dòng)態(tài)性,隨著這類語(yǔ)言的流行,也越來(lái)越容易遇到如下的溢出場(chǎng)景。
VM Args:-XX:PermSize=10M-XX:MaxPermSize=10M

public?class?JavaMethodAreaOOM{?
public?static?void?main(String[]args){
?while(true){?
Enhancer?enhancer=new?Enhancer();
?enhancer.setSuperclass(OOMObject.class);
?enhancer.setUseCache(false);?
enhancer.setCallback(new?MethodInterceptor(){?
public?Object?intercept(Object?obj,Method?method,Object[]args,MethodProxy?proxy)throws?Throwable{
?return?proxy.invokeSuper(obj,args);
?}?});
?enhancer.create();?}?}
?static?class?OOMObject{?}?}


運(yùn)行結(jié)果:

Caused?by:java.lang.OutOfMemoryError:
PermGen?space?at?java.lang.ClassLoader.defineClass1(Native?Method)
?at?java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
at?java.lang.ClassLoader.defineClass(ClassLoader.java:616)?……8?more


方法區(qū)溢出也是一種常見(jiàn)的內(nèi)存溢出異常,一個(gè)類要被垃圾收集器回收掉,判定條件是比較苛刻的。在經(jīng)常動(dòng)態(tài)生成大量Class的應(yīng)用中,需要特別注意類的回收狀況。這類場(chǎng)景除了上面提到的程序使用了CGLib字節(jié)碼增強(qiáng)和動(dòng)態(tài)語(yǔ)言之外,常見(jiàn)的還有:大量JSP或動(dòng)態(tài)產(chǎn)生JSP文件的應(yīng)用(JSP第一次運(yùn)行時(shí)需要編譯為Java類)、基于OSGi的應(yīng)用(即使是同一個(gè)類文件,被不同的加載器加載也會(huì)視為不同的類)等。

本機(jī)直接內(nèi)存溢出 DirectMemory容量可通過(guò)-XX:MaxDirectMemorySize指定,如果不指定,則默認(rèn)與Java堆最大值(-Xmx指定)一樣,下例越過(guò)了DirectByteBuffer類,直接通過(guò)反射獲取Unsafe實(shí)例進(jìn)行內(nèi)存分配(Unsafe類的getUnsafe()方法限制了只有引導(dǎo)類加載器才會(huì)返回實(shí)例,也就是設(shè)計(jì)者希望只有rt.jar中的類才能使用Unsafe的功能)。因?yàn)椋m然使用DirectByteBuffer分配內(nèi)存也會(huì)拋出內(nèi)存溢出異常,但它拋出異常時(shí)并沒(méi)有真正向操作系統(tǒng)申請(qǐng)分配內(nèi)存,而是通過(guò)計(jì)算得知內(nèi)存無(wú)法分配,于是手動(dòng)拋出異常,真正申請(qǐng)分配內(nèi)存的方法是unsafe.allocateMemory()。

*VM Args:-Xmx20M-XX:MaxDirectMemorySize=10M

public?class?DirectMemoryOOM{
?private?static?final?int_1MB=1024*1024;?
public?static?void?main(String[]args)throws?Exception{?
Field?unsafeField=Unsafe.class.getDeclaredFields()[0];
?unsafeField.setAccessible(true);
?Unsafe?unsafe=(Unsafe)unsafeField.get(null);?
while(true){?
unsafe.allocateMemory(_1MB);
?}?}?}


Exception?in?thread"main"java.lang.OutOfMemoryError
at?sun.misc.Unsafe.allocateMemory(Native?Method)
at?org.fenixsoft.oom.DMOOM.main(DMOOM.java:20)


由DirectMemory導(dǎo)致的內(nèi)存溢出,一個(gè)明顯的特征是在Heap Dump文件中不會(huì)看見(jiàn)明顯的異常,如果發(fā)現(xiàn)OOM之后Dump文件很小,而程序中又直接或間接使用了NIO,那就可以考慮檢查一下是不是這方面的原因。

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請(qǐng)聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請(qǐng)及時(shí)聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫?dú)角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

倫敦2024年8月29日 /美通社/ -- 英國(guó)汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認(rèn)證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開(kāi)發(fā)耗時(shí)1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動(dòng) BSP

北京2024年8月28日 /美通社/ -- 越來(lái)越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運(yùn)行,同時(shí)企業(yè)卻面臨越來(lái)越多業(yè)務(wù)中斷的風(fēng)險(xiǎn),如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報(bào)道,騰訊和網(wǎng)易近期正在縮減他們對(duì)日本游戲市場(chǎng)的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國(guó)國(guó)際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)開(kāi)幕式在貴陽(yáng)舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國(guó)國(guó)際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語(yǔ)權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機(jī) 衛(wèi)星通信

要點(diǎn): 有效應(yīng)對(duì)環(huán)境變化,經(jīng)營(yíng)業(yè)績(jī)穩(wěn)中有升 落實(shí)提質(zhì)增效舉措,毛利潤(rùn)率延續(xù)升勢(shì) 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長(zhǎng) 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競(jìng)爭(zhēng)力 堅(jiān)持高質(zhì)量發(fā)展策略,塑強(qiáng)核心競(jìng)爭(zhēng)優(yōu)勢(shì)...

關(guān)鍵字: 通信 BSP 電信運(yùn)營(yíng)商 數(shù)字經(jīng)濟(jì)

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺(tái)與中國(guó)電影電視技術(shù)學(xué)會(huì)聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會(huì)上宣布正式成立。 活動(dòng)現(xiàn)場(chǎng) NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長(zhǎng)三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會(huì)上,軟通動(dòng)力信息技術(shù)(集團(tuán))股份有限公司(以下簡(jiǎn)稱"軟通動(dòng)力")與長(zhǎng)三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉