天天看點

JVM 一 Java記憶體區域

分區

Java記憶體區域分為五個塊

JVM 一 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/doubles

ints

short/chars

bytes/booleans

oops

,就是同等大小的資料類型一起配置設定,然後在這個前提下,父類的資料先被配置設定。

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

對象的通路定位

棧如何通過

reference

來操作堆上的對象,主流的定位方法有兩種

第一種是句柄通路,就是在堆中再劃分出一個額外區域用來存儲所有對象的執行個體位址和類型資料位址,而

reference

儲存的是對象的句柄位址。這樣就是棧通過

reference

去句柄池中找到真實位址,再去通路真實位址。

JVM 一 Java記憶體區域

第二種是直接通路,就是

reference

存儲的直接是對象在堆中的位址,但這樣對象的類型資料位址就必須想辦法在對象的記憶體布局中存儲,比如在對象頭中添加一個新字段指向對象類型位址。

JVM 一 Java記憶體區域