1.虛拟機對象的建立
虛拟機遇到new指令
(1) 類加載:
檢查該指令參數是否能在常量池中定位到一個類的符号引用,并且檢查這個符号引用代表的類是否已被加載,解析和初始化過。如果沒有,必須先執行相應的類加載過程;
(2) 為新生對象配置設定記憶體:
對象所需記憶體的大小在類加載完成後便可完全确定,為對象配置設定空間的任務等同于把一塊确定大小的記憶體從Java堆中劃分出來;
配置設定記憶體方式:
— 指針碰撞:Java堆中記憶體是絕對規整的,所有用過的記憶體都放在一邊,空閑的記憶體放在另一邊,中間放着一個指針作為分界
點的訓示器,那所配置設定記憶體就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離(使用Serial,ParNew等帶
Compact過程的收集器時,采用的配置設定算法是指針碰撞)
— 空閑清單:Java堆中的記憶體并不是規整的,已使用的記憶體和空閑的記憶體并不是規整的,已使用的記憶體和空閑的記憶體互相交
錯,那就沒有辦法簡單地進行指針碰撞了,虛拟機就必須維護一個清單,記錄上哪些記憶體塊是可用的,在配置設定的時候從清單
中找到一塊足夠大的空間劃分給對象執行個體,并更新清單上的記錄(使用CMS這種基于Mark-Sweep算法收集器時,采用空閑
清單)
對象建立在虛拟機中是頻繁的并發情況下線程不安全:
即:正在給A配置設定記憶體,指針還沒來得及修改,對象B又同時使用了原來的指針來配置設定記憶體的情況。
解決方案:
— 對配置設定記憶體空間的動作進行同步處理:實際上虛拟機采用CAS配上失敗重試的方式保證更新操作的原子性;
— 把記憶體配置設定的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先配置設定一小塊記憶體,稱為本地線程配置設定
緩沖(TLAB),虛拟機是否使用TLAB,可以通過-XX:+/-UseTLAB參數來設定。
(3) 将配置設定到的記憶體空間都初始化為零值(不包括對象頭):
如果使用TLAB,這一工作過程也可以提前至TLAB配置設定時進行
該操作保證了對象的執行個體字段在Java代碼中可以不賦初始值就直接使用,程式能通路到這些字段的資料類型所對應的零值
(4) 對對象進行必要的設定:
該對象是哪個類的執行個體,如何才能找到類的中繼資料,對象的hashCode,對象的GC分代年齡等資訊。
這些資訊存放在對象的對象頭(Object Header)之中。
(5) 執行init方法初始化:
在上面工作完成之後,從虛拟機角度看,一個新的對象已經産生了,但從Java程式的視角來看,對象建立才剛開始,
執行<init>方法進行初始化,這樣一個真正的對象才算完全産生出來
2.虛拟機對象的記憶體布局
對象在記憶體中存儲的布局可以分為:對象頭(Header),執行個體資料(Instance Data)和對齊填充(Padding)。
(1) 虛拟機對象頭包含:
— 用于存儲對象自身的運作時資料:HashCode,GC分代年齡,鎖狀态标志,線程持有的鎖,偏向線程ID,偏向時間戳
— 類型指針:對象指向它的類中繼資料的指針,虛拟機通過這個指針來确定這個對象是哪個類的執行個體
(2) 執行個體資料部分:
對象真正存儲的有效資訊,也是在程式代碼中所定義的各種類型的字段内容。父類繼承下來的和子類中定義的,都需要記錄起來。
這部分存儲順序會受到虛拟機配置設定政策參數和字段在Java源碼中定義順序的影響。
預設配置設定政策:
— longs/doubles, ints, shorts/chars, bytes/booleans, oops(Ordinary Object Pointers), 從配置設定政策可以看出相同寬度的字段總
是被配置設定到一起。在滿足這個條件的情況下,父類中定義的變量會出現在子嘞之前。如果CompactFields參數值為true(預設
為true),那麼子類之中較窄的變量也可能會插入到父類變量的空隙之中。
(3) 對齊填充:
對齊填充并不是必然存在的,也沒有特殊含義,僅僅起着占位符的作用。HotSpot VM的自動記憶體管理系統要求對象起始位址必須是8位元組的整數倍,即對象的大小必須是8位元組的整數倍。而對象頭部分正好是8位元組的倍數(1或2倍),是以,當對象執行個體資料部分沒有對齊時,就需要通過對齊填充來補齊全。
3.對象的通路定位
Java程式通過棧上的reference資料來操作堆上的具體對象。
對象的通路方式:
--句柄:優點是reference中存儲的是穩定的句柄位址,對象被移動時隻改變句柄中的執行個體資料指針,reference本身不需要改變
--直接指針:優點是速度快,節省了一次指針定位開銷

4.OOM異常處理
-Xms:堆的最小值
-Xmx:堆的最大值(最大堆容量)
堆的最小值與最大值設成一樣即可避免自動擴充
-XX:+HeapDumpOnOutOfMemoryError:虛拟機在出現記憶體溢出異常時可以Dump出目前的記憶體堆轉儲快照
-XX:MaxPermSize:最大方法區容量
-Xss:棧記憶體容量
(1)Java堆溢出
首先要厘清是記憶體洩漏還是記憶體溢出:
— 記憶體洩漏(Memory Leak)可以通過dump檔案分析洩漏對象到GC Roots的引用鍊。找到洩漏對象是通過怎樣的路徑與GC Roots相關聯并導緻垃圾收集器無法自動回收他們的。
— 記憶體溢出(Memory Overflow):存在某些對象生命周期過長,持有狀态時間過長,嘗試減少程式運作期的記憶體消耗
(2)虛拟機棧和本地方法棧溢出
32位Windows系統限制為2GB。虛拟機提供參數來控制Java堆和方法區這兩部分記憶體最大值。剩餘2GB記憶體 - Xmx(最大堆容量) - MaxPermSize(最大方法區容量) - 程式計數器消耗記憶體(可忽略不計),剩下的記憶體由虛拟機棧和本地方法棧所有。每個線程配置設定到棧容量越大,可建立的線程數就越少,就餘額容易耗盡記憶體。
是以,若為建立過多線程導緻記憶體溢出時,在不能減少線程數量時應當減少最大堆和減少棧容量來換取更多線程。
(3)方法區和運作時常量池溢出
運作時常量池是方法區的一部分
eg:String.intern()
Native方法,
作用:如果字元串常量池中已經包含一個等于此String對象的字元串,則傳回代表池中這個字元串的String對象;
否則将此String對象包含的字元串添加到常量池中,并傳回此String對象的引用
/**
* OOM後跟随提示:PermGenspace說明是永久代記憶體溢出
* JDK1.6将出現OutOfMemory:PermGenspace
* JDK1.7中不出現錯誤
* JDK1.6及之前的版本将常量池配置設定在永久代内,可以通過配置-XX:PermSize -XX:MaxPermSize限制方法區大小,
* 進而間接限制常量池的容量
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
//使用List保持着常量池引用,避免Full GC回收常量池行為
List<String> list = new ArrayList<String>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
System.out.println(i);
}
}
}
public static void main(String[] args) {
String str1 = new StringBuilder("計算機").append("軟體").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
JDK1.7在常量池中記錄首次出現的執行個體引用,intern()傳回的引用和由StringBuilder建立的那個字元串執行個體是同一個,
若字元串常量池中已經有它的引用了,不符合首次出現的原則,則傳回false
方法區記憶體溢出:
— 方法區存放Class相關資訊:類名/通路修飾符/常量池/字段描述...
— 運作時産生大量的類(持續建立類)去填滿方法區,直到溢出
— 目前很多主流架構:Spring,Hibernate,在類進行增強時,都會使用到CGLib這類位元組碼技術,增強的類越多,就需要越大的方法區來保證動态生成的Class可以加載入記憶體
— Caused by:java.lang.OutOfMemoryError:PermGen space
(4)本機直接記憶體溢出
DirectMemory容量可以通過-XX:MaxDirectMemorySize指定,若不指定則預設與Java堆最大值(-Xmx指定)一樣。
由DirectMemory導緻的記憶體溢出,一個明顯的特征就是在Heap Dump檔案中不會看見明星異常,若OOM時Dump檔案很小,程式又直接或間接使用了NIO,則需要考慮檢查下是否為本機直接記憶體溢出