天天看點

JVM Heap 堆

一個java應用在運作中所建立的所有類執行個體或數組都放在了同一個堆中,并由應用所有的線程共享。因為一個java應用

唯一對應了一個jvm執行個體,是以每個應用都獨占了一個堆,它不可能對另一個應用的堆進行破壞。然而,一個多線程應用必須考慮同步問題。

jvm有在堆中配置設定對象的指令,卻沒有釋放對象的指令。正如你無法用java代碼去釋放一個對象一樣,位元組碼也沒有對應的

功能。應用本身不用去考慮何時和用什麼方法去回收不用對象所占用的記憶體。通常,jvm把這個任務交給垃圾收集器。

垃圾收集

一個垃圾收集器的主要工作是回收不再被引用的對象所占用的記憶體。它也可能去移動仍然使用的對象以減少記憶體碎片。

jvm規範沒有指定垃圾收集使用什麼技術,這些都由jvm的實作者去定奪。因為對象的引用可能存在很多地方,如java堆棧,堆,方法區,native方法棧。是以垃圾收集技術的使用在很大程度上影響了運作資料區的設計。象方法區一樣,堆不必是一塊連續的記憶體區,也可以根據應用的需要動态調整大小。可以把方法區放在堆的頂部,換句話說就是類型資訊和實際

對象都在同一個堆上。負責清理對象的垃圾收集器可能也要負責類的回收。堆的初始化大小,最大最小尺寸可以由使用者或程式指定。

對象表現( Object Representation)

(譯者:C++中稱為對象模型)jvm規範沒有規定對象在堆中該如何表現。對象的表現會影響堆和垃圾收集的整個設計,它由jvm的實作者決定。對象的主要資料是由對應類和其父類聲明的執行個體變量組成(instance variables 譯者:對應Class variables, Class variables存儲在方法區中,這在上篇譯文中有講)jvm應該既能夠從一個對象引用快速的找到執行個體變量,也能夠快速的找到存儲在方法區中的類資料。是以在對象中常常會有一個指向

方法區的指針。

一個可能的實作是把堆分成兩部分:一個句柄池和一個對象池。如圖5-5一個對象引用是一個指向句柄池的native指針。句柄池的每個條目有兩部分:一個指向對象執行個體變量的指針,一個指向方法區類型資料的指針。這種設計的好處是利于堆碎片的整理,當移動對象以減少碎片時不用更新每個對象引用而隻更改句柄就可以了。缺點是每次通路對象都要經過兩次

指針傳遞。

另一種設計是使對象指針直接指向對象執行個體變量,而在對象執行個體内包含一個指向方法區類型資料的指針。這樣的設計的優缺點正好與前面的方法相反。如圖5-6.

jvm有若幹理由使它能夠從對象引用中得到對應類的資料。

1。 當應用試圖轉型(cast)時,jvm需要保證要轉的類型是此類型本身

    或是這個類型的父類型。

2。 當應用進行 instanceof 操作時

3。 當應用激活一個執行個體方法時,jvm必須進行動态綁定,而它所依賴的資訊

    不是這個引用的類型,而是這個對象對應的類的資訊。    不管對象以什麼形式表現,好像都有一個能夠友善通路的方法表。由于方法表能加速執行個體方法的調用,是以對jvm的性能有重要的影響。jvm規範并沒有規定必須要使用方發表,例如在記憶體稀少的環境下,可能不能負擔方法表的記憶體支出。

然而如果使用了方法表,它就應該能夠快速的從一個對象引用中獲得。圖5-7顯示了一種連結方法表和對象引用的實作。每個對象的資料包含一個指向特殊資料結構的指針,這個資料結構位于方法區,它包括兩部分:

     一 一個指向方法區對應類資料的指針

    二 此對象的方法表

方法表的每一項都是一個指向方法資料的指針,方法資料包括:

   一 此方法的操作數堆棧和局部變量區的大小

   二 方法的位元組碼

   三 異常表

這些資訊足夠jvm去激活一個方法了。方法表的函數指針包括類或其父類聲明的函數。也就是說,方法表所指向的函數可能是此類聲明的,也可能是它繼承下來的。

如果你熟悉c++的内部工作原理,你會發現這和c++的vtbl非常相似。在c++中,對象由執行個體資料和一組指向虛拟函數的指針組成,jvm也可以采用這種方法。jvm可以在堆中為每個對象都附加一個方法表,這樣較之圖5-7會占用更多的記憶體,

但可提高一些效率。這個方案适用在記憶體足夠充裕的系統。(譯者:總覺得作者對c++有些誤解,c++的對象模型在函數表上的設計和圖5-7是類似的,在有虛拟函數的情況下(不考慮多繼承),每個對象也隻多出一個指向vtable的指針,而vtable也是與類關聯的。)

除了圖5-5和5-6顯示的執行個體資料外,對象資料還有一個邏輯部分,那就是對象鎖(object's lock)。在jvm中每個對象都有一個鎖,以用于多線程通路時的同步。在某個時刻隻有一個線程擁有這個對象鎖,而且隻有這個線程才可以對對象的資料進行通路。其他要通路這個對象的線程隻有等待,直到擁有對象鎖的線程釋放所有權。當一個線程擁有對象鎖後,可以繼續對鎖追加請求。但請求幾次,必須對應釋放幾次。

許多對象在其生命期内可能不需要加鎖,這樣也不需要附加資料,正如圖5-5 5-6所示,對象資料内沒有一個指向鎖資料(lock data)的指針。而隻有當需要加鎖的時候才配置設定對應鎖資料,但這時需要其他的方法來聯系對象資料和對應的鎖資料,例如把鎖資料放在一個以對象位址為索引的樹中。除了實作鎖需要的資料,每個java對象邏輯上還有為實作同步而添加的資料。

鎖是用來實作多個線程對共享資料的互斥通路,而同步則是實作多個線程為完成一個共同目标而協調工作。同步由等待方法和通知方法共同實作。每個類都從Object那裡繼承了三個等待方法(三個名為wait()過載函數)和兩個通知方法(notify()

和notifyAll())。當一個線程在一個對象上調用wait方法,jvm就阻塞了這個線程并把它放在了這個對象的等待集(wait set)中。當有一個線程在這個對象調用了通知方法,jvm就會在将來的某個時間喚醒一個或多個在等待集中阻塞的線程。正像鎖資料一樣,并不是每個對象都需要同步資料。許多jvm實作都把同步資料與對象資料分開,隻有在需要時才為此對象建立同步資料,一般是在第一次調用等待方法或通知方法時。

最後,一個對象還可能要包含與垃圾收集有關的資料。垃圾收集必須要跟蹤每個對象,這個任務不可避免的要附加一些資料,資料的類型要視垃圾收集的算法而定。例如,假如垃圾收集使用标志清除算法,必須要一個資料來标志此對象是否被引用。像線程鎖一樣,這些資料也可以放在對象外。一些垃圾收集技術隻在運作時需要額外資料。例如标志清除算法使用一個位圖來标志對象的引用情況。除了标志對象的引用情況外,垃圾收集還要區分一個對象是否調用了finalizer。在收集一個對象之前,垃圾收集器必須調用聲明了finalizer的類的對象。java語言規範指出垃圾收集器對某個對象隻能調用finalizer一次,在finalizer中允許這個對象複生(resurrect),即使之再次被引用。這樣當這個對象再次被收集時,就不再調用finalizer了。需要finalizer的對象不多,而複生的對象更少,是以對一個對象回收兩次的情況很少見。這樣用來标志

finalizer的資料雖然邏輯上是對象的一部分,但通常與對象分開儲存。

數組表現

再java中,數組是一個成熟的對象。像其他對象一樣,數組也存儲在堆上,jvm實作的設計者也有權決定數組的表現。

數組也有一個相關的類執行個體(Class instance),所有具有相同次元和類型的數組同為一個類,而不管數組的長度(多元數組每一維的長度)。

例如一個有三個ints的數組和一個有六個ints的數組都是同一個類。數組的長度隻與執行個體資料有關。數組類的名稱由兩部分組成,一個是用'['表示的維和一個字元表示的類型。

例如,類型為ints的一維數組的類名為“[I”。類型為bytes的三維數組為“[[[B”。類型為Object的二維數組為“[[Ljava.lang.Object”。多元數組被表示為數組的數組。例如,類型為ints的二維數組,将表示為

一個一維數組,數組元素是一個一維ints數組的引用。如圖5-8每個數組必須儲存的資料是數組的長度,jvm必須能夠從一個數組的引用得到此數組的長度,通過下标通路數組元素,檢查數組下标是否越界,激活Object聲明的方法。

繼續閱讀