前言
C或C++開發人員,在內存管理區域,需要手動申請內存并手動釋放內存,否則將出現內存泄漏等問題。
而Java虛擬機自動管理內存,不需要人為回收。不過這也是有代價的,虛擬機回收內存時會阻塞進程,如果代碼不合理,回收內存頻繁,性能就會受到很大影響。另外,Java也是有可能出現內存泄漏的,開發人員必須理解Java的內存使用規則,才能有效應對內存泄漏等情況。
理解Java內存使用,也會明白進程間通信與線程通信的實質。
理解Java內存使用,從下邊這張圖開始
java中,一個進程對應著一個虛擬機實例,每個虛擬機實例都管理著不同的內存區域。進程中存在著多個線程,不同線程會共享進程中的部分內存區域,也會存在著線程自己的專屬內存區域。
程序計數器
程序計數器是一塊較小的內存空間,它的作用可以看做是當前線程所執行的字節碼的行號指示器。
簡單理解就是指示程序運行到哪一行了或者哪個方法了,程序中判斷、循環等實現,運行方法、退出方法等,都需要使用程序計數器正確指示。
它是線程私有的內存區域,也是虛擬機中唯一不會出現oom的區域。
Java虛擬機棧
Java虛擬機棧也是線程私有的內存區域
每個方法在運行時都會同時創建一個棧幀,用于存儲局部變量表、操作棧、動態鏈接、方法出口等消息。每一個方法被調用直至執行完成的過程,就對應著一個棧楨在虛擬機棧中從入棧到出棧的過程。
局部變量表存放了預編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型)和returnAddress類型(指向了一條字節碼指令的地址)。其中64位長度的long和double類型的數據會占用2個局部變量空間(Slot),其余的數據類型只占用1個。局部變量空間是以Slot(32位)為空間單位的。就算原本一字節大小的byte類型在局部變量表也要占一個Slot。
注意對象引用不是對象本身,對象引用根據不同虛擬的不同實現,存儲著對象的指針或名柄。
在Java虛擬機規范中,Java虛擬機棧規定了兩種異常情況:如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError異常;如果虛擬機??梢詣討B擴展,如果擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常。
如果線程棧的大小有1MB,如果當前線程大量使用了遞歸,那么當線程的棧幀總和超過1MB,JVM就會拋出StackOverflowError。另外,創建線程數量是需要分配線程棧內存的,但系統沒有內存可以分配時,就會拋出OutOfMemoryError。
private int stackLength = 1;
public void stackLeak(){
stackLength ++;
stackLeak();
}
/**
* 測試棧溢出
* 測試參數為:-verbose:gc -Xms20M -Xmx20M -Xmn10M -Xss128K -XX:+PrintGCDetails -XX:SurvivorRatio=8
*/
public static void testStackOOM(){
DemoMemory memory = new DemoMemory();
memory.stackLeak();
}
上面的代碼可以模擬StackOverflowError。
本地方法棧對應著非Java代碼,和Java虛擬機棧類似,不再詳述。
Java堆
Java堆,是虛擬機所管理的內存中最大的一塊區域,并且它是被各線程所共享的,在虛擬機啟動時創建。此區域唯一目的就是存放對象實例,所有的對象實例及數組均在堆上分配。
Java堆如果沒有足夠的內存分配,將出現OutOfMemoryError異常。
/*
* 測試堆內存溢出
* 測試參數為:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
*/
private static void testHeapOOM(){
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
Java堆是垃圾收集器管理的主要區域,如果從內存回收的角度講,現在的收集器基本采用分代收集算法,所以Java堆還可以細分為:新生代和老年代,再細致一點的有Eden空間,From Survivor空間,To Survivor空間等。
注意:有些版本的JDK已經沒有Permanent(永久代)了。
方法區
方法區,與java堆一樣,也是各線程所共享的,它存儲著已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。
運行時常量池(Runtime Constant Pool)是方法區的一部分,用于存放Class文件在編譯期生成的各種字面量和符號引用,因為Class文件除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table)。這部分內容將在類加載后進入方法區的運行時常量池中存放。同時運行時常量池具備動態性,并非預置入Class文件中常量池的內存才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,例如String類的inter()方法。既然運行時常量池是方法區的一部分,自然受到方法區內存限制,當常量池無法再申請到內存時會拋出OutOfMemoryError異常。
如下代碼可制造出內存溢出。
/**
* 測試運行時常量池內存溢出
* -verbose:gc -Xms40M -Xmx40M -Xmn20M -XX:PermSize=10M -XX:MaxPermSize=10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
*/
public static void testConstantOOM(){
List<String> list = new ArrayList<>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
調試參數
- -Xms20M:設置堆最小值為20M,也是java堆的初始化大小
- -Xmx20M:設置堆最大值為20M,注意以上參數后面必須無空格加數字,否則會報錯。
- -Xss128K:設置java棧大小為128k
- -XX:MaxPermSize:設置方法區最大值
結束語
在前言中提到,理解Java內存區域有助于發現進程通信的實質。為何進程通信這么麻煩呢?那是因為內存區域都不一樣了,進程A向進程B發送一個字符串,B怎么知道A發送的就是字符串呢?如果不是發送字符串,而是發送的任意一個自定義類的實例,B要怎么將A發送的數據轉換成正確的類實例呢?所以進程通信間的數據,都要實現序列化接口。
另外對象的訪問也是值得一提的,方法執行時會在Java虛擬機棧中生成棧楨,棧楨里存儲的只是對象的引用,對象的訪問都是通過訪問存在棧中的對象引用,去訪問存在堆中的對象實例。