對象創建
• 給對象分配內存
• 線程安全性問題
• 初始化對象
• 執行構造方法
給對象分配內存
- 指針碰撞
假設Java堆中內存是絕對規整的,所有用過的內存都放在一邊,空閑的內存放在另一邊,中間放着一個指針作為分界點的指示器,那所分配內存就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離
- 空間列表
如果Java堆中的內存並不是規整的,已使用的內存和空閑的內存相互交錯,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間划分給對象實例,並更新列表上的記錄
線程安全性問題
在並發情況下,可能出現正在給對象A分配內存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內存的情況。解決這個問題有兩種方案:
- 線程同步
對分配內存空間的動作進行同步處理——實際上虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性
- 本地線程分配緩沖(TLAB)
把內存分配的動作按照線程划分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存,稱為本地線程分配緩沖(Thread Local Allocation Buffer,TLAB)。哪個線程要分配內存,就在哪個線程的TLAB上分配,只有TLAB用完並分配新的TLAB時,才需要同步鎖定。虛擬機是否使用TLAB,可以通過-XX:+/-UseTLAB參數來設定。
對象的結構(對象的內存布局)
-
Header(對象頭)
非固定的數據結構。一來是用來存儲對象自身的運行時數據,如哈希碼、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等。二來是類型指針(對象指向它的元數據的指針),JVM通過這個指針來確定該對象是哪個類的實例對象,如果對象是一個Java數組,對象頭中還需要有一塊用來記錄數組長度的數據。
- 自身運行時數據(Mark Word)
- 哈希值
- GC分代年齡
- 鎖狀態標志
- 線程持有鎖
- 偏向線程ID
- 偏向時間戳
- 類型指針
- 數組長度(只有數組對象才有)
-
InstanceData(實例數據)
存儲對象真正有效的數據,也就是程序代碼中定義的各種類型的字段內容。不論是從父類繼承的,還是類定義的。這部分的存儲順序會受到Java源碼中的定義順序的影響,相同寬度的數據分配到一(long,double)
-
Padding(對齊填充)
8個字節的整數倍,不一定必須存在,起到占位符的作用。因為JVM的自動內存管理系統要求對象的起始地址必須是8字節的整數倍,即對象的大小必須是8字節的整數倍。故當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。
垃圾回收
判斷對象為垃圾對象
-
引用計數法
給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就+1;當引用失效時,計數器值就-1;任何時刻計數器為0的對象就是不能再被使用的垃圾對象。
引用計數算法的實現簡單,判定效率高。在大部分情況下它都是一個不錯的算法。但是,至少主流的Java虛擬機里面沒有選用引用計數算法來管理內存,主要原因是它很難解決對象之間的相互循環引用的問題,這種情況下,即使斷開了對象在虛擬機棧中的reference,引用計數器永遠都不會為0,這樣就會造成內存泄漏
-
可達性分析
基本思路就是通過一系列成為“GC Roots”的對象作為起點,從這些節點開始向下搜索,搜索所走過的路徑稱為“引用鏈”(Reference Chain)。當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話說,就是從GC Roots到這個對象不可達)時,則證明這個對象是不可用的,如下圖中的object5、object6、object7雖然相互關聯,但是它們到GC Roots是不可達的,所以它們將會被判定是可回收對象。
擴展:
輸出jvm中gc的詳細信息參數配置:-verbose:gc -XX:+PrintGCDetails
[Full GC 168K->97K(1984K), 0.0253873 secs]
箭頭前后的數據168K和97K分別表示GC前后所有存活對象使用的內存容量,說明有168K-97K=71K的對象容量被回收,括號內的數據1984K為堆內存的總容量,收集所花費的時間是0.0253873秒(這個時間在每次執行的時候會有所不同)
Note:GC會暫用CPU時間片,有可能造成應用程序在某個時刻極短的停頓(stop the world).
GC Roots
在Java中,可作為GC Roots的對象包括下面幾種:
1.虛擬機棧(棧幀中的本地變量表)中引用的對象
2.方法區中類屬性引用的對象
3.方法區中常量引用的對象
4.本地方法棧中JNI(即一般說的Native方法)引用的對象
如何回收
回收策略
-
標記清除
標記-清除算法采用從根集合(GC Roots)進行掃描,對存活的對象進行標記,標記完畢后,再掃描整個空間中未被標記的對象,進行回收,如下圖所示。標記-清除算法不需要進行對象的移動,只需對不存活的對象進行處理,在存活對象比較多的情況下極為高效,但由於標記-清除算法直接回收不存活的對象,因此會造成內存碎片。
-
復制
復制算法的提出是為了克服句柄的開銷和解決內存碎片的問題。它開始時把堆分成一個對象 面和多個空閑面, 程序從對象面為對象分配空間,當對象滿了,基於copying算法的垃圾收集就從根集合(GC Roots)中掃描活動對象,並將每個活動對象復制到空閑面(使得活動對象所占的內存之間沒有空閑洞),這樣空閑面變成了對象面,原來的對象面變成了空閑面,程序會在新的對象面中分配內存。
-
標記整理
標記-整理算法采用標記-清除算法一樣的方式進行對象的標記,但在清除時不同,在回收不存活的對象占用的空間后,會將所有的存活對象往左端空閑空間移動,並更新對應的指針。標記-整理算法是在標記-清除算法的基礎上,又進行了對象的移動,因此成本更高,但是卻解決了內存碎片的問題。具體流程見下圖:
-
分代算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根據對象存活的生命周期將內存划分為若干個不同的區域。一般情況下將堆區划分為老年代(Tenured Generation)和新生代(Young Generation),在堆區之外還有一個代就是永久代(Permanet Generation)。老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那么就可以根據不同代的特點采取最適合的收集算法。
- 年輕代(Young Generation)的回收算法
a) 所有新生成的對象首先都是放在年輕代的。年輕代的目標就是盡可能快速的收集掉那些生命周期短的對象。b) 新生代內存按照8:1:1的比例分為一個eden區和兩個survivor(survivor0,survivor1)區。一個Eden區,兩個 Survivor區(一般而言)。大部分對象在Eden區中生成。回收時先將eden區存活對象復制到一個survivor0區,然后清空eden區,當這個survivor0區也存放滿了時,則將eden區和survivor0區存活對象復制到另一個survivor1區,然后清空eden和這個survivor0區,此時survivor0區是空的,然后將survivor0區和survivor1區交換,即保持survivor1區為空, 如此往復。
c) 當survivor1區不足以存放 eden和survivor0的存活對象時,就將存活對象直接存放到老年代。若是老年代也滿了就會觸發一次Full GC,也就是新生代、老年代都進行回收。
d) 新生代發生的GC也叫做Minor GC,MinorGC發生頻率比較高(不一定等Eden區滿了才觸發)。
- 年老代(Old Generation)的回收算法
a) 在年輕代中經歷了N次垃圾回收后仍然存活的對象,就會被放到年老代中。因此,可以認為年老代中存放的都是一些生命周期較長的對象。b) 內存比新生代也大很多(大概比例是1:2),當老年代內存滿時觸發Major GC即Full GC,Full GC發生頻率比較低,老年代對象存活時間比較長,存活率標記高。
- 持久代(Permanent Generation)的回收算法
用於存放靜態文件,如Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者調用一些class,例如Hibernate 等,在這種時候需要設置一個比較大的持久代空間來存放這些運行過程中新增的類。持久代也稱方法區。
內存分配策略
-
優先分配Eden區
-
大對象直接分配到老年代(-XX:PretenureSizeThreshold)
-
長期存活的對象分配老年代(-XX:MaxTenuringThreshold=15)
-
空間分配擔保(-XX:+HandlePromotionFailure)
檢查老年代最大可用的連續空間是否大於歷次晉升到老
年代對象的平均大小。 -
動態對象年齡對象(-XX:TargetSurvivorRatio)
如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一
半,年齡大於或等於該年齡的對象就可以直接進入老年代
逃逸分析與棧上分配
-
棧上分配
棧上分配主要是指在Java程序的執行過程中,在方法體中聲明的變量以及創建的對象,將直接從該線程所使用的棧中分配空間。 一般而言,創建對象都是從堆中來分配的,這里是指在棧上來分配空間給新創建的對象,這樣對象內存就可以隨棧針出棧而銷毀,減少GC壓力。 對棧上分配發生影響的參數就是三個,-server、-XX:+DoEscapeAnalysis和-XX:+EliminateAllocations,任何一個發生變化都不會發生棧上分配,因為啟用逃逸分析和標量替換默認是打開的。
-
逃逸分析
逃逸是指在某個方法之內創建的對象,除了在方法體之內被引用之外,還在方法體之外被其它變量引用到;這樣帶來的后果是在該方法執行完畢之后,該方法中創建的對象將無法被GC回收,由於其被其它變量引用。正常的方法調用中,方法體中創建的對象將在執行完畢之后,將回收其中創建的對象;故由於無法回收,即成為逃逸。
public class EscapeAnalysis { public static Object object; public void globalVariableEscape(){//全局變量賦值逃逸 object =new Object(); } public Object methodEscape(){ //方法返回值逃逸 return new Object(); } public void instancePassEscape(){ //實例引用發生逃逸 this.speak(this); } public void speak(EscapeAnalysis escapeAnalysis){ System.out.println("Escape Hello"); } }
- 全局變量賦值逃逸
- 方法返回值逃逸
- 實例引用逃逸分析
比如將this給其他線程或者方法使用
- 線程逃逸
賦值給類變量或可以在其他線程中訪問的實例變量 -server JVM運行的模式之一, server模式才能進行逃逸分析, JVM運行的模式還有mix/client。 -XX:+DoEscapeAnalysis:啟用逃逸分析(默認打開)
標量替換和同步消除
-
標量替換
標量就是不可再分解的量,JAVA的基本數據類型就是標量,反之就是聚合量,比如對象。如果逃逸分析確定對象不會被外部使用,並且可以再分。jvm不會創建該對象,而是將該對象成員變量分解若干個被這個方法使用的成員變量所代替。這些代替的成員變量在棧幀或寄存器上分配空間。
通過-XX:+EliminateAllocations可以開啟標量替換, -XX:+PrintEliminateAllocations查看標量替換情況(Server VM 非Product版本支持) -
同步消除
通過逃逸分析,可以確定一個對象是否被其他線程所使用。如果沒有,而對象的方法上又有同步鎖。jvm會消除對象的同步鎖 。
通過-XX:+EliminateLocks可以開啟同步消除,進行測試執行的效率