--- title: 'Java 創建物件、JVM 引用細節' disqus: kyleAlien --- Java 創建物件、JVM 引用細節 === ## Overview of Content 如有引用參考請詳註出處,感謝 :smile_cat: 在 JVM 中,物件的通常都會建立在堆區,而堆區也是 GC 最常處理的區塊;以物件的生命週期來說,在堆區建立後就是”生“,被 GC 回收後就是 “死” 但這個章節主要說明的是 Java 如何創建物件(顯示、隱式)、引用關係 :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**深入探索 Java 物件創建與引用細節:Clone 和finalize 特性,以及強、軟、弱、虛引用**](https://devtechascendancy.com/java-object-creation_jvm-reference-details/) ::: [TOC] ## Java 建立物件的方式 在 Java 中有多種建立物件的方式,而不同的物件建立方式又會有不同的特性;分別有以下方法 1. **使用 `new` 關鍵字**:最常見的方案 2. **使用 JVM Bytecode 反射創建**:在設計、使用框架時常見;通常會使用到 `java.lang.Class`、`java.lang.Constructor` 這兩個類 3. **使用 Clone 機制**:實作 Cloneable 這個標示界面 4. **反序列化**:這個方案較少見;通常會使用到 `java.io.ObjectInputStream` 類來將物件實體化 :::info * **物件的建立前提是類已經被 ClassLoader 加載進方法區,並初始化完成** ::: ### Clone 特性 * Object#**`clone` 方法**:它是 Object 中的方法,它的源碼實現如下(以下我們看的是 `Java 15` 的源碼) ```java= // Object.java public class Object { ... 省略部份 protected native Object clone() throws CloneNotSupportedException; } ``` 我們可以看到 ^1.^ **`clone` 是一個 Native 方法**,代表他是在 Native 層被實作的,^2.^ 它是 `protected` 方法,並且 ^3.^ 可能會拋出 `CloneNotSupportedException` * 關於第二點與 **`Cloneable` 標示介面** 有關係 ```java= // java.lang.Cloneable public interface Cloneable { } ``` :::success * **標示界面** 是甚麽? 標示介面通常是拿來判斷的,**標示界面並不具有任何需要實作的方法** * **`Protected` 方法** 介於它是 Protected 方法,所以如果要讓外部呼叫,我們可以使用 `overload` 的技巧,對外開放 ::: * 如果我們呼叫 `clone` 方法,但並沒有實作 `Cloneable` 介面,那就會拋出 `CloneNotSupportedException` 異常 ```java= class NoCloneable { @Override public NoCloneable clone() { try { return (NoCloneable) super.clone(); } catch (CloneNotSupportedException e) { // 例外轉譯 throw new AssertionError(); } } } ``` > ![](https://hackmd.io/_uploads/HJs3NYFp2.png) * 只要我們標示該介面是 `Cloneable` 就不會拋出異常 ```java= class WithCloneable implements Cloneable { @Override public WithCloneable clone() { try { return (WithCloneable) super.clone(); } catch (CloneNotSupportedException e) { // 例外轉譯 throw new AssertionError(); } } } ``` * **Clone 預設是 ==淺拷貝==**!當它拷貝物件時會 **針對類型的不同而有不同的操作**,我們接著來看看這兩種不同類型 clone 會如何操作 * **基礎類型**:在呼叫 clone 方法,JVM 會對基礎類型(`int, long, double`... 等等)創建一個全新的基礎類型物件,並賦予 clone 物件 ``` mermaid graph TD; 物件-->| JVM clone |Clone_物件 Clone_物件-->新的基礎類型 ``` * **參考類型**:參考類型(`ref`)像是物件、陣列;**JVM 會直接複製該參考引用**,而不會自動幫你創建一個新的參考物件!**也就是說 clone 出來的物件,會與原先的物件使用相同的引用!** > **深拷貝**:如果有需拷貝全新參考類型,那要則需要自己手動做參考類型的 clone 方法 ``` mermaid graph TD; 物件-->| JVM clone |Clone_物件; 物件-->參考物件; Clone_物件-->參考物件 ``` ```java= class WithCloneable implements Cloneable { public String myStr = "Hello"; public final int[] intArray = new int[1000]; @Override public WithCloneable clone() { try { return (WithCloneable) super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(); } } public static void main(String[] args) { WithCloneable wc = new WithCloneable(); WithCloneable c1 = wc.clone(); System.out.println("Is clone object hashcode same: " + (wc.hashCode() == c1.hashCode())); System.out.println("Before set clone object's array(hashcode): " + (Arrays.hashCode(wc.intArray) == Arrays.hashCode(c1.intArray))); System.out.println("Before set clone object's string(hashcode): " + (wc.myStr.hashCode() == c1.hashCode())); System.out.println("Before set clone object's string(value): " + (wc.myStr.equals(c1.myStr))); System.out.println(); c1.intArray[0] = 1000; System.out.println("After set clone object's array(hashcode): " + (Arrays.hashCode(wc.intArray) == Arrays.hashCode(c1.intArray))); c1.myStr = "World"; System.out.println("After set clone object's string(value): " + (wc.myStr.equals(c1.myStr))); System.out.println("After set clone object's string(value): " + (wc.myStr.equals(c1.myStr))); } } ``` > ![](https://hackmd.io/_uploads/ByTxFYKT3.png) ### Clone 注意事項:不經建構函數 * 對於 **使用 `clone` 方法來創過的物件,是 ++完全不會經過建構函數++**,它會直接拷貝記憶體內容! ```java= class CloneAndConstructor implements Cloneable { private final String msg; CloneAndConstructor() { this.msg = "No message"; System.out.println("Constructor without params"); } CloneAndConstructor(String msg) { this.msg = msg; System.out.println("Constructor with params"); } @Override public CloneAndConstructor clone() { try { return (CloneAndConstructor) super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(e); } } public static void main(String[] args) { CloneAndConstructor cc = new CloneAndConstructor("Hello Clone"); System.out.println("Original object: " + cc.msg + "\n"); CloneAndConstructor cloneObj = cc.clone(); System.out.println("Clone object: " + cloneObj.msg); } } ``` > ![](https://hackmd.io/_uploads/SyBwoKYTh.png) ### 建構函數 - 反序列化 * 序列化/反序列化物件是一種比較少見的行為,但仍是一種創建物件的方式;它的特性如下 * 序列化物件 **必須實作 `Serializable` 界面**,該介面同樣也是一個標示介面 ```java= // java.io.Serializable public interface Serializable { } ``` * 反序列化會拷貝原先物件的參考引用物件 ref,但在改變參考引用物件 ref 的數值後,JVM 會自動幫我們創建一個新的物件 > 這類似一種 Copy on write 的技術 * 示意圖如下,在更改物件之前,指向同一個參考物件 ``` mermaid graph TD; 物件-->| JVM clone |Clone_物件; 物件-->參考物件; Clone_物件-->參考物件 ``` * 示意圖如下,在更改物件之後,指向全新的參考物件 ``` mermaid graph TD; 物件-->|JVM Clone|Clone_物件; 物件-->參考類型; Clone_物件-->改動參考物件 改動參考物件-->新的參考類型 ``` * 反序列化後,**會創建一個全新的物件**,**有新的記憶體位置**(以引用來對照是不同的物件,但內容值相同),這種用法請特別注意只能用在「**值物件**」 ```java= class AccountInfo implements Serializable { public final String name; public final long id; public final String password; public final String[] otherMessages = new String[1000]; public AccountInfo() { this.name = ""; this.id = -1; this.password = "-"; System.out.println("Construct without params"); } public AccountInfo(String name, long id, String password) { this.name = name; this.id = id; this.password = password; System.out.println("Construct with params"); } @Override public String toString() { return "Name: " + name + "\n" + "id: " + id + "\n" + "password: " + password + "\n" + "otherMessages: " + Arrays.toString(otherMessages); } public static void main(String[] args) { String filename = "DemoAccount"; // 序列化 AccountInfo accountInfo = new AccountInfo("Alien", 9527, "HelloWorld"); try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename))){ oos.writeObject(accountInfo); oos.flush(); System.out.println("寫入對象成功,物件原本 hashcode: " + accountInfo.hashCode() + "\n"); } catch (IOException e) { e.printStackTrace(); } // 反序列化 try (ObjectInputStream oos = new ObjectInputStream(new FileInputStream(filename))){ AccountInfo deserializeObj = (AccountInfo) oos.readObject(); System.out.println("讀取對象成功,反序列化後 hashcode: " + deserializeObj.hashCode()); System.out.println(("Is same content: " + accountInfo).equals(deserializeObj.toString())); System.out.println("clone's array hashcode: " + Arrays.hashCode(deserializeObj.otherMessages)); System.out.println("original's array hashcode: " + Arrays.hashCode(accountInfo.otherMessages) + "\n"); accountInfo.otherMessages[0] = "123"; System.out.println("clone's array index 0: " + deserializeObj.otherMessages[0]); System.out.println("clone's array index 0: " + Arrays.hashCode(deserializeObj.otherMessages) + "\n"); System.out.println("original's array index 0: " + accountInfo.otherMessages[0]); System.out.println("original's array index 0: " + Arrays.hashCode(accountInfo.otherMessages) + "\n"); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } } } ``` > ![](https://hackmd.io/_uploads/rJPnl5K6h.png) ## 觸發 JVM GC:觀察物件回收 :::success * Java GC 是否回收物件,這會有關到 JVM 判斷物件的 **可達性分析** 機制,當物件不可達時,就會被認定該物件不再使用,這時就可以回收 > **JVM 又會因為 Java 引用的差異而會有不同的處理方式** ::: :::warning * 有關於 **手動觸發 GC** 當我們使用 `System.gc()`、`Runtime.getRuntime().gc()` **並非是直接驅動立即開始 GC** > GC 是另外一個低優先度的線程去處理的行為,所以通常我們測試時會使用 Thread#sleep 讓主線程休眠,等待 GC 回收 ::: ### finalize 方法、特性 * 該方法的原形在 `Object.java` 中,**可以用來拯救被回收的對象**,有以下特點 1. 是只能拯救回收的對象一次 (**一次 new 會有一次 finalize 機會,拯救後如果再次被加入回收列表,就不會再通知!**) 2. **運行 `finalize` 的方法跑在另一個線程**,該線程優先權較低 ```java= package GC; public class testFinalize { public static testFinalize intance = null; @Override public void finalize() { System.out.println("finalize work"); // 牽引回 GC Root intance = this; } public static void main(String[] args) throws InterruptedException { intance = new testFinalize(); intance = null; // First:手動 GC System.gc(); // 由於 finally Thread 優先權太低,所以延遲主線程 Thread.sleep(1000); if(intance == null) { System.out.println("First intance == null"); } else { System.out.println("First intance != null"); } intance = null; // Second:手動 GC System.gc(); Thread.sleep(1000); if(intance == null) { System.out.println("Second intance == null"); } else { System.out.println("Second intance != null"); } } } ``` **--實做結果--** > ![](https://i.imgur.com/wmISS8I.png) :::warning * **`Object#finalize` 方法也被遺棄**,不再推薦被使用 * 為何需要休眠一秒? **因為 finalize 線程是運行在別的線程**,優先權沒有主線程高,不一定會被先執行到,**==fialize 可以算是歷史的產物==** 如果沒有休眠可以看到下方的 `finalize` 根本來不及拯救 > ![](https://i.imgur.com/yo2ealp.png) ::: 3. **在 `finalize` 方法中拋出異常,GC 也不會有問題**(GC 會吞下這個異常) ```java= public class GCTest { private static GCTest gcTest; @Override protected void finalize() throws Throwable { System.out.println("GC - finalize"); gcTest = this; // 並不會產生異常,GC 會吞下這筆異常 throw new Exception("Test exception"); } public static void main(String[] args) throws InterruptedException { gcTest = new GCTest(); System.out.println("Before GC, GCTest: " + gcTest); gcTest = null; System.gc(); Thread.sleep(1000); System.out.println("After GC, GCTest: " + gcTest); } } ``` > ![](https://hackmd.io/_uploads/HJ-VMPs6n.png) ## Java 物件引用差異 JDK 1.2 後就有出現不同的引用方式,**強引用、軟引用、弱引用、虛引用**,以下會使用 `System.gc()` 強制會收並觀察其引用狀況 > 這些引用物件都在 `java.lang.ref` 套件 ### 強引用:GC 不回收 * 如果對象具有強引用 (new 物件),**GC 就絕對不會回收它,寧可拋出 OOM (`Out of Memory`) 異常也不會回收該對象** 來解決內存不足的問題 > 可以拿引用指針指向 null,促進 GC 回收(讓該對象不可達) ```java= Object o = new Object(); o = null; // 將引用指針指向 null ``` ### 參考佇列 ReferenceQueue 使用:追蹤回收 * 參考佇列 (`ReferenceQueue`) 可以使用在 **軟引用、弱引用、虛引用** 之上,**可以用來追蹤 JVM 回收所參考的物件的活動** 範例如下 ```java= void useWeakWithQueue() throws InterruptedException { String msg = new String("Apple"); ReferenceQueue<String> rq = new ReferenceQueue<>(); WeakReference<String> wf = new WeakReference<>(msg, rq); System.out.println("Before GC - WeakReference: " + wf.get()); System.out.println("Before GC - ReferenceQueue: " + rq.poll()); // 移除強參考(必須) msg = null; System.gc(); Thread.sleep(1000); System.out.println("Before GC - WeakReference: " + wf.get()); // 被回收後 wf 才加入 ReferenceQueue System.out.println("Before GC - ReferenceQueue: " + rq.poll()); } ``` 藉由結果我們可以看到,當物件被 GC 回收後,才會出現在 `ReferenceQueue` 列表中,如果物件尚未被回收則不會出現在列表 > ![](https://hackmd.io/_uploads/ryatowoT3.png) ### 軟引用 SoftReference * 當 **++內存不夠++ (OOM) 時就會 GC 軟引用對象**,並回收他們來解決內存不足的問題 :::info * 但如果 GC 回收後發現內存仍不足就會再往外拋出 ::: ```java= // 設定 VM Options: -Xmx20m -Xms20m public class testSoftRef { public static void main(String[] args) { String str = new String("Alien"); SoftReference<String> soft = new SoftReference<>(str); str = null; // 移除強引用 System.out.println("soft String: " + soft.get()); System.gc(); System.out.println("After GC, soft String: " + soft.get()); List<byte[]> list = new LinkedList<>(); try { // 100 個 1M 對象 for(int i = 0; i < 100; i++) { list.add(new byte[1024*1024]); } System.gc(); } catch (Throwable e) { System.out.println("OOM-----------: " + soft.get()); } } } ``` 1. **將強引用標記為軟引用** > 使用 `new SoftReference<>(str)` 2. **促進回收,將引用指向於 null,讓它離開 GC 判定的可達性區塊** > `str = null` :::success * 如何觸發 OOM 1. 由於 JVM 通常在 **內存足夠的狀況下不會回收 `SoftReference` 物件**,所以在這邊設定堆區的 `最大 -Xmx`、`-Xms 最小值` 2. 手動創建超出堆區大小的強引用(`list.add(new byte[1024*1024])`),就會發生 OOM > 這時 GC 就會被喚起去檢查 `SoftReference` 物件 ::: > ![](https://i.imgur.com/7haLsmB.png) ### 弱引用 WeakReference * 生命週期比軟引用更短,**每次 ++GC 發現後立刻回收++**,**不管內存是否足夠** (就算內存足夠也回收) > GC 回收的前提:該物件是不可達物件 ```java= import java.lang.ref.WeakReference; public class ReferenceTest { public static void main(String[] args) { String str = new String("Alien"); //"1. " WeakReference<String> weak = new WeakReference<>(str); System.out.println("weak Integer: " + weak.get()); System.gc(); System.out.println("After GC, weak Integer: " + weak.get()); //"2. " str = null; System.gc(); System.out.println("After GC & ptr to null, weak Integer: " + weak.get()); } } ``` 1. 將強引用標記為弱引用 > `new WeakReference<>(str)` 2. 促進回收,**將引用指向於 null (至其為不可打狀態) 並且再次回收才可回收掉** > 不管記憶體是否足夠,**只要觸發 GC 後,被發現就會直接回收** > > ![](https://i.imgur.com/vSewKWz.png) ### 虛引用 PhantomReference * **任何時候都可能被回收**,其目的是,**當被回收時會收到系統通知,功能是確認 GC 有正常運作** > 有可能你剛創建出 `PhantomReference` 引用物件後,立即就無法透過 `PhantomReference` 取得物件了 ```java= public class ReferenceTest { public static void main(String[] args) { String str = new String("Alien"); ReferenceQueue<String> queue = new ReferenceQueue<>(); PhantomReference<String> phantom = new PhantomReference<>(str, queue); System.out.println("phantom Integer: " + phantom.get()); System.gc(); System.out.println("After GC, phantom Integer: " + phantom.get()); str = null; System.gc(); System.out.println("After GC & ptr to null, phantom Integer: " + phantom.get()); } } ``` **--實做結果--** > ![](https://i.imgur.com/MJ1Yuhu.png) ## 其他知識點 ### 清除物件引用 * 通常當我們不需要一個物件時,最好有習慣去手動清理物件,這可以幫助 GC 對於物件的可達性分析判斷;這裡我們可以針對常見的兩種狀況做分析 1. **方法中創建的物件**: 這種物件的引用在 JVM 中的 Java 棧,而物件本身則在 Java 堆中;在離開該方法後,引用就不會再可達!這種就會自動回收 ```java= void showMessage(String... strings) { StringBuffer stringBuffer = new StringBuffer(); for (String string : strings) { stringBuffer.append(string).append("\t"); } System.out.println(stringBuffer.toString()); // 沒必要! // 因為離開該方法後該 StringBuffer 物件就會被回收(非即時) stringBuffer = null; } ``` 2. **方法中創建,但同時引用到類中(全局)**: ```java= class MessageHandler { private List<StringBuffer> history = new ArrayList<>(); void collectMessage(String... strings) { StringBuffer stringBuffer = new StringBuffer(); for (String string : strings) { stringBuffer.append(string).append("\t"); } // 將區域變量中的物件 加入到 全局變量,該物件就不會被回收(因為可達) history.add(stringBuffer); } String popMessage(int index) throws IllegalAccessException { if (index >= history.size()) { throw new IllegalAccessException(); } StringBuffer tmp = history.get(index); String result = tmp.toString(); // 移除全局變量對物件的引用 history.remove(tmp); // 手動解除引用 tmp = null; return result; } } ``` ### 不可變類別、可變類別:執行序的保護性複製 * **不可變類別**:通常是一個 Data Class,它負責攜帶資料,其重點是 1. 不可變類別成員皆為 `final` (Kotlin 中就是 `val`) **不可改變** 2. 由於它的不可變性,也就代表他是 **read only 資料** 3. 又由於它是 read only 資料,所以它 **執行序(Thread, 線程)安全** ```java= class ImmutableData { public final String message; public final String name; ImmutableData(String message, String name) { this.message = message; this.name = name; } } ``` * **可變類別**:與其相反的就是可變類別,它與不可變類別的特性就剛好相反,所以在多執行序(Thread, 線程)使用中要小心 ```java= class MutableData { public String message; public String name; } ``` :::success * **保護性複製**: 在某些情況之下,雖然我們的類別是不可變,但其實其成員內是可變的,所以我們要用保護性複製來保護不可變資料;範例如下 * 非安全性使用範例: ```java= public class CopyFinalObj { public static void main(String[] args) { unsafeOperation(); } public static void unsafeOperation() { LinkedList<String> myMessages = new LinkedList<>() { { push("HelloWorld"); } }; UnsafeMessageBox box = new UnsafeMessageBox(myMessages); System.out.println(box); myMessages.push("Yoyo123"); System.out.println(box); } } class UnsafeMessageBox { private final LinkedList<String> messages; public UnsafeMessageBox(LinkedList<String> message) { this.messages = message; } @Override public String toString() { return messages.toString(); } } ``` 外部操縱影響到內部資料 > ![](https://hackmd.io/_uploads/r1vFQx96n.png) * **保護性複製範例**: ```java= public class CopyFinalObj { public static void main(String[] args) { safeOperation(); } public static void safeOperation() { LinkedList<String> myMessages = new LinkedList<>() { { push("HelloWorld"); } }; SafeMessageBox box = new SafeMessageBox(myMessages); System.out.println(box); myMessages.push("Yoyo123"); System.out.println(box); } } class SafeMessageBox { private final LinkedList<String> messages; public SafeMessageBox(LinkedList<String> message) { // 保護性複製! this.messages = new LinkedList<>(message); } @Override public String toString() { return messages.toString(); } } ``` **外部操縱不會影響到內部資料** > ![](https://hackmd.io/_uploads/BkAB4g56n.png) ::: ## 更多的 Java 語言相關文章 ### Java 語言深入 * 在這個系列中,我們深入探討了 Java 語言的各個方面,從基礎類型到異常處理,從運算子到物件創建與引用細節。點擊連結了解更多! :::info * [**深入探索 Java 基礎類型、編碼、浮點數、參考類型和變數作用域 | 探討細節**](https://devtechascendancy.com/basic-types_encoding_reference_variables-scopes/) * [**深入了解 Java 應用與編譯:從原始檔到命令產出 JavaDoc 文件 | JDK 結構**](https://devtechascendancy.com/java-compilation_jdk_javadoc_jar_guide/) * [**深入理解 Java 異常處理:從基礎概念到最佳實踐指南**](https://devtechascendancy.com/java-jvm-exception-handling-guide/) * [**深入理解 Java 運算子與修飾符 | 重要概念、細節 | equals 比較**](https://devtechascendancy.com/java-operators-modifiers-key-concepts_equals/) * [**深入探索 Java 物件創建與引用細節:Clone 和finalize 特性,以及強、軟、弱、虛引用**](https://devtechascendancy.com/java-object-creation_jvm-reference-details/) ::: ### Java IO 相關文章 * 探索 Java IO 的奧秘,了解檔案操作、流處理、NIO等精彩內容! :::warning * [**Java File 操作指南:基礎屬性判斷、資料夾和檔案的創建、以及簡單示範**](https://devtechascendancy.com/java-file-operations-guide/) * [**深入探索 Java 編碼知識**](https://devtechascendancy.com/basic-types_encoding_reference_variables-scopes/) * [**深入理解 Java IO 操作:徹底了解流、讀寫、序列化與技巧**](https://devtechascendancy.com/deep-in-java-io-operations_stream-io/) * [**深入理解 Java NIO:緩衝、通道與編碼 | Buffer、Channel、Charset**](https://devtechascendancy.com/deep-dive-into-java-nio_buf-channel-charset/) ::: ### 深入 Java 物件導向 * 探索 Java 物件導向的奧妙,掌握介面、抽象類、繼承等重要概念! :::danger * [**深入比較介面與抽象類:從多個角度剖析**](https://devtechascendancy.com/comparing-interfaces-abstract-classes/) * [**深度探究物件導向:繼承的利與弊 | Java、Kotlin 為例 | 最佳實踐 | 內部類細節**](https://devtechascendancy.com/deep-dive-into-oop-inheritance/) * [**「類」的生命週期、ClassLoader 加載 | JVM 與 Class | Java 為例**](https://devtechascendancy.com/class-lifecycle_classloader-exploration_jvm/) ::: ## Appendix & FAQ :::info ::: ###### tags: `Java 基礎`