基礎 : 可達性分析算法 GC ROOTShtml
如下部份內容 來自 這個博主的文章java
標記清除法/標記壓縮法、複製收集算法、引用計數法程序員
這裏的 引用計數法 由於書中講解少,因此講一下:
引用計數法,它的基本原理是,在每一個對象中保存該對象的引用計數,當引用發生增減時對計數進行更新。引用計數的增減,通常發生在變量賦值、對象內容更新、函數結束(局部變量再也不被引用)等時間點。當一個對象的引用計數變爲0時,則說明它未來不會再被引用,所以能夠釋放相應的內存空間。
缺點:算法
分代回收的基本思路,是利用了通常性程序所具有的性質,即大部分對象都會在短期內成爲垃圾,而通過必定時間依然存活的對象每每擁有較長的壽命。
HotSpot 虛擬機中,在新生代用複製算法,老年代使用標記清除/整理算法。express
問題:若是存在老生代對象對新生代對象的引用。若是隻掃描新生代區域的話,那麼從老生代對新生代的引用就不會被檢測到。
這樣一來,若是一個年輕的對象只有來自老生代對象的引用,就會被誤認爲已經「死亡」了。
所以,在分代回收中,會對對象的更新進行監視,將從老生代對新生代的引用,
記錄在一個叫作記錄集 Rset(remembered set)的表中。在執行小回收(Minor Gc)的過程當中,這個記錄集也做爲一個根來對待。segmentfault
解決方案:在老生代到新生代的引用產生的瞬間,就必須對該引用進行記錄,而負責執行這個操做的子程序,須要被嵌入到全部涉及對象更新操做的地方。
這個負責記錄引用的子程序是這樣工做的。設有兩個對象:A和B,當對A的內容進行改寫,並加入對B的引用時,
若是①A屬於老生代對象,②B屬於新生代對象,則將該引用添加到記錄集中。
這種檢查程序須要對全部涉及修改對象內容的地方進行保護,所以被稱爲寫屏障(Write barrier)。數組
不過,相對於在一個CPU上進行GC任務分割的增量回收來講,並行回收能夠利用多CPU的性能,儘量讓這些GC任務並行(同時)進行。安全
爲了支持高頻率的新生代的回收,虛擬機使用一種叫作卡表(Card Table)的數據結構.
卡表做爲一個比特位的集合,每個比特位能夠用來表示年老代的某一區域中的全部對象是否持有新生代對象的引用。服務器
1、做用
卡表中每個位表示年老代4K的空間,
卡表記錄爲 0 的年老代區域沒有任何對象指向新生代,
卡表記錄爲 1 的區域纔有對象包含新生代引用,
所以在新生代GC時,只須要掃描卡表位爲1所在的年老代空間。使用這種方式,能夠大大加快新生代的回收速度。數據結構
2、結構
卡表是個單字節數組,每一個數組元素對應堆中的一張卡。
每次年老代對象中某個引用新生代的字段發生變化時,Hotspot VM就必須將該卡所對應的卡表元素設置爲適當的值,從而將該引用字段所在的卡標記爲髒。
以下圖:
在Minor GC過程當中,垃圾收集器只會在髒卡中掃描查找年老代-新生代引用。
Hotspot VM的字節碼解釋器和JIT編譯器使用寫屏障 維護卡表。
寫屏障 (Write barrier) 是一小段將卡狀態設置爲髒的代碼。 解釋器每次執行更新引用的字節碼時,都會執行一段寫屏障,JIT編譯器在生成更新引用的代碼後,也會生成一段寫屏障。
雖然寫屏障使得應用線程增長了 -- 性能開銷,但Minor GC變快了許多,總體的垃圾收集效率也提升了許多,一般應用的吞吐量也會有所改善。
一、 吞吐量
應用系統的生命週期內,應用程序所花費的時間和系統總運行時間的比值
系統總運行時間=應用程序耗時+GC耗時
二、 垃圾回收器負載
垃圾回收器負載=GC耗時/系統總運行時間
三、 停頓時間
垃圾回收器運行時,應用程序的暫停時間
四、 垃圾回收頻率
垃圾回收器多長時間運行一次。通常而言,頻率越低越好,一般增大堆空間能夠有效下降垃圾回收發生的頻率,可是會增長回收時產生的停頓時間。
五、 反應時間
當一個對象成爲垃圾後,多長時間內,它所佔用的內存空間會被釋放掉。
-Xms 堆大小
-Xmx 可擴展大小
-Xmn 老年代大小
-XX:SurvivorRatio Eden 區與 Survivor 區大小比例
注: surivor 區分爲 from 區與 to 區
- 在GC開始的時候,對象只會存在於Eden區和名爲「From」的Survivor區,Survivor區「To」是空的。 - 緊接着進行GC,Eden區中全部存活的對象都會被複制到「To」,而在「From」區中,仍存活的對象會根據他們的年齡值來決定去向。 - 年齡達到必定值(年齡閾值,能夠經過-XX:MaxTenuringThreshold來設置)的對象會被移動到年老代中,沒有達到閾值的對象會被複制到「To」區域 - 通過此次GC後,Eden區和From區已經被清空。這個時候,「From」和「To」會交換他們的角色,也就是新的「To」就是上次GC前的「From」 - 新的「From」就是上次GC前的「To」。 - 無論怎樣,都會保證名爲To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到「To」區被填滿,「To」區被填滿以後,會將全部對象移動到年老代中。
分配擔保機制
Minor GC觸發條件:當Eden區滿時,觸發Minor GC。
Full GC觸發條件:
(1)調用System.gc時,系統建議執行Full GC,可是沒必要然執行
(2)老年代空間不足
(3)方法去空間不足
(4)經過Minor GC後進入老年代的平均大小大於老年代的可用內存
(5)由Eden區、From Space區向To Space區複製時,對象大小大於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小
4個特色:
4個步驟:
G1的GC模式
Young GC:選定全部年輕代裏的Region。經過控制年輕代的region個數,即年輕代內存大小,來控制young GC的時間開銷。
Mixed GC:選定全部年輕代裏的Region,外加根據global concurrent marking統計得出收集收益高的若干老年代Region。在用戶指定的開銷目標範圍內儘量選擇收益高的老年代Region。
注意:Mixed GC不是full GC,它只能回收部分老年代的Region,若是mixed GC實在沒法跟上程序分配內存的速度,致使老年代填滿沒法繼續進行Mixed GC,就會使用serial old GC(full GC)來收集整個GC heap。
global concurrent marking:相似CMS,爲Mixed GC提供標記服務。
四個過程:
G1 中的幾個重要概念 -- 原文連接--美團點評
1、Region
傳統的GC收集器將連續的內存空間劃分爲新生代、老年代和永久代(JDK 8去除了永久代,引入了元空間Metaspace),這種劃分的特色是各代的存儲地址(邏輯地址,下同)是連續的。
以下圖所示:
而G1的各代存儲地址是不連續的,每一代都使用了n個不連續的大小相同的Region,每一個Region佔有一塊連續的虛擬內存地址。以下圖所示:
在上圖中,咱們注意到還有一些Region標明瞭H,它表明Humongous,這表示這些Region存儲的是巨大對象(humongous object,H-obj),即大小大於等於region一半的對象。H-obj有以下幾個特徵:
2、SATB
全稱是Snapshot-At-The-Beginning,由字面理解,是GC開始時活着的對象的一個快照。它是經過Root Tracing獲得的,做用是維持併發GC的正確性。
那麼它是怎麼維持併發GC的正確性的呢?根據三色標記算法,咱們知道對象存在三種狀態:
因爲併發階段的存在,Mutator(更改器和)Garbage Collector線程同時對對象進行修改,就會出現白對象漏標的狀況,這種狀況發生的前提是:
對於第一個條件,在併發標記階段,若是該白對象是new出來的,並無被灰對象持有,那麼它會不會被漏標呢?Region中有兩個top-at-mark-start(TAMS)指針,分別爲prevTAMS和nextTAMS。在TAMS以上的對象是新分配的,這是一種隱式的標記。
對於在GC時已經存在的白對象,若是它是活着的,它必然會被另外一個對象引用,即條件二中的灰對象。若是灰對象到白對象的直接引用或者間接引用被替換了,或者刪除了,白對象就會被漏標,從而致使被回收掉,這是很是嚴重的錯誤,因此SATB破壞了第二個條件。
也就是說,一個對象的引用被替換時,能夠經過 write barrier 將舊引用記錄下來。(並無 看懂在說什麼)
SATB也是有反作用的,若是被替換的白對象就是要被收集的垃圾,此次的標記會讓它躲過GC,這就是float garbage。由於SATB的作法精度比較低,因此形成的float garbage也會比較多。
3、RSet
全稱是Remembered Set,是輔助GC過程的一種結構,典型的空間換時間工具,和Card Table有些相似。
還有一種數據結構也是輔助GC的:Collection Set(CSet),它記錄了 GC要收集的Region集合 ,集合裏的Region能夠是任意年代的。
在GC的時候,對於old->young和old->old的跨代對象引用,只要掃描對應的CSet中的RSet便可。
Rset : 屬於points-into結構(誰引用了個人對象)
Card Table : 則是一種points-out(我引用了誰的對象)的結構
G1的RSet是在Card Table的基礎上實現的:每一個Region會記錄下別的Region有指向本身的指針,並標記這些指針分別在哪些Card的範圍內。
這個RSet實際上是一個Hash Table,Key -- 別的Region的起始地址,Value是一個集合 -- 裏面的元素是Card Table的Index。
這裏解釋一下 :
上圖有三個 Region 。紅色表明 Rset , 灰色大方框表明 Card Table。
Region2 的 Rset2 中有兩個 Region 的起始地址,分別指向 Region1 , Region3。 -- 表明 Region1 與 Region3 引用了個人對象。
Region1 的 Card Table 位置上,存在一個 對 Region2 的引用。 -- 表明 Region1 引用了 Region2 的對象。
Region3 同理。
做用:
在作YGC(Minor GC)的時候,只須要選定young generation region的RSet做爲根集,這些RSet記錄了old->young的跨代引用,避免了掃描整個old generation。
而mixed gc的時候,old generation中記錄了old->old的RSet,young->old的引用由掃描所有young generation region獲得,這樣也不用掃描所有old generation region。因此RSet的引入大大減小了GC的工做量。
4、Pause Prediction Model
G1 uses a pause prediction model to meet a user-defined pause time target and selects the number of regions to collect based on the specified pause time target.
G1 GC是一個響應時間優先的GC算法,它與CMS最大的不一樣是,用戶能夠設定整個GC過程的指望停頓時間,參數'-XX:MaxGCPauseMillis'指定一個G1收集過程目標停頓時間,默認值200ms。
G1 經過這個模型統計計算出來的歷史數據來預測本次收集須要選擇的Region數量,從而儘可能知足用戶設定的目標停頓時間。
停頓預測模型是以衰減標準誤差爲理論基礎實現的。
這裏就不詳細介紹了,有興趣的,能夠看 美團大神的文章
若是虛擬機在擴展棧時沒法申請到足夠的內存空間,則拋出 OutOfMemoryError 異常。
經過 動態代理 或 經過 CGLIB 動態生成大量的類,以及大量 JSP與 動態JSP 文件的應用 。
一. 可經過命令按期抓取heap dump或者啓動參數OOM時自動抓取heap dump文件。
二. 經過對比多個heap dump,以及heap dump的內容,分析代碼找出內存佔用最多的地方。
三. 分析佔用的內存對象,是不是由於錯誤致使的內存未及時釋放,或者數據過多致使的內存溢出。
啓動(Bootstrap)類加載器:採用 C++ 實現,它負責將 <Java_Runtime_Home>/lib下面的核心類庫或-Xbootclasspath選項指定的jar包加載到內存中。因爲啓動類加載器到本地代碼的實現,開發者沒法直接獲取到啓動類加載器的引用,因此不容許直接經過引用進行操做。編寫自定義類加載器時,若是須要把加載請求委派給啓動類加載器,直接使用 null 代替.
擴展(Extension)類加載器:擴展類加載器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它負責將< Java_Runtime_Home >/lib/ext或者由系統變量-Djava.ext.dir指定位置中的類庫加載到內存中。開發者能夠直接使用標準擴展類加載器。
系統(System)類加載器:系統類加載器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。它負責將系統類路徑java -classpath或-Djava.class.path變量所指的目錄下的類庫加載到內存中。開發者能夠直接使用系統類加載器。
工做過程:
若是一個類加載器收到了類的加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器去完成。直到頂層的啓動類加載器中,當父加載器反饋本身沒法完成這個加載請求時,子加載器會嘗試本身去加載。
方便 JNDI 服務:SPI 的接口是 Java 核心庫的一部分,是由引導類加載器來加載的;SPI 實現的 Java 類通常是由系統類加載器來加載的。引導類加載器是沒法找到 SPI 的實現類的,由於它只加載 Java 的核心庫。它也不能代理給系統類加載器,由於它是系統類加載器的祖先類加載器。也就是說,類加載器的代理模式沒法解決這個問題。
解決方法:Java 應用的線程的上下文類加載器 默認 就是系統上下文類加載器。在 SPI 接口的代碼中使用線程上下文類加載器,就能夠成功的加載到 SPI 實現的類。線程上下文類加載器在不少 SPI 的實現中都會用到。
Java默認的線程上下文類加載器是系統類加載器(AppClassLoader)。如下代碼摘自sun.misc.Launch的無參構造函數Launch()。
能夠經過 java.lang.Thread類 的 setContextClassLoader() 設置。
方便執部署的實現。能夠在不重啓服務器的狀況下,對其中的邏輯代碼進行更新。
由 父類加載器 與 Bundle 組成 , 每一個 Bundle 的功能都是 發佈 export 與依賴 import。從而造成複雜的網狀結構
原理:
OSGi 中的每一個模塊都有對應的一個類加載器。它負責加載模塊本身包含的 Java 包和類。
當它須要加載 Java 核心庫的類時(以 java開頭的包和類),它會代理給父類加載器(一般是啓動類加載器)來完成。
當它須要加載所導入的 Java 類時,它會代理給導出此 Java 類的模塊來完成加載。
在雙親委派模型的基礎上加入了 Common類加載器,Catalina類加載器,Shared類加載器,WebApp類加載器,Jsp類加載器
Common類加載器, /common 目錄 被 Tomcat 與 因此 Web 應用程序共同使用
Catalina類加載器, /server 目錄中, 被 Tomcat 使用
Shared類加載器, /shared 目錄中 ,被全部 Web 應用程序共同使用
WebApp類加載器,Jsp類加載器 , /WebApp/WEB-INF 目錄中,只能被此 Web 應用程序使用。
-version 命令,能夠輸出顯示這三種模式
JDK1.7 中的 Server 模式虛擬機中被做爲默認編譯策略。
由於存在屢次執行的循環體,因此觸發 OSR 編譯,以整個方法 做爲編譯對象。
發生在方法執行過程當中,因此叫( On Stack Replacement ) 方法棧幀還在棧上,方法就被替換了。
熱點代碼的分類:
熱點探測(Hot Spot Detection)
基於計數器 -- HotSpot 虛擬機中採用。
計數器分類:
方法調用計數器(Invocation Counter) :
**統計一段時間內**,方法被調用的次數,若是超過期間限度,則將這個方法的調用計數器減小一半,稱爲**衰減**
回邊計數器(Back Edge Counter) : 統計一個方法中循環體被執行的次數 -- OSR 編譯
在字節碼中遇到控制流向後跳轉的指令,稱爲回邊。
hotspot中內嵌有2個JIT編譯器,分別爲Client Compiler,Server Compiler,但大多數狀況下咱們稱之爲C1編譯器和C2編譯器。
client compiler,又稱C1編譯器,較爲輕量,只作少許性能開銷比較高的優化,它佔用內存較少,適合於桌面交互式應用。
在寄存器分配策略上,JDK6之後採用的爲線性掃描寄存器分配算法,其餘方面的優化,主要有方法內聯、去虛擬化、冗餘消除等。
A、方法內聯
多個方法調用,執行時要經歷屢次參數傳遞,返回值傳遞及跳轉等,C1採用方法內聯,把調用到的方法的指令直接植入當前方法中。-XX:+PringInlining來查看方法內聯信息,-XX:MaxInlineSize=35控制編譯後文件大小。
B、去虛擬化
是指在裝載class文件後,進行類層次的分析,若是發現類中的方法只提供一個實現類,那麼對於調用了此方法的代碼,也能夠進行方法內聯,從而提高執行的性能。
C、冗餘消除
在編譯時根據運行時情況進行代碼摺疊或消除。
Server compiler,稱爲C2編譯器,較爲重量,採用了大量傳統編譯優化的技巧來進行優化,佔用內存相對多一些,適合服務器端的應用。和C1的不一樣主要在於寄存器分配策略及優化範圍.
寄存器分配策略上C2採用的爲傳統的圖着色寄存器分配算法,因爲C2會收集程序運行信息,所以其優化範圍更多在於全局優化,不只僅是一個方塊的優化。
收集的信息主要有:分支的跳轉/不跳轉的頻率、某條指令上出現過的類型、是否出現過空值、是否出現過異常等。
逃逸分析(Escape Analysis) 是C2進行不少優化的基礎,它根據運行狀態來判斷方法中的變量是否會被外部讀取,如不會則認爲此變量是不會逃逸的,那麼在編譯時會作標量替換、棧上分配和同步消除等優化。
若是證實一個對象不會逃逸到方法或線程以外,則:
- 棧上分配(Stack Allocation) :肯定不會逃逸到**方法外**,讓這個對象在棧上分配內存,對象佔用的內存空間能夠隨棧幀的出棧而銷燬。 - 同步消除(Synchronization Elimination) :肯定不會逃逸到**線程外**,則沒法被其餘線程訪問,因此能夠取消同步措施。 - 標量替換(Scalar Repalcement) : 標量(Scalar)指一個數據沒法再分解成更小的數據來表示 -- Java 中的原始數據類型 聚合量(Aggregate)指一個數據能夠繼續分解 -- Java 中的對象 **原理:**直接建立若干個能夠被方法使用的成員變量來替代。
若是一個表達式E 已經計算過,而且從先前的計算 到如今 值不曾改變,那麼若是 E 再次出現,則能夠直接使用以前的表達式結果,代替 E 。
靜態分派 : 依靠靜態類型 定位方法。
編譯階段:Human man = new Man(); // 靜態類型爲 Human
運行階段:man.sayHello() // 動態類型爲 Man
重載的優先級
sayHello(char arg);
char -> int -> long -> float -> double // 不可轉化爲 byte short , 由於char 轉化是不安全的。
-> Character -> Serializable/Comparable -> Object -> char...(變長參數)
宗量:方法的接收者與方法的參數統稱爲宗量
單分派 根據一個宗量對目標方法進行選擇
多分派 根據多個宗量對目標方法進行選擇
public class QQ{}; public class _360{}; public static class Father { public void hardChoice(QQ arg); public void hardChoice(_360 arg); } public static class Son extends Father{ public void hardChoice(QQ arg); public void hardChoice(_360 arg); } Father father = new Father(); Father son = new Son(); // 靜態多分派 - 編譯 : 方法的接收者 Father - Son, 參數 QQ - _360 father.hardChoice(_360); // 動態多分派 - 運行 : 已經肯定 參數爲 QQ ,再判斷 實際類型 , son的實際類型爲 Son 。 son.hardChoice(QQ);
都看到這裏了,點個關注,點波贊再走,QAQ。
你的小手輕點,是我最大的動力哦。
一隻想當程序員的1米88處女座大可愛如此說道。