天天看點

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技術采用類似方法,代碼每次執行多會做一些優化,執行次數越多,速度就越快。