你有認真瞭解過自己的“Java物件”嗎? 渣男
阿新 • • 發佈:2020-07-13
> 物件在 JVM 中是怎麼儲存的
>
> 物件頭裡有什麼?
>
> 文章收錄在 GitHub [JavaKeeper](https://github.com/Jstarfish/JavaKeeper) ,N線網際網路開發必備技能兵器譜,有你想要的。
作為一名 Javaer,生活中的我們可能暫時沒有物件,但是工作中每天都會建立大量的 Java 物件,你有試著去了解下自己的“物件”嗎?
我們從四個方面重新認識下自己的“物件”
1. 建立物件的 6 種方式
2. 建立一個物件在 JVM 中都發生了什麼
3. 物件在 JVM 中的記憶體佈局
4. 物件的訪問定位
## 一、建立物件的方式
- 使用 new 關鍵字
這是建立一個物件最通用、常規的方法,同時也是最簡單的方式。通過使用此方法,我們可以呼叫任何要呼叫的建構函式(預設使用無參建構函式)
```java
Person p = new Person();
```
- 使用 Class 類的 newInstance(),只能呼叫空參的構造器,許可權必須為 public
```java
//獲取類物件
Class aClass = Class.forName("priv.starfish.Person");
Person p1 = (Person) aClass.newInstance();
```
- Constructor 的 newInstance(xxx),對構造器沒有要求
```java
Class aClass = Class.forName("priv.starfish.Person");
//獲取構造器
Constructor constructor = aClass.getConstructor();
Person p2 = (Person) constructor.newInstance();
```
- clone()
深拷貝,需要實現 Cloneable 介面並實現 clone(),不呼叫任何的構造器
```java
Person p3 = (Person) p.clone();
```
- 反序列化
通過序列化和反序列化技術從檔案或者網路中獲取物件的二進位制流。
每當我們序列化和反序列化物件時,JVM 會為我們建立了一個獨立的物件。在 deserialization 中,JVM 不使用任何建構函式來建立物件。(序列化的物件需要實現 Serializable)
```java
//準備一個檔案用於儲存該物件的資訊
File f = new File("person.obj");
FileOutputStream fos = new FileOutputStream(f);
ObjectOutputStream oos = new ObjectOutputStream(fos);
//序列化物件,寫入到磁碟中
oos.writeObject(p);
//反序列化
FileInputStream fis = new FileInputStream(f);
ObjectInputStream ois = new ObjectInputStream(fis);
//反序列化物件
Person p4 = (Person) ois.readObject();
```
- 第三方庫 Objenesls
Java已經支援通過 `Class.newInstance()` 動態例項化 Java 類,但是這需要Java類有個適當的構造器。很多時候一個Java類無法通過這種途徑建立,例如:構造器需要引數、構造器有副作用、構造器會丟擲異常。Objenesis 可以繞過上述限制
## 二、建立物件的步驟
這裡討論的僅僅是普通 Java 物件,不包含陣列和 Class 物件(普通物件和陣列物件的建立指令是不同的。建立類例項的指令:new,建立陣列的指令:newarray,anewarray,multianewarray)
#### 1. new指令
虛擬機器遇到一條 new 指令時,首先去檢查這個指令的引數是否能在 Metaspace 的常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被載入、解析和初始化過(即判斷類元資訊是否存在)。如果沒有,那麼須在雙親委派模式下,先執行相應的類載入過程。
#### 2. 分配記憶體
接下來虛擬機器將為新生代物件分配記憶體。物件所需的記憶體的大小在類載入完成後便可完全確定。如果例項成員變數是引用變數,僅分配引用變數空間即可,即 4 個位元組大小。分配方式有“**指標碰撞**(Bump the Pointer)”和“**空閒列表**(Free List)”兩種方式,具體由所採用的垃圾收集器是否帶有壓縮整理功能決定。
- 如果記憶體是規整的,就採用“指標碰撞”來為物件分配記憶體。意思是所有用過的記憶體在一邊,空閒的記憶體在另一邊,中間放著一個指標作為分界點的指示器,分配記憶體就僅僅是把指標指向空閒那邊挪動一段與物件大小相等的距離罷了。如果垃圾收集器採用的是 Serial、ParNew 這種基於壓縮演算法的,就採用這種方法。(一般使用帶整理功能的垃圾收集器,都採用指標碰撞)
![](https://img-blog.csdnimg.cn/2020060220241513.png)
- 如果記憶體是不規整的,虛擬機器需要維護一個列表,這個列表會記錄哪些記憶體是可用的,在為物件分配記憶體的時候從列表中找到一塊足夠大的空間劃分給該物件例項,並更新列表內容,這種分配方式就是“空閒列表”。使用CMS 這種基於Mark-Sweep 演算法的收集器時,通常採用空閒列表。
![](https://img-blog.csdnimg.cn/20200602202424136.png)
> 我們都知道堆記憶體是執行緒共享的,那在分配記憶體的時候就會存在併發安全問題,JVM 是如何解決的呢?
一般有兩種解決方案:
1. 對分配記憶體空間的動作做同步處理,採用 CAS 機制,配合失敗重試的方式保證更新操作的原子性
2. 每個執行緒在 Java 堆中預先分配一小塊記憶體,然後再給物件分配記憶體的時候,直接在自己這塊"私有"記憶體中分配,當這部分割槽域用完之後,再分配新的"私有"記憶體。這種方案稱為 **TLAB**(Thread Local Allocation Buffer),這部分 Buffer 是從堆中劃分出來的,但是是本地執行緒獨享的。
**這裡值得注意的是,我們說 TLAB 是執行緒獨享的,只是在“分配”這個動作上是執行緒獨佔的,至於在讀取、垃圾回收等動作上都是執行緒共享的。而且在使用上也沒有什麼區別。**另外,TLAB 僅作用於新生代的 Eden Space,物件被建立的時候首先放到這個區域,但是新生代分配不了記憶體的大物件會直接進入老年代。**因此在編寫 Java 程式時,通常多個小的物件比大的物件分配起來更加高效。**
虛擬機器是否使用 TLAB 是可以選擇的,可以通過設定 `-XX:+/-UseTLAB` 引數來指定,JDK8 預設開啟。
#### 3. 初始化
記憶體分配完成後,虛擬機器需要將分配到的記憶體空間都初始化為零值(不包括物件頭),這一步操作保證了物件的例項欄位在 Java 程式碼中可以不賦初始值就直接使用,程式能訪問到這些欄位的資料型別所對應的零值。如:byte、short、long 轉化為物件後初始值為 0,Boolean 初始值為 false。
#### 4. 物件的初始設定(設定物件的物件頭)
接下來虛擬機器要對物件進行必要的設定,例如這個物件是哪個類的例項、如何才能找到類的元資料資訊、物件的雜湊碼、物件的GC分代年齡等資訊。這些資訊存放在物件的物件頭(Object Header)之中。根據虛擬機器當前的執行狀態的不同,如對否啟用偏向鎖等,物件頭會有不同的設定方式。
#