Java內存區域

前言

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虛擬機棧中生成棧楨,棧楨里存儲的只是對象的引用,對象的訪問都是通過訪問存在棧中的對象引用,去訪問存在堆中的對象實例。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。