該讀書筆記用於記錄在學習《深刻理解Java虛擬機——JVM高級特性與最佳實踐》一書中的一些重要知識點,對其中的部份內容進行概括,或者是對其中不明白的地方作一些註釋。主要是方便以後進行復習。html
在上一節中有提到在運行時數據區域包括:堆、虛擬機棧、本地方法棧、程序計數器、方法區(JDK1.7及以前)、元空間(JDK1.8及以後)。在這些區域中,程序計數器佔用內存極小,能夠忽略;棧區域在編譯期就能夠肯定下來,而且其聲明週期隨線程保持一致,也不用管;而Java堆和方法區、元空間中接口的不一樣實現類須要的內存不一樣,方法的不一樣實現須要的內存也不一樣,並且這些所需的內存須要在運行時才能肯定,因此垃圾回收關注的主要內容就是這些區域。算法
給對象添加一個引用計數器,每當有一個地方引用它時,計數器加一;引用失效的時候,計數器就減一;在任什麼時候候只要計數器爲0就表明該對象就是不會再被使用的。數組
該方法的優勢:安全
缺點:bash
很難解決對象之間相互循環引用的問題。即兩個對象相互持有對方的引用,除此以外再沒有別的地方使用這兩個對象,可是由於相互引用致使計數器不可能爲0,因此沒法被回收數據結構
經過選擇一些知足必定條件的對象做爲節點,從這些節點開始往下搜索,搜索通過的路徑被稱爲引用鏈(有直接或間接引用關係的對象都在引用鏈上),這些對象被成爲"GC Roots",當一個對象達到GC Roots沒有任何引用鏈時則斷定該對象不可用,即便該對象仍舊被其餘對象引用,只要其與GC Roots沒有關係既是不可用的。多線程
簡單來講包括如下幾種類型:併發
在最初的Java中,引用僅僅是指一個對象的數據中存儲的值是另一塊內存的起始地址。在JDK1.2以後將引用分爲多種:jvm
強引用:強引用是相似於User user = new User()
,是在代碼中最經常使用的一種方式。只要強引用存在,那麼垃圾回收器就永遠不會回收掉被引用的對象。
軟引用:軟引用用於描述一些有用可是並非必定須要的對象,對於軟引用的對象,當內存將要發生溢出時,會將這些對象列入回收範圍中進行一次回收,若是將軟引用的對象回收後內存仍是不足纔會拋出內存溢出異常。在JDK中使用SoftReference
類實現軟引用。SoftReference<Object> softReference = new SoftReference<Object>(new Object());
弱引用:弱引用用於描述非必須的對象,弱引用對象在下一次垃圾回收時必定會被回收,不管當前內存是否足夠。在JDK中使用WeakReference
定義弱引用。
虛引用:一個對象是否存在虛引用對其生存時間不會有任何關係,只是在這個對象唄收集器回收時收到一個系統通知。在JDK中使用PhantomReference
來實現虛引用。
實際上,在可達性算法中即便是不可達的對象也並不是必定會被回收的,判斷其是否會被回收還須要走如下流程:
若是對象在可達性分析中被斷定沒有與GC Roots相鏈接的引用鏈那麼改對象將會被標記,而後進行一次篩選。
篩選的條件是判斷該對象是否有必要執行finalize()方法。是否有必要執行finalize()方法的條件是當對象沒有覆蓋finalize()方法或者該對象的finalize()方法已經被虛擬機調用過,這兩種狀況都會被斷定爲沒有必要執行。
若是被斷定爲有必要執行finalize方法,則會將其放在一個隊列中,稍後執行。在finalize()方法中是對象逃脫被回收的最後機會,只要從新與引用鏈中的任何一個對象創建關係便可。
public class FinalizeEscape {
private static FinalizeEscape escape = null;
public static void main(String[] args) throws InterruptedException {
escape = new FinalizeEscape();
//模擬對象使用後斷開引用鏈
escape = null;
//對象自救
System.gc();
Thread.sleep(500);
if(escape != null){
System.out.println("對象沒有被清除!");
}else {
System.out.println("對象已經被清除!");
}
//模擬第二次逃脫gc
escape = null;
System.gc();
Thread.sleep(500);
if(escape != null){
System.out.println("對象沒有被清除!");
}else {
System.out.println("對象已經被清除!");
}
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize execute!");
escape = this;
}
}
運行結果:
finalize execute!
對象沒有被清除!
對象已經被清除!
複製代碼
在對同一對象進行兩次模擬逃脫gc,第一次成功第二次失敗,是由於一個對象的finalize()方法只會被調用一次。
在方法區的回收主要包括兩個方面:
廢棄常量是指在常量池中存在一個值,假設爲一個字面量,可是在當前系統中沒有任何的一個對象引用了該字面量。那麼久認爲該字面量是廢棄的,在下一次垃圾回收的時候將其進行回收。同理常量池中的其餘類、接口、方法、字段等的符號引用的回收也是相似。
要斷定一個類是否能夠被回收須要知足如下幾個條件:
當一個類知足以上條件後就容許被回收,但不是必定會被回收。是否對類進行回收再HotSpot虛擬機中提供了-Xnoclassgc
參數進行控制。也可使用-verbose:class,-XX:+TraceClassLoading,-XX:+TraceClassUnloading
參數查看類加載和卸載信息。
在使用反射、動態代理、動態生成jsp和OSGI等頻繁自定義ClassLoader的場景都須要虛擬機具有卸載類的功能,保證永久代不會溢出。
須要注意的是在JDK1.7時HotSpot就已經將運行時常量池遷移到堆中,在JDK1.8中更是直接移除了方法區,因此上面的介紹須要對應到具體的版本,並非指着必定是在方法區完成。雖然區域發生變化可是回收的原則基本仍是這樣。
JDK1.8開始把類的元數據放到本地堆內存(native heap)中,若是Metaspace的空間佔用達到了設定的最大值,那麼就會觸發GC來收集死亡對象和進行類卸載,這一塊的回收要求較高,上文中有簡單說過。
有關元空間的JVM參數:
標記-清除算法的基本內容就同其名字同樣,存在着標記和清除兩個階段:首先查找與GC Roots無任何關聯的對象,標記處所需回收的對象(如何標記在內存回收中已經介紹了,經過判斷是否有必要或已經執行了finalize()方法),在標記完成以後再統一清除。
標記過程:虛擬機從做爲GC Roots的根節點出發進行搜索,對可被訪問到的對象作一個標記,其餘未被標記的對象就是須要被回收的。效率低是由於目前來講項目中的對象極多,單單是進行遍歷就須要耗費較長的時間。
好處:實現簡單,標記-清除算法流程十分簡單,實現也沒有很複雜的地方。
缺點: 1.效率較低:由於標記和清除的過程效率都不高 2.浪費內存空間:在清除標記的對象後形成了內存中大量不連續的空間,一旦有大的對象進入可能會由於沒有合適的存放的地方而形成再一次的GC。
複製算法的基本內容是要求虛擬機並不將全部的內存空間用來存放對象。複製算法將內存分爲兩塊,每一次都只是使用其中的一塊,當觸發GC時,將存放對象的那一塊內存上還存活的對象複製到另外一塊上去,而後將以前的內存塊所有清除。
優勢:實現簡單,並且由於在將存活對象轉移時順序內存存放不用考慮內存碎片的問題,效率較高。
缺點: 1.始終有一部份內存沒有獲得使用,形成空間浪費。要保證存活的對象可以徹底複製,那麼就要求兩塊內存大小一致(50%),由於可能存在沒有任何對象死亡的極端狀況,可是這樣將會極其浪費,而若是不這樣分配,就必須引入其餘機制保證對象可以被完整的複製。
標記整理算法的標記階段同標記-清除算法一致,不過標記後並不當即清除,而是將存活(不會被GC)的對象移向內存的一端,將存活的對象所有移動後將邊界外的清除掉。
優勢:解決了內存碎片的問題
缺點:標記階段效率自己較低,還多加了一個整理階段,仍是在於整體效率較低
分代收集算法實際上並非一個新的實現方式,只是將虛擬機分紅幾塊,每一塊根據它的實際做用來選擇適合的算法,這些算法能夠是標記-清除,複製算法等等。
基於分代的收集思想,將堆內存分爲如下幾個部分:
將堆內存分爲新生代(Young)和老年代(Old),新生代又分爲Eden區、from區和to區,默認Eden:from:to=8:1:1。通常狀況下,新建立的對象都會被分配到Eden區(一些大對象可能會直接放到老年代),具體的內存分配在後面記錄。
在可達性分析中,可做爲GC Roots的節點主要是全局性的引用與執行上下文,若是要逐個檢查引用,必然消耗時間。 另外可達性分析對執行時間的敏感還體如今GC停頓上,由於這項分析工做必須在一個能確保一致性的時間間隔中進行,這裏的「一致性」的意思是指整個分析期間整個系統執行系統看起來就像被暫停在某個時間點,不能夠出現分析過程當中對象引用關係還在不斷變化的狀況,也就是在分析過程當中用戶線程還在工做。這點是致使GC進行時必須暫停全部Java執行線程的其中一個重要緣由。
可是目前主流的Java虛擬機都是準確式GC(準確式GC是指就是讓JVM知道內存中某個位置數據的類型是什麼),因此在執行系統停頓下來以後,並不須要一個不漏的檢查執行上下文和全局的引用位置,虛擬機是有辦法得知哪些地方存放的是對象的引用。在HotSpot的實現中,是使用一組OopMap的數據結構來達到這個目的的。
在OopMap的協助下,HotSpot能夠快速且準確的完成GC Roots的枚舉,但可能致使引用關係變化的指令很是多,若是爲每一條指令都生成OopMap,那將會須要大量的額外空間,這樣GC的空間成本會變的很高。
實際上,HotSpot也的確沒有爲每條指令生成OopMap,只是在特定的位置記錄了這些信息,這些位置被稱爲安全點(SafePoint)。SafePoint的選定既不能太少,以至讓GC等待時間過久,也不能設置的太頻繁以致於增大運行時負荷。因此安全點的設置是以讓程序「是否具備讓程序長時間執行的特徵」爲標準選定的。「長時間執行」最明顯的特徵就是指令序列的複用,例如方法調用、循環跳轉、異常跳轉等,因此具備這些功能的指令纔會產生SafePoint。
對於SafePoint,另外一個問題是如何在GC發生時讓全部線程都跑到安全點在停頓下來。這裏有兩種方案:搶先式中斷和主動式中斷。搶先式中斷不須要線程代碼主動配合,當GC發生時,首先把全部線程中斷,若是發現線程中斷的地方不在安全點上,就恢復線程,讓他跑到安全點上。如今幾乎沒有虛擬機實現採用搶先式中斷來暫停線程來響應GC。 而主動式中斷的思想是當GC須要中斷線程的時候,不直接對線程操做,僅僅簡單的設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就本身中斷掛起,輪詢標誌的地方和安全點是重合的另外再加上建立對象須要分配的內存的地方。
使用安全點彷佛已經完美解決了如何進入GC的問題,但實際狀況卻並不必定,安全點機制保證了程序執行時,在不太長的時間內就會進入到可進入的GC的安全點。可是程序若是不執行呢?所謂的程序不執行就是沒有分配cpu時間,典型的例子就是線程處於sleep狀態或者blocked狀態,這時候線程沒法響應jvm中斷請求,走到安全的地方中斷掛起,jvm顯然不太可能等待線程從新分配cpu時間,對於這種狀況,咱們使用安全區域來解決。
安全區域是指在一段代碼片斷之中,你用關係不會發生變化。在這個區域的任何地方開始GC都是安全的,咱們能夠把安全區域看作是擴展了的安全點。
當線程執行到安全區域中的代碼時,首先標識本身已經進入了安全區,那樣當在這段時間裏,JVM要發起GC時,就不用管標識本身爲安全區域狀態的線程了。當線程要離開安全區域時,他要檢查系統是否完成了根節點枚舉,若是完成了,那線程就繼續執行,不然他就必須等待,直到收到能夠安全離開安全區域的信號爲止。
Serial是一個單線程的收集器,這表示其Serial只會使用一個CPU或者是一條收集線程進行垃圾回收的工做,同時須要注意的是它在進行回收工做是會停掉全部的其餘工做線程(Stop the World),知道它的回收工做結束。
Serial雖然存在上面的問題,可是這並不表示它是一個無用的收集器,反而到目前爲止Serial收集器在Client模式下被用在新生代的收集(64位虛擬機默認支持Server模式,而且沒法切換;32位虛擬機可在Client和Server之間切換。正常狀況下,Server模式啓動較慢,但啓動後性能遠高於Client模式)。可是實際上使用也很少了。。。
Serial收集器的優勢在於:在單CPU環境中,因爲Serial因爲沒有線程的開銷,專心作垃圾回收天然能得到極高的回收效率。
ParNew實際上就是一個多線程版的Serial收集器,除了多線程進行垃圾回收外其餘都和Serial基本一致。
ParNew在不少運行於Server模式下的虛擬機中被用於新生代的首選。最大的緣由在於目前爲止只有其能CMS(Concurrent Mark Sweep)收集器配合使用。
併發與並行
Parallel Scavenge是一個新生代的收集器,同時它是一個並行的多線程的收集器,其使用複製算法。Parallel Scavenge的目標是達到一個可控制的吞吐量(throughput)。
吞吐量:CPU用於運行用戶代碼的時間和CPU總共運行時間的比值
吞吐量越高表示停頓時間短,程序響應速度快,CPU利用率越高。
Parallel Scavenge提供了幾個用於精確控制吞吐量的參數:
Serial Old是Serial的老年代版本,它也是一個單線程收集器,使用「標記-整理」算法。它的做用主要是兩個:一個是搭配Parallel Scavenge收集器使用;另外就是當CMS收集器發生Concurrency Mode Failure時做爲備用收集器。
同Serial Old同樣,Parallel Old是Parallel Scavenge的老年代版本。在注重吞吐量和CPU資源敏感的地方均可以優先考慮Parallel Old能夠和Parallel Scavenge一塊兒搭配使用。
CMS(Concurrency Mark Sweep)是一個以獲取最短回收停頓時間爲目標的收集器,容許垃圾回收線程和用戶工做線程同時運行。其使用「標記-清除」算法。目前來講例如淘寶等大型互聯網企業都但願請求響應時間能儘可能短,而且垃圾回收的停頓時間也儘可能短,這種狀況就可使用CMS收集器。
CMS的「標記-清除」算法分爲多個步驟:
其中併發標記和併發清除過程耗費時間最長,可是這兩個階段均可以併發進行,因此對用戶的影響也不會太大。
雖然CMS確實是一款很不錯的垃圾收集器,可是其也還有幾個缺點:
G1是一款面向服務端應用的垃圾收集器。G1具有以下特色:
在G1中Heap被分紅一塊塊大小相等的region,Region的大小能夠經過參數-XX:G1HeapRegionSize
設定,取值範圍從1M到32M,且是2的指數。若是不指定,那麼G1會根據Heap大小自動決定。保留新生代和老年代的概念,但它們不須要物理上的隔離。每塊region都會被打惟一的分代標誌(eden,survivor,old),表明一個分代類型的region能夠是不連續的。eden regions構成Eden空間,survivor regions構成Survivor空間,old regions構成了old 空間。經過命令行參數-XX:NewRatio=n
來配置新生代與老年代的比例,n爲整數,默認爲2,即比例爲2:1;-XX:SurvivorRatio=n
能夠配置Eden與Survivor的比例,默認爲8。
G1收集器進行回收大體可分爲如下幾個階段:
當Eden空間不足時就會觸發YGC。在G1中YGC也是採用複製存活對象到survivor空間,對於對象的存活年齡知足晉升條件時,把對象移到老年代。
在對新生代進行垃圾回收時,須要判斷哪些對象可以會被回收。這裏判斷的方法也是採用可達性分析,標記與GC Roots直接或間接關聯的對象。在CMS中使用了Card Table的結構,裏面記錄了老年代對象到新生代引用。G1也是使用這個思路,定義了一個新的數據結構:Remembered Set。在G1收集器中,Region之間的對象引用以及其餘收集器中的新生代與老年代之間的對象引用,虛擬機都是使用Remembered Set來避免全堆掃描的。G1中每一個Region都有一個與之對應的Remembered Set,虛擬機發現程序在對Reference類型的數據進行寫操做時,會產生一個Write Barrier暫時中斷寫操做,檢查Reference引用的對象是否處於不一樣的Region之中(在分代的例子中就是檢查是否老年代中的對象引用了新生代中的對象),若是是,便經過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remembered Set之中。在進行內存回收時,在GC根節點的枚舉範圍中加入Remembered Set便可保證不對全堆掃描也不會有遺漏。
full gc是指對包括新生代、老年代和方法區(元空間)等地區進行垃圾回收。
full gc的觸發包括如下幾種狀況:
這裏介紹一些打印出的gc日誌的信息:
爲了觸發gc寫一段代碼,實際上也能夠直接使用System.gc()
:
public class Test {
public static void main(String[] args) {
byte[] bytes1 = new byte[1024 * 1024];
byte[] bytes2 = new byte[1024 * 1024];
byte[] bytes3 = new byte[1024 * 1024];
byte[] bytes4 = new byte[1024 * 1024];
byte[] bytes5 = new byte[1024 * 1024];
}
public static void test(){
test();
}
}
複製代碼
要在控制檯打印gc信息須要咱們手動的配一些參數:
我這裏使用Idea,直接在VM args配置便可:
如今運行上面的程序便可在控制檯得到gc信息:
2019-01-24T20:08:25.811+0800: [GC (Allocation Failure) [PSYoungGen: 1019K->488K(1536K)] 1019K->608K(5632K), 0.0036115 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-01-24T20:08:25.872+0800: [GC (Allocation Failure) [PSYoungGen: 1504K->488K(1536K)] 1624K->780K(5632K), 0.0016239 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-01-24T20:08:25.879+0800: [GC (Allocation Failure) [PSYoungGen: 653K->504K(1536K)] 4017K->3940K(5632K), 0.0009844 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-01-24T20:08:25.880+0800: [GC (Allocation Failure) [PSYoungGen: 504K->504K(1536K)] 3940K->3948K(5632K), 0.0006796 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-01-24T20:08:25.881+0800: [Full GC (Allocation Failure) [PSYoungGen: 504K->0K(1536K)] [ParOldGen: 3444K->3832K(4096K)] 3948K->3832K(5632K), [Metaspace: 3426K->3426K(1056768K)], 0.0076471 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
2019-01-24T20:08:25.888+0800: [GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 3832K->3832K(5632K), 0.0003390 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-01-24T20:08:25.889+0800: [Full GC (Allocation Failure) Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at Test.main(Test.java:17)
[PSYoungGen: 0K->0K(1536K)] [ParOldGen: 3832K->3814K(4096K)] 3832K->3814K(5632K), [Metaspace: 3426K->3426K(1056768K)], 0.0065960 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 1536K, used 65K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
eden space 1024K, 6% used [0x00000000ffe00000,0x00000000ffe104d8,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 4096K, used 3814K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
object space 4096K, 93% used [0x00000000ffa00000,0x00000000ffdb9a60,0x00000000ffe00000)
Metaspace used 3472K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 377K, capacity 388K, committed 512K, reserved 1048576K
複製代碼
上面的gc信息取一條分析:
[GC/Full GC (Allocation Failure) [PSYoungGen: 1019K->488K(1536K)] 1019K->608K(5632K), 0.0036115 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
最前面的GC/FullGC表示gc類型,GC表示新生代gc(Minor GC),Full GC表示對新生代和老年代一塊兒收集。
[PSYoungGen: 1019K->488K(1536K)]這個表示GC前該內存區域已使用容量-->GC後該內存區域已使用容量,後面圓括號裏面的1536K爲該內存區域的總容量。
緊跟着後面的1019K->608K(5632K),表示GC前Java堆已使用容量->GC後Java堆已使用容量,後面圓括號裏面的5632K爲Java堆總容量。
[Times: user=0.00 sys=0.00, real=0.00 secs]分別表示用戶消耗的CPU時間,內核態消耗的CPU時間和操做從開始到結束所通過的牆鍾時間,CPU時間和牆鍾時間的差異是,牆鍾時間包括各類非運算的等待耗時,例如等待磁盤I/O、等待線程阻塞,而CPU時間不包括這些耗時。由於這裏是測試在幾乎一開始就發生了gc,而且設置的堆棧容量都較小,因此看不出時間。
PSYoungGen和ParOldGen分別表明新生代和老年代所使用的垃圾收集器。PSYoungGen表示Parallel Scavenge收集器,ParOldGen表示Parallel Old。要查看當前jvm使用那種收集器可使用-XX:+PrintCommandLineFlags
,命令行下運行便可。
java -XX:PrintCommandLineFlags -version
-XX:InitialHeapSize=132485376 -XX:MaxHeapSize=2119766016 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_152"
Java(TM) SE Runtime Environment (build 1.8.0_152-b16)
Java HotSpot(TM) 64-Bit Server VM (build 25.152-b16, mixed mode)
複製代碼
其中的-XX:+UseParallelGC表示使用Parallel Scavenge+Serial Old的組合,可是上面是Parallel Scavenge+parallel old的組合,這是爲何???
這裏有一篇不錯的文章總結gc中的參數,比較詳細:GC
對象內存的分配,通常是在堆上進行分配,可是隨着JIT技術的發展,部分對象直接在棧上進行內存分配。
在前面的分代收集算法小節處,已經描述了jvm中的分代,將堆分爲新生代和老年代。在描述內存分配前,咱們先來了解下不一樣的GC類型:
在大多數狀況下,對象的內存分配都優先在Eden中進行分配,當Eden區可分配的內存不足以建立對象時就會觸發一次Minor GC。將Eden區和其中一塊Survivor區內尚存活的對象放入另外一塊Survivor區域。如Minor GC時survivor空間不夠,對象提早進入老年代,老年代空間不夠時就進行Full GC。大對象直接進入老年代,避免在Eden區和Survivor區之間產生大量的內存複製,虛擬機提供了一個-XX:PretureSizeThreshold
參數,令大於這個值得對象直接進入老年代,可是該參數支隊Serial和ParNew收集器有效。 此 外大對象容易致使還有很多空閒內存就提早觸發GC以獲取足夠的連續空間。
這裏大對象主要是指那種須要大量連續內存的java對象,好比大數組或者特別長的字符串等。
年齡閾值:虛擬機爲每一個對象定義了一個對象年齡(Age)計數器, 經第一次Minor GC後 仍然存活,被移動到Survivor空間中, 並將年齡設爲1。之後對象在Survivor區中每熬 過一次Minor GC年齡就+1。 當增長到必定程度(默認 15),將會晉升到老年代(晉級的年齡能夠經過-XX:MaxTenuringThreshold
進行設置)。
提早晉升: 動態年齡斷定,若是在Survivor空間中相同年齡全部對象大小的總和大 於Survivor空間的一半, 年齡大於或等於該年齡的對象就能夠直接進入老年代,而無 須等到晉升年齡。
在前面說垃圾收集算法時關於複製對象有說過可能會存在存活下來的對象沒法被survivor容納,這時就須要老年代容納沒法被survivor容納的對象。而若是老年代也沒有足夠的空間來存放這些對象的話就會觸發一次Full GC。