Java 內存區域詳解

引言

學習Java也有一段時間了,總感受有些東西學的不是很精通。例如Java內存區域究竟是怎麼樣的?程序是怎麼跑的?對象是怎麼存放的?這些都影響了我對本身的程序運行的熟悉程度。java

一. 運行時數據區域

圖片描述

Java虛擬機在執行java程序的過程當中,會把它所管理的內存劃分紅若干個不一樣的數據區域(每當運行一個java程序都會啓動一個虛擬機)。有一本書叫作《Java虛擬機規範》,講述了Sun公司對Java虛擬機實現的相關規範,其中講了虛擬機將所管理的內存分爲如下幾個部分:數組

程序計數器
虛擬機棧數據結構

本地方法區

方法區多線程

其中方法區和堆是由全部線程共享的,例如使用ThreadPoolExecutor建立多個線程時,堆與方法區均可以被多個線程讀取。佈局

程序計數器 學過計算機組成原理的人都會知道在CPU的寄存器中有一個PC寄存器,存放下一條指令地址,這裏,虛擬機不使用CPU的程序計數器,本身在內存中設立一片區域來模擬CPU的程序計數器。只有一個程序計數器是不夠的,當多個線程切換執行時,那就單個程序計數器就沒辦法了,虛擬機規範中指出,每一條線程都有一個獨立的程序計數器。注意,Java虛擬機中的程序計數器指向正在執行的字節碼地址,而不是下一條。學習

虛擬機棧線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每一個方法執行的時候都會建立一個棧幀(我以爲能夠把它看做是一個快照,記錄下進入方法前的一些參數,其實是方法運行時的基礎數據結構),用於存放局部變量表,操做數棧,動態連接,方法出口等信息。每個方法從調用直到執行完成的過程都對應着一個棧幀在虛擬機中的入棧到出棧的過程。咱們平時把內存分爲堆內存和棧內存,其中的棧內存就指的是虛擬機棧的局部變量表部分。局部變量表存放了編譯期能夠知道的基本數據類型,對象引用,和返回後所指向的字節碼的地址。spa

本地方法區虛擬機棧 所發揮的做用很相似,可是要注意一下,虛擬機規範中沒有對本地方法區中的方法做強制規定,虛擬機能夠自由實現,便可以不是字節碼。可是也能夠是字節碼,這樣虛擬機棧和本地方法區就能夠合二爲一,事實上,OpenJDKSunJDK所自帶的HotSpot虛擬機就直接將虛擬機棧和本地方法區合二爲一。操作系統

這個概念應該不少人都很熟悉,例如初學C語言的時候,老師就會講malloc方法會在堆中分配空間,這裏也同樣。這個區域是用來存放對象實例的,幾乎全部對象實例都會在這裏分配內存,虛擬機規範中講:全部對象的實例以及數組都要在堆上分配。可是隨着JIT(Just-in-time) 編譯期的發展,有些時候也有可能在棧上分配(這裏我也不是很明白其中的道理)。堆是java垃圾收集器管理的主要區域(不少時候會稱爲GC堆,不叫垃圾堆),垃圾收集器實現了對象的自動銷燬。線程

方法區 也是各個線程共享的區域,它用於存儲已經被虛擬機加載過的類信息,常量,靜態變量,及時編譯期編譯後的代碼(類方法)等數據。這裏要講一下運行時常量池,它是方法區的一部分,用於存放編譯期生成的各類字面量和符號引用(其實就是八大基本類型的包裝類型和String類型數據)。3d

最後還有一個直接內存,在JDK1.4版本中加入了NIO類,引入了基於通道(Channel)與緩衝區(Buffer)的I/O方式,也就是說經過這種方式,不會在運行時數據區域分配內存,這樣就避免了在運行時數據區域來回複製數據,直接調用外部內存。

二. 對象的建立

對於面向對象的一門語言,咱們無時不在經過new關鍵字建立對象,那麼這個過程又是怎樣的呢?

當虛擬機遇到一條new指令的時候,首先會去檢查所new的類是否已經被加載,在哪裏檢查?固然在方法區,方法區存放了加載過的類信息。若是沒有加載,那麼先執行類的加載。

經過類加載檢查後,虛擬機開始爲新生對象分配內存,對象所須要的內存大小在類加載完成後已經能夠肯定,這時候只要在堆中分配空間便可。分配內存有兩種方式,第一種,咱們假設內存絕對規整,那麼只要在用過的內存和沒用過的內存間放置一個指針便可,每次分配空間的時候只要把指針向空閒空間移動相應距離便可。第二種,咱們假設空閒內存和非空閒內存夾雜在一塊兒,實際上就是這種狀況,那麼就須要一個列表,去記錄堆內存的使用狀況,操做系統對內存的管理就是這樣的。

那麼,咱們還要考慮一個問題,即在多線程的狀況下,只有一個指針怎麼能確保一個線程分配了內存指針沒修改的時候另外一個線程又分配內存不會覆蓋以前的內存呢?這裏有一種方法,讓每個線程在堆中先預分配一小塊內存(TLAB本地線程分配緩衝),每一個線程只在本身的內存中分配內存。

最後,對象被成功分配內存。咱們知道經過一個對象,咱們能夠經過getClass()方法獲取類,默認比較兩個對象實際比較的是對象內存的哈希值,這又是怎麼實現的呢?其實在分配完內存後,虛擬機會對對象進行必要的設置,對象的類,對象的哈希碼等信息都存放在對象的對象頭中,因此分配的內存大小毫不止屬性的總和。

三. 對象的內存佈局

對象在堆中的佈局分爲三個區域:對象頭實例數據對齊填充

  • 對象頭 包括兩個部分,第一部分用於存儲自身運行時的數據例如GC標誌位,MonirGC次數,哈希碼,鎖狀態,哪一個線程能夠擁有等被稱爲MarkWord(標記字)。第二部分存放指向方法區類數據的指針。在32位系統中,class指針大小爲4字節,標記字大小爲4字節。在64位系統中標記字大小爲8字節。

  • 實例數據 存放類的屬性信息,包括父類的屬性信息。數組的實例部分還包括數組的長度。實例信息按類分別4字節對齊。

  • 對齊填充 這是虛擬機要求對象起始地址必須是8字節的整數倍,能夠說對齊填充沒有什麼特別的含義。

四. 對象的訪問定位

咱們知道,引用是引用,對象實例是對象實例。引用存放在虛擬機棧中,數據類型爲reference,對象實例存放在堆中。那麼引用是如何指向對象實例的呢?

主流的訪問方式有兩種,第一種是經過句柄池,若是使用句柄池,那麼java堆中將會劃分出一部份內存做爲句柄池,句柄包含對象類型指針指向方法區的類型信息,還有對象實例指針,指向堆中的實例地址。

第二種是reference引用直接指向堆中的對象實例,對象實例的對象頭存放對象類型指針。

兩種方法各有優點,第一種能夠在對象實例在GC時移動的時候只改變句柄池中的對象實例指針,而不用改變reference引用自己。第二種方法就是訪問速度快,減小了一次指針定位的時間開銷。目前HotSpot虛擬機就採用的第二種方式。

總結

瞭解java內存區域是對java的深刻學習,之前只知道有堆和棧的區分,如今咱們瞭解到了具體的堆棧的做用。內存是怎麼劃分的,對象是怎麼存儲的,方法和屬性的存放區別。經過對這些內容的瞭解,會讓咱們寫java程序更加遊刃有餘,有的放矢。
更多文章:http://blog.gavinzh.com

相關文章
相關標籤/搜索