參考資料:《深入了解Java虛拟機:JVM進階特性與最佳實踐》 周志明 著
因為有虛拟機的自動記憶體管理機制,Java程式員不再需要為每個new操作去寫配對的delete/free代碼(相對C/C++程式員),而且不容易出現記憶體洩漏和記憶體溢出問題。
本文就Java虛拟機(JVM,下文以JVM代替)記憶體的各個區域,做一個詳細的介紹。
運作時資料區域
JVM在執行Java程式的過程中會把它所管理的記憶體劃分為若幹個不同的資料區域。根據《Java虛拟機規範(第二版)》的規定,JVM所管理的記憶體将會包括以下幾個運作時資料區域,如下圖1:程式計數器
程式計數器(Program Counter Register):一塊較小的記憶體空間。
- 作用:目前線程所執行的位元組碼的行号訓示器。在虛拟機的概念模型裡,位元組碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,包括分支、循環、跳轉、異常處理和線程回複等基礎功能。
- 類型:線程私有。由于JVM的多線程是通過線程輪流切換并配置設定處理器執行時間的方式來實作的,在任何一個确定的時刻,一個處理器隻會執行一條線程的指令。為確定線程切換後能恢複到正确的執行位置,每條線程必須有獨立的程式計數器。
- 存儲内容:
若線程執行的是Java方法,程式計數器記錄的是正在執行的虛拟機位元組碼指令的位址;
若線程執行的是Native(本地方法),程式計數器值為空(Undefined)。
- 異常情況:不會出現異常(唯一一個在JVM規範中沒有規定任何OutOfMemoryError情況的記憶體區域)。
Java虛拟機棧
- 作用:描述Java方法執行的記憶體模型:每個方法被執行時都會建立一個棧幀用于存儲局部變量表、操作棧、動态連結、方法出口等資訊。每一個方法被調用一直到執行完成的過程,對應一個棧幀在Java虛拟機棧中從入棧到出棧的過程。
- 類型:線程私有;生命周期與線程相同;可擴充。
- 存儲内容:方法被調用時建立的棧幀。
- 異常情況:兩種。
StackOverflowError異常:線程請求棧深度大于JVM所允許的深度時,抛出該異常;
OutOfMemoryError異常:虛拟機棧擴充到無法申請到足夠記憶體時,抛出該異常。
本地方法棧
- 作用:類似于虛拟機棧。為JVM使用到的Native方法服務。
- 類型:線程私有。
- 存儲内容:棧幀。
- 異常情況:與Java虛拟機棧相同。
- 補充:虛拟機規範中對本地方法棧中的方法使用的詞語、使用方式、資料結構沒有強制規定,具體的JVM可以自由實作它。有些JVM把虛拟機棧和本地方法棧合二為一。
Java堆
Java堆(Java Heap):JVM所管理的記憶體中最大的一塊。JVM啟動時建立。
- 作用:
1.存放對象執行個體;
2.垃圾收集器(GC)管理的主要區域,别名“GC堆”;
3.從記憶體回收角度,Java堆可細分為:新生代和老年代;
更細可分為:Eden空間、From Survivor空間、To Survivor空間;
4.從記憶體配置設定角度,可劃分出多個線程私有的配置設定緩沖區(Thread Local Allocation Buffer,TLAB)。
注意:随着JIT編譯器的發展與逃逸分析技術的逐漸成熟,所有的對象都在配置設定在堆上不是那麼絕對了,有些對象執行個體可以配置設定在棧上。
- 類型:線程共享;java堆可以處于實體上不連續的記憶體空間中,邏輯連續即可;可擴充。
- 存儲内容:對象執行個體。
- 異常情況:堆中沒有記憶體完成執行個體配置設定,且無法擴充時,抛出OutOfMemoryError異常。
方法區
- 作用:用于存儲已被虛拟機加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料。
- 注意:Java虛拟機規範把方法區描述為堆的一個邏輯部分,但它卻非堆。
- 類型:線程共享;記憶體在實體上可不連續;可擴充;可選擇不實作垃圾收集。
- 存儲内容:
方法區:被虛拟機加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料;運作時常量池(具有動态性)。
運作時常量池:存放Class檔案中的常量池、翻譯出來的直接引用;
Class檔案:存放有類的版本、字段、方法、接口等描述資訊、常量池;
Class檔案中常量池:存放編譯期生成的各種字面量和符号引用。
- 異常情況:
方法區無法滿足記憶體配置設定需求時,抛出OutOfMemoryError異常;
運作時常量池無法申請到記憶體時,抛出OutOfMemoryError異常。
直接記憶體
1.不是JVM運作時資料區的一部分,也不是Java虛拟機規範中定義的記憶體區域。但也是被頻繁使用的記憶體部分。
2.在JDK1.4中新加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖去(Buffer)的I/O方式,它可以使用Native函數庫直接配置設定堆外記憶體,然後通過一個存儲在Java堆裡面的DirectByteBuffer對象作為這塊記憶體的引用操作。避免了在Java堆和Native堆中來回複制資料,在一些場景中可以顯著提高性能。
3.本機直接記憶體的配置設定不受Java堆大小的限制,受本機總記憶體的大小和處理器尋址空間的限制。
4.由于經常忽略直接記憶體,使得各個記憶體區域的總和大于實體記憶體限制,導緻動态擴充時出現OutOfMemoryError異常。
對象通路
對象通路涉及Java棧、Java堆、方法區3個重要記憶體區域聯系。以下面代碼來分析。
Object obj = new Object();
假設這句代碼出現在方法體中,
- “Object obj”這部分語義将反映到Java棧的本地變量表中,作為一個reference類型資料出現;
- “new Object()”這部分語義将會反映到Java堆中,形成一塊存儲了Object類型所有執行個體資料值的結構化記憶體,根據具體類型以及虛拟機實作的對象記憶體布局的不同,這塊記憶體長度不固定。另外,在Java堆中還必須包含能查找到此對象類型資料(對象類型、父類、實作的接口、方法等)的位址資訊,這些類型資料存儲在方法區中。
reference類型,主流的通路方式有2種:使用句柄、直接指針。
- 使用句柄通路方式:Java堆中将會劃分出一塊記憶體來作為句柄池,reference中存儲的是對象的句柄位址,而句柄中包含了對象執行個體資料和類型資料各自的具體位址資訊。如下圖:
- 使用直接指針通路方式:Java堆對象的布局中就必須考慮如何放置通路類型資料的相關資訊,reference中直接存儲的就是對象位址。如下圖:
使用句柄通路方式的最大好處:reference中存儲的是穩定的句柄位址,在對象被移動(垃圾收集是移動對象是非常普遍的行為)時隻會改變句柄中的執行個體資料指針,而reference本身不需要被修改。
使用直接指針通路方式的最大好處:速度更快,節省了一次指針定位的時間開銷,由于對象的通路在Java中非常頻繁,是以這類開銷積少成多後也是一項非常可觀的執行成本。(Sun HotSpot使用的是直接指針通路方式)