- 在此之前我們先來看一下JDK和JRE,
我們可以把Java程式設計語言、Java虛擬機、Java類別庫這三部分統稱為JDK(Java Development Kit),JDK是用于支持Java程式開發的最小環境,
可以把Java類別庫API中的Java SE API子集和Java虛擬機這兩部分統稱為 JRE(Java Runtime Environment),JRE是支持Java程式運行的標準環境,
Java記憶體區域
一、運行時資料區域
Java虛擬機在執行Java程式的程序中會把它所管理的記憶體劃分為若干個不同的資料區域,這些區域 有各自的用途,以及創建和銷毀的時間,有的區域隨著虛擬機行程的啟動而一直存在,有些區域則是 依賴用戶執行緒的啟動和結束而建立和銷毀,(源于 :深入理解Java虛擬機(第3版))

這里我從執行緒是否共享來分別介紹這幾個區域:
執行緒共享的有:堆、方法區
執行緒獨享的包括:虛擬機堆疊、本地方法堆疊、程式計數器
-
執行緒共享
- 堆
Java堆(Java Heap)是虛擬機所管理的記憶體中最大的一塊,Java堆是被所 有執行緒共享的一塊記憶體區域,在虛擬機啟動時創建,此記憶體區域的唯一目的就是存放物件實體,Java 世界里“幾乎”所有的物件實體都在這里分配記憶體,
如果從分配記憶體的角度看,所有執行緒共享的Java堆中可以劃分出多個執行緒私有的分配緩沖區 (Thread Local Allocation Buffer,TLAB),以提升物件分配時的效率,
將Java 堆細分的目的只是為了更好地回收記憶體,或者更快地分配記憶體,也就是說堆其實是用來存放物件實體的,除此之外陣列的的資料也是放在堆里面的,
- 方法區
方法區(Method Area)與Java堆一樣,是各個執行緒共享的記憶體區域,它用于存盤已被虛擬機加載的型別資訊、常量、靜態變數、即時編譯器編譯后的代碼快取等資料,
在JDK 6的 時候HotSpot開發團隊就有放棄永久代,逐步改為采用本地記憶體(Native Memory)來實作方法區的計 劃了,到了JDK 7的HotSpot,已經把原本放在永久代的字串常量池、靜態變數等移出,而到了 JDK 8,終于完全廢棄了永久代的概念,改用與JRockit、J9一樣在本地記憶體中實作的元空間(Meta-space)來代替,把JDK 7中永久代還剩余的內容(主要是型別資訊)全部移到元空間中,
類被決議之后的資訊是存在方法區的,
-
執行緒獨享
- 虛擬機堆疊
Java虛擬機堆疊(Java Virtual Machine Stack)也是執行緒私有的,它的生命周期與執行緒相同,
虛擬機堆疊描述的是Java方法執行的執行緒記憶體模型:每個方法被執行的時候,Java虛擬機都 會同步創建一個堆疊幀(Stack Frame)用于存盤區域變數表、運算元堆疊、動態連接、方法出口等資訊,每一個方法被呼叫直至執行完畢的程序,就對應著一個堆疊幀在虛擬機堆疊中從入堆疊到出堆疊的程序,也就是說:堆疊是進行程式指令順序控制的,
- 本地方法堆疊
本地方法堆疊(Native Method Stacks)與虛擬機堆疊所發揮的作用是非常相似的,其區別只是虛擬機 堆疊為虛擬機執行Java方法(也就是位元組碼)服務,而本地方法堆疊則是為虛擬機使用到的本地(Native) 方法服務,
- 程式計數器
程式計數器(Program Counter Register)是一塊較小的記憶體空間,它可以看作是當前執行緒所執行的位元組碼的行號指示器,在Java虛擬機的概念模型里,位元組碼解釋器作業時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,它是程式控制流的指示器,分支、回圈、跳轉、例外處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成,
由于Java虛擬機的多執行緒是通過執行緒輪流切換、分配處理器執行時間的方式來實作的,在任何一 個確定的時刻,一個處理器(對于多核處理器來說是一個內核)都只會執行一條執行緒中的指令,因 此,為了執行緒切換后能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各條執行緒 之間計數器互不影響,獨立存盤,我們稱這類記憶體區域為“執行緒私有”的記憶體,
通俗來講就是:什么時候進堆疊、什么時候出堆疊,何時開始何時結束,都是由計數器控制的,
-
下面我們用一段程式來舉例,在一個方法執行的程序中到底是怎樣進行的:
class BirthDate { private int day; private int month; private int year; public BirthDate(int d, int m, int y) { day = d; month = m; year = y; } } public class Test{ public static void main(String args[]){ int date = 9; Test test = new Test(); test.change(date); BirthDate d1= new BirthDate(7,7,1970); } public void change1(int i){ i = 1234; } }- Test類以及BirthDate 類都經過編譯,他們各自的類資訊存盤在方法區里面
- main方法開始執行,main方法入堆疊,同時為main() 創建一個堆疊幀
- int data = 9;data是基礎型別,并且是在堆疊中宣告的,所以他的句柄資訊(參考)以及他的值都在堆疊中,
- Test test = new Test(); test存在堆疊中,(new Test())放在堆里,并且由物件中指向類資訊的指標指向方法區中對應的類
- test.change(date); change()方法入堆疊并開辟堆疊幀,i 為形參并且為基本資料型別,在堆疊中宣告,同 data ; change方法在執行完畢之后就會出堆疊,
- BirthDate d1= new BirthDate(7,7,1970); d1(句柄資訊)為物件參考,存放在堆疊中,(new BirthDate()) 存放在堆中,BirthDate()函式執行,開辟堆疊幀,三個形參都是基本資料型別,在堆疊中宣告,同 data 和 i , 建構式在執行完畢之后,堆疊幀銷毀(出堆疊,對應的區域變數表也會被銷毀,他主要保存函式的引數以及區域的變數資訊,也就是說 d、m、y也會被銷毀)
- .main方法執行完之后出堆疊,date變數,test,d1參考將從堆疊中消失,new Test(),new BirthDate()將等待垃圾回收,
-
運行時常量池
運行時常量池(Runtime Constant Pool)是方法區的一部分,Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池表(Constant Pool Table),用于存放編譯期生成的各種字面量與符號參考,這部分內容將在類加載后存放到方法區的運行時常量池中,
二、物件創建
-
在代碼的層面,物件創建通常就是通過 new 關鍵字,但是在Java虛擬機里面創建一個物件,可決不是這么簡單的,
-
當Java虛擬機遇到一條位元組碼new指令時,首先去檢查這個指令的引數是否能在常量池中定位到 一個類的符號參考,并且檢查這個符號參考代表的類是否已被加載、決議和初始化過,如果沒有,那必須先執行相應的類加載程序,
-
在類加載檢查通過后,接下來虛擬機將為新生物件分配記憶體,物件所需記憶體的大小在類加載完成 后便可完全確定,為物件分配空間的任務實際上便等同于把一塊確定 大小的記憶體塊從Java堆中劃分出來,
- 在這里有兩種方式:
- 指標碰撞:假設Java堆中記憶體是絕對規整的,所有被使用過的記憶體都被放在一 邊,空閑的記憶體被放在另一邊,中間放著一個指標作為分界點的指示器,那所分配記憶體就僅僅是把那個指標向空閑空間方向挪動一段與物件大小相等的距離,這種分配方式稱為"指標碰撞"
- 這有一個問題是需要考慮的:物件創建在虛擬機中是非常頻繁的行 為,即使僅僅修改一個指標所指向的位置,在并發情況下也并不是執行緒安全的,可能出現正在給物件 A分配記憶體,指標還沒來得及修改,物件B又同時使用了原來的指標來分配記憶體的情況,
- 解決上述的情況有兩種方式
- 一種是對分配記憶體空間的動作進行同步處理——實際上虛擬機是采用CAS配上失敗 重試的方式保證更新操作的原子性;
- 另外一種是把記憶體分配的動作按照執行緒劃分在不同的空間之中進 行,即每個執行緒在Java堆中預先分配一小塊記憶體,稱為本地執行緒分配緩沖(Thread Local Allocation Buffer,TLAB),哪個執行緒要分配記憶體,就在哪個執行緒的本地緩沖區中分配,只有本地緩沖區用完 了,分配新的快取區時才需要同步鎖定,
- 空閑串列:但如果Java堆中的記憶體并不是規整的,已被使用的記憶體和空閑的記憶體相互交錯在一起,那就沒有辦法簡單地進行指標碰撞了,虛擬機就必須維護一個串列,記錄上哪些記憶體塊是可用的,在分配的時候從串列中找到一塊足夠大的空間劃分給物件實體,并更新串列上的記錄,這種分配方式稱為“空閑串列”(Free List),
三、物件的記憶體布局
在HotSpot虛擬機里,物件在堆記憶體中的存盤布局可以劃分為三個部分:物件頭(Header)、實體資料(Instance Data)和 對齊填充(Padding),
- 物件頭:HotSpot虛擬機物件的物件頭部分包括兩類資訊,
- 第一類是用于存盤物件自身的運行時資料,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等,官方稱它為 Mark Word(一個有著動態定義的資料結構),
- 物件頭的另外一部分是型別指標,即物件指向它的型別元資料的指標,在上述代碼中的例子: new BirthDate(),物件實體存放在堆里,物件頭中的這部分型別指標,指向的就是方法區對應的類資訊,
- 實體資料部分是物件真正存盤的有效資訊,
- 物件的第三部分是對齊填充,這并不是必然存在的,也沒有特別的含義,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/262539.html
標籤:java
上一篇:Java-NIO

