如果把內(nèi)存比作蛋糕,那么堆、棧不過是其中的一小塊沸毁。
Java內(nèi)存區(qū)域
運行時數(shù)據(jù)區(qū)域
線程共享數(shù)據(jù)區(qū):方法區(qū)、堆
線程私有數(shù)據(jù)區(qū):虛擬機棧傻寂、本地方法棧息尺、程序計數(shù)器
程序計數(shù)器
主要記錄當(dāng)前線程正在執(zhí)行的虛擬機字節(jié)碼指令。因為程序計數(shù)器是線程私有的數(shù)據(jù)區(qū)疾掰,所以在多線程切換時搂誉,也不會造成指令錯亂。
另外静檬,這也是唯一一個Java虛擬機規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域炭懊。
虛擬機棧
虛擬機棧的生命周期和線程一樣,它描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的同時拂檩,都會創(chuàng)建出一個棧幀(Stack Frame)用于存儲局部變量表侮腹、操作數(shù)棧、動態(tài)鏈接稻励、方法出口等信息父阻。每一個方法從調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機棧中入棧到出棧的過程。
通常所說的堆棧中的“椉用”钠署,指的就是這個虛擬機棧,更恰當(dāng)?shù)恼f荒椭,應(yīng)該是虛擬機棧中的局部變量表谐鼎。
局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean、byte趣惠、char狸棍、short、int味悄、float草戈、long、double)侍瑟、對象引用(reference類型唐片,它不等同于對象本身,可能是一個指向?qū)ο笃鹗嫉刂返囊弥羔樥茄眨部赡苁侵赶蛞粋€代表對象的句柄或其它與此對象相關(guān)的位置)和returnAddress類型(指向了一條字節(jié)碼指令的地址)费韭。
注意!注意庭瑰!注意星持!局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進入一個方法時弹灭,這個方法需要在幀中分配多大的局部變量空間是完全確定的督暂,在方法運行期間不會改變局部變量表的大小。
兩種異常:
- StackOverflowError
如果線程請求的棧深度大于虛擬機鎖允許的深度穷吮,將拋出該異常 - OutOfMemoryError
如果虛擬機可以動態(tài)擴展逻翁,當(dāng)擴展時無法申請到足夠的內(nèi)存,就拋出該異常捡鱼。
本地方法棧(Native Method Stack)
它和虛擬機棧的區(qū)別是:虛擬機棧為虛擬機執(zhí)行Java方法(也就是字節(jié)碼)服務(wù)八回,而本地方法棧則為虛擬機使用到的Native方法服務(wù)。
虛擬機規(guī)范沒有限制本地方法棧做特殊使用的語言堰汉、方法辽社、數(shù)據(jù)結(jié)構(gòu)等伟墙,所以不同的虛擬機可以有不同的實現(xiàn)翘鸭,甚至如HotSpot虛擬機,將本地方法棧和虛擬機棧合二為一戳葵。
Java堆
堆是Java虛擬機管理的最大的一塊內(nèi)存就乓。
所有實例對象以及數(shù)組都要在堆上分配。
堆也是垃圾收集器管理的主要區(qū)域(GC堆,Garbage Collected Heap)生蚁。
從內(nèi)存回收角度來看噩翠,由于現(xiàn)在的收集器基本都采用分代收集算法,所以Java堆中還可以分為:新生代邦投、老年代伤锚。再細(xì)致一點,可分為Eden空間志衣、From Survivor空間屯援、To Survivor空間等。
從內(nèi)存分配角度看念脯,線程共享的Java堆中可能劃分出多個線程私有的份額皮緩沖區(qū)(Thread Local Allocation Buffer狞洋, TLAB)。
方法區(qū) Method Area
用于存儲已被虛擬機加載的類信息绿店、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。
運行時常量池 Runtime Constant Pool
運行時常量池是方法區(qū)的一部分,Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息時常量池腕唧,用于存放編譯期產(chǎn)生的各種字面量和符號引用缺谴,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池中存放。
直接內(nèi)存 Direct Memory
它不是虛擬機運行時數(shù)據(jù)區(qū)的一部分苫纤,也不是Java虛擬機規(guī)范中定義的內(nèi)存區(qū)域。但是會經(jīng)常使用到乍赫。
在JDK1.4中新加入的NIO類改鲫,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方式稽亏,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個存在Java堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進行操作缕题。這樣能在一些場景中顯著提高性能截歉,因為避免了在Java堆和Native堆中來回復(fù)制區(qū)域。
HotSpot虛擬機
對象創(chuàng)建
- Java中的
new
關(guān)鍵字避除,對于虛擬機來說是一條new
指令怎披,當(dāng)虛擬機接收到這條指令后,首先去檢查這個指令的參數(shù)是否能在常量池中定位到一個類的符號引用瓶摆,并且檢查這個符號引用代表的類是否已經(jīng)被加載凉逛、解析和初始化過,如果沒有群井,那必須先執(zhí)行相應(yīng)的類加載過状飞。 - 類加載完成后,虛擬機將為新生對象分配內(nèi)存(虛擬機如何保證操作原子性书斜?)诬辈。對象所需內(nèi)存的 大小在類加載的時候是完成可確定的,為對象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來荐吉。常用的內(nèi)存分配方式有“指針碰撞Bump the Pointer焙糟、空閑列表Free List”。另外样屠,選擇哪種分配方式由Java堆是否規(guī)整決定穿撮,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。
3.分配完成后痪欲,虛擬機會自動分配零值(不包括對象頭)悦穿,以保證對象的實例字段在Java代碼中可以不賦值就可以直接使用。
4.虛擬機設(shè)置對象頭业踢。
對象的內(nèi)存布局
在HotSpot虛擬機中栗柒,對象在內(nèi)存中存儲的布局可以分為3塊區(qū)域:對象頭(Header)、實例數(shù)據(jù)(Instance Data)和對齊填充(Padding)知举。
對象頭分為兩部分:
- 第一部分用于存儲對象自身的運行時數(shù)據(jù)瞬沦,如哈希嗎(HashCode)、GC分代年齡雇锡、鎖狀態(tài)標(biāo)志蛙埂、線程持有的鎖、偏向線程ID遮糖、偏向時間戳等绣的。
- 另一分部是類型指針,即對象指向它的類元數(shù)據(jù)的指針欲账,虛擬機通過這個指針來確定這個對象是哪個類的實例屡江。并不是所有的虛擬機實現(xiàn)都必須在對象數(shù)據(jù)上保留類型指針
實例數(shù)據(jù):
對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內(nèi)容赛不。
對齊填充:
占位符的左右惩嘉。
由于HotSpot VM的自動內(nèi)存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數(shù)倍,換句話說踢故,就是對象的大小必須是8字節(jié)的整數(shù)倍文黎。而對象頭部分正好是8字節(jié)的倍數(shù)(1倍或2倍)惹苗,因此,當(dāng)對象實例數(shù)據(jù)部分沒有對齊時耸峭,就需要通過對齊填充來補全桩蓉。
對象的訪問定位
-
通過句柄訪問對象
如果使用句柄訪問對象,nameJava堆中將會劃分出一塊內(nèi)存來作為句柄池劳闹,reference引用中存儲的就是對象的句柄地址院究,而句柄中包含了對象實例數(shù)據(jù)和類型數(shù)據(jù)各自的具體地址信息。
通過句柄訪問對象 -
通過直接指針訪問對象
如果使用直接指針訪問本涕,那么Java堆對象的布局就必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息业汰,而reference中存儲的直接就是對象地址。
通過直接指針訪問對象
Java 溢出異常
OutOfMemoryrror異常
堆溢出
Java堆用于存儲對象實例菩颖,只要不斷地創(chuàng)建對象样漆,并且保證GC Roots到對象之間有可達路徑來避免垃圾回收機制清楚這些對象,那么在對象數(shù)量到達最大對的容量限制后晦闰,就會產(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)存溢出的時候鹅髓,保存堆溢出的文件舞竿,后期可以通過工具分析該文件,找出具體的原因是什么窿冯。-XX:HeapDumpPath=/home/error
是為了指定堆溢出時骗奖,文件存儲的位置,如果不指定醒串,默認(rèn)存儲到了項目根目錄下(我電腦上是這樣)执桌。
錯誤日志:
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虛擬機來說,棧容量只由-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;
}
}
}
在這種單線程下仰挣,無論是由于棧幀太大還是虛擬機棧容量太小,當(dāng)內(nèi)存無法分配的時候缠沈,虛擬機拋出的都是StackOverflowError異常膘壶。
另外,如果測試不限于單線程洲愤,通過不斷創(chuàng)建線程的方式可以產(chǎn)生內(nèi)存溢出異常颓芭,原因是:操作系統(tǒng)分配給每個進程的內(nèi)存是有限的,譬如32位Windows限制為2GB柬赐。虛擬機提供了參數(shù)來控制Java堆和方法區(qū)的這兩部分內(nèi)存的最大值亡问。剩余的內(nèi)存為2GB(操作系統(tǒng)限制)-Xmx(最大堆容量)-MaxPermSize(最大方法區(qū)容量)-程序計數(shù)器(實際可忽略其內(nèi)存大小)肛宋。如果虛擬機進程本身消耗的內(nèi)存不計算在內(nèi)州藕,剩下 的內(nèi)存就由虛擬機棧和本地方法棧瓜分了束世,每個線程分配到的棧容量越大,可以建立的線程數(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ū)和運行時常量池溢出
/**
* 該代碼是在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ū)溢出遇西,因為方法區(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'
注釋:
虛擬機如何保證操作原子性:虛擬機采用CAS+失敗重試的方式保證更新操作的原子性渗常。
指針碰撞:假設(shè)Java堆中內(nèi)存是絕對規(guī)整的壮不,所有用過的內(nèi)存都放在一邊,空閑的內(nèi)存放在另一邊皱碘,中間放著一個指針作為分界點的指示器询一。那所分配內(nèi)存就僅僅把那個指針向空閑空間那邊挪動一段與對象大小相等的距離。
空閑列表:如果Java堆中的內(nèi)存不是規(guī)整的癌椿,已使用的內(nèi)存和空閑的內(nèi)存相互交錯健蕊,那就沒有辦法簡單的進行指針碰撞了,虛擬機就必須維護一個列表踢俄,記錄哪些內(nèi)存是可用的缩功,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的記錄都办。