"??????This tutorial is experimental and unsupported."
內(nèi)存模型
下圖是描述JVM內(nèi)存模型的圖:
JVM包含兩個(gè)子系統(tǒng)和兩個(gè)組件病线,兩個(gè)子系統(tǒng)為Class loader(類(lèi)裝載)蒂窒、Execution engine(執(zhí)行引擎)惫东;兩個(gè)組件為Runtime data area(運(yùn)行時(shí)數(shù)據(jù)區(qū))芹缔、Native Interface(本地接口)捶闸。
Class loader(類(lèi)裝載): 根據(jù)給定的全限定名類(lèi)名(如:java.lang.Object)來(lái)裝載class文件到Runtime data area中的method area。
Execution engine(執(zhí)行引擎): 執(zhí)行classes中的指令乾忱。
Native Interface(本地接口): 與native libraries交互彤守,是其它編程語(yǔ)言交互的接口。
Runtime data area(運(yùn)行時(shí)數(shù)據(jù)區(qū)): 這就是我們常說(shuō)的JVM的內(nèi)存洒缀。
- 程序計(jì)數(shù)器(Program Counter Register): 一塊較小的內(nèi)存空間瑰谜,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。
- Java虛擬機(jī)棧(Java Virtual Machine Stacks): 是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame[1])用于存儲(chǔ)局部變量表树绩、操作數(shù)棧萨脑、動(dòng)態(tài)鏈接、方法出口等信息饺饭。
- 本地方法棧(Native Method Stack): 與虛擬機(jī)棧所發(fā)揮的作用是非常相似的渤早,為虛擬機(jī)使用到的Native方法服務(wù)。
- Java堆(Java Heap): 是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊瘫俊,唯一目的就是存放對(duì)象實(shí)例鹊杖,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存悴灵。
- 方法區(qū)(Method Area): 用于存儲(chǔ)已被虛擬機(jī)加載的類(lèi)信息、常量骂蓖、靜態(tài)變量积瞒、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。
- 運(yùn)行時(shí)常量池(Runtime Constant Pool): 是方法區(qū)的一部分涯竟,Class文件中除了有類(lèi)的版本赡鲜、字段空厌、方法庐船、接口等描述信息外,還有一項(xiàng)信息是常量池(Constant Pool Table)嘲更,用于存放編譯期生成的各種字面量和符號(hào)引用筐钟,這部分內(nèi)容將在類(lèi)加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。
- 直接內(nèi)存(Direct Memory): 并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分赋朦,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域篓冲。JDK 1.4中新加入了NIO(New Input/Output)類(lèi),引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方式宠哄,它可以使用Native函數(shù)庫(kù)直接分配堆外內(nèi)存壹将,然后通過(guò)一個(gè)存儲(chǔ)在Java堆中的DirectByteBuffer對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。
Heap和Method Area是被所有線程的共享使用的毛嫉;而Java stack, Program counter 和Native method stack是以線程為粒度的诽俯,每個(gè)線程獨(dú)自擁有自己的部分。
對(duì)象探秘
創(chuàng)建
下面是對(duì)象創(chuàng)建的主要流程:
虛擬機(jī)遇到一條new指令時(shí)承粤,先檢查常量池是否已經(jīng)加載相應(yīng)的類(lèi)暴区,如果沒(méi)有,必須先執(zhí)行相應(yīng)的類(lèi)加載辛臊。類(lèi)加載通過(guò)后仙粱,接下來(lái)分配內(nèi)存。若Java堆中內(nèi)存是絕對(duì)規(guī)整的彻舰,使用“指針碰撞“方式分配內(nèi)存伐割;如果不是規(guī)整的,就從空閑列表中分配刃唤,叫做”空閑列表“方式隔心。劃分內(nèi)存時(shí)還需要考慮一個(gè)問(wèn)題-并發(fā),也有兩種方式: CAS同步處理透揣,或者本地線程分配緩沖(Thread Local Allocation Buffer, TLAB)济炎。然后內(nèi)存空間初始化操作,接著是做一些必要的對(duì)象設(shè)置(元信息辐真、哈希嗎...)须尚,最后執(zhí)行<init>方法崖堤。
內(nèi)存布局
一個(gè)Java對(duì)象在內(nèi)存中包括對(duì)象頭、實(shí)例數(shù)據(jù)和補(bǔ)齊填充3個(gè)部分:
對(duì)象頭:
- Mark Word:包含一系列的標(biāo)記位耐床,比如輕量級(jí)鎖的標(biāo)記位密幔,偏向鎖標(biāo)記位等等。在32位系統(tǒng)占4字節(jié)撩轰,在64位系統(tǒng)中占8字節(jié)胯甩;
- Class Pointer:用來(lái)指向?qū)ο髮?duì)應(yīng)的Class對(duì)象(其對(duì)應(yīng)的元數(shù)據(jù)對(duì)象)的內(nèi)存地址。在32位系統(tǒng)占4字節(jié)堪嫂,在64位系統(tǒng)中占8字節(jié)偎箫;
- Length:如果是數(shù)組對(duì)象,還有一個(gè)保存數(shù)組長(zhǎng)度的空間皆串,占4個(gè)字節(jié)淹办;
對(duì)象實(shí)際數(shù)據(jù):
對(duì)象實(shí)際數(shù)據(jù)包括了對(duì)象的所有成員變量,其大小由各個(gè)成員變量的大小決定恶复,比如:byte和boolean是1個(gè)字節(jié)怜森,short和char是2個(gè)字節(jié),int和float是4個(gè)字節(jié)谤牡,long和double是8個(gè)字節(jié)副硅,reference是4個(gè)字節(jié)(64位系統(tǒng)中是8個(gè)字節(jié))。
對(duì)齊填充:
Java對(duì)象占用空間是8字節(jié)對(duì)齊的翅萤,即所有Java對(duì)象占用bytes數(shù)必須是8的倍數(shù)恐疲。例如,一個(gè)包含兩個(gè)屬性的對(duì)象:int和byte断序,這個(gè)對(duì)象需要占用8+4+1=13個(gè)字節(jié)流纹,這時(shí)就需要加上大小為3字節(jié)的padding進(jìn)行8字節(jié)對(duì)齊,最終占用大小為16個(gè)字節(jié)违诗。
訪問(wèn)定位
目前主流的訪問(wèn)方式有通過(guò)句柄和直接指針兩種方式漱凝。
這兩種訪問(wèn)方式各有利弊,使用句柄訪最大的好處是reference中存儲(chǔ)著穩(wěn)定的句柄地址诸迟,當(dāng)對(duì)象移動(dòng)之后(垃圾收集時(shí)移動(dòng)對(duì)象是非常普遍的行為)茸炒,只需要改變句柄中的對(duì)象實(shí)例地址即可,reference不用修改阵苇。
使用指針訪問(wèn)的好處是訪問(wèn)速度快壁公,它減少了一次指針定位的時(shí)間開(kāi)銷(xiāo),由于java是面向?qū)ο蟮恼Z(yǔ)言绅项,在開(kāi)發(fā)中java對(duì)象的訪問(wèn)非常的頻繁紊册,因此這類(lèi)開(kāi)銷(xiāo)積少成多也是非常可觀的快耿,反之則提升訪問(wèn)速度囊陡。對(duì)于HotSpot虛擬機(jī)來(lái)說(shuō)芳绩,使用的就是直接指針訪問(wèn)的方式。
實(shí)戰(zhàn): OutOfMemoryError異常
Java 堆溢出
Java堆用來(lái)存儲(chǔ)對(duì)象撞反,因此只要不斷創(chuàng)建對(duì)象妥色,并保證 GC Roots 到對(duì)象之間有可達(dá)路徑來(lái)避免垃圾回收機(jī)制清楚這些對(duì)象,那么當(dāng)對(duì)象數(shù)量達(dá)到最大堆容量時(shí)就會(huì)產(chǎn)生 OOM遏片。
/**
* java堆內(nèi)存溢出測(cè)試
* 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_pid7164.hprof …
Heap dump file created [27880921 bytes in 0.193 secs]
Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2245)
at java.util.Arrays.copyOf(Arrays.java:2219)
at java.util.ArrayList.grow(ArrayList.java:242)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208)
at java.util.ArrayList.add(ArrayList.java:440)
at com.jvm.oom.HeapOOM.main(HeapOOM.java:17)
堆內(nèi)存 OOM 是經(jīng)常會(huì)出現(xiàn)的問(wèn)題嘹害,異常信息會(huì)進(jìn)一步提示 Java heap space
虛擬機(jī)棧和本地方法棧溢出
在 HotSpot 虛擬機(jī)中不區(qū)分虛擬機(jī)棧和本地方法棧,棧容量只由 -Xss 參數(shù)設(shè)定吮便。關(guān)于虛擬機(jī)棧和本地方法棧笔呀,在 Java 虛擬機(jī)規(guī)范中描述了兩種異常:
- 如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的最大深度,將拋出 StackOverflowError 異常线衫。
- 如果虛擬機(jī)在擴(kuò)展棧時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存空間凿可,則拋出 OutOfMemoryError 異常。
/**
* 虛擬機(jī)棧和本地方法棧內(nèi)存溢出測(cè)試授账,拋出stackoverflow exception
* VM ARGS: -Xss128k 減少棧內(nèi)存容量
*/
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 = 11420
Exception in thread “main” java.lang.StackOverflowError
at com.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
at com.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
at com.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
以上代碼在單線程環(huán)境下,無(wú)論是由于棧幀太大還是虛擬機(jī)棧容量太小惨驶,當(dāng)內(nèi)存無(wú)法分配時(shí)白热,拋出的都是 StackOverflowError 異常。
如果測(cè)試環(huán)境是多線程環(huán)境粗卜,通過(guò)不斷建立線程的方式可以產(chǎn)生內(nèi)存溢出異常屋确,代碼如下所示。但是這樣產(chǎn)生的 OOM 與椥樱空間是否足夠大不存在任何聯(lián)系攻臀,在這種情況下,為每個(gè)線程的棧分配的內(nèi)存足夠大纱昧,反而越容易產(chǎn)生OOM 異常刨啸。這點(diǎn)不難理解,每個(gè)線程分配到的棧容量越大识脆,可以建立的線程數(shù)就變少设联,建立多線程時(shí)就越容易把剩下的內(nèi)存耗盡。這點(diǎn)在開(kāi)發(fā)多線程的應(yīng)用時(shí)要特別注意灼捂。如果建立過(guò)多線程導(dǎo)致內(nèi)存溢出离例,在不能減少線程數(shù)或更換64位虛擬機(jī)的情況下,只能通過(guò)減少最大堆和減少棧容量來(lái)?yè)Q取更多的線程悉稠。
/**
* JVM 虛擬機(jī)棧內(nèi)存溢出測(cè)試, 注意在windows平臺(tái)運(yùn)行時(shí)可能會(huì)導(dǎo)致操作系統(tǒng)假死
* VM Args: -Xss2M -XX:+HeapDumpOnOutOfMemoryError
*/
public class JVMStackOOM {
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) {
JVMStackOOM oom = new JVMStackOOM();
oom.stackLeakByThread();
}
}
方法區(qū)和運(yùn)行時(shí)常量池溢出
方法區(qū)用于存放Class的相關(guān)信息宫蛆,對(duì)這個(gè)區(qū)域的測(cè)試,基本思路是運(yùn)行時(shí)產(chǎn)生大量的類(lèi)去填滿方法區(qū)的猛,直到溢出耀盗。使用CGLib實(shí)現(xiàn)辑甜。
方法區(qū)溢出也是一種常見(jiàn)的內(nèi)存溢出異常,在經(jīng)常生成大量Class的應(yīng)用中袍冷,需要特別注意類(lèi)的回收情況磷醋,這類(lèi)場(chǎng)景除了使用了CGLib字節(jié)碼增強(qiáng)和動(dòng)態(tài)語(yǔ)言外,常見(jiàn)的還有JSP文件的應(yīng)用(JSP第一次運(yùn)行時(shí)要編譯為Java類(lèi))胡诗、基于OSGI的應(yīng)用等邓线。
/**
* 測(cè)試JVM方法區(qū)內(nèi)存溢出
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class MethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
static class OOMObject{}
}
本機(jī)直接內(nèi)存溢出
DirectMemory 容量可通過(guò) -XX:MaxDirectMemorySize 指定,如不指定煌恢,則默認(rèn)與Java堆最大值一樣骇陈。測(cè)試代碼使用了 Unsafe 實(shí)例進(jìn)行內(nèi)存分配。
由 DirectMemory 導(dǎo)致的內(nèi)存溢出瑰抵,一個(gè)明顯的特征是在Heap Dump 文件中不會(huì)看見(jiàn)明顯的異常你雌,如果發(fā)現(xiàn) OOM 之后 Dump 文件很小,而程序直接或間接使用了NIO二汛,那就可以考慮檢查一下是不是這方面的原因婿崭。
/**
* 測(cè)試本地直接內(nèi)存溢出
* VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
* @author linli.cro
*/
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);
}
}
}
課后作業(yè)
經(jīng)典面試問(wèn)題: JVM內(nèi)存區(qū)域劃分為哪些區(qū)域,以及哪些區(qū)域可能發(fā)生OutOfMemoryError肴颊?
參考
- 《深入理解Java虛擬機(jī): JVM高級(jí)特性與最佳實(shí)踐》