1. 對象的創建
1. 遇到 new 指令時,首先檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已經被加載、解析和初始化過。如果沒有,執行相應的類加載。
2. 類加載檢查通過之后,為新對象分配內存(內存大小在類加載完成后便可確認)。在堆的空閑內存中划分一塊區域(‘指針碰撞-內存規整’或‘空閑列表-內存交錯’的分配方式)。
A、假設Java堆是規整的,所有用過的內存放在一邊,空閑的內存放在另外一邊,中間放着一個指針作為分界點的指示器。那分配內存只是把指針向空閑空間那邊挪動與對象大小相等的距離,這種分配稱為“指針碰撞”
B、假設Java堆不是規整的,用過的內存和空閑的內存相互交錯,那就沒辦法進行“指針碰撞”。虛擬機通過維護一個列表,記錄哪些內存塊是可用的,在分配的時候找出一塊足夠大的空間分配給對象實例,並更新表上的記錄。這種分配方式稱為“空閑列表“。
C、使用哪種分配方式由Java堆是否規整決定。Java堆是否規整由所采用的垃圾收集器是否帶有壓縮整理功能決定。
D、分配對象保證線程安全的做法:虛擬機使用CAS失敗重試的方式保證更新操作的原子性。(實際上還有另外一種方案:每個線程在Java堆中預先分配一小塊內存,稱為本地線程分配緩沖,TLAB。哪個線程要分配內存,就在哪個線程的TLAB上分配,只有TLAB用完並分配新的TLAB時,才進行同步鎖定。虛擬機是否使用TLAB,由-XX:+/-UseTLAB參數決定)
3. 每個線程在堆中都會有私有的分配緩沖區(TLAB),這樣可以很大程度避免在並發情況下頻繁創建對象造成的線程不安全。
4. 內存空間分配完成后會初始化為 0(不包括對象頭)
5. 接下來就是填充對象頭,把對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的 GC 分代年齡等信息存入對象頭。
6. 執行<init>方法,把對象按照程序員的意願進行初始化。執行 init 方法后才算一份真正可用的對象創建完成。
2. 對象的內存布局
在 HotSpot 虛擬機中,分為 3 塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)
1. 對象頭(Header):包含兩部分,第一部分用於存儲對象自身的運行時數據,如哈希碼、GC 分代年齡、鎖狀態標志、線程持有的鎖、偏向線程 ID、偏向時間戳等,32 位虛擬機占 32 bit,64 位虛擬機占 64 bit。官方稱為 ‘Mark Word’。第二部分是類型指針,即對象指向它的類的元數據指針,虛擬機通過這個指針確定這個對象是哪個類的實例。另外,如果是 Java 數組,對象頭中還必須有一塊用於記錄數組長度的數據,因為普通對象可以通過 Java 對象元數據確定大小,而數組對象不可以。
2. 實例數據(Instance Data):程序代碼中所定義的各種類型的字段內容(包含父類繼承下來的和子類中定義的)。
3. 對齊填充(Padding):不是必然需要,主要是占位,保證對象大小是某個字節的整數倍。
3. 對象的訪問定位
一般來說,一個Java的引用訪問涉及到3個內存區域:JVM棧,堆,方法區。
以最簡單的本地變量引用:
Object objRef = new Object()為例: Object objRef 表示一個本地引用,存儲在JVM棧的本地變量表中,表示一個reference類型數據;
new Object()作為實例對象數據存儲在堆中;
堆中還記錄了能夠查詢到此Object對象的類型數據(接口、方法、field、對象類型等)的地址,實際的數據則存儲在方法區中;
在Java虛擬機規范中,只規定了指向對象的引用,對於通過reference類型引用訪問具體對象的方式並未做規定,不過目前主流的實現方式主要有兩種:
1. 通過句柄訪問
通過句柄訪問的實現方式中,JVM堆中會划分單獨一塊內存區域作為句柄池,句柄池中存儲了對象實例數據(在堆中)和對象類型數據(在方法區中)的指針。這種實現方法由於用句柄表示地址,因此十分穩定。 Java 堆中會分配一塊內存作為句柄池。reference 存儲的是句柄地址。詳情見圖。
2. 使用直接指針訪問
通過直接指針訪問的方式中,reference中存儲的就是對象在堆中的實際地址,在堆中存儲的對象信息中包含了在方法區中的相應類型數據。這種方法最大的優勢是速度快,在HotSpot虛擬機中用的就是這種方式。
比較:
使用句柄的最大好處是 reference 中存儲的是穩定的句柄地址,在對象移動(GC)是只改變實例數據指針地址,reference 自身不需要修改。
直接指針訪問的最大好處是速度快,節省了一次指針定位的時間開銷。
如果是對象頻繁 GC 那么句柄方法好,
如果是對象頻繁訪問則直接指針訪問好。
4. HotSpot的GC算法實現
(1)HotSpot怎么快速找到GC Root?
HotSpot使用一組稱為OopMap的數據結構。
在類加載完成的時候,HotSpot就把對象內什么偏移量上是什么類型的數據計算出來,
在JIT編譯過程中,也會在棧和寄存器中哪些位置是引用。
這樣子,在GC掃描的時候,就可以直接知道哪些是可達對象了。
(2)安全點:
A、HotSpot只在特定的位置生成OopMap,這些位置稱為安全點。
B、程序執行過程中並非所有地方都可以停下來開始GC,只有在到達安全點是才可以暫停。
C、安全點的選定基本上以“是否具有讓程序長時間執行“的特征選定的。比如說方法調用、循環跳轉、異常跳轉等。具有這些功能的指令才會產生Safepoint。
(3)中斷方式:
A、搶占式中斷:在GC發生時,首先把所有線程中斷,如果發現有線程不在安全點上,就恢復線程,讓它跑到安全點上。
B、主動式中斷:GC需要中斷線程時,不直接對線程操作,僅僅設置一個標志,各個線程執行時主動去輪詢這個標志,當發現中斷標記為真就自己中斷掛起。輪詢標記的地方和安全點是重合的。
(4)安全區域
一段代碼片段中,對象的引用關系不會發生變化,在這個區域中任何地方開始GC都是安全的。
在線程進入安全區域時,它首先標志自己已經進入安全區域,在這段時間里,當JVM發起GC時,就不用管進入安全區域的線程了。
在線程將要離開安全區域時,它檢查系統是否完成了GC過程,如果完成了,它就繼續前行。否則,它就必須等待直到收到可以離開安全區域的信號。
(5)GC時為什么要停頓所有Java線程?
因為GC先進行可達性分析。
可達性分析是判斷GC Root對象到其他對象是否可達,
假如分析過程中對象的引用關系在不斷變化,分析結果的准確性就無法得到保證。
5. 舉個栗子
public class Test{ public static void main(String[] args){ Student stu = new Student(); stu.setName("John"); System.out.println(stu); } } 1. 通過java.exe運行Test.class,Test.class文件會被AppClassLoader加載器(因為ExtClassLoader和BootStrap加載器都不會加載它[雙親委派模型])加載到JVM中,元空間存儲着類的信息(包括類的名稱、方法信息、字段信息..)。 2. 然后JVM找到Test的主函數入口(main),為main函數創建棧幀,開始執行main函數 3. main函數的第一條命令是Student Student stu= new Student();就是讓JVM創建一個Student對象,但是這時候方法區中沒有Student類的信息,所以JVM馬上加載Student類,把Student類的類型信息放到方法區中(元空間) 4. 加載完Student類之后,Java虛擬機做的第一件事情就是在堆區中為一個新的Student實例分配內存, 然后調用構造函數初始化Student實例,這個Student實例持有着指向方法區的Student類的類型信息(其中包含有方法表,java動態綁定的底層實現)的引用 5. 當使用Student.setName("John");的時候,JVM根據Student引用找到Student對象,然后根據Student對象持有的引用定位到方法區中Student類的類型信息的方法表,獲得setName()函數的字節碼的地址 6. 為setName()函數創建棧幀,開始運行setName()函數