如果把內(nèi)存比作蛋糕,那么堆、棧不過是其中的一小塊。
Java內(nèi)存區(qū)域
運(yùn)行時(shí)數(shù)據(jù)區(qū)域
線程共享數(shù)據(jù)區(qū):方法區(qū)、堆
線程私有數(shù)據(jù)區(qū):虛擬機(jī)棧、本地方法棧、程序計(jì)數(shù)器

程序計(jì)數(shù)器
主要記錄當(dāng)前線程正在執(zhí)行的虛擬機(jī)字節(jié)碼指令。因?yàn)槌绦蛴?jì)數(shù)器是線程私有的數(shù)據(jù)區(qū),所以在多線程切換時(shí),也不會造成指令錯亂。
另外,這也是唯一一個Java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。
虛擬機(jī)棧
虛擬機(jī)棧的生命周期和線程一樣,它描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的同時(shí),都會創(chuàng)建出一個棧幀(Stack Frame)用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。每一個方法從調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機(jī)棧中入棧到出棧的過程。
通常所說的堆棧中的“棧”,指的就是這個虛擬機(jī)棧,更恰當(dāng)?shù)恼f,應(yīng)該是虛擬機(jī)棧中的局部變量表。
局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,它不等同于對象本身,可能是一個指向?qū)ο笃鹗嫉刂返囊弥羔槪部赡苁侵赶蛞粋€代表對象的句柄或其它與此對象相關(guān)的位置)和returnAddress類型(指向了一條字節(jié)碼指令的地址)。
注意!注意!注意!局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進(jìn)入一個方法時(shí),這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運(yùn)行期間不會改變局部變量表的大小。
兩種異常:
- StackOverflowError
如果線程請求的棧深度大于虛擬機(jī)鎖允許的深度,將拋出該異常 - OutOfMemoryError
如果虛擬機(jī)可以動態(tài)擴(kuò)展,當(dāng)擴(kuò)展時(shí)無法申請到足夠的內(nèi)存,就拋出該異常。
本地方法棧(Native Method Stack)
它和虛擬機(jī)棧的區(qū)別是:虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則為虛擬機(jī)使用到的Native方法服務(wù)。
虛擬機(jī)規(guī)范沒有限制本地方法棧做特殊使用的語言、方法、數(shù)據(jù)結(jié)構(gòu)等,所以不同的虛擬機(jī)可以有不同的實(shí)現(xiàn),甚至如HotSpot虛擬機(jī),將本地方法棧和虛擬機(jī)棧合二為一。
Java堆
堆是Java虛擬機(jī)管理的最大的一塊內(nèi)存。
所有實(shí)例對象以及數(shù)組都要在堆上分配。
堆也是垃圾收集器管理的主要區(qū)域(GC堆,Garbage Collected Heap)。
從內(nèi)存回收角度來看,由于現(xiàn)在的收集器基本都采用分代收集算法,所以Java堆中還可以分為:新生代、老年代。再細(xì)致一點(diǎn),可分為Eden空間、From Survivor空間、To Survivor空間等。
從內(nèi)存分配角度看,線程共享的Java堆中可能劃分出多個線程私有的份額皮緩沖區(qū)(Thread Local Allocation Buffer, TLAB)。
方法區(qū) Method Area
用于存儲已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。
運(yùn)行時(shí)常量池 Runtime Constant Pool
運(yùn)行時(shí)常量池是方法區(qū)的一部分,Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息時(shí)常量池,用于存放編譯期產(chǎn)生的各種字面量和符號引用,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。
直接內(nèi)存 Direct Memory
它不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域。但是會經(jīng)常使用到。
在JDK1.4中新加入的NIO類,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方式,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個存在Java堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場景中顯著提高性能,因?yàn)楸苊饬嗽贘ava堆和Native堆中來回復(fù)制區(qū)域。
HotSpot虛擬機(jī)
對象創(chuàng)建
- Java中的
new關(guān)鍵字,對于虛擬機(jī)來說是一條new指令,當(dāng)虛擬機(jī)接收到這條指令后,首先去檢查這個指令的參數(shù)是否能在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已經(jīng)被加載、解析和初始化過,如果沒有,那必須先執(zhí)行相應(yīng)的類加載過。 - 類加載完成后,虛擬機(jī)將為新生對象分配內(nèi)存(虛擬機(jī)如何保證操作原子性?)。對象所需內(nèi)存的 大小在類加載的時(shí)候是完成可確定的,為對象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來。常用的內(nèi)存分配方式有“指針碰撞Bump the Pointer、空閑列表Free List”。另外,選擇哪種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。
3.分配完成后,虛擬機(jī)會自動分配零值(不包括對象頭),以保證對象的實(shí)例字段在Java代碼中可以不賦值就可以直接使用。
4.虛擬機(jī)設(shè)置對象頭。
對象的內(nèi)存布局
在HotSpot虛擬機(jī)中,對象在內(nèi)存中存儲的布局可以分為3塊區(qū)域:對象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)和對齊填充(Padding)。
對象頭分為兩部分:
- 第一部分用于存儲對象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希嗎(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時(shí)間戳等。
- 另一分部是類型指針,即對象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個指針來確定這個對象是哪個類的實(shí)例。并不是所有的虛擬機(jī)實(shí)現(xiàn)都必須在對象數(shù)據(jù)上保留類型指針
實(shí)例數(shù)據(jù):
對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內(nèi)容。
對齊填充:
占位符的左右。
由于HotSpot VM的自動內(nèi)存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數(shù)倍,換句話說,就是對象的大小必須是8字節(jié)的整數(shù)倍。而對象頭部分正好是8字節(jié)的倍數(shù)(1倍或2倍),因此,當(dāng)對象實(shí)例數(shù)據(jù)部分沒有對齊時(shí),就需要通過對齊填充來補(bǔ)全。
對象的訪問定位
-
通過句柄訪問對象
如果使用句柄訪問對象,nameJava堆中將會劃分出一塊內(nèi)存來作為句柄池,reference引用中存儲的就是對象的句柄地址,而句柄中包含了對象實(shí)例數(shù)據(jù)和類型數(shù)據(jù)各自的具體地址信息。
通過句柄訪問對象 -
通過直接指針訪問對象
如果使用直接指針訪問,那么Java堆對象的布局就必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息,而reference中存儲的直接就是對象地址。
通過直接指針訪問對象
Java 溢出異常
OutOfMemoryrror異常
堆溢出
Java堆用于存儲對象實(shí)例,只要不斷地創(chuàng)建對象,并且保證GC Roots到對象之間有可達(dá)路徑來避免垃圾回收機(jī)制清楚這些對象,那么在對象數(shù)量到達(dá)最大對的容量限制后,就會產(chǎn)生內(nèi)存溢出異常。
/**
* -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/error
*/
public class Main {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
}
其中:-Xms20m -Xmx20m用來控制堆大小為20m,-設(shè)置XX:+HeapDumpOnOutOfMemoryError是為了在內(nèi)存溢出的時(shí)候,保存堆溢出的文件,后期可以通過工具分析該文件,找出具體的原因是什么。-XX:HeapDumpPath=/home/error是為了指定堆溢出時(shí),文件存儲的位置,如果不指定,默認(rèn)存儲到了項(xiàng)目根目錄下(我電腦上是這樣)。
錯誤日志:
Connected to the target VM, address: '127.0.0.1:14065', transport: 'socket'
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid20124.hprof ...
Disconnected from the target VM, address: '127.0.0.1:14065', transport: 'socket'
Heap dump file created [28058107 bytes in 0.138 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at com.Main.main(Main.java:21)
棧溢出
對于HotSpot虛擬機(jī)來說,棧容量只由-Xss參數(shù)設(shè)定。
/**
* -Xss128k
*/
public class JavaVMStackSOF {
private int stackLenth = 1;
public void stackLeak() {
stackLenth ++;
stackLeak();
}
public static void main(String[] args) {
JavaVMStackSOF stackSOF = new JavaVMStackSOF();
try {
stackSOF.stackLeak();
} catch (Throwable e) {
e.printStackTrace();
throw e;
}
}
}
在這種單線程下,無論是由于棧幀太大還是虛擬機(jī)棧容量太小,當(dāng)內(nèi)存無法分配的時(shí)候,虛擬機(jī)拋出的都是StackOverflowError異常。
另外,如果測試不限于單線程,通過不斷創(chuàng)建線程的方式可以產(chǎn)生內(nèi)存溢出異常,原因是:操作系統(tǒng)分配給每個進(jìn)程的內(nèi)存是有限的,譬如32位Windows限制為2GB。虛擬機(jī)提供了參數(shù)來控制Java堆和方法區(qū)的這兩部分內(nèi)存的最大值。剩余的內(nèi)存為2GB(操作系統(tǒng)限制)-Xmx(最大堆容量)-MaxPermSize(最大方法區(qū)容量)-程序計(jì)數(shù)器(實(shí)際可忽略其內(nèi)存大?。?。如果虛擬機(jī)進(jìn)程本身消耗的內(nèi)存不計(jì)算在內(nèi),剩下 的內(nèi)存就由虛擬機(jī)棧和本地方法棧瓜分了,每個線程分配到的棧容量越大,可以建立的線程數(shù)量自然就越少,建立線程的時(shí)候就越容易把剩下的內(nèi)存耗盡。
錯誤日志:
Connected to the target VM, address: '127.0.0.1:8169', transport: 'socket'
java.lang.StackOverflowError
at com.memory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:11)
at com.memory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
at com.memory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
at com.memory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
at com.memory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
方法區(qū)和運(yùn)行時(shí)常量池溢出
/**
* 該代碼是在jdk1.6上跑的,會出現(xiàn)OOM,但是在jdk1.7或以上版本,while循環(huán)會一直下去
* -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class RuntimeConstantPoolOOM{
public static void main(String[] args) {
List<String> list = Lists.newArrayList();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
錯誤日志:
Connected to the target VM, address: '127.0.0.1:8336', transport: 'socket'
Disconnected from the target VM, address: '127.0.0.1:8336', transport: 'socket'
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
方法區(qū)溢出,因?yàn)榉椒▍^(qū)存放的是Class相關(guān)信息,如類名、訪問修飾符、常量池、字段描述、方法描述等。咱們可以通過不斷的創(chuàng)建類對象,撐爆方法區(qū)。
/**
* -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class JavaMethodAreaOOM{
static class OOMObject {}
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 o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, objects);
}
});
enhancer.create();
}
}
}
Connected to the target VM, address: '127.0.0.1:8417', transport: 'socket'
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
Disconnected from the target VM, address: '127.0.0.1:8417', transport: 'socket'
注釋:
虛擬機(jī)如何保證操作原子性:虛擬機(jī)采用CAS+失敗重試的方式保證更新操作的原子性。
指針碰撞:假設(shè)Java堆中內(nèi)存是絕對規(guī)整的,所有用過的內(nèi)存都放在一邊,空閑的內(nèi)存放在另一邊,中間放著一個指針作為分界點(diǎn)的指示器。那所分配內(nèi)存就僅僅把那個指針向空閑空間那邊挪動一段與對象大小相等的距離。
空閑列表:如果Java堆中的內(nèi)存不是規(guī)整的,已使用的內(nèi)存和空閑的內(nèi)存相互交錯,那就沒有辦法簡單的進(jìn)行指針碰撞了,虛擬機(jī)就必須維護(hù)一個列表,記錄哪些內(nèi)存是可用的,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對象實(shí)例,并更新列表上的記錄。

