物件生命週期
| 導航 類和物件 主題: |
在建立 Java 物件之前,必須從檔案系統(使用 .class 副檔名)將類位元組碼載入到記憶體中。此過程被稱為類載入,它包括找到給定類名的位元組碼並將該程式碼轉換為 Java 類 例項。對於每種型別的 Java 類,都會建立一個類。
Java 程式中的所有物件都在堆記憶體中建立。物件是根據其類建立的。您可以將類視為藍圖、模板或建立物件的描述。當建立物件時,會分配記憶體來儲存物件屬性。還會建立一個指向該記憶體位置的物件引用。為了將來使用該物件,必須將該物件引用儲存為區域性變數或物件成員變數。
Java 虛擬機器 (JVM) 會跟蹤物件引用的使用情況。如果沒有更多引用指向該物件,則該物件就無法再使用,併成為垃圾。一段時間後,堆記憶體中會充滿了未使用的物件。JVM 會收集這些垃圾物件並釋放它們分配的記憶體,以便在建立新物件時可以再次使用該記憶體。請參見下面一個簡單的示例
程式碼部分 4.30:物件建立。
{
// Create an object
MyObject obj = new MyObject();
// Use the object
obj.printMyValues();
}
|
obj 變數包含指向從 MyObject 類建立的物件的物件引用。obj 物件引用在 { } 內具有作用域。在 } 之後,該物件將成為垃圾。物件引用可以傳遞到方法中,也可以從方法中返回。
使用 new 關鍵字建立物件
[edit | edit source]99% 的新物件都是使用 new 關鍵字建立的。
程式碼清單 4.13:MyProgram.java
public class MyProgram {
public static void main(String[] args) {
// Create 'MyObject' for the first time the application is started
MyObject obj = new MyObject();
}
}
|
當第一次從 MyObject 類建立物件時,JVM 會搜尋檔案系統以查詢類的定義,即 Java 位元組碼。該檔案具有 *.class 的副檔名。CLASSPATH 環境變數包含 Java 類儲存的位置。JVM 正在查詢 MyObject.class 檔案。根據類所屬的包,包名將被轉換為目錄路徑。
當找到 MyObject.class 檔案時,JVM 的類載入器會將該類載入到記憶體中,並建立一個 java.lang.Class 物件。JVM 將程式碼儲存在記憶體中,為 static 變數分配記憶體,並執行任何靜態初始化塊。此時不會為物件成員變數分配記憶體,而是在建立類的例項(物件)時為它們分配記憶體。
可以建立任意多個來自同一類的物件。程式碼和 static 變數只儲存一次,無論建立多少個物件。當建立物件時,會為物件成員變數分配記憶體。因此,物件的尺寸不是由其程式碼的尺寸決定的,而是由它儲存成員變數所需的記憶體決定的。
透過克隆物件建立物件
[edit | edit source]克隆功能不是所有類都自動具備的。不過有一些幫助,因為所有 Java 物件都繼承了 protected Object clone() 方法。此基本方法會分配記憶體並逐位複製物件的狀態。
您可能會問為什麼要使用此 clone 方法。我們不能建立建構函式,傳入同一個物件並逐個變數進行復制嗎?例如(請注意,訪問 obj 的私有 memberVar 變數是合法的,因為這是在同一個類中)
程式碼清單 4.14:MyObject.java
public class MyObject {
private int memberVar;
...
MyObject(MyObject obj) {
this.memberVar = obj.memberVar;
...
}
...
}
|
此方法有效,但使用 new 關鍵字建立物件很耗時。clone() 方法在一個操作中複製整個物件的記憶體,這比使用 new 關鍵字並複製每個變數快得多。因此,如果您需要建立大量相同型別的物件,那麼使用克隆方法建立第一個物件,然後克隆它來建立新的物件,效能會更好。請參見下面使用克隆返回新物件的工廠方法。
程式碼部分 4.31:物件克隆。
HashTable cacheTemplate = new HashTable();
...
/** Clone Customer object for performance reason */
public Customer createCustomerObject() {
// See if a template object exists in our cache
Customer template = cacheTemplate.get("Customer");
if (template == null) {
// Create template
template = new Customer();
cacheTemplate.put("Customer", template);
}
return template.clone();
}
|
現在,讓我們看看如何使 Customer 物件可克隆。
- 首先,
Customer類需要實現Cloneable介面。 - 覆蓋並使
clone()方法為public,因為在 Object 類中它是protected。 - 在您的
clone方法開頭呼叫super.clone()方法。 - 在所有
Customer子類中覆蓋clone()方法。
程式碼清單 4.15: Customer.java
public class Customer implements Cloneable {
...
public Object clone() throws CloneNotSupportedException {
Object obj = super.clone();
return obj;
}
}
|
在 程式碼清單 4.15 中,我們使用克隆來加快物件建立速度。克隆的另一個用途是獲取可以隨時間變化的物件的快照。假設我們想將 Customer 物件儲存在集合中,但我們希望將它們與“即時”物件分離。因此,在新增物件之前,我們克隆它們,這樣如果原始物件從那時起發生更改,新增的物件就不會發生更改。另外,假設 Customer 物件引用一個 Activity 物件,該物件包含客戶活動。現在我們遇到了一個問題,克隆 Customer 物件還不夠,我們還需要克隆引用的物件。解決方案
- 使 Activity 類也可克隆
- 確保如果 Activity 類有其他“可變”物件引用,那麼這些引用也必須被克隆,如下所示
- 更改 Customer 類的
clone()方法,如下所示
程式碼清單 4.16: Customer.java
public class Customer implements Cloneable {
Activity activity;
...
public Customer clone() throws CloneNotSupportedException {
Customer clonedCustomer = (Customer) super.clone();
// Clone the object referenced objects
if (activity != null) {
clonedCustomer.setActivity((Activity) activity.clone());
}
return clonedCustomer;
}
}
|
注意,只有可變物件需要被克隆。對不可變物件的引用,例如 String,可以在克隆物件中使用,無需擔心。
重新建立從遠端源接收的物件
[edit | edit source]當物件透過網路傳送時,需要在接收主機上重新建立該物件。
- 物件序列化
- 術語物件序列化是指將物件轉換為位元組流的過程。位元組流可以儲存在檔案系統上,也可以透過網路傳送。
- 在以後,可以從該位元組流重新建立物件。唯一的要求是,在序列化物件和重新建立物件時,必須使用相同的類。如果這發生在不同的伺服器上,那麼這兩臺伺服器上都必須存在相同的類。相同的類意味著必須存在完全相同版本的類,否則將無法重新建立物件。對於那些使用 Java 序列化來使物件持久化或透過網路傳送物件的應用程式來說,這是一個維護問題。
- 當修改類時,可能會遇到使用早期版本的類序列化的物件的重新建立問題。
Java 透過使用 Serializable 介面內建支援序列化;但是,類首先必須實現 Serializable 介面。
預設情況下,當類轉換為資料流時,它將序列化其所有欄位(transient 欄位除外)。如果需要超出寫入所有欄位預設值的額外處理,您需要為以下三種方法提供實現
private void writeObject(java.io.ObjectOutputStream out) throws IOException;
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;
如果物件需要在序列化期間寫入或提供替換物件,則需要實現以下兩種方法,可以使用任何訪問說明符
Object writeReplace() throws ObjectStreamException;
Object readResolve() throws ObjectStreamException;
通常,對類的微小更改會導致序列化失敗。您可以透過定義序列化版本 ID 來允許載入該類
程式碼部分 4.32:序列化版本 ID。
private static final long serialVersionUID = 42L;
|
銷燬物件
[edit | edit source]與許多其他面向物件的程式語言不同,Java 執行自動垃圾回收——任何未引用的物件都會自動從記憶體中刪除——並且禁止使用者手動銷燬物件。
finalize()
[edit | edit source]當物件被垃圾回收時,程式設計師可能希望手動執行清理操作,例如關閉所有開啟的輸入/輸出流。要實現此目的,可以使用 finalize() 方法。請注意,finalize() 永遠不應該被手動呼叫,除非從派生類的 finalize 方法中呼叫超類的 finalize 方法。此外,我們無法依賴 finalize() 方法的呼叫時間。如果 Java 應用程式在物件被垃圾回收之前退出,則 finalize() 方法可能永遠不會被呼叫。
程式碼部分 4.33:終結。
protected void finalize() throws Throwable {
try {
doCleanup(); // Perform some cleanup. If it fails for some reason, it is ignored.
} finally {
super.finalize(); // Call finalize on the parent object
}
}
|
垃圾回收執行緒的優先順序低於其他執行緒。如果應用程式建立物件的速率快於垃圾回收器回收記憶體的速率,則程式可能會耗盡記憶體。
只有在需要清理超出 Java 虛擬機器直接控制的資源時才需要 finalize 方法。特別是,不需要顯式關閉 OutputStream,因為 OutputStream 在被終結時會自動關閉。相反,finalize 方法用於釋放類控制的本地或遠端資源。
類載入
[edit | edit source]編寫熱部署應用程式的開發人員最關心的問題之一是瞭解類載入機制的工作原理。在類載入機制的內部,可以找到以下問題的答案
- 如果我在應用程式中打包了一個更新版本的實用程式庫,而相同庫的舊版本仍然存在於伺服器的 lib 目錄中,會發生什麼?
- 如何在應用程式伺服器的同一例項中同時使用相同實用程式庫的兩個不同版本?
- 我當前使用的是哪個版本的實用程式類?
- 為什麼我需要以這種方式處理所有這些類載入問題?
