Overview of Content
在Java虛擬機(JVM)中,物件的建立方式和引用關係至關重要
這篇文章將帶您深入了解Java中創建物件的方式,包括 Clone 特性、建構函數和反序列化。我們還將探討觸發JVM垃圾回收的方法,並詳細介紹finalize方法的特性。
此外,我們會深入探討Java中不同類型的物件引用,包括強引用、軟引用、弱引用和虛引用,以及它們之間的差異和使用情況。無論您是Java開發新手還是有經驗的專業人士,這篇文章都將為您提供深入且實用的知識,幫助您更好地理解和應用Java中的物件創建和引用概念
在 JVM 中,物件的通常都會建立在堆區,而堆區也是 GC 最常處理的區塊;以物件的生命週期來說,在堆區建立後就是”生“,被 GC 回收後就是 “死”(以下的內存就是記憶體)
寫文章分享不易,如有引用參考請詳註出處,如有指導、意見歡迎留言(如果覺得寫得好也請給我一些支持),感謝 😀
Java 建立物件的方式
在 Java 中有多種建立物件的方式,而不同的物件建立方式又會有不同的特性;分別有以下方法
A. 使用 new
關鍵字:最常見的方案
B. 使用 JVM Bytecode 反射創建:在設計、使用框架時常見;通常會使用到 java.lang.Class
、java.lang.Constructor
這兩個類
C. 使用 Clone 機制:實作 Cloneable 這個標示界面
D. 反序列化:這個方案較少見;通常會使用到 java.io.ObjectInputStream
類來將物件實體化
● 物件的建立前提是類已經被 ClassLoader 加載進方法區,並初始化完成
Clone 特性
● Object#clone
方法:它是 Object 中的方法,它的源碼實現如下(以下我們看的是 Java 15
的源碼)
// Object.java
public class Object {
... 省略部份
protected native Object clone() throws CloneNotSupportedException;
}
我們可以看到 1. clone
是一個 Native 方法,代表他是在 Native 層被實作的,2. 它是 protected
方法,並且 3. 可能會拋出 CloneNotSupportedException
● 關於第二點與 Cloneable
標示介面 有關係
// java.lang.Cloneable
public interface Cloneable {
}
● 標示界面 是甚麽?
標示介面通常是拿來判斷的,標示界面並不具有任何需要實作的方法
●
Protected
方法介於它是 Protected 方法,所以如果要讓外部呼叫,我們可以使用
overload
的技巧,對外開放
● 如果我們呼叫 clone
方法,但並沒有實作 Cloneable
介面,那就會拋出 CloneNotSupportedException
異常
class NoCloneable {
@Override
public NoCloneable clone() {
try {
return (NoCloneable) super.clone();
} catch (CloneNotSupportedException e) {
// 例外轉譯
throw new AssertionError();
}
}
}
● 只要我們標示該介面是 Cloneable
就不會拋出異常
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 物件
● 參考類型:參考類型(ref
)像是物件、陣列;JVM 會直接複製該參考引用,而不會自動幫你創建一個新的參考物件!也就是說 clone 出來的物件,會與原先的物件使用相同的引用!
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 注意事項:不經建構函數
● 對於 使用 clone
方法來創過的物件,是 完全不會經過建構函數,它會直接拷貝記憶體內容!
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.io.Serializable
public interface Serializable {
}
● 反序列化會拷貝原先物件的參考引用物件 ref,但在改變參考引用物件 ref 的數值後,JVM 會自動幫我們創建一個新的物件
這類似一種 Copy on write 的技術
● 示意圖如下,在更改物件之前,指向同一個參考物件
● 示意圖如下,在更改物件之後,指向全新的參考物件
● 反序列化後,會創建一個全新的物件,有新的記憶體位置(以引用來對照是不同的物件,但內容值相同),這種用法請特別注意只能用在「值物件」
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:觀察物件回收
● Java GC 是否回收物件,這會有關到 JVM 判斷物件的 可達性分析 機制,當物件不可達時,就會被認定該物件不再使用,這時就可以回收
JVM 又會因為 Java 引用的差異而會有不同的處理方式
● 有關於 手動觸發 GC
當我們使用
System.gc()
、Runtime.getRuntime().gc()
並非是直接驅動立即開始 GCGC 是另外一個低優先度的線程去處理的行為,所以通常我們測試時會使用 Thread#sleep 讓主線程休眠,等待 GC 回收
finalize 方法、特性
● 該方法的原形在 Object.java
中,可以用來拯救被回收的對象,有以下特點
A. 是只能拯救回收的對象一次 (一次 new 會有一次 finalize 機會,拯救後如果再次被加入回收列表,就不會再通知!)
B. 運行 finalize
的方法跑在另一個線程,該線程優先權較低
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");
}
}
}
--實做結果--
●
Object#finalize
方法也被遺棄,不再推薦被使用● 為何需要休眠一秒?
因為 finalize 線程是運行在別的線程,優先權沒有主線程高,不一定會被先執行到,fialize 可以算是歷史的產物
如果沒有休眠可以看到下方的
finalize
根本來不及拯救
C. 在 finalize
方法中拋出異常,GC 也不會有問題(GC 會吞下這個異常)
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
) 異常也不會回收該對象 來解決內存不足的問題
Object o = new Object();
o = null; // 將引用指針指向 null
參考佇列 ReferenceQueue 使用:追蹤回收
可以拿引用指針指向 null,促進 GC 回收(讓該對象不可達)
● 參考佇列 (ReferenceQueue
) 可以使用在 軟引用、弱引用、虛引用 之上,可以用來追蹤 JVM 回收所參考的物件的活動
範例如下
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 軟引用對象,並回收他們來解決內存不足的問題
● 但如果 GC 回收後發現內存仍不足就會再往外拋出
// 設定 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());
}
}
}
A. 將強引用標記為軟引用
使用
new SoftReference<>(str)
B. 促進回收,將引用指向於 null,讓它離開 GC 判定的可達性區塊
str = null
● 如何觸發 OOM
A. 由於 JVM 通常在 內存足夠的狀況下不會回收
SoftReference
物件,所以在這邊設定堆區的最大 -Xmx
、-Xms 最小值
B. 手動創建超出堆區大小的強引用(
list.add(new byte[1024*1024])
),就會發生 OOM這時 GC 就會被喚起去檢查
SoftReference
物件
弱引用 WeakReference
● 生命週期比軟引用更短,每次 GC 發現後立刻回收,不管內存是否足夠 (就算內存足夠也回收)
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());
}
}
GC 回收的前提:該物件是不可達物件
A. 將強引用標記為弱引用
new WeakReference<>(str)
B. 促進回收,將引用指向於 null (至其為不可打狀態) 並且再次回收才可回收掉
不管記憶體是否足夠,只要觸發 GC 後,被發現就會直接回收
虛引用 PhantomReference
● 任何時候都可能被回收,其目的是,當被回收時會收到系統通知,功能是確認 GC 有正常運作
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());
}
}
有可能你剛創建出
PhantomReference
引用物件後,立即就無法透過PhantomReference
取得物件了
--實做結果--
其他知識點
清除物件引用
● 通常當我們不需要一個物件時,最好有習慣去手動清理物件,這可以幫助 GC 對於物件的可達性分析判斷;這裡我們可以針對常見的兩種狀況做分析
A. 方法中創建的物件:
這種物件的引用在 JVM 中的 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;
}
B. 方法中創建,但同時引用到類中(全局):
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,它負責攜帶資料,其重點是
A. 不可變類別成員皆為 final
(Kotlin 中就是 val
) 不可改變
B. 由於它的不可變性,也就代表他是 read only 資料
C. 又由於它是 read only 資料,所以它 執行序(Thread, 線程)安全
class ImmutableData {
public final String message;
public final String name;
ImmutableData(String message, String name) {
this.message = message;
this.name = name;
}
}
● 可變類別:與其相反的就是可變類別,它與不可變類別的特性就剛好相反,所以在多執行序(Thread, 線程)使用中要小心
class MutableData {
public String message;
public String name;
}
● 保護性複製:
在某些情況之下,雖然我們的類別是不可變,但其實其成員內是可變的,所以我們要用保護性複製來保護不可變資料;範例如下
● 非安全性使用範例:
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(); } }
外部操縱影響到內部資料
● 保護性複製範例:
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 語言的各個核心主題,旨在幫助你徹底掌握這門強大的編程語言。無論你是想深入理解 Java 的基礎類型與變數作用域,還是探索異常處理與運算子的細節,這些文章都將為您提供寶貴的知識
此外,我們還涵蓋了物件創建、函數式編程、註解應用以及泛型的深入分析,幫助您提升在實際開發中的技能和效率
點擊以下連結,開始你的學習之旅~
● 深入探索 Java 基礎類型、編碼、浮點數、參考類型和變數作用域 | 探討細節
● 深入了解 Java 應用與編譯:從原始檔到命令產出 JavaDoc 文件 | JDK 結構
● 深入理解 Java 運算子與修飾符 | 重要概念、細節 | equals 比較
● 深入探索 Java 物件創建與引用細節:Clone 和finalize 特性,以及強、軟、弱、虛引用
● 認識 Java 函數式編程:從 Lambda 表達式到方法引用 | 3 種方法引用
Java IO 相關文章
● 探索 Java IO 的奧秘,了解檔案操作、流處理、NIO等精彩內容!
深入 Java 多執行緒
● 這一系列文章將帶你深入了解 Java 多執行緒技術的各個方面,從基礎知識到進階應用,涵蓋了多執行緒編程的核心概念與實踐
深入 Java 物件導向
● 探索 Java 物件導向的奧妙,掌握介面、抽象類、繼承等重要概念!幫助你針對物件導向設計有更深入的了解!