標籤:
引言
學習Java也有一段時間了,總感覺有些東西學的不是很精通。例如Java記憶體地區到底是怎麼樣的?程式是怎麼跑的?對象是怎麼存放的?這些都影響了我對自己的代碼的熟悉程度。
一 運行時資料區域
Java虛擬機器在執行java程式的過程中,會把它所管理的記憶體劃分成若干個不同的資料區域(每當運行一個java程式都會啟動一個虛擬機器)。有一本書叫做《Java虛擬機器規範》,講述了Sun公司對Java虛擬機器實現的相關規範,其中講了虛擬機器將所管理的記憶體分為以下幾個部分:
其中方法區和堆是由所有線程共用的,例如使用ThreadPoolExecutor建立多個線程時,堆與方法區都可以被多個線程讀取。
程式計數器學過電腦群組成原理的人都會知道在CPU的寄存器中有一個PC寄存器,存放下一條指令地址,這裡,虛擬機器不使用CPU的程式計數器,自己在記憶體中設立一片地區來類比CPU的程式計數器。只有一個程式計數器是不夠的,當多個線程切換執行時,那就單個程式計數器就沒辦法了,虛擬機器規範中指出,每一條線程都有一個獨立的程式計數器。注意,Java虛擬機器中的程式計數器指向正在執行的位元組碼地址,而不是下一條。
是線程私人的,它的生命週期與線程相同。虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法執行的時候都會建立一個棧幀(我覺得可以把它看作是一個快照,記錄下進入方法前的一些參數,實際上是方法運行時的基礎資料結構),用於存放局部變數表,運算元棧,動態連結,方法出口等資訊。每一個方法從調用直到執行完成的過程都對應著一個棧幀在虛擬機器中的入棧到出棧的過程。我們平時把記憶體分為堆記憶體和棧記憶體,其中的棧記憶體就指的是虛擬機器棧的局部變數表部分。局部變數表存放了編譯期可以知道的基礎資料型別 (Elementary Data Type),對象引用,和返回後所指向的位元組碼的地址。
本地方法區與虛擬機器棧所發揮的作用很類似,但是要注意一下,虛擬機器規範中沒有對本地方法區中的方法作強制規定,虛擬機器可以自由實現,即可以不是位元組碼。但是也可以是位元組碼,這樣虛擬機器棧和本地方法區就可以合二為一,事實上,OpenJDK和SunJDK所內建的HotSpot虛擬機器就直接將虛擬機器棧和本地方法區合二為一。
堆這個概念應該很多人都很熟悉,例如初學C語言的時候,老師就會講malloc方法會在堆中分配空間,這裡也一樣。這個地區是用來存放對象執行個體的,幾乎所有對象執行個體都會在這裡分配記憶體,虛擬機器規範中講:所有對象的執行個體以及數組都要在堆上分配。但是隨著JIT(Just-in-time) 編譯期的發展,有些時候也有可能在棧上分配(這裡我也不是很明白其中的道理)。堆是java垃圾收集器管理的主要區域(很多時候會稱為GC堆,不叫垃圾堆),垃圾收集器實現了對象的自動銷毀。
方法區也是各個線程共用的地區,它用於儲存已經被虛擬機器載入過的類資訊,常量,靜態變數,及時編譯期編譯後的代碼(類方法)等資料。這裡要講一下運行時常量池,它是方法區的一部分,用於存放編譯期產生的各種字面量和符號引用(其實就是八大基本類型的封裝類型和String類型資料)。
最後還有一個直接記憶體,在JDK1.4版本中加入了NIO類,引入了基於通道(Channel)與緩衝區(Buffer)的I/O方式,也就是說通過這種方式,不會在運行時資料區域分配記憶體,這樣就避免了在運行時資料區域來回複製資料,直接調用外部記憶體。
二 對象的建立
對於物件導向的一門語言,我們無時不在通過new關鍵字建立對象,那麼這個過程又是怎樣的呢?
當虛擬機器遇到一條new指令的時候,首先會去檢查所new的類是否已經被載入,在哪裡檢查?當然在方法區,方法區存放了載入過的類資訊。如果沒有載入,那麼先執行類的載入。
通過類載入檢查後,虛擬機器開始為新生對象分配記憶體,對象所需要的記憶體大小在類載入完成後已經可以確定,這時候只要在堆中分配空間即可。分配記憶體有兩種方式,第一種,我們假設記憶體絕對規整,那麼只要在用過的記憶體和沒用過的記憶體間放置一個指標即可,每次分配空間的時候只要把指標向空閑空間移動相應距離即可。第二種,我們假設空閑記憶體和非空閑記憶體夾雜在一起,實際上就是這種情況,那麼就需要一個列表,去記錄堆記憶體的使用方式,作業系統對記憶體的管理就是這樣的。
那麼,我們還要考慮一個問題,即在多線程的情況下,只有一個指標怎麼能確保一個線程分配了記憶體指標沒修改的時候另一個線程又分配記憶體不會覆蓋之前的記憶體呢?這裡有一種方法,讓每一個線程在堆中先預分配一小塊記憶體(TLAB本地線程分配緩衝),每個線程只在自己的記憶體中分配記憶體。
最後,對象被成功分配記憶體。我們知道通過一個對象,我們可以通過getClass()方法擷取類,預設比較兩個對象實際比較的是對象記憶體的雜湊值,這又是怎麼實現的呢?其實在分配完記憶體後,虛擬機器會對對象進行必要的設定,對象的類,對象的雜湊碼等資訊都存放在對象的對象頭中,所以分配的記憶體大小絕不止屬性的總和。
三 對象的記憶體布局
對象在堆中的布局分為三個地區:對象頭,執行個體資料,對齊填充。
對象頭包括兩個部分,第一部分用於儲存自身運行時的資料例如雜湊碼,鎖狀態,哪個線程可以擁有。第二部分存放指向方法區的類別中繼資料。
執行個體資料存放類的屬性資訊,包括父類的屬性資訊。
對齊填充這是虛擬機器要求對象起始地址必須是8位元組的整數倍,可以說對齊填充沒有什麼特別的含義。
四 對象的訪問定位
我們知道,變數引用是變數引用,具體對象是具體對象。變數引用存放在虛擬機器棧中,資料類型為reference,對象存放在堆中。那麼引用是如何指向對象執行個體的呢?
主流的訪問方式有兩種,第一種是通過控制代碼池,如果使用控制代碼池,那麼java堆中將會劃分出一部分記憶體作為控制代碼池,控制代碼包含物件類型指標指向方法區的類別中繼資料,還有對象執行個體指標,指向堆中的執行個體地址。
第二種是reference引用直接指向堆中的對象執行個體,對象執行個體的對象頭存放物件類型指標。
兩種方法各有優勢,第一種可以在對象執行個體遭到移動的時候只改變控制代碼池中的對象執行個體指標,而不用改變reference引用本身。第二種方法就是快,減少了一次指標定位的時間開銷。目前HotSpot虛擬機器就採用的第二種方式。
總結
瞭解java記憶體地區是對java的深入學習,以前只知道有堆和棧的區分,現在我們瞭解到了具體的堆棧的作用。記憶體是怎麼劃分的,對象是怎麼儲存的,方法和屬性的存放區別。通過對這些內容的瞭解,會讓我們寫java程式更加遊刃有餘,有的放矢。
Java 記憶體地區詳解