學習Java也有一段時間了,總感覺有些東西學的不是很精通。例如Java內存區域到底是怎麼樣的?程序是怎麼跑的?對象是怎麼存放的?這些都影響了我對自己的程序運行的熟悉程度。
Java虛擬機在執行java程序的過程中,會把它所管理的內存劃分成若干個不同的數據區域(每當運行一個java程序都會啟動一個虛擬機)。有一本書叫做《Java虛擬機規范》 【PDF 版下載見 http://www.linuxidc.com/Linux/2015-07/120683.htm】,講述了Sun公司對Java虛擬機
實現的相關規范,其中講了虛擬機將所管理的內存分為以下幾個部分:
程序計數器
虛擬機棧
本地方法區
堆
方法區
其中方法區和堆是由所有線程共享的,例如使用ThreadPoolExecutor
創建多個線程時,堆與方法區都可以被多個線程讀取。
程序計數器
學過計算機組成原理的人都會知道在CPU的寄存器中有一個PC寄存器,存放下一條指令地址,這裡,虛擬機不使用CPU的程序計數器,自己在內存中設立一片區域來模擬CPU的程序計數器。只有一個程序計數器是不夠的,當多個線程切換執行時,那就單個程序計數器就沒辦法了,虛擬機規范中指出,每一條線程都有一個獨立的程序計數器。注意,Java虛擬機中的程序計數器指向正在執行的字節碼地址,而不是下一條。
虛擬機棧
是線程私有的,它的生命周期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法執行的時候都會創建一個棧幀(我覺得可以把它看作是一個快照,記錄下進入方法前的一些參數,實際上是方法運行時的基礎數據結構),用於存放局部變量表,操作數棧,動態鏈接,方法出口等信息。每一個方法從調用直到執行完成的過程都對應著一個棧幀在虛擬機中的入棧到出棧的過程。我們平時把內存分為堆內存和棧內存,其中的棧內存就指的是虛擬機棧的局部變量表部分。局部變量表存放了編譯期可以知道的基本數據類型,對象引用,和返回後所指向的字節碼的地址。
本地方法區
與 虛擬機棧
所發揮的作用很類似,但是要注意一下,虛擬機規范中沒有對本地方法區中的方法作強制規定,虛擬機可以自由實現,即可以不是字節碼。但是也可以是字節碼,這樣虛擬機棧和本地方法區就可以合二為一,事實上,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引用直接指向堆中的對象實例,對象實例的對象頭存放對象類型指針。
兩種方法各有優勢,第一種可以在對象實例在GC
時移動的時候只改變句柄池中的對象實例指針,而不用改變reference引用本身。第二種方法就是訪問速度快,減少了一次指針定位的時間開銷。目前HotSpot虛擬機
就采用的第二種方式。
了解java內存區域是對java的深入學習,以前只知道有堆和棧的區分,現在我們了解到了具體的堆棧的作用。內存是怎麼劃分的,對象是怎麼存儲的,方法和屬性的存放區別。通過對這些內容的了解,會讓我們寫java程序更加游刃有余,有的放矢。