---
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();
}
}
}
```
> 
* 只要我們標示該介面是 `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)));
}
}
```
> 
### 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);
}
}
```
> 
### 建構函數 - 反序列化
* 序列化/反序列化物件是一種比較少見的行為,但仍是一種創建物件的方式;它的特性如下
* 序列化物件 **必須實作 `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();
}
}
}
```
> 
## 觸發 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");
}
}
}
```
**--實做結果--**
> 
:::warning
* **`Object#finalize` 方法也被遺棄**,不再推薦被使用
* 為何需要休眠一秒?
**因為 finalize 線程是運行在別的線程**,優先權沒有主線程高,不一定會被先執行到,**==fialize 可以算是歷史的產物==**
如果沒有休眠可以看到下方的 `finalize` 根本來不及拯救
> 
:::
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);
}
}
```
> 
## 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` 列表中,如果物件尚未被回收則不會出現在列表
> 
### 軟引用 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` 物件
:::
> 
### 弱引用 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 後,被發現就會直接回收**
>
> 
### 虛引用 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());
}
}
```
**--實做結果--**
> 
## 其他知識點
### 清除物件引用
* 通常當我們不需要一個物件時,最好有習慣去手動清理物件,這可以幫助 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();
}
}
```
外部操縱影響到內部資料
> 
* **保護性複製範例**:
```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();
}
}
```
**外部操縱不會影響到內部資料**
> 
:::
## 更多的 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 基礎`