物件生命週期
| 導航 類和物件 主題: |
在建立 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 物件引用在 { } 內的範圍內。在 } 之後,該物件將成為垃圾。物件引用可以傳遞給方法,也可以從方法返回。
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 變數僅儲存一次,無論建立了多少個物件。在建立物件時,會為物件成員變數分配記憶體。因此,物件的尺寸不是由程式碼的尺寸決定的,而是由儲存其成員變數所需的記憶體決定的。
克隆功能不是類的自動功能。但有一些幫助,因為所有 Java 物件都繼承了 protected Object 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目錄中的某個位置,會發生什麼?
- 如何在應用程式伺服器的同一個例項中同時使用相同實用程式庫的兩個不同版本?
- 我當前使用的是哪個版本的實用程式類?
- 為什麼我需要以這種方式使用所有這些類載入內容?
