1.JVM 運行時數據區域
一、定義
JVM 在執行Java程序的過程中會把它所管理的內存劃分爲若干個不同的數據區域
二、類型
程序計數器、虛擬機棧、本地方法棧、Java堆、方法區(永久區、運行時常量池)、直接內存
程序計數器:
較小的內存空間、當前線程執行的字節碼行號指示器;各個線程之間獨立存儲,互
不影響。
Java棧:
線程私有、生/‘’命週期和線程,每個方法在執行的同時都會創建一個棧幀,用於儲存
局部變量表,操作數棧,動態鏈接,方法出口等信息。方法的執行就對應着棧幀在
虛擬機棧中入棧和出棧的過程,棧裏面存放這各種基本信息和對象引用
(-Xss 爲jvm啓動的每個線程分配的棧內存大小,默認JDK1.4中是256K,JDK1.5+中是1M,可用參數 –Xss調整大小,例如-Xss256k)
本地方法棧(native method):
本地方法棧保存的是 native method 的信息,當一個jvm 創建的線程調
用native 方法後,jvm 不再爲其在虛擬機中創建棧幀,jvm只是簡單地動態鏈接並直
接調用native 方法。
Java堆(heap):
Java堆是Java裏面重點關注的一塊區域,因爲涉及到內存分配(例如:new 關鍵字
,反射)與回收(回收算法,收集器等)。
(-Xms 爲jvm啓動時佔用內存的大小,堆分配內存的最小值 缺省 爲物理內存的
1/64;
-Xmx 堆分配內存的最大值 缺省 爲物理內存的1/4;
-Xmn 新生代的大小--------------------------------待補充
-NewSize 新生代最小值--------------------------------待補充
-MaxNewSize 新生代最大值--------------------------------待補充
公式:老生代 = Xmx - Xmn;
)
用native 方法後,jvm 不再爲其在虛擬機中創建棧幀,jvm只是簡單地動態鏈接並直
接調用native 方法。
方法區(永久區):
用於存儲已經被虛擬機加載的的類信心,常量("aaa",'1111"),靜態變量 (static
變量) 。
(
JDK1.7 及以前
-XX:PermSize 爲非堆區初始內存分配大小(permanent size 持久化內存);
-XX:MaxPermSiez 爲非堆區分配的內存的最大上限;
JDK1.8 以後
-XX:MetaSpaceSize 爲非堆區初始內存分配大小(達到該值就會觸發gc 進行
類型卸載,同時gc會對該值進行調整:如果釋放了大量的空間,就適當降低該
值;如果釋放的很少空間,會適當提升該值,最大值不會超過
MaxMetaSpaceSize);
-XX:MaxMetaSpaceSize 爲非堆區內存分配最大值(只會受物理機內存大小限
制);
)
運行時常量池(方法區的一部分):
運行時常量池是方法區的一部分(JDK1.6),運行時常量池是堆的一部(JDK1.7,
JDK1.8),用於存儲編譯期生成的各種字面量("aaa",'1111")等和符號引用。
直接內存:
不是虛擬機運行時數據區的一部分,也不Java虛擬機規範中定義的內存區域;
1.如果使用了NIO,這塊區域會被頻繁使用,在Java堆內可以用directByteBuffer對
象直接引用並操作;
2.這塊區域不堆內存大小限制、只受物理機內存大小限制;可以通過
MaxDirectByteBuffer來設置(缺省爲堆內存最大值)所以也會oom異常;
三、各個版本內存區域的變化
四、深入辨析堆和內存
功能:
1. 以棧幀的方式儲存方法調用的過程,並儲存方法調用方程中基本數據類型的變量
(int,short,long,byte,float,double,boolean,char)以及對象的的引用變量,其內存分配還棧
上,變量出了作用域就會自動釋放;
2. 而堆內存用來儲存Java中的對象,無論是成員變量、局部變量、還是類變量,他們指向的
對象都存儲在堆內存中;
線程獨享還是共享:
1. 棧內存歸屬於單個線程(線程私有),每個線程都會有一個棧內存,其儲存的變量只能該
線程可見;
2 .堆內存中的對象對所有線程可見(線程共享),堆內存中的對象可以被所有線程訪問;
(int,short,long,byte,float,double,boolean,char)以及對象的的引用變量,其內存分配還棧
3 .棧內存要遠遠小於堆內存,棧的深度是有限制的,可能發生StackOverFlowError;
五、方法出入棧
1.方法會打包成棧幀,然後存入棧裏面(存儲結構先進後出)一個棧幀最少包括局部變量表,操作數棧和幀數據區。
2.棧上分配(幾乎所以的對象都是堆上分配的,但也有例外)。
虛擬機提供的一種優化技術,基本思想是,對於線程私有的對象,將打散分配在棧上,而不分配在堆上。好處是對象跟隨着方法的調用結束自行銷燬,不需要GC回收,可以提升性能;棧上分配技術的基礎(逃逸分析----判斷對象是否會逃逸出該方法體(作用域) 例如:return;)
“-” 關閉 ”+“開啓
JVM 運行模式(-mix 自動識別/-client 客戶端 /-server 服務器(只有在該模式下可以開啓逃逸分析))
-XX:+DoEscapeAnalysis (開啓逃逸分析 默認打開)
-XX:+PrintGC(打印GC)
-XX:+EliminateAllocations (標量替換 例如:User 對象的兩個成員變量有可能被識別爲獨立的局部變量在棧上分配 默認打開)
-XX:-UseTLAB
TLAB:ThreadLocalAllocationBuffer(線程本地分配緩存)
案例:
VM arguments:
-server -Xms10m -Xmx10m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations -XX:-UseTLAB
六、虛擬機中內的對象
對象的分配(當虛擬機接收一條new obj 指令時)
1. 先執行想對應的類加載過程(先根據對象檢查是否加載過該類,如果不存在則會加載這個類)
2. 爲該對象在內存中分配對象(確定大小的一塊內存從Java 堆中 劃分出來)
如果Java堆中內存是絕對規整的所有用過的內存都放在一邊,空閒的內存放在另一邊,中間放着一個指針作爲分界點的指示器,那所分配的內存就僅僅是把那個指針向空閒的區域那邊挪動一段對象大小的內存區域。這種分配方式稱之爲”指針碰撞“。
如果Java堆中內存並不是規整的,已使用的內存和空閒的內存相互交錯,那就沒有辦法簡單地進行指針碰撞了,虛擬機必須維護一個列表(關係映射表)記錄那些內存是空閒的可用的,在分配的時候從列表種找一塊足夠大的區域劃分給實例對象,並更新表上的記錄。這種分配稱之爲”空閒列表“。
選擇那種分配方式由Java堆是否規整決定,而Java堆是否規整又由採用的GC是否帶有壓縮整理功能決定。
爲該對象
除如何劃分可用空間之外,還有另一個需要考慮的問題是對象創建在虛擬機種非常頻繁的行爲(高併發),即使是僅僅修改一個指針所指向的位置,在併發情況下也並不是線程安全的,可能存在現在正在給A分配內存,指針還沒來的急修改,對象B又同時使用原來的指針來分配內存的情況。
解決這個問題有兩種方案
一種是對分配內存空間的動作做同步處理(實際虛擬機上採用CAS配上失敗重試的方式保證更新操作的原子性)。
另一種是把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆種預先分配一塊私有空間內存,也是就本地線程分配緩存(ThreadLocalAllocationBuffer,TLAB)如果設置了虛擬機參數-XX:UseTLAB,在線程初始化時的同時也會申請一塊指定大小的內存,只給當前線程使用,這樣線程單獨都擁有一個Buffer,如果需要分配內存,就在自己的Buffer上分配,就不會存在競爭關係,可以提升分配效率。當Buffer 容易不夠時,再重新從Eden區域(HotSpot JVM 新生代分爲三個部分 一個Eden區和兩個Survivor區(from/to))申請一塊繼續使用。
TLAB 的目的是爲在新對象分配空間時,讓每個Java應用線程在使用自己專屬的分配指針來分配空間,減少同步開銷。
TLAB 只是讓每個線程有私有的分配指針,但底下對象的內存空間還是給所有線程訪問的,只是無法在該區域分配(類似於分蛋糕,我們只負責分配蛋糕到你手上了,至於誰吃了我們並不管);當一個TLAB用滿(分配指針top撞到分配極限end了),就在新申請一個TLAB;
3. 內存分配完成後,虛擬機需要將分配到內存空間的初始化爲零值(如:private int userId = 0 ; 等等)。這一步保證操作了對象的實例字段在Java代碼種可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的值。
4. 虛擬機對對象進行必要的設置,例如這個對象的是那個類的實例,如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息放在對象的對象頭之中。
5.前面四步工作完成之後,從虛擬機的視角來看,一個新的對象已經產生了,但是從Java程序的視角來看,對象創建纔剛剛開始,所有的字段都還是零值,所以一般來說,執行new指令之後接着把對象按照程序猿的意願初始化,這樣一個真正可用的對象纔算完全產生出來。
七、對象的內存佈局
1. 在HotSpot JVM 中,對象在內存中存儲的佈局分爲3塊區域(對象頭(header)、實例數據(Instance Data )、對齊填充(Padding)) :
對象頭(header):包括兩部分信息,
第一部分 用於存儲對象自身的運行數據,如哈希碼、GC分年齡,鎖狀態標誌、線程持有的鎖、線程偏向ID、偏向時間戳等。
第二部分 類型指針,即對象指向它類元數據的指針,JVM 通過這個指針來確定這個對象屬於那個類的實例。
對齊填充(Padding):對齊填充不是必然存在的,無特殊涵義,起到佔位符的作用。由於HotSpot VM 的自動內存管理系統要求對象必須是8字節的整數倍,當對象其他數據部分沒有對齊時,就需要通過對齊填充來補全。
八、對象訪問的定位
1.主流的訪問對象方式(Java程序需要通過棧上reference數據來操作堆上的具體對象)
對象被移動:GC回收內存進行內存規整時會造成實例數據指針變動。
句柄訪問:Java堆中會劃出一塊內區區域作爲句柄池,reference中儲存了對象的句柄地址,而句柄中包含了對象實例數據和類型數據各自的具體地址信息。(優點:使用句柄訪問,reference 儲存的是穩定句柄地址,在對象被移動時只會更改句柄中對象實例數據指針,而reference不會修改)
直接指針訪問(Sun HotSpot 使用):reference中存儲的直接就是對象地址。(優點:因爲是直接訪問所以速度快,節省了一次定位的時間開銷,在Java中對象的訪問是非常頻繁的,積少成多這類開銷也是非常可觀的執行成本)。
九、堆參數設置和內存溢出模擬
1.堆溢出
案例:
VM arguments:-Xms2m -Xmx2m -XX:+PrintGC
java.lang.OutOfMemoryError: GC overhead limit exceeded 數據緩慢堆積在堆裏面直接撐爆內存。GC回收次數超過上限(通常是死循環、遞歸)
java.lang.OutOfMemoryError: Java heap space 你要分配的數據大小 超過 內存分配的最大值(通常是分配了巨形對象)
2. 棧溢出
案例:
VM arguments:-Xss256K
java.lang.StackOverflowError (棧被棧幀給撐爆了)