JVM-java記憶體區域
一、介紹一下Java記憶體區域(執行時資料區)
執行緒私有:
虛擬機器棧、本地方法區、程式計數器PC
執行緒共享:
堆區、方法區、直接記憶體。
程式計數器功能:
1.位元組碼直譯器通過改變程式計數器來依次讀取指令,從而實現程式碼流程,如:順序執行、選擇、迴圈異常處理等。
2.在多執行緒情況下,程式計數器用來記錄當前執行緒執行的位置,從而切換回來的時候知道上一次執行的位置。
程式計數器是唯一一個不會出現OOM的記憶體區域,它的生命週期隨著執行緒建立而建立,隨著執行緒結束而結束。(因為它只存一條資料)
java虛擬機器棧
java虛擬機器棧是由一個一個的棧幀組成,而每個棧幀中都擁有:區域性變量表、運算元棧、動態連結、方法出口資訊
區域性變量表包括(boolean、byte、char、short、int、float、long、double)、物件引用
java虛擬機器棧會出現兩種錯誤:stackoverflowerror和outofmemoryerror
第一個錯誤:若Java虛擬機器棧的記憶體大小不允許動態擴充套件,那麼當執行緒請求棧深度大於虛擬機器棧的最大深度,丟擲異常
第二個錯誤:java虛擬機器記憶體大小可以動態擴充套件,若虛擬機器在動態擴充套件棧的時候無法申請到足夠大的記憶體就會爆出oom的異常。
本地方法棧:和虛擬機器棧發揮的作用非常相似,區別是:虛擬機器棧為虛擬機器執行java方法(也就是位元組碼服務),而本地方法棧為
虛擬機器使用到的Native方法服務。
本地方法被執行的時候,在本地方法棧會建立一個棧幀用於存該本地方法的區域性變量表、運算元棧、動態連結、出口資訊
方法執行也會出現StackOverflow和OOM
堆
java虛擬機器管理的記憶體彙總最大的一塊,java對是所有執行緒共享的一塊記憶體區域,在虛擬機器啟動的時候建立。
此記憶體區域唯一的目的就是存放物件例項,幾乎所有物件例項以及陣列都在這裡分配記憶體。
Java堆是垃圾收集器管理的主要區域,因此也備稱為GC堆,從垃圾回收的角度,由於現在收集器基本都採用分代回收,
分為Eden、Survivor、old等空間,進一步劃分是為了更好回收記憶體,或者更快分配記憶體。
大部分情況,物件會首先在Eden區域分配,在一次新生代垃圾回收後,如果物件還活著,則會進入S0或者S1
並且物件的年齡會加1,按照年齡從小到大對其所佔有的大小進行累積,當累積的某個年齡大小超過了survivor區的一半取和max更小的值
,大於閾值晉升到老年代中,可以通過引數XX:MaxTenuringThreashold來設定。
方法區
方法區和堆區一樣,是各個執行緒共享的記憶體區域,它用於儲存已經被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼。
雖然java虛擬機器把方法區描述為堆的一個邏輯部分,但是名字又稱為非堆(no-heap)。
(方法區和永久代的關係)方法區是一種規範,永久代和元空間都是它的實現,jdk7是永久代,jdk8是元空間。
執行時常量池
執行時常量池是方法區的一部分,Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有常量池表(用於存放編譯器生成的各種字面量和符號引用)
既然執行時常量池是方法區的一部分,自然受到方法區記憶體的限制,當常量池無法申請到記憶體時就丟擲異常oom。
jdk8中,hotspot移除了永久代用的元空間,但是字串常量池還在堆,執行時常量池還在方法區,只不過實現從永久代成了元空間。
二、Java物件建立的過程(五步,建議能夠默寫出來,並且知道每一步虛擬機器做了什麼)
Java物件建立過程
1、類載入檢查
2、分配記憶體
3、初始化零值
4、設定物件投
5、執行init方法
Step1:類載入檢查
虛擬機器遇到一條new指令時,首先去檢查這個指令的引數是否在常量池中定位到這個類的符號引用,
並且檢查這個符號引用代表的類是否被載入過、解析和初始化過。如果沒有必須執行相應的類載入過程
step2:分配記憶體
在類載入檢查通過後,接下來虛擬機器將為新生物件分配記憶體。物件所需要的記憶體大小在類載入完成後便可確定,
為物件分配空間的任務等同於把一塊確定大小的記憶體從java堆是否規整決定,而java對是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。
記憶體分配的兩種方式:
在建立物件的時候有一個很重要的問題,就是執行緒安全,因為在實際開發過程中建立物件是非常頻繁的事情。
虛擬機器通常用兩種方式來保證執行緒安全:
1、CAS+失敗重試:Cas是樂觀鎖的一種實現方式,所謂樂觀鎖就是指每次不加鎖而是假設沒有衝突而去完成某項操作,
如果因為衝突失敗了就重試,直到成功為止。虛擬機器採用cas配上失敗重試的方式保證更新操作的原子性。
2、TLAB:為每一個執行緒在Eden分配一塊記憶體,JVM在給執行緒中記憶體分配物件的時候,首先在TLAB分配
當物件大於TLAB中剩餘記憶體或者TLAB記憶體用盡。再採用上述CAS進行記憶體分配
Step3:初始化零值
記憶體分配完後,虛擬機器需要將分配到的記憶體空間都初始化為零值(不包括物件頭),這一步操作保證了
物件的例項欄位在java程式碼中可以不賦初始值直接用,程式訪問到這些欄位的資料型別所對應的零值。
step4:設定物件頭
初始化零值完成以後,虛擬機器需要對物件進行必要的設定,例如物件是哪個類的例項,如何才能找到元資料
物件的雜湊碼,物件的GC分代年齡等資訊,都存放在物件頭中。另外根據虛擬機器不同的執行狀態,是否使用偏向鎖
會有不同的設定方式。
step5:執行init方法
在上面工作完成之後,從虛擬機器的視角來看,一個物件已經產生了,但從java程式來看物件建立才剛剛開始,<init>方法還沒有執行,所有欄位都還是0
所以一般執行new指令之後會接著執行<init>方法,把物件按照程式設計師的意願進行初始化。
問題:物件的記憶體佈局
物件在記憶體中的佈局分為3個區域:物件頭、例項資料、對齊填充
hotspot虛擬機器的物件頭包括兩部分資訊,第一部分用於儲存物件自身的執行時資料(雜湊碼、GC、鎖狀態等)
型別指標,及物件指向它的類元資料的指標,虛擬機器通過這個指標這個物件是那個類的例項。
例項資料部分是物件真正儲存的有效資訊,也是程式鎖定的各種型別的欄位內容
對其填充部分不是必然存在的,也沒有特別含義,僅僅起一個佔位的作用。因為hotspot虛擬機器的自動記憶體管理系統要求
物件其實地址必須是8位元組的整數倍,換句話說就是物件的大小必須是8位元組的整數倍。而物件頭部分剛好是8的整數倍
因此例項資料如果沒對齊,就需要對齊填充來補全。
3、物件的訪問定位的兩種方式(控制代碼和直接指標兩種方式)
建立物件就是為了使用物件,java程式通過棧上的reference資料來操作堆上的具體物件。
物件的訪問由虛擬機器實現而定,目前主流的訪問方式1、使用控制代碼和2、直接指標兩種
1、控制代碼:如果使用控制代碼那麼java堆中將會劃分出一塊記憶體來作為控制代碼池,reference中儲存的就是物件的控制代碼地址
而控制代碼中包含了物件例項資料與型別資料各種的具體地址資訊
2、直接指標:如果使用直接指標訪問,那麼java堆物件的佈局中必須考慮如何訪問型別資料的相關資訊,而reference中存的直接就說物件的地址
使用控制代碼來訪問最大好處是refernce中儲存的是穩定的控制代碼地址,在物件被移動時只會改變控制代碼中的例項資料指標而reference本身不需要修改。
使用直接指標訪問訪問好處是速度快,節省了一次指標開銷。
擴充套件問題
1、String類和常量池
2、8種基本型別的包裝類和常量池