JVM&GC
JVM 常用參數設定積累
# 堆的初始值,預設實體記憶體的1/64
-Xms:
# 堆的最大值,預設實體記憶體的1/4
-Xmx:
# 年輕代大小「在整個堆記憶體大小确定的情況下,增大年輕代将會減小年老代,反之亦然。此值關系到 JVM 垃圾回收,對系統性能影響較大,官方推薦配置為整個堆大小的3/8」
-Xmn:
# 設定年輕代初始值為 1024 M
-XX:NewSize=1024
# 設定年輕代最大值為 1024 M
-XX:MaxNewSize=1024m
# 設定線程棧大小,設定越小,說明一個線程棧裡面能配置設定的棧幀數就越少,但對于 JVM 來講,能開啟的線程數就越多;
-Xss128k
# 方法區大小設定「jdk1.8 之後使用元空間替換了方法區,也使用了其他指令」
-XX:MaxPermSize
# 元空間大小設定
-XX:MetaspaceSize
-XX:MaxMetaspaceSize
# 設定大對象的大小,如果對象超過設定大小會直接進入老年代,不會進入年輕代「隻在 Serial 和ParNew兩個收集 器下有效」
-XX:PretenureSizeThreshold=1000000
# 設定對象晉升到老年代的年齡門檻值「設定經曆 10 次拷貝後,如果對象依舊存活則晉升至老年代,反之則清理掉,實際上絕大多數的對象都是朝生夕死」
-XX:MaxTenuringThreshold=10
# jdk1.8 預設設定了下述參數,設定該參數,就會在每次 minor gc 之前看看老年代的可用記憶體大小,是否大于之前每一次 minor gc 後進入老年代的對象的平均大小,如果小于則那麼就會觸發一次 Full gc
-XX:-HandlePromotionFailure
JVM 排查問題指令積累
# 查詢執行個體個數和占用空間大小
jmap -histo pid
# 導出堆記憶體資訊
jmap -dump:format=b,file=test.hprof pid
# 查找死鎖,列印出線程的狀态
jstack pid
# 檢視目前運作 java 應用的擴充參數
jinfo pid
# 檢視記憶體中各個部分的使用情況「eden、survivor、old」
jstat -gc pid
# 堆記憶體統計
jstat -gccapacity pid
# 新生代記憶體統計
jstat -gcnewcapacity pid
# 新生代垃圾回收統計
jstat -gcnew pid
# 老年代記憶體統計
jstat -gcoldcapacity pid
# 老年代垃圾回收統計
jstat -gcold pid
# 中繼資料空間統計
jstat -gcmetacapacity pid
# 總結垃圾回收統計
jstat -gcutil pid
JVM 的運作模式有三種:
- 解釋模式「-Xint」:隻使用解釋器,執行一行位元組碼就編譯一次機器碼「不會去緩存」;
- 優點:啟動塊;
- 缺點:整體執行相比編譯模式慢;
- 編譯模式「-Xcomp」:隻使用編譯器,現将所有的位元組碼檔案一次性編譯為機器碼,然後一次性去執行所有的機器碼;
- 優點:好處是執行快;
- 缺點:啟動比解釋模式慢;
- 混合模式:依舊采用解釋模式執行代碼,但是對于一些“熱點”代碼采用編譯模式執行,JVM 一般采用混合模式執行代碼;
- 優點:相比解釋模式,執行會快,相比編譯模式,啟動會快;
針對混合模式,JVM 有對應的技術去實作,比如 JIT,也就是即時編輯技術。
JVM 記憶體配置設定與回收
- jvm 記憶體區域圖
graph LR
runTime[JVM 運作時記憶體區域] --> B[堆] & mateData[方法區/元空間] & T[虛拟機棧] & time[程式計數器] & method[本地方法棧]
B --> B1[堆内記憶體]
B --> B2[堆外記憶體]
mateData --> 常量[常量]
mateData --> 靜态變量[靜态變量]
mateData --> 類元資訊[類資訊]
B1 --> young[年輕代]
B1 --> old[老年代]
T --> stack[棧幀]
time[程式計數器] --> time1[線程私有,記錄程式執行的位置,為線程切換用]
method[本地方法棧] --> method1[線程私有,面向 native 方法]
young --> eden[伊甸園]
young --> survivor[survivor 區]
survivor --> survivor1[survivor from 區]
survivor --> survivor2[survivor to 區]
堆
- 堆内記憶體:堆内記憶體 = 年輕代 + 老年代
- 優點:
- 缺點:
- 堆外記憶體:把記憶體對象配置設定在 Java 虛拟機的堆以外的記憶體「比如:java.nio.DirectByteBuffer」
- 減少了垃圾回收機制(GC 會暫停其他的工作);
- 加快了複制的速度「堆内在flush到遠端時, 會先複制到直接記憶體(非堆記憶體), 然後再發送,而堆外記憶體(本身就是實體機記憶體)幾乎省略了該步驟」。
- 記憶體難以控制「使用了堆外記憶體就間接失去了JVM管理記憶體的可行性,改由自己來管理,當發生記憶體溢出時排查起來非常困難」。
年輕代
- 伊甸園:survivor from 區:survivor to 區 = 8:1:1
伊甸園
- 大部分對象都在這裡誕生;
- 當 Eden 區滿時, 依然存活的對象将被複制到 Survivor 區,當一個 Survivor 區滿時,此區的存活對象将被複制到另外一個 Survivor 區;
survivor 區
- survivor from 區
- survivor to 區
老年代
方法區/元空間
- 在 jdk1.8 之後取消了方法區,命名為元空間
Minor GC & Young GC & Major GC & Full GC 的差別:
Minor GC 發生在 Eden 區域,Young GC 發生在 Eden 、from 和 to 區域,Major GC 發生在 old 區域,Full GC 發生在整個堆空間,包括年輕代和老年代;
線程棧
什麼場景下對象會進入老年代
- 即将存儲的大對象在 Eden 區域是發現存儲不下「就算 Minor gc 之後還是存儲不下」;
- 長期存活下來的對象;
- Minor gc 後存活的對象 Survivor 區放不下;
什麼是老年代空間配置設定擔保機制
年輕代每次 Minor gc 之前 JVM 都會計算下老年代剩餘可用空間,如果這個可用空間小于年輕代裡現有的所有對象大小之和(包括垃圾對象)就會看一個 “-XX:-HandlePromotionFailure”(jdk1.8 預設就設定了)的參數是否設定了,如果有這個參數,就會看看老年代的可用記憶體大小,是否大于之前每一次 Minor gc 後進入老年代的對象的平均大小。如果上一步結果是小于或者之前說的參數沒有設定,那麼就會觸發一次 Full gc,對老年代和年輕代一起回收一次垃圾,如果回收完還是沒有足夠空間存放新的對象就會發生 OOM,當然,如果 Minor gc 之後剩餘存活的需要挪動到老年代的對象大小還是大于老年代可用空間,那麼也會觸發 Full gc,Full gc 完之後如果還是沒用空間放 Minor gc 之後的存活對象,則也會發生 “OOM”。
觸發 Full gc 的時機
- 老生代記憶體不足的時候;「使用率達到 92%」
- 即将要放進老年代的對象過大,需要進行老年代回收;「和上面類似」
- 老年代空間配置設定擔保機制中有可能觸發;「也就是老年代空間配置設定擔保失敗」
- 執行 jmap -histo:live 或者 jmap -dump:live 的時候;
如何判斷對象可以被回收
- 引用計數法
給對象添加一個引用計數器,沒增加一個地方引用它,計數器就加一,減少一個,計數器就減一,但是解決不了循環引用的問題「會導緻記憶體洩露」,主流的虛拟機都沒有使用這個。
- 可達性分析
通過一系列的稱為 GC Roots 的對象作為起點,從這些節點開始向下搜尋,找到的對象都标記為非垃圾對象,其餘未标記的對象都是垃圾對象。
GC Roots 根節點:線程棧的本地變量、靜态變量、本地方法棧的變量等等。
具體操作:從gc root根往下搜尋,然後三色标記,黑灰白,剛開始是白色,如果搜尋到A節點,A節點的子節點還沒被搜尋,則A節點是灰色,A節點包括子節點全部搜尋完畢标記為黑色,到最後白色的就回收了
-
依據引用類型
java的引用類型一般分為四種:強引用、軟引用、弱引用、虛引用。
-
強引用
普通的變量引用
public static Person person = new Person();
-
軟引用
将對象用 SoftReference 軟引用類型的對象包裹,正常情況不會被回收,但是GC做完後發現釋放不出空間存放新的對象,則會把這些軟引用的對象回收掉。軟引用可用來實作記憶體敏感的高速緩存。
public static SoftReference<Person> person = new SoftReference<Person>(new Person());
-
弱引用
将對象用 WeakReference 軟引用類型的對象包裹,弱引用跟沒引用差不多,GC 會直接回收掉,很少用。
public static WeakReference<Person> person = new WeakReference<Person>(new Person());
- 虛引用
虛引用也稱為幽靈引用或者幻影引用,是最弱的一種引用關系,幾乎不用。
一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來擷取一個對象的執行個體。為一個對象設定虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。虛引用和弱引用對關聯對象的回收都不會産生影響,如果隻有虛引用活着弱引用關聯着對象,那麼這個對象就會被回收。它們的不同之處在于弱引用的get方法,虛引用的get方法始終傳回null,弱引用可以使用ReferenceQueue,虛引用必須配合ReferenceQueue使用。
jdk中直接記憶體的回收就用到虛引用,由于jvm自動記憶體管理的範圍是堆記憶體,而直接記憶體是在堆記憶體之外(其實是記憶體映射檔案,自行去了解虛拟記憶體空間的相關概念),是以直接記憶體的配置設定和回收都是有Unsafe類去操作,java在申請一塊直接記憶體之後,會在堆記憶體配置設定一個對象儲存這個堆外記憶體的引用,這個對象被垃圾收集器管理,一旦這個對象被回收,相應的使用者線程會收到通知并對直接記憶體進行清理工作。
- 通過 finalize() 方法最終判定對象是否存活
即使在可達性分析算法中不可達的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑”階段,要真正宣告一個對象死亡,至少要經曆再次标記過程。标記的前提是對象在進行可達性分析後發現沒有與 GC Roots 相連接配接的引用鍊。
-
第一次标記并進行一次篩選。
篩選的條件是此對象是否有必要執行 finalize() 方法。當對象沒有覆寫 finalize 方法,對象将直接被回收。
-
第二次标記
如果這個對象覆寫了 finalize 方法,finalize 方法是對象脫逃死亡命運的最後一次機會,如果對象要在 finalize() 中成功拯救自己,隻要重新與引用鍊上的任何的一個對象建立關聯即可,譬如把自己指派給某個類變量或對象的成員變量,那在第二次标記時它将移除出“即将回收”的集合。如果對象這時候還沒逃脫,那基本上它就真的被回收了。
垃圾回收算法
graph
A[垃圾回收算法] --> B[标記清除算法] & C[标記整理算法] & D[複制算法] & E[分代回收算法]
1、标記-清除算法
分為兩個階段,即标記和清除,首先會标記所有需要被回收的對象,在标記完成後統一對已經标記的對象進行回收,是最基礎的收集算法。
- 優點
- 實作簡單
- 缺點
- 記憶體碎片化
- 效率不高
- 使用場景:主流虛拟機不使用
2、标記-整理算法「也叫标記-壓縮算法」
針對老年代進行回收的一種算法,标記的過程和『标記-清除算法』一樣,隻是在清除完成後,會将還存活的對象朝着一個方向移動,然後固定的清理靠近邊界的對象。
- 解決了碎片化
- 移動了對象位址,需要更新對象的引用
- 使用場景
用于老年代垃圾回收
3、複制算法「比标記清理和标記整理快 10 倍以上」
能解決「标記-清理算法」帶來碎片化問題,複制算法首先将記憶體分為大小相同的兩塊,每次隻使用其中的一塊,但這一塊被使用完後「或者是沒法提供所需的連續長度的記憶體」,就會将這一塊的記憶體複制到另一塊去,然後再一次性将這塊的記憶體空間全部清理掉。
- 效率高
- 記憶體使用率不高「總有至少一半記憶體空間沒有使用」
用于年輕代垃圾回收
4、分代回收算法
這種算法不是新鮮的算法,而是針對不同的記憶體分區,采用不同的回收算法,比如在新生代中,每次收集都會有大量對象(近 99%)死去,是以可以選擇複制算法,隻需要付出少量對象的複制成本就可以完成每次垃圾收集。而老年代的對象存活幾率是比較高的,而且沒有額外的空間對它進行配置設定擔保「老年代多是大對象,很可能是需要連續記憶體位址的對象」,是以我們必須選擇「标記清除算法」或「标記整理算法」進行垃圾收集。
垃圾收集器「回收算法的具體實作」
A[垃圾收集器] --> B[Serial 收集器] & C[ParNew 收集器] & D[Parallel 收集器] & E[CMS 收集器] & G[G1 收集器]
1、Serial 收集器「-XX:+UseSerialGC -XX:+UseSerialOldGC」
新生代采用複制算法,老年代采用标記-整理算法
Serial(串行)收集器是最基本、曆史最悠久的垃圾收集器了。是一個單線程收集器了。它的 “單線程” 的意義不僅僅意味着它隻會使用一條垃圾收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集工作的時候必須暫停其他所有的工作線程「也就是應用程式線程」,直到它收集結束。
Serial 收集器執行過程
- 沒有多線程互動,單線程實作簡單;
- 相比其他單線程收集器,效率最高「當然是比不上多線程收集器」;
- STW 時間長,使用者體驗不好
- 使用場景:
一種用途是在 JDK1.5 以及以前的版本中與 Parallel Scavenge 收集器搭配使用,另一種用途是作為 CMS 收集器的後備方案。
2、ParNew 收集器「-XX:+UseParNewGC」
ParNew 收集器其實就是 Serial 收集器的多線程版本,除了使用多線程進行垃圾收集外,其餘行為(控制參數、收集算法、回收政策等等)和 Serial 收集器完全一樣。預設的收集線程數跟 CPU 核數相同,當然也可以用參數(-XX:ParallelGCThreads)指定收集線程數,但是一般不推薦修改。
ParNew 收集器執行過程
- 相比 Serial 效率高
- 實作稍複雜
3、Parallel 收集器「-XX:+UseParallelGC(年輕代) -XX:+UseParallelOldGC(老年代)」
Parallel Scavenge 收集器關注點是吞吐量(高效率的利用CPU)。CMS 等垃圾收集器的關注點更多的是使用者線程的停頓時間(提高使用者體驗)。所謂吞吐量就是 CPU 中用于運作使用者代碼的時間與 CPU 總消耗時間的比值。 Parallel Scavenge 收集器提供了很多參數供使用者找到最合适的停頓時間或最大吞吐量,如果對于收集器運作不太了解的話,可以選擇把記憶體管理優化交給虛拟機去完成也是一個不錯的選擇。
Parallel 收集器執行過程
4、CMS 收集器「-XX:+UseConcMarkSweepGC(old)」
CMS(Concurrent Mark Sweep)以擷取最短回收停頓時間為目标的收集器。它非常符合在注重使用者體驗的應用上使用,它是 HotSpot 虛拟機第一款真正意義上的并發收集器,它第一次實作了讓垃圾收集線程與使用者線程(基本上)同時工作。
CMS 收集器執行過程
初始标記-》并發标記-》重新标記-》并發清理-》并發重置
其中隻有『初始标記』不能和使用者線程并發,其他的四個是可以的。
- 并發收集、低停頓;
- 吞吐量高;
- 對 CPU 資源敏感(會和服務搶資源);
- 無法處理浮動垃圾(即在并發清理階段又産生垃圾,這種浮動垃圾隻能等到下一次 GC 再清理了);
- 它使用的回收算法“标記-清除”算法會導緻收集結束時會有大量空間碎片産生,當然通過參數 -XX:+UseCMSCompactAtFullCollection 可以讓 jvm 在執行完标記清除後再做整理;
- 執行過程中的不确定性,會存在上一次垃圾回收還沒執行完,然後垃圾回收又被觸發的情況,特别是在并發标記和并發清理階段會出現,一邊回收,系統一邊運作,也許沒回收完就再次觸發 Full gc,也就是“concurrent mode failure”,此時會進入 stop the world,使用 serial old 垃圾收集器來回收;
注重使用者體驗的系統,低延時。
何為 concurrent mode failure 錯誤
5、G1 收集器「-XX:+UseG1GC」
一款面向伺服器的垃圾收集器,主要針對配備多顆處理器及大容量記憶體的機器,以極高機率滿足 GC 停頓時間要求的同時,還具備高吞吐量性能特征。會預測的停頓的時間,以及一些抉擇,比如 200ms 回收 10MB 和 50ms 回收 20MB 兩種選擇,會選擇第二種,用以達到有限時間内最大的回收效率;
逃逸分析
public void test() {
Person person = new Person();
}
上面的代碼,會經曆如下幾個步驟:
- 加載 Person.class 到記憶體上;
- 在棧中開辟一段空間,用于 test 方法的入棧,然後在 test 方法的棧空間配置設定一個變量 p;
- 在堆記憶體中建立一塊區域空間,配置設定記憶體位址「new 關鍵字的作用」;
- 對空間的屬性空間配置設定,預設初始化;
- 構造函數初始化;
- 将配置設定的位址指派給變量 p,即 p 指向了剛剛劃分并且初始化好的堆位址;
按照上面的步驟,每個對象的配置設定,對象會直接配置設定在堆上,但如果需要配置設定的對象非常多,并且生命周期都比較短,比如在某個循環中一直 new 某一個類的對象,并且建立的對象不會作為傳回值『或者是傳回值的一部分 』,傳回給調用者,那麼這些數量多且生命周期短的對象,将會占用較多的堆空間,這些被占用的會由 GC 定時去清理,但如果有一種手段,盡量的讓這些對象都存儲在棧裡面,也就是方法棧,這些對象的銷毀會随着方法的出棧而消亡,就不再需要 GC 去耗費寶貴的時間和資源去回收堆記憶體了,STW 的時間自然也會短,GC 的次數也會少,這種手段就是逃逸分析,在 JDK8 中逃逸分析是預設開啟。
『但一種手段的出現,肯定是有利也有弊,開啟逃逸分析也會耗費時間和資源,就需要我們自己去測試分析,手上的項目是否合适,不能保證逃逸分析的性能收益必定高于它的消耗』
逃逸分析的分類
- 方法逃逸
- 線程逃逸
當一個對象在方法裡面被定義後,它可能被外部方法所引用,例如作為調用參數傳遞到其它方法中。
這個對象甚至可能被其它線程通路到,例如指派給類變量或可以在其它線程中通路的執行個體變量。
逃逸分析總結
當一個對象,在其生命周期内,被其他對象所持有,那麼就會發生逃逸。
作者:碼上猿夢
出處:http://www.cnblogs.com/daimajun/
代碼路上一隻猿,手敲轉載請聲明。有時間請關注同名微信訂閱号【碼上猿夢】,謝謝。
總是有錯誤,希望有人能直接指出我的錯誤。