1. 程式人生 > >Java程式設計思想之清理與初始化

Java程式設計思想之清理與初始化

Java資料儲存

物件存在何處,C++認為效率控制是最重要的議題,所以給程式設計師提供了選擇的權利。為了追求最大的執行速度,物件的儲存空間與生命週期可以在編寫程式時確定,可以將物件置於堆疊,限域變數或者靜態儲存區。
第二種方式是在被稱為堆的記憶體池中動態建立物件,在這種方式中,知道執行時才知道需要多少物件。

物件生命週期。對於允許在堆疊中建立物件的語言,編譯器可以確定物件存活的時間,並自動銷燬它。而對於堆上建立的物件,編譯器則對它的生命週期一無所知。

Java物件儲存位置

  • 暫存器。最快的儲存區,不受控制
  • 堆疊。位於通用RAM中,通過堆疊指標可以從處理器哪裡直接獲得支援。堆疊支援向下移動,則分配新的記憶體;向上移動,則釋放那些記憶體。這是一種快速有效的分配儲存方式,僅次於暫存器。建立程式時Java必須知道堆疊內所有項的生命週期,物件引用存於其中,Java物件並不儲存其中。
  • 堆。一種通用的記憶體池,也位於RAM,用於存放所有的Java物件。相比棧,編譯器不需要知道堆中資料的存活時間,具有很大的靈活性。但這種靈活性也有很大代價,用堆進行儲存分配和清理比棧需要更長的時間。
  • 常量儲存。常量值通常直接存放在程式程式碼內部,因為常量永遠不會改變。
  • 非RAM儲存。如果資料完全存活於程式之外,那麼它可以不受程式的任何控制,在程式沒有執行時也可以存在。兩個基本粒子是流物件和持久化物件。
  • 特例:基本型別。對於一些基本型別,Java採用和C++相同方法,不用new建立變數,而是建立一個並非是引用的“自動”變數。這個變數直接儲存“值”,並置於堆疊中,因此更加高效。
  • 特例:Java中的陣列。當建立一個數組物件,實際建立了一個引用陣列,並且每個引用都會自動被初始化為一個特定值null。當然,還可以建立存放基本資料型別的陣列,編譯器也能確保這種陣列的初始化。

finalize方法

Java垃圾回收器只知道如何釋放那些由new分配的記憶體,對於非使用new建立的特殊記憶體區域(比如Java本地方法,呼叫C/C++),垃圾回收器不能有效的進行釋放。
在這種情況下,Java提供了finalize方法。
這裡與C++的解構函式進行對比。在C++中,解構函式使得物件一定會被銷燬,而Java中呼叫finalize並非總能被垃圾回收。

  • 物件可能不被垃圾回收
  • 垃圾回收並不等於析構
  • 垃圾回收只與記憶體有關

使用垃圾回收器的唯一原因就是為了回收程式不再使用的內容,所以對於垃圾回收有關的任何行為(尤其finalize方法),它們也必須與記憶體及其回收有關。
無論物件如何建立,垃圾回收期都會負責釋放物件佔據的所有記憶體。所以一般情況下都不需要通過finalize方法釋放。除非某些特殊情況,例如本地方法呼叫C/C++,malloc的記憶體需要在finalize方法中呼叫free釋放。
所以,不要過多的使用finalize方法。

finalize方法可以用來驗證物件終結條件。
比如對於一批書籍,new Book生成的物件釋放的條件是這本書需要錄入系統,但如果存在某些書籍在沒有錄入系統的情況下被回收了可以使用finalize方法查出異常。

protected void finalize() {
    if(checkedOut)
        System.out.println("Error: check out");
}

初始化

靜態資料的初始化
靜態資料初始化只有在必要時刻才會進行,此後,靜態物件不會再次被初始化。
初始化的順序是先靜態物件,而後是非靜態物件。比如建立了一個Dog的類:

  • 即使沒有顯式使用static,構造器實際也是靜態方法。因此首次建立Dog物件時,或者Dog類的靜態方法/域被訪問時,Java直譯器必須查詢類路徑,定位Dog.class檔案
  • 載入Dog.class,有關靜態初始化的所有動作都會執行,並且靜態初始化僅執行一次
  • 當用new Dog()建立物件時,在堆上為Dog物件分配足夠的儲存空間
  • 這塊儲存空間清零(基本型別賦預設值,引用賦null)
  • 執行所有出現於欄位定義處的初始化動作
  • 執行構造器

陣列初始化
所有陣列都有一個固定成員,通過它可以獲得陣列內包含了多少個元素,但不能進行修改。length

垃圾回收機制

一般程式語言在堆上分配物件的代價十分高昂,然後對於Java,垃圾回收器對於提高物件的建立速度具有明顯效果。Java從堆分配空間的速度,可以和其他語言從棧中分配空間的速度相媲美。

打個比方,C++的堆可以想象一個院子,裡面每個物件都負責管理自己的地盤,一段時間後,物件可能被銷燬,但地盤必須加以重用;而對於Java,堆更像一個傳送帶,每分配一個新物件,它就往前移動一格,這意味著物件儲存空間分配速度非常快,效率可以比得上C++棧上分配空間。

Java的堆並不完全像傳送帶那樣工作,不然會導致頻繁的記憶體頁面排程,嚴重影響效能。其中的祕密在於垃圾回收器的介入,當它工作時,將一面回收空間,一面使堆中的物件緊湊,這樣堆指標很容易移動到更靠近傳送帶的開始處。

現在理解其他語言的垃圾回收機制,引用計數器是一種簡單但速度很慢的方法。每個物件都有一個引用計數器,當有引用連線物件時,引用計數器+1,當引用離開作用域或置null時,-1。雖然引用計數開銷不大,但這項開銷在整個程式生命週期持續發生,垃圾回收器在含有全部物件的列表遍歷,某個物件引用為0是釋放空間。這種方法有個缺陷,如果物件之間存在迴圈引用,可能出現“物件應該被回收,但引用計數不為0”。所以引用計數這種方式常常用來說明垃圾回收機制,並未被真正使用。

在一些更快的模式中,垃圾回收器並非基於引用計數技術,它們依據的思想是:對任何“活”的物件,一定能最終追溯到其存活在堆疊或靜態儲存區之中的引用。這個引用鏈可能穿過數個物件層次。因此,從堆疊和靜態儲存區開始,遍歷所有的引用,就能找到“活”的物件。

在這種方式下,Java虛擬機器採用了一種自適應的垃圾回收技術。至於如何處理存活的物件,取決於不同的Java虛擬機器實現。有一種做法“停止-複製(stop-and-copy)”先暫停程式的執行,然後將所有存活的物件從當前堆複製到另一個堆,沒有被複制的全是垃圾。

對於這種複製式回收器而言,效率很低。

  • 兩個堆倒騰,需要雙倍空間。某些虛擬機器解決方法是,按需從堆中分配幾塊較大的記憶體,複製動作發生在這些大塊記憶體之間。
  • 複製本身。程式穩定後可能只產生了少量垃圾,甚至沒有垃圾。複製必然會造成浪費。解決方法是如果沒有新垃圾產生,會切換到另一種模式(自適應)“標記-清掃(mark-and-sweep)”

“標記-清掃”思路同樣是從堆疊和靜態儲存區觸發,遍歷所有引用,找到存活物件進行標記。標記完成後對沒有標記的物件進行清理。這樣剩下的堆空間可能不連續,垃圾回收器希望得到連續空間的話需要重新整理剩下的物件。

“停止-複製”,“標記-清掃”都必須在程式暫停的情況下進行。

Java虛擬機器中,記憶體分配以較大的“塊”為單位,如果物件較大,會佔用單獨的“塊”。“停止-複製”要求釋放物件前把所有存活物件從舊堆複製到新堆,這會造成大量記憶體複製行為。有了塊以後,垃圾回收器在回收時可以往廢棄的塊拷貝物件。每個塊都有相應的代數來記錄是否存活,某個塊被引用,代數增加。垃圾回收器對上次回收動作之後新分配塊進行整理,大型物件不會被複制,小型物件的那些塊則被複制並整理。如果所有物件都很穩定,垃圾回收器的效率低的話,就切換到“標記-清掃”,如果堆中出現很多碎片,則切換回“停止-複製”方式,這就是“自適應”技術。

Java虛擬機器有很多附加技術用以提升速度,尤其與載入器操作相關的,被稱為“即時”(Just-In-Time,JIT)編譯器的技術。這種技術把程式全部或部分翻譯成本地機器碼,程式執行速度得以提升。當需要裝載某個類,編譯器首先找到其class檔案,然後將該類的位元組碼載入記憶體。此時,有兩種方法選擇

  • 即時編譯器編譯所有程式碼。缺陷很明顯:載入動作散落在整個程式生命週期,累計起來耗時更大;增加可執行程式碼的長度,導致頁面排程,降低程式速度
  • 惰性評估,即時編譯器只需要在必要的時候編譯程式碼,這樣不執行的程式碼不會被JIT編譯。

新版Jdk中的Java Hotspot技術採用類似方法,程式碼每次執行多會做一些優化,執行次數越多,速度就越快。