淺談Java記憶體區域劃分和記憶體分配策略
如果不知道,類的靜態變數儲存在那? 方法的區域性變數儲存在那? 趕快收藏
Java記憶體區域主要可以分為共享記憶體,堆、方法區和執行緒私有記憶體,虛擬機器棧、本地方法棧和程式計數器。如下圖所示,本文將詳細講述各個區域,同時也會講述建立物件過程,記憶體分配策略,和物件訪問定位原理。覺得寫得好的,可以點個收藏,絕對不虧。
Java記憶體區域
程式計數器
程式計數器,可以看作程式當前執行緒所執行的位元組碼行號指示器。位元組碼直譯器工作時就是通過改變計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理都需要依賴計數器完成。執行緒執行Java方法時,記錄其正在執行的虛擬機器位元組碼指令地址,執行緒執行Native方法時,計數器記錄為空。程式計數器時唯一在Java虛擬機器規範中沒有規定任何OutOfMemoryError
理論可知,執行緒是通過輪流獲取CPU執行時間以實現多執行緒的併發。為了暫停的執行緒下一次獲得CPU執行時間,能正常執行,每一個執行緒內部都需要維護一個程式計數器,用來記住暫停執行緒暫停的位置。
注意:光理論是不夠的,在此送大家一套2020最新Java架構實戰教程+大廠面試寶典,點選此處 進來獲取 一起交流進步哦!
Java虛擬機器棧
Java虛擬機器棧同程式計數器一樣,也是執行緒私有的,虛擬機器棧描述的是Java方法執行的記憶體模型,每個方法在執行的同時都會建立一個棧幀,用於儲存區域性變量表,運算元棧、動態連結和方法出入口等資訊。每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器中入棧到出棧的過程。
本地方法棧
與虛擬機器棧相似。虛擬機器棧為虛擬機器執行Java方法服務,而本地方法棧則為虛擬機器使用到的Native方法服務。
Java堆
所有執行緒共享的一塊記憶體區域。Java虛擬機器所管理的記憶體中最大的一塊,因為該記憶體區域的唯一目的就是存放物件例項。幾乎所有的物件例項都在這裡分配記憶體,同時堆也是垃圾收集器管理的主要區域。因此很多時候被稱為"GC堆"
方法區
和堆一樣,是各個執行緒共享的記憶體區域,用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、和編譯器編譯後的程式碼(也就是儲存位元組碼檔案.class)等資料。
方法區中有一個執行時常量池,編譯後期生成的各種字面量和符號引用,存放在位元組碼檔案中的常量池中。當類載入進入方法區時,就會把該常量池中的內容放入方法區中的執行時常量池。此外也可以在程式執行期間,將新的常量放入執行時常量池,比如String.intern()
例項詳講
class Demo1_Car{ public static void main(String[] args) { Car c1 = new Car(); //呼叫屬性並賦值 c1.color = "red"; c1.num = 8; //呼叫行為 c1.run(); Car c2 = new Car(); c2.color = "black"; c2.num = 4; c2.run(); } } Class Car{ String color; int num; public void run() { System.out.println(color + ".." + num); } }
- 首先執行程式,Demo1_car.java就會變為Demo1_car.class,Demo1_car.class加入方法區,檢查是否位元組碼檔案常量池中是否有常量值,如果有,那麼就加入執行時常量池。
- 遇到main方法,建立一個棧幀,入虛擬機器棧,然後開始執行main方法中的程式。
- Car c1 = new Car(),第一次遇到Car這個類,所以將Car.java編譯為Car.class檔案,然後加入方法區.然後new Car(),在堆中建立一塊區域,用於存放創建出來的例項物件,地址為0X001.其中有兩個屬性值color和num。預設值是null和 0
- 然後通過c1這個引用變數去設定color和num的值,呼叫run方法,然後會建立一個棧幀,用來儲存run方法中的區域性變數等。run 方法中就列印了一句話,結束之後,該棧幀出虛擬機器棧。又只剩下main方法這個棧幀。
- 接著又建立了一個Car物件,所以又在堆中開闢了一塊記憶體,之後就是跟之前的步驟一樣了。
建立物件過程
虛擬機器在遇到一條new指令時,會首先檢查這個指令的引數是否可以在方法區中定位到一個類的符號引用,並且檢查這個符號引用所代表的類是否已經被載入,解析和初始化過。如果沒有,則必須先執行類載入過程.
類載入完之後,需要為物件分配記憶體,有兩種分配記憶體的方法
- 指標碰撞法(要求堆記憶體規整)
Java堆中空閒記憶體和已使用記憶體分別存放在堆的兩邊,中間存放一個指標作為分界點的指示器,在為物件分配記憶體時只需要將指標向空閒區域移動建立物件所需要的記憶體大小即可。
- 空閒列表法
如果堆記憶體中已使用記憶體區域和空閒區域相互交錯,此時虛擬機器需要維護一個列表,記錄哪些記憶體塊是可用的,在分配時從列表中找到一塊足夠大的記憶體區域劃分給物件例項並更新列表上的記錄。
多執行緒情況下,執行緒同時分配記憶體可能會造成衝突,比如使用指標碰撞法,執行緒A正在分配記憶體,還沒有改變指標指向,執行緒B,又同時使用原來的指標進行記憶體分配。防止衝突有兩種方法
CAS
操作:虛擬機器採用CAS
操作,加上失敗重試的方式保證記憶體分配的原子性- 本地執行緒分配緩衝(
TLAB
):預先為執行緒分配一部分堆記憶體空間(執行緒私有,所以不存在同步問題)用於物件例項的記憶體分配。只有當TLAB
用完,需要分配新的TLAB
時,才需要進行同步操作。
記憶體分配完之後,虛擬機器需要將分配到的記憶體空間均初始化為零值(不包括物件頭)。在虛擬機器中,執行完new指令後會接著執行方法,把物件按照程式設計師的意願進行初始化,這樣一個真正可用的物件才算完全產生出來
物件在記憶體中的佈局
物件在記憶體中的佈局如下圖所示,分為物件頭、例項資料、對齊填充
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳
- 物件頭(可以參考Java鎖升級)
mark Word
,用於儲存物件自身的執行時資料,如雜湊碼、GC
分代年齡以及鎖狀態標誌等。型別指標,即物件指向它的類元資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。
- 例項資料
物件真正儲存的有效資訊,也是程式程式碼中所定義的各種型別的欄位內容。
- 對齊填充
並非必然存在,僅僅起著佔位符的作用。
物件的訪問定位
Java程式需要通過棧上的reference資料來操作堆上的具體物件。共有兩種策略進行物件的訪問定位
- 控制代碼訪問
Java堆中劃分出一塊記憶體來作為控制代碼池,reference中儲存的是物件的控制代碼地址,而控制代碼中包含了物件例項資料與型別資料各自的具體地址資訊,需要兩次定址。
- 直接指標訪問
Java堆中物件的佈局中需要考慮如何放置訪問型別資料的相關資訊,而reference中儲存的直接就是物件地址。
使用控制代碼訪問的最大好處就是reference中儲存的是穩定的控制代碼地址,在物件被移動(垃圾收集時移動物件是非常普遍的行為)時只會改變控制代碼中例項資料指標,而reference本身不需要修改。
問題
只需要記住一件事,就是Java物件的記憶體分配均是在堆中進行的。所以物件都儲存在堆中。
但是有人可能會懷疑方法的臨時變數不是儲存在虛擬機器棧中嗎?這裡我要解釋一下,虛擬機器棧維護了一個區域性變量表,表中儲存的是物件的引用,而真正儲存物件的地方在堆,如果區域性變數都在堆裡分配,那麼虛擬機器棧早爆滿了
同樣類的靜態變數,有人又會懷疑在方法區中儲存。其實不是的,方法區只儲存引用,具體物件是儲存在堆中的,具體實現可以發現,類靜態物件是與class物件一起分配的記憶體。
參考
深入理解java虛擬機器
到此這篇關於淺談Java記憶體區域劃分和記憶體分配策略的文章就介紹到這了,更多相關Java記憶體區域劃分和記憶體分配內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!