對於C++程序員,內存分配與回收的處理一直是使人頭疼的問題。Java因爲自身的自動內存管理機制,使得管理內存變得很是輕鬆,不容易出現內存泄漏,溢出的問題。java
不容易不表明不會出現問題,一旦內存泄漏或溢出的狀況發生,調試起來會變得很是困難。這就要求咱們對虛擬機的內存區域有深刻的理解。最終可以判斷內存方面的異常發生時,具體在JVM中的位置。程序員
JVM運行時,首先須要類加載器(ClassLoader) 加載所需類的字節碼,加載完畢交由執行引擎執行,執行過程當中須要一段空間來存儲數據(類比CPU與主存)。這段內存空間的分配和釋放過程正是咱們所關心的,稱爲運行時數據區。多線程
對於CS相關從業者,深刻理解操做系統的內存的層次結構,分配與垃圾收集過程都是大有裨益的。同理,欲定位內存問題的出現區域,必須剖析運行時數據區。併發
如上圖所示,運行時數據區包括:程序計數器(即PC寄存器),Java 虛擬機棧(VM Stack),Java 堆(Heap),方法區(Method Area),本地方法棧(Native Method Stack)。下面帶領你們深刻理解各個數據區域。學習
JVM實際上就是一臺虛擬的計算機,目的是爲了實現"一次編譯,到處執行"。因此,在理解運行時數據區時,徹底能夠與操做系統系統 內存,寄存器類比學習。操作系統
每條虛擬機中的線程都有本身的寄存器,稱之爲程序計數器(PC)。爲了保證線程之間的獨立性,於是PC內的空間是線程私有的。線程
虛擬機中的多線程經過線程輪轉調度,爲每條線程分配時間片來實現併發執行。同一時刻,處理機只能執行一條線程。當切換到另一條線程時,若不保存當前未執行完線程的執行位置,下次處理機再執行這條線程時,又要從新開始執行。這種狀況顯然是不能容忍的。調試
引入程序計數器的目的,就是爲了記錄線程的執行狀況,便於下次切換後進行線程恢復。code
如何記錄線程的執行狀況? 其實也並不複雜,只須要記錄正在執行的虛擬機字節碼指令的地址。若是運行的是Native(本地)方法,計數器的值爲Undefined。對象
程序計數器是惟一沒有OutOfMemoryError異常的區域。
每一個Java方法執行時,須要分配內存空間來存儲局部變量表,操做數棧,動態連接,方法出口等信息。將這部份內存稱之爲棧幀(Stack Frame)。虛擬機棧用於存儲棧幀,是Java方法執行的內存模型。
顯然咱們須要爲每一個執行的方法分配棧空間,所以Java虛擬機棧也是線程私有的。
虛擬機棧記錄Java方法執行的過程。每一個方法開始執行時,爲之建立一個棧幀記錄信息;方法執行到完成的過程,對應棧幀在虛擬機棧中入棧到出棧的過程。
局部變量表是棧幀中的重要部分。存放編譯期定義的基本數據類型, 對象引用(至關於對象地址),及returnAddress類型(字節碼指令地址)。
局部變量表空間在編譯期間分配,執行方法的過程當中不會改變其大小。
本地方法棧與虛擬機棧相似,區別是虛擬機棧記錄執行的Java方法,本地方法棧則記錄Native方法。
本地方法棧一樣會拋出StackOverflowError與OutOfMemoryError異常。
Java堆用於存儲對象實例,爲全部對象分配內存空間。
全部對象實例都要在堆上分配空間,所以Java堆是全部線程的共享區域。對象的生命週期結束後,Java堆還要負責內存回收,所以Java堆也常被稱之爲GC堆(Garbage Collected Heap)。
從內存回收的角度,Java堆能夠分爲新生代(Young Generation)與老生代(Old Generation)。這種劃分的方式,是爲了更好的回收內存(老生代內存會被優先回收)。
如圖,新生代還能夠分爲Eden空間、From Survivor空間、To Survivor空間。
永久代(Permanent Generation)用於存儲靜態類型數據,與垃圾收集器關係不大。
注意:本圖展現的是JVM堆的內存模型,JVM堆內存包括Java堆區域 和 永久代區域。所以,永久代不屬於Java堆。
Java堆一樣可擴展(-Xmx與-Xms參數)。若堆中內存已沒法爲對象實例分配且沒法再擴展,拋出OutOfMemoryError異常。
方法區存儲類信息、常量、靜態變量等數據,是線程共享的區域。爲與Java堆區分,方法區還有一個別名Non-Heap(非堆)。
方法區就是永久代?並不是如此。
HotSpot虛擬機選擇用永久代來實現方法區,從而省去了爲方法區編寫內存管理代碼的工做。這只是一種實現方式,其餘虛擬機(BEA JRockit,IBM J9)都不存在永久代這一律念。
經過永久代來實現方法區容易形成內存溢出,將來也可能會被替代。
在虛擬機規範中,方法區的實現沒有明確的規定,所以不能將方法區等同於永久代。
當方法區沒法知足內存分配的須要時,拋出OutOfMemoryError異常。
運行時常量池(Runtime Constant Pool)用於存放編譯期生成的各類字面量和符號引用。
運行時常量池具有動態性,使得運行期間也可將新的常量放入池中。例如String類的intern() 方法。
package intern; public class Main1 { public static void main(String[] args) { String s0= "I'm coding"; String s1=new String("I'm coding"); String s2=new String("I'm coding"); System.out.println( s0==s1 ); System.out.println( s0==s1.intern()); s2=s2.intern(); System.out.println( s0==s2 ); } }
輸出結果
false true true
本例中,s0直接保存在常量池,s1與s2的對象實例存儲在Java堆中。==直接比較對象的hashCode,所以第一行輸出false。s1.intern()方法返回s1在常量池中的引用,沒有則建立。
s1存放的字符串已經在常量池中存在,直接返回s0的引用,第二行輸出true。
同理,s2接收了s2.intern()的返回值,字符串值與s0相同,第三行輸出true。
運行時常量池是方法區的一部分,所以受方法區內存的限制。當沒法申請到內存時,拋出OutOfMemoryError異常。
對於JVM的內存管理, 最重要的仍是與OS內存管理知識進行類比以及結合實踐來學習。理解JVM內存區域的目的也是爲了在工程中出現內存相關異常時可以準確的定位所在區域,及時處理。
後續咱們將在本文的基礎上來理解對象的建立過程以及OutOfMemoryError異常。
做者: I'm coding
連接:ACFLOOD
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
若是您以爲本文對您有所幫助,就給俺點個贊吧!