跳轉到內容

物件生命週期

75% developed
來自華夏公益教科書,開放的書籍,開放的世界

導航 類和物件 主題:v  d  e )

在建立 Java 物件之前,必須從檔案系統(副檔名為 .class)載入類位元組碼到記憶體中。此過程稱為類載入,即查詢給定類名的位元組碼,並將該程式碼轉換為 Java 例項。每個型別的 Java 類都有一個類被建立。

Java 程式中的所有物件都在堆記憶體中建立。物件是基於其類建立的。可以將類看作是建立物件的藍圖、模板或描述。當建立物件時,會分配記憶體來儲存物件的屬性。還會建立一個指向該記憶體位置的物件引用。為了將來使用該物件,必須將該物件引用儲存為區域性變數或物件成員變數。

Java 虛擬機器 (JVM) 會跟蹤物件引用的使用情況。如果不再有任何引用指向該物件,則該物件將無法再使用,併成為垃圾。一段時間後,堆記憶體將充滿未使用的物件。JVM 會收集這些垃圾物件並釋放它們佔用的記憶體,以便在建立新物件時可以再次使用該記憶體。請參見以下簡單示例

Example 程式碼部分 4.30:物件建立。
{
  // Create an object
  MyObject obj = new MyObject();

  // Use the object
  obj.printMyValues();
}

obj 變數包含指向從 MyObject 類建立的物件的物件引用。obj 物件引用在 { } 內的 作用域 中。在 } 之後,該物件將成為垃圾。物件引用可以作為引數傳遞給方法,也可以從方法中返回。

使用 new 關鍵字建立物件

[編輯 | 編輯原始碼]

99% 的新物件都是使用 new 關鍵字建立的。

Computer code 程式碼清單 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 位元組碼。該副檔名為 *.classCLASSPATH 環境變數包含儲存 Java 類的位置。JVM 正在尋找 MyObject.class 檔案。根據類所屬的包,包名將被轉換為目錄路徑。

找到 MyObject.class 檔案後,JVM 的類載入器會將該類載入到記憶體中,並建立一個 java.lang.Class 物件。JVM 會將程式碼儲存在記憶體中,為 static 變數分配記憶體,並執行任何靜態初始化塊。此時不會為物件成員變數分配記憶體,只有在建立類的例項(物件)時才會為它們分配記憶體。

可以從同一個類中建立任意多個物件,沒有限制。程式碼和 static 變數僅儲存一次,無論建立多少個物件。在建立物件時會為物件成員變數分配記憶體。因此,物件的尺寸不是由程式碼的尺寸決定,而是由儲存其成員變數所需的記憶體決定。

透過克隆物件建立物件

[編輯 | 編輯原始碼]

類不自動支援克隆。不過,也有一些幫助,因為所有 Java 物件都繼承了 protected Object clone() 方法。此基本方法會分配記憶體並逐位複製物件的狀態。

您可能會問為什麼我們需要這個 clone 方法。難道不能建立一個建構函式,傳入同一個物件,然後逐個變數進行復制嗎?例如(請注意,訪問 obj 的私有 memberVar 變數是合法的,因為這在同一個類中)

Computer code 程式碼清單 4.14:MyObject.java
public class MyObject {
   private int memberVar;
...
   MyObject(MyObject obj) {
      this.memberVar = obj.memberVar;
    ...
   }
...
}

這種方法可行,但使用new關鍵字建立物件非常耗時。clone()方法在一項操作中複製整個物件的記憶體,這比使用new關鍵字並複製每個變數快得多。因此,如果您需要建立許多相同型別的物件,建立單個物件並從其克隆新的物件將提高效能。請參閱下方使用克隆返回新物件的工廠方法。

Example 程式碼部分 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物件可克隆。

  1. 首先,Customer類需要實現Cloneable介面。
  2. 覆蓋並使clone()方法public,因為它是protectedObject類中。
  3. 在您的clone方法開始處呼叫super.clone()方法。
  4. 覆蓋Customer所有子類的clone()方法。
Computer code 程式碼清單 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物件是不夠的,我們還需要克隆引用的物件。解決方法是

  1. 使Activity類也可克隆
  2. 確保如果Activity類具有其他“可變”物件引用,則也必須克隆它們,如下所示
  3. 更改Customer類的clone()方法,如下所示
Computer code 程式碼清單 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;
   }
}

請注意,只有可變物件需要克隆。可以對克隆物件使用對不可變物件的引用(例如字串),而無需擔心。

重新建立從遠端源接收到的物件

[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 來仍然允許載入該類

Example 程式碼部分 4.32:序列化版本 ID。
private static final long serialVersionUID = 42L;

銷燬物件

[edit | edit source]

與許多其他面向物件的程式語言不同,Java 執行自動垃圾收集(任何未引用的物件都會自動從記憶體中刪除),並禁止使用者手動銷燬物件。

finalize()

[edit | edit source]

當物件被垃圾收集時,程式設計師可能希望手動執行清理,例如關閉任何開啟的輸入/輸出流。為此,使用finalize()方法。請注意,finalize()永遠不應該被手動呼叫,除非從派生類的finalize方法中呼叫超類的finalize方法。此外,我們無法依賴何時呼叫finalize()方法。如果 Java 應用程式在物件被垃圾收集之前退出,則可能永遠不會呼叫finalize()方法。

Example 程式碼部分 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目錄中的某個位置,會發生什麼情況?
  • 如何在應用程式伺服器的同一例項中同時使用同一實用庫的兩個不同版本?
  • 我當前使用的是實用庫的哪個版本?
  • 為什麼我需要處理所有這些類載入問題?


Clipboard

待辦事項
新增一些類似於變數中的練習


華夏公益教科書