文章目錄
- 對象的結構
-
-
-
- 對象頭:
- 判斷對象是否存活的算法:
-
- 引用計數算法:
- 可達性分析算法
- 對象引用的分類
-
- 強引用
- 軟引用
-
- 弱引用
- 虛引用
-
-
- JVM記憶體區域
-
-
- 1. 程式計數器
- 2. Java棧(虛拟機棧)
- 3.本地方法棧
- 4.Java堆
- 5.方法區
- PS:JDK8後為什麼用元空間替代永久代?
-
- 垃圾回收算法
-
-
- 标記-清除算法
- 複制算法
- 标記-整理算法
- 分代收集算法
- 垃圾回收過程
-
- 思考:對象如何晉升到老年代?
- 常見的調優參數
- 觸發Full GC的條件
-
- 常見的垃圾收集器
-
- 年輕代常見的垃圾收集器
-
- Serial收集器(-XX:+UseSerialGC,複制算法)
- ParNew收集器(-XX:+UseParNewGC,複制算法)
- Parallel Scavenge收集器(-XX:+UseParallelGC,複制算法)
- 老年代常用的垃圾收集器
-
- Serial Old收集器(-XX:+UseSerialOldGC,标記-整理算法)
- Parallel Old收集器(-XX:+UseParallelOldGC,标記-整理算法)
- CMS收集器(-XX:+UseConcMarkSweepGC,标記清除算法)
- G1收集器(-XX:+UseG1GC,複制+标記-整理算法,同時适用于年輕代和老年代)
- JDK11:Epsilon GC 和 ZGC(比G1效率更高)
對象的結構
在HotSpot虛拟機中,對象在記憶體中的布局可以分為3塊區域:對象頭,執行個體資料,對齊填充
對象頭:
包括兩部分資訊
第一部分**(Mark Word标記字段)**:哈希碼、GC分代年齡、鎖狀态标志、線程持有的鎖、偏向線程ID、偏向時間戳等
存儲内容 | 标志位 | 狀态 |
---|---|---|
對象哈希碼、對象分代年齡 | 01 | 未鎖定(确定對象的記憶體位址) |
指向鎖記錄的指針 | 00 | 輕量級鎖定 |
指向重量級鎖的指針 | 10 | 膨脹(重量級鎖定) |
空,不需要記錄的資訊 | 11 | GC标記 |
偏向線程ID、偏向時間戳、對象分代年齡 | 01 | 可偏向 |
另一部分(klass point):類型指針,即對象指向它的類中繼資料的指針,虛拟機通過這個指針
補充:如果對象是一個Java數組,在對象頭中還必須有一塊用于記錄數組長度的資料,因為虛拟機可以通過普通Java對象的中繼資料資訊确定Java對象大小
判斷對象是否存活的算法:
引用計數算法:
- 給對象添加一個引用計數器,每當有一個地方引用它時,計數器加一;當引用失效時,計數器減一;任何時刻計數器為0 的對象就是不能再被使用的。
優點:實作簡單、效率高
缺點:難以解決對象之間的循環引用的問題(A對象引用着B對象,B對象引用着A對象)
可達性分析算法
- 通過一系列的“GC Roots”的對象作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鍊,當一個對象到GC Roots沒有任何引用鍊相連(也就是GC Roots到這個對象不可達)時,則證明此對象是不可用的。
可作為GC Roots的對象包括以下幾種:
- 虛拟機棧(棧幀中的本地變量表)中引用的對象
- 方法區中類靜态屬性引用對象
- 方法區中常量引用的對象
- 本地方法棧中JIN(就是Native方法)引用的對象。
- 活躍線程引用的對象
對象引用的分類
強引用
- 指在程式代碼中普遍存在的,類似
這類的引用,隻要強引用還存在,垃圾回收器永遠不會回收被引用的對象。Object obj = new Object()
軟引用
- 用來描述一些還有用但是非必要的對象。軟引用關聯的對象,在系統要OOM之前,會把這些對象列為回收範圍中進行二次回收。(JDK1.2之後提供了
類來實作軟引用)。SoftReference
弱引用
- 也是用來描述一些還有用但是非必要的對象,但強度比軟引用更弱,被弱引用關聯的對象隻能生存到下次垃圾回收之前,在下次垃圾回收時,無論此時是否正在被引用都會回收弱引用的對象。(JDK1.2後提供了
類來實作弱引用)。WeakReference
虛引用
- 是最弱的一種引用關系,一個對象是否有虛引用存在,完全不會對其生存時間構成影響,也無法通過虛引用擷取一個對象的執行個體。(JDK1.2之後提供了
類來實作虛引用)PhantomReference
JVM記憶體區域
1. 程式計數器
- 線程私有的,可以看作是目前線程說執行的位元組碼的行号訓示器, 儲存的是程式目前執行的指令的位址(也可以說儲存下一條指令的所在存儲單元的位址) ,保證每個線程都線上程切換後能夠恢複在切換之前的程式執行位置 。
- 在JVM規範中規定,如果線程執行的是非native方法,則程式計數器中儲存的是目前需要執行的指令的位址;如果線程執行的是native方法,則程式計數器中的值為空是(undefined)。由于程式計數器中存儲的資料所占空間的大小不會随程式的執行而發生改變,是以,對于程式計數器是不會發生記憶體溢出現象(OutOfMemory)的。
2. Java棧(虛拟機棧)
- 線程私有的,描述的是Java方法執行的記憶體模型:每個方法在執行時都會建立一個棧幀,用于存儲局部變量表、操作數棧、動态連結、方法出口等資訊。每個方法從調用到執行完成的過程,就對應着一個棧幀在虛拟機棧中入棧到出棧的過程。
- 局部變量表:存放了編譯期可知的各種基本類型(int,char,long,booblean,double,byte,short,float)、對象引用(reference類型,可能是一個指向對象起始位址的引用指針,也可能是指向一個對象的句柄或其他與此對象相關的位置)、returnAddress類型(指向了一條位元組碼指令的位址)。
3.本地方法棧
和虛拟機棧差別就是,虛拟機棧是為Java的方法(就是位元組碼)服務的,而本地方法棧是為虛拟機用到的Native方法服務的。
4.Java堆
- 線程共享的,Java堆是所有線程間共享的一塊記憶體區域,在虛拟機啟動時就建立。用于存放對象的執行個體,幾乎所有對象執行個體都在這裡配置設定記憶體,但随着JIT編譯器的發展與逃逸分析技術的逐漸成熟,棧上配置設定、标量替換(11章)等優化技術導緻所有對象都配置設定在堆上的結論也不是那麼絕對了。
- Java堆是垃圾收集器管理的主要區域,由于現在的垃圾收集器基本都采用分代收集算法,是以可細分為新生代和老年代,再細一點可分為:Eden區、From Survivor區、To Survivor區和老年代。
補充:通過-Xms(最小)和-Xmx(最大堆)可控制堆的大小。
5.方法區
- 線程共享的,各個線程線程間共享的記憶體區域,用于存儲已被虛拟機加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料。
- 運作時的常量池是方法區的一部分。class檔案中除了有類的版本、字段、方法、接口等資訊,還有常量池:用與存放編譯期生成的各種字面量和符号引用。
PS:JDK8後為什麼用元空間替代永久代?
元空間和永久代的差別:元空間是使用的是作業系統的記憶體,永久代使用的是JVM的記憶體。
為什麼要在直接記憶體裡拿出來一塊記憶體作為元空間取代永久代呢?主要的說法有以下幾個:
(1)類及方法的資訊等比較難确定其大小,是以對于永久代的大小指定比較困難,太小容易出現永久代溢出,太大則容易導緻老年代溢出。
(2)永久代會為 GC 帶來不必要的複雜度,并且回收效率偏低。
即友善配置設定管理,因為直接記憶體空間比較充足;便于回收,因為永久代本來回收垃圾的事件發生機率很低,直接從拿到系統記憶體中可以提高回收效率。
方法區與永久代的關系
- 很多文章裡喜歡把方法區等同與永久代,永久代既然沒了,方法區也就沒了。但我認為方法區隻是一種邏輯上的概念,永久代指實體上的堆記憶體的一塊空間,這塊實際的空間完成了方法區存儲位元組碼、靜态變量、常量的功能等等。既然如此,現在元空間也可以認為是新的方法區的實作了。
垃圾回收算法
标記-清除算法
算法分為"标記"和"清除"兩個階段:首先标記出所有需要回收的對象,在标記完成後統一回收所有标記的對象。他的标記過程就是使用了“GC Roots(可達性分析算法)”,其他垃圾回收算法就是針對其缺點進行優化的。
缺點:
- 1.效率不高,标記和清楚兩個階段的效率都不高
- 2.空間問題,使用标記清楚後會産生大量不連續的記憶體碎片,空間碎片可能會導緻程式運作過程中需要配置設定大對象時,無法找到連續的記憶體空間而再次觸發垃圾回收。
複制算法
将可以的記憶體分為大小相等的兩塊,每次隻使用其中一塊。當這一塊用完了,就将還存活的對象複制到另一塊上面,然後把已使用過的記憶體一次清理掉。
優點:
- 1.解決了碎片化的問題:因為是直接對整半個區域進行回收,不需要考慮記憶體碎片等複雜情況,隻需要移動堆頂指針,按順序配置設定記憶體即可
- 2.實作簡單、運作高效
缺點:
- 記憶體相當于直接減少了一半,代價太高。
優化方案:
因為新生代中98%的對象都是“朝生夕死”,所有并不需要按1:1來劃分記憶體空間,而是将記憶體劃分為一塊較大的Eden區和兩塊較小的Survivor區(細分為From區和To區,預設配置設定大小為8:1:1)。當回收時,Eden和Survivor中還存活的對象一次性地複制到另外一個Survivor空間上,最後清理Eden和剛才用過的Survivor區。
标記-整理算法
算法分為标記、整理兩個部分,标記:從根集合進行掃描,對存活的對象進行标記;清除:移動所有存活的對象,且按照記憶體位址次序依次排列,然後将末端記憶體位址以後的記憶體全部回收。
優點:
- 避免記憶體的不連續性
- 不用設定兩塊記憶體互換區域
- 适用于對象存活率較高的場景(老年代)
分代收集算法
主流虛拟機的算法,如HotSport将記憶體區域分為新生代和老年代(1.8後移除了永久代),不同的區域采用不同的垃圾回收算法,如新生代采用複制算法,老年代采用标記-整理算法。
- Minor GC 年輕代的垃圾回收
- Full GC 和 Major GC 老年代垃圾回收
- Yong和Old 的預設比例大約為:1:3
垃圾回收過程
1.對象建立首先在Eden區,當Eden區滿會将存活的對象拷貝到From區(對象年齡+1),然後引發Minor GC回收Eden區;
2.當Eden區再次滿了,JVM會将Eden區、From區存活的對象拷貝到To區(對象年齡+1),然後引發Minor GC回收Eden區和From區;
3.當Eden區再再次滿了,JVM會将Eden區、To區存活的對象拷貝到From區(對象年齡+1),然後引發Minor GC回收Eden區和To區;
說明:當對象的分代年齡到達某個值(預設為15,可用-xx:MaxTenuringThreshold參數調整);老年代的擔保機制:當大對象建立時,Eden區和Survivor區裝不下時,會配置設定到老年代。
思考:對象如何晉升到老年代?
- 經曆過一定的Minor GC次數依然存活的對象
- Survivor區中存放不下的對象
- 新生成的大對象(可通過:-XX:+PretenuerSizeThreshold,參數設定)
常見的調優參數
- -XX:SurvivorRatio :Eden和Survivor的比值,預設為8:1
- -XX:NewRatio :老年代和年輕代記憶體大小的比例
- -XX:MaxTenuringThreshold :對象從年輕代晉升到老年代經過GC次數的最大門檻值
觸發Full GC的條件
- 老年代空間不足
- 永久代空間不足(JDK1.7之前)
- Minor GC晉升到老年代的平均大小大于老年代剩餘空間
- 程式調用System.gc()
- 使用RMI來進行RPC或管理的JDK應用,預設每小時執行1次Full GC
常見的垃圾收集器
分為年輕代收集器和老年代收集器,有連線表示兩者可搭配使用
年輕代常見的垃圾收集器
Serial收集器(-XX:+UseSerialGC,複制算法)
- 單線程收集,進行垃圾回收時,必須暫停所有工作線程
- 簡單高效,Client模式下預設的年輕代收集器
ParNew收集器(-XX:+UseParNewGC,複制算法)
- 多線程收集,其餘行為、特點和Serial收集器一樣
- 單核CPU下執行效率不如Serial,多核CPU下有很大優勢,預設開啟的回收線程數與CPU數量相同
- 除Serial,隻要ParNew可以和CMS收集器配合工作
Parallel Scavenge收集器(-XX:+UseParallelGC,複制算法)
- 吞吐量=運作使用者代碼時間/(運作使用者代碼時間+垃圾收集時間)
- 多線收集,比起關注使用者線程停頓時間,更關注系統的吞吐量
- 多核下執行有優勢,Server模式下預設的年輕代收集器
如果不知道用什麼垃圾收集器,可以使用:-XX:UseAdaptiveSizePolicy 參數讓JVM自适應調節垃圾收集器
老年代常用的垃圾收集器
Serial Old收集器(-XX:+UseSerialOldGC,标記-整理算法)
- Serial的老年代版本,單線程收集,收集時必須暫停所有工作線程
- 簡單高效,Client模式下預設的老年代收集器
Parallel Old收集器(-XX:+UseParallelOldGC,标記-整理算法)
- 多線程,吞吐量優先,主要和Parallel Scavenge收集器組合使用
CMS收集器(-XX:+UseConcMarkSweepGC,标記清除算法)
當下主流的老年代垃圾收集器
- 停頓時間短,幾乎可以和使用者線程同時工作(僅初始标記和重新标記需要暫停)
回收過程:
- 初始标記:stop-the-world
- 并發标記:并發追朔标記(GC Roots),程式不會停頓
- 并發預清理:查找執行并發标記階段從年輕代晉升到老年代的對象
- 重新标記:暫停虛拟機,掃描CMS堆中的剩餘對象
- 并發清理:清理垃圾對象,程式不會停頓
- 并發重置:重置CMS收集器的資料結構
缺點:
- 因為采用的是标記清除算法,是以無法避免記憶體空間碎片化的問題,當配置設定大對象沒有連續的空間時,隻能再次引發GC
G1收集器(-XX:+UseG1GC,複制+标記-整理算法,同時适用于年輕代和老年代)
- 并行和并發,使用多個CPU來縮短stop-the-world的時間,并發的進行垃圾清理
- 分代收集,既可以用于年輕代,又可以用與老年代,且不同代采用不同的垃圾回收算法
- 空間整合,采用了标記-整理算法,解決了記憶體碎片化的問題
- 可預測的停頓
原理:
- 将整個Java堆記憶體劃分成多個大小相等的Region
- 雖然還有新生代、老年代的概念,但兩者不再是“實體”上的隔離的了
JDK11:Epsilon GC 和 ZGC(比G1效率更高)
持續更新中……