java虛拟機在執行Java程式的過程中會把它所管理的記憶體劃分為若幹不同的資料區域,這些區域都有着各自不同的用途,他們的建立和銷毀的時間也會不同,Java虛拟機所管理的記憶體将會包括以下幾個運作時資料區域,如下圖所示:
下面就每一個區域進行闡述。
二、運作時資料區域
程式計數器
程式計數器,可以看做是目前線程所執行的位元組碼的行号訓示器。在虛拟機的概念模型裡,位元組碼解釋器工作就是通過改變程式計數器的值來選擇下一條需要執行的位元組碼指令,分支、循環、跳轉、異常處理、線程恢複等基礎功能都要依賴這個計數器來完成。
多線程中,為了讓線程切換後能恢複到正确的執行位置,每條線程都需要有一個獨立的程式計數器,各條線程之間互不影響、獨立存儲,是以這塊記憶體是線程私有的。
當線程正在執行的是一個Java方法,這個計數器記錄的是在正在執行的虛拟機位元組碼指令的位址;當執行的是Native方法,這個計數器值為空。
此記憶體區域是唯一一個沒有規定任何OutOfMemoryError情況的區域。
Java虛拟機棧
Java虛拟機棧也是線程私有的,它的生命周期與線程相同。虛拟機棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀用于存儲局部變量表、操作數棧、動态連結清單、方法出口資訊等。每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛拟機棧中入棧到出棧的過程。
局部變量表中存放了編譯器可知的各種基本資料類型(boolean、byte、char、short、int、float、long、double)、對象引用和returnAddress類型(指向了一條位元組碼指令的位址)。
如果擴充時無法申請到足夠的記憶體,就會抛出OutOfMemoryError異常。
本地方法棧
本地方法棧與虛拟機的作用相似,不同之處在于虛拟機棧為虛拟機執行的Java方法服務,而本地方法棧則為虛拟機使用到的Native方法服務。有的虛拟機直接把本地方法棧和虛拟機棧合二為一。
會抛出stackOverflowError和OutOfMemoryError異常。
Java堆
Java堆是所有線程共享的一塊記憶體區域,在虛拟機啟動時建立,此記憶體區域的唯一目的就是存放對象執行個體。
Java堆是垃圾收集器管理的主要區域。由于現在收集器基本采用分代回收算法,是以Java堆還可細分為:新生代和老年代。從記憶體配置設定的角度來看,線程共享的Java堆中可能劃分出多個線程私有的配置設定緩沖區(TLAB)。
Java堆可以處于實體上不連續的記憶體空間,隻要邏輯上連續的即可。在實作上,既可以實作固定大小的,也可以是擴充的。
如果堆中沒有記憶體完成執行個體配置設定,并且堆也無法完成擴充時,将會抛出OutOfMemoryError異常。
方法區
方法區是各個線程共享的記憶體區域,它用于存儲已被虛拟機加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料。
相對而言,垃圾收集行為在這個區域比較少出現,但并非資料進了方法區就永久的存在了,這個區域的記憶體回收目标主要是針對常量池的回收和對類型的解除安裝,
當方法區無法滿足記憶體配置設定需要時,将抛出OutOfMemoryError異常。
運作時常量池:
是方法區的一部分,它用于存放編譯期生成的各種字面量和符号引用。
直接記憶體
直接記憶體不是虛拟機運作時資料區的一部分,在NIO類中引入一種基于通道與緩沖區的IO方式,它可以使用Native函數庫直接配置設定堆外記憶體,然後通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊記憶體的引用進行操作。
直接記憶體的配置設定不會受到Java堆大小的限制,但是會受到本機記憶體大小的限制,所有也可能會抛OutOfMemoryError異常。
回到頂部
三、對象的建立、布局和通路過程
對象的建立
建立一個對象通常是需要new關鍵字,當虛拟機遇到一條new指令時,首先檢查這個指令的參數是否在常量池中定位到一個類的符号引用,并且檢查這個符号引用代表的類是否已被加載、解析和初始化過。如果那麼執行相應的類加載過程。
類加載檢查通過後,虛拟機将為新生對象配置設定記憶體。為對象配置設定空間的任務等同于把一塊确定大小的記憶體從Java堆中劃分出來。配置設定的方式有兩種:一種叫指針碰撞,假設Java堆中記憶體是絕對規整的,用過的和空閑的記憶體各在一邊,中間放着一個指針作為分界點的訓示器,配置設定記憶體就是把那個指針向空閑空間的那邊挪動一段與對象大小相等的距離。另一種叫空閑清單:如果Java堆中的記憶體不是規整的,虛拟機就需要維護一個清單,記錄哪個記憶體塊是可用的,在配置設定的時候從清單中找到一塊足夠大的空間劃分給對象執行個體,并更新清單上的記錄。采用哪種配置設定方式是由Java堆是否規整決定的,而Java堆是否規整是由所采用的垃圾收集器是否帶有壓縮整理功能決定的。另外一個需要考慮的問題就是對象建立時的線程安全問題,有兩種解決方案:一是對配置設定記憶體空間的動作進行同步處理;另一種是吧記憶體配置設定的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先配置設定一小塊記憶體(TLAB),哪個線程要配置設定記憶體就在哪個線程的TLAB上配置設定,隻有TLAB用完并配置設定新的TLAB時才需要同步鎖定。
記憶體配置設定完成後,虛拟機需要将配置設定到的記憶體空間初始化為零值。這一步操作保證了對象的執行個體字段在Java代碼中可以不賦初始值就可以直接使用。
接下來虛拟機要對對象進行必要的設定,例如這個對象是哪個類的執行個體、如何才能找到類的中繼資料資訊等,這些資訊存放在對象的對象頭中。
上面的工作都完成以後,從虛拟機的角度來看一個新的對象已經産生了。但是從Java程式的角度,還需要執行init方法,把對象按照程式員的意願進行初始化,這樣一個真正可用的對象才算完全産生出來。
對象的記憶體布局
在HotSpot虛拟機中,對象在記憶體中存儲的布局可分為三個部分:對象頭、執行個體資料和對齊填充。
對象頭包括兩個部分:第一部分用于存儲對象自身的運作時資料,如哈希碼、GC分代年齡、線程所持有的鎖等。官方稱之為“Mark Word”。第二個部分為是類型指針,即對象指向它的類中繼資料的指針,虛拟機通過這個指針來确定這個對象是哪個類的執行個體。
執行個體資料是對象真正存儲的有效資訊,也是程式代碼中所定義的各種類型的字段内容。
對齊填充并不是必然存在的,僅僅起着占位符的作用。、Hotpot VM要求對象起始位址必須是8位元組的整數倍,對象頭部分正好是8位元組的倍數,是以當執行個體資料部分沒有對齊時,需要通過對齊填充來對齊。
對象的通路定位
Java程式通過棧上的reference資料來操作堆上的具體對象。主要的通路方式有使用句柄和直接指針兩種:
句柄:Java堆将會劃出一塊記憶體來作為句柄池,引用中存儲的就是對象的句柄位址,而句柄中包含了對象執行個體資料與類型資料各自的具體位址資訊。如圖所示:

直接指針:Java堆對象的布局要考慮如何放置通路類型資料的相關資訊,引用中存儲的就是對象位址。如圖所示:
兩個方式各有優點,使用句柄最大的好處是引用中存儲的是穩定的句柄位址,對象被移動時隻會改變句柄中執行個體的位址,引用不需要修改、使用直接指針通路的好處是速度更快,它節省了一次指針定位的時間開銷。