JVM調優(一)——java內存區域

大家是不是經常聽到過堆、棧、內存溢出? 在面試的時候是不是也經常被問道:瞭解JVM嗎?講講。。
是不是很苦惱啊,這怎麼講?從哪開始講?是不是很苦惱? 就算你講出來,面試官會接着問:說說垃圾回收器和GC吧。 然後你。。。。

如果你對JVM是很瞭解,或者面試的時候不知道怎麼回答,那麼《JVM調優》系列會幫助你 這篇文章參考 《深入理解java虛擬機》-周志明
大家有時間可以去買這本書看一看,很有意思。

一、jvm運行時數據區

在這裏插入圖片描述
這裏我畫了一張圖,這張圖就是jvm運行時數據區,它會把內存劃分爲不同的內存區域,不同的內存區域存儲不同的數據。

方法區
線程共享區域,主要用於存儲已經被jvm加載的類信息,常量,靜態變量,即時編譯器編譯後的代碼等數據。當方法區無法進行內存分配時將拋出:OutOfMemoryError異常


這個大家聽的最多了,也是jvm所管理內存的最大一塊。也是線程共享的區域。在jvm規範中是這樣描述的:所有的對象實例以及數組都要在堆上進行分配。隨着JIT編譯器的發展和逃逸分析技術的成熟,所有的對象都分配在堆上也就沒有那麼絕對了。有興趣的可以瞭解一下棧上分配、標量替換。當然,在面試的時候說堆中存儲着我們new的對象和數組也是沒毛病的。當堆區無法進行內存分配時將拋出:OutOfMemoryError異常

虛擬機棧
這個就是我們經常聽的棧,線程私有的,棧的生命週期和線程相同。這個棧主要是描述了我們方法執行的內存模型。方法執行的時候都會在棧中創建一個棧針,棧針中保存着我們的局部變量表,操作數棧,動態連接,方法出口等信息。方法的執行到結束其實就是棧針在棧中的入棧和出棧的操作。而我們經常說的棧中存放着我們對象的引用,這個太粗魯了,準確來講,對象的引用存儲在棧針中的局部變量表裏。這個區域會發生兩種異常:
StackOverflowError 函數調用棧太深了,注意代碼中是否有了循環調用方法而無法退出的情況 OutOfMemoryError
內存分配不足

本地方法棧
這個沒什麼好講的,和虛擬機棧發揮的作用是一樣的,不一樣的是本地方法棧調用的是native方法服務。比如我們調用C語言的一些方法服務。發生的異常和虛擬機棧發生的異常一樣

程序計數器
我們java是執行字節碼文件的,這個程序計數器可以理解爲記錄當前線程執行的字節碼文件的行號。以便我們線程切換的時候能夠繼續執行。

運行時常量池
這個沒有在圖中體現,因爲運行時常量池是方法區的一部分,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。例如字符串常量池
當然在1.7之後字符串常量池就被移動到了堆空間裏

二、對象的創建過程

不是寫的jvm調優嗎?爲什麼要寫對象的創建過程?
在jvm調優之前要先對我們的一些概念有些瞭解之後才能更好的分析和調優。
在語言層面,我們創建一個對象通常使用一個new關鍵字就可以了,那麼這個對象到底是怎麼創建的,對象的內存結構又是怎樣的呢?下面來分析一下。
在這裏插入圖片描述

  • 當jvm收到一條new指令的時候,首先要去類加載檢查,檢查這個指令的參數是否能在常量池中定位到一個類的符號引用。並且檢查這個符號引用代表的類是否被加載、解析、初始化過。如果沒有的話先去進行類加載(關於類加載後面會有文章單獨介紹)
  • 在類加載檢查通過之後,jvm將爲新生對象分配內存。對象所需的內存大小在類加載後是可以完全確定的。怎麼分配內存呢,其實就是在堆中劃分出一塊區域來存儲我們的對象對象的內存分配

指針碰撞
假設Java堆中內存時完整的,已分配的內存和空閒內存分別在不同的一側,通過一個指針作爲分界點,需要分配內存時,僅僅需要把指針往空閒的一端移動與對象大小相等的距離。使用的GC收集器:Serial、ParNew,適用堆內存規整(即沒有內存碎片)的情況下。
在這裏插入圖片描述

空閒列表

事實上,Java堆的內存並不是完整的,已分配的內存和空閒內存相互交錯,JVM通過維護一個列表,記錄可用的內存塊信息,當分配操作發生時,從列表中找到一個足夠大的內存塊分配給對象實例,並更新列表上的記錄。使用的GC收集器:CMS,適用堆內存不規整的情況下。
在這裏插入圖片描述
上述兩幅圖僅供參考。

  • 內存分配完成之後,jvm需要將分配到的內存空間都初始化爲零(不包括對象頭),這一步保證了對象的實例字段在java代碼中不需初始化就可以直接使用,程序能夠訪問到這些字段的數據類型所對應的0值。
  • 接下來,jvm就需要對對象進行必要的設置,例如,對象是哪個類的實例,對象的哈希碼,GC年齡等等。
  • 上面的步驟結束之後,從jvm角度來看,一個對象就產生了。但是,從程序角度來看,對象創建纔剛開始,init方法沒有執行,所有的字段都爲0。所以,new指令之後會接着執行init方法,把對象按照程序員的意願進行初始化。

三、對象的內存佈局

HotSpot虛擬機中,對象在內存中存儲的佈局可以分爲三塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。

對象頭
標記字(32位虛擬機4B,64位虛擬機8B) + 類型指針(32位虛擬機4B,64位虛擬機8B)+[數組長(對於數組對象才需要此部分信息)]

實例數據

對齊填充:對於64位虛擬機來說,對象大小必須是8B的整數倍,不夠的話需要佔位填充

HotSpot虛擬機的對象頭(Object Header)包括兩部分信息:

第一部分"Mark Word":用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等等.

第二部分"Klass Pointer":對象指向它的類的元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。(數組,對象頭中還必須有一塊用於記錄數組長度的數據,因爲虛擬機可以通過普通Java對象的元數據信息確定Java對象的大小,但是從數組的元數據中無法確定數組的大小。
)

32位的HotSpot虛擬機對象頭存儲結構:(下圖摘自網絡)

在這裏插入圖片描述

四、對象的訪問定位

對象的訪問定位也取決於具體的虛擬機實現。當我們在堆上創建一個對象實例後,就要通過虛擬機棧中的reference類型數據來操作堆上的對象。現在主流的訪問方式有兩種(HotSpot虛擬機採用的是第二種):

使用句柄訪問對象
即reference中存儲的是對象句柄的地址,而句柄中包含了對象實例數據與類型數據的具體地址信息,相當於二級指針。
在這裏插入圖片描述

直接指針訪問對象 即reference中存儲的就是對象地址,相當於一級指針。
在這裏插入圖片描述

兩種方式有各自的優缺點。當垃圾回收移動對象時,對於方式一而言,reference中存儲的地址是穩定的地址,不需要修改,僅需要修改對象句柄的地址;而對於方式二,則需要修改reference中存儲的地址。從訪問效率上看,方式二優於方式一,因爲方式二隻進行了一次指針定位,節省了時間開銷,而這也是HotSpot採用的實現方式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章