1. 程式人生 > 其它 >JVM 一 Java記憶體區域

JVM 一 Java記憶體區域

分割槽

Java記憶體區域分為五個塊

深色的部分被所有執行緒共享,淺色的部分執行緒私有。

程式計數器

用來記錄當前執行緒正在執行的位元組碼的行號,就是用來找執行到哪裡了的一個記錄指標。它很小。

如果程式都是簡單的順序執行,可能也就不需要程式計數器了,但是程式中有各種選擇、分支、異常處理等結構,必須有一個指標來選擇下一條要執行的位元組碼命令。

如果正在執行的是Java方法,計數器的值是正在執行的位元組碼指令的地址,如果是Native方法,則值為空。

程式計數器區域不會產生OutOfMemoryError。

虛擬機器棧

虛擬機器棧就是方法執行的記憶體模型,它是執行緒私有的,每個執行緒都會有一個虛擬機器棧,執行緒銷燬時棧也銷燬。每個方法被執行時,虛擬機器都會建立一個棧幀壓到棧中,代表一次方法呼叫,棧幀中有區域性變量表、運算元棧、動態連線和方法出口等資訊。當方法執行結束,對應的棧幀出棧。

區域性變量表中存放的就是該方法呼叫中的所有變數,包括基本變數型別和物件引用(不是物件,是引用)和returnAddress返回地址。

區域性變量表中的資料使用槽(Slot)表示,在編譯時就已經可以確定一個方法中需要多少個槽才能容納方法中的所有變數,這個槽的數量在執行期不會改變,而有的變數佔用一個槽,有的佔用兩個。一個槽用多大的空間來實現是Java虛擬機器規範中沒有限制的。

棧空間可能觸發兩種異常:

  1. StackOverflowError,當執行緒請求的棧深度大於虛擬機器所允許的深度時丟擲
  2. OutOfMemoryError,當虛擬機器可以並被允許動態擴容棧空間,並在擴容時無法獲取到足夠的容量時丟擲

本地方法棧

和虛擬機器棧一致,只不過用來執行native方法。

堆,基本上是最受關注的記憶體區域,因為基本上所有物件都存在於這裡,並且是GC機制主要作用的部分。

它被所有執行緒共享,在虛擬機器啟動時被建立,一個昔日準確而今已經不準確了的說法是:“所有的物件都存放在堆中”,已經不夠準確了。各種優化技術,如標量替換和棧上分配等技術的出現已經可以將一些輕量級物件分配在堆以外的位置來加速程式運行了。

提到堆就不得不說垃圾清理,一提到垃圾清理就會想到什麼,新生代,老年代,永久代,什麼伊甸園區,倖存者區等等。其實如果執著於這些就片面了,這只是大部分JVM自己選擇的分代收集方式——一種用於實現垃圾清理的方式。而JVM規範本身並未對如何實現垃圾清理做限制,就是說你完全可以編寫一個脫離分代模型的JVM。

因為堆是被多個執行緒共享的,那麼就要想辦法保證堆中資料的執行緒安全問題,而保證執行緒安全就會降低實際的執行效率,TLAB(Thread Local Allocation Buffer,執行緒私有分配緩衝區)技術為每個執行緒在堆中建立了一個緩衝區,各個執行緒向堆中建立資料時先放到各自的緩衝區中。

Java虛擬機器並不要求堆必須在一段連續的記憶體空間上,但它們必須邏輯連續。

Java堆可以被設計成可動態擴充套件容量的,也可以被設計成固定大小的,當堆不能再繼續放進去例項,並且動態擴容也失敗的話,就會丟擲OutOfMemoryError。

方法區

方法區用來儲存Class檔案中的一些描述資訊、型別資訊,常量,靜態變數等等。方法區在JVM規範中被描述成堆的一個邏輯部分,但它也有一個別名,是“非堆”,就是方法區是方法區,堆是堆,二者無法混為一談。

早先很多人認為方法區和永久代是一回事兒,這種誤會隨著永久代被取締已經差不多被根除了。JDK8以前的預設虛擬機器HotSpot虛擬機器使用永久代來實現方法區,這才有了這個誤會。永久代給HotSpot帶來了很多困難,比如更容易產生OOM(永久代有記憶體大小的上限),並且有一些方法會因為永久代的存在(如String.intern),會和執行在其他虛擬機器上的時候產生一些細微的差別,還有就是當Oracle收購了越來越多的第三方虛擬機器並想把它們的優點引入到HotSpot中時,對方法區實現的差異會產生很多麻煩。所以JDK7時很多東西從永久代中被移出,在JDK8徹底去除了永久代採用元空間來實現方法區。

Java虛擬機器規範不限制這個區域必須實現垃圾回收,不限制這個區域的記憶體地址必須連續等。

方法區當無法滿足新的記憶體分配需求時,丟擲OOM。

執行時常量池

是方法區的一部分,當載入一個類時,類中檔案中的常量池中的資料會被載入到執行時常量池中。

執行時常量池可以在執行期間動態向其中新增常量。如String.intern方法。

直接記憶體

虛擬機器規範並未定義這塊記憶體區域,但總會用到,它受外界實體記憶體和作業系統的限制,也會出現OOM。

物件的建立

程式設計師呼叫new關鍵字建立一個物件時,虛擬機器幹了啥?

首先虛擬機器先去常量池中找到這個類的符號引用,找到後檢測這個類是否已經被載入、解析和初始化過,如果沒有則執行類載入。

然後虛擬機器就會給物件分配記憶體,一個類需要的記憶體大小在類載入完後就已經可以確定了。介紹兩種分配方式,第一種是指標碰撞,這種方法的前提是整個記憶體區域是規整的,指標左側是已經分配過的記憶體區域,指標右側是還沒分配的記憶體區域,這時如果你想分配記憶體給一個物件,那麼直接將指標向後移動一段距離即可,要實現這個必須在垃圾清理的時候對清理掉的記憶體進行壓縮,整理。還有一種方式是空閒列表,這時記憶體空間不一定是規整的,有的地方有資料有的地方沒資料,需要維護一個空閒列表,並且從這個空閒列表中找出一塊足夠容納這個物件大小的空間分配給物件。

注意在多執行緒的環境下指標碰撞會產生執行緒安全問題,常見的解決辦法是CAS或TLAB。

再然後,虛擬機器會給物件分配到的記憶體空間初始化為0值。

然後對物件進行一些設定,設定物件頭中的資訊。

從虛擬機器的視角來看,一個物件現在已經建立完畢,但是從程式設計師的視角來看,建構函式還沒有被呼叫。如果是使用new方式來建立的物件,編譯器會轉換成兩條指令,一個是new位元組碼命令,就是剛剛的那些步驟,一個是invokespecial,這個指令用於呼叫對應的建構函式,但也有其他的物件構造方式,不執行這個構造方法。

物件記憶體佈局

物件在堆記憶體中的佈局在HotSpot虛擬機器中如下:物件頭,例項資料,填充資料。

物件頭中儲存的就是類似雜湊碼,GC分代年齡,鎖狀態等等資訊。

例項資料是物件的例項資訊內容,比如其中的屬性,無論是父類中的還是自己的。HotSpot的預設分配順序是longs/doublesintsshort/charsbytes/booleansoops,就是同等大小的資料型別一起分配,然後在這個前提下,父類的資料先被分配。

填充資料就是有些虛擬機器的自動記憶體管理系統要求物件起始地址必須是8位元組的整數倍,所以如果沒有對齊時就填充齊。

物件的訪問定位

棧如何通過reference來操作堆上的物件,主流的定位方法有兩種

第一種是控制代碼訪問,就是在堆中再劃分出一個額外區域用來儲存所有物件的例項地址和型別資料地址,而reference儲存的是物件的控制代碼地址。這樣就是棧通過reference去控制代碼池中找到真實地址,再去訪問真實地址。

第二種是直接訪問,就是reference儲存的直接是物件在堆中的地址,但這樣物件的型別資料地址就必須想辦法在物件的記憶體佈局中儲存,比如在物件頭中新增一個新欄位指向物件型別地址。