我对JVM的理解
一、JVM簡介
? ? ? ?JVM總體上是由類裝載子系統(ClassLoader)、運行時數據區、執行引擎、內存回收這四個部分組成。
? ? ? ?其中我們最為關注的運行時數據區,也就是JVM的內存部分則是由方法區(Method Area)、JAVA堆(Heap)、虛擬機棧(Stack)、程序計數器、本地方法棧這幾部分組成。
1.1、JDK1.7與JDK1.8的對比
???????JDK 1.7 及以往的 JDK 版本中,Java 類信息、常量池、靜態變量都存儲在 Perm(永久代)里。類的元數據和靜態變量在類加載的時候分配到 Perm,當類被卸載的時候垃圾收集器從 Perm 處理掉類的元數據和靜態變量。當然常量池的東西也會在 Perm 垃圾收集的時候進行處理。
???????JDK 1.8 的對 JVM 架構的改造將類元數據放到本地內存中,另外,將常量池和靜態變量放到 Java 堆里。HotSopt VM 將會為類的元數據明確分配和釋放本地內存。在這種架構下,類元信息就突破了原來 -XX:MaxPermSize 的限制,現在可以使用更多的本地內存。這樣就從一定程度上解決了原來在運行時生成大量類的造成經常 Full GC 問題,如運行時使用反射、代理等。
1.2、JDK1.8 MetaSpace優勢
???????permSize:原來的jar包及你自己項目的class存放的內存空間,這部分空間是固定的,啟動參數里面-permSize確定,如果你的jar包很多,經常會遇到permSize溢出,且每個項目都會占用自己的permGen空間
???????改成metaSpaces,各個項目會共享同樣的class內存空間,比如兩個項目都用了fast-json開源包,在mentaSpaces里面只存一份class,提高內存利用率,且更利于垃圾回收
1.3、MetaSpace的理解
? ? ? ?Metaspace元空間并不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制
???????-XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那么在不超過MaxMetaspaceSize時,適當提高該值。
二、GC簡介
???????GC(GarbageCollection)是垃圾回收機制,GC是JVM對內存(實際上就是對象)進行管理的方式。GC使得Java開發人員擺脫了繁瑣的內存管理工作,讓程序的開發更有效率。GC將負責回收所有"不可達"對象的內存空間。
Minor GC、Major GC和Full GC之間的區別?
???????清理Eden區和 Survivor區叫Minor GC。
???????清理Old區叫Major GC。
???????清理整個堆空間—包括年輕代和老年代叫Full GC。
? ? ? ?HotSpot虛擬機:
2.1、回收策略
? ? ? ?在JVM中內存分配的基本粒度主要是對象、基本類型。而基本類型的使用主要是包括在對象中的局部變量,所以回收對象所占用的內存是JAVA垃圾回收的主要目標。
? ? ? ?如何判斷對象是處于可回收狀態的呢?
? ? ? ?在主流的JVM中是采用“可達性分析算法”來進行判斷的。此算法的基本思路就是通過一系列的稱為“GC Roots”的對象作為起始點,并從這些節點開始往下進行搜索,搜索走過的路徑我們稱之為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,我們就稱之為對象引用不可達,則證明這個對象是不可用的,就可以暫時判定這個對象為可回收對象。
? ? ? ?什么樣的對象可以作為GC Roots對象呢?
? ? ? ?在JAVA中可以被作為GC Roots的對象主要是:虛擬機棧-棧幀中的本地變量表所引用的對象、方法區(<JDK1.8)中類靜態屬性所引用的對象/常量屬性所引用的對象、本地方法棧中引用的對象。
2.2、回收算法
(1)、標記-清除(Mark-Sweep)算法
? ? ? ?這種算法的不足主要體現在效率和空間,從效率的角度講,標記和清除兩個過程的效率都不高;從空間的角度講,標記清除后會產生大量不連續的內存碎片, 內存碎片太多可能會導致以后程序運行過程中在需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發一次垃圾收集動作。
(2)、復制(Copying)算法
? ? ? ?復制算法是為了解決效率問題而出現的,它將可用的內存分為兩塊,每次只用其中一塊,當這一塊內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已經使用過的內存空間一次性清理掉。
(3)、標記-整理(Mark-Compact)算法
? ? ? ?復制算法在對象存活率較高的場景下要進行大量的復制操作,效率很低。萬一對象100%存活,那么需要有額外的空間進行分配擔保。老年代都是不易被回收的對象,對象存活率高,因此一般不能直接選用復制算法。根據老年代的特點,有人提出了另外一種標記-整理算法,過程與標記-清除算法一樣,不過不是直接對可回收對象進行清理,而是讓所有存活對象都向一端移動,然后直接清理掉邊界以外的內存。
(4)、分代收集算法
? ? ? ?現代商用虛擬機基本都采用分代收集算法來進行垃圾回收。這種算法沒什么特別的,無非是上面內容的結合罷了,根據對象的生命周期的不同將內存劃分為幾塊,然后根據各塊的特點采用最適當的收集算法。大批對象死去、少量對象存活的(新生代),使用復制算法,復制成本低;對象存活率高、沒有額外空間進行分配擔保的(老年代),采用標記-清理算法或者標記-整理算法。
2.3、垃圾收集器
? ? ? ?不同虛擬機所提供的垃圾收集器可能會有很大差別,我們使用的是HotSpot,HotSpot這個虛擬機所包含的所有收集器如圖:
???????上圖展示了7種作用于不同分代的收集器,如果兩個收集器之間存在連線,那說明它們可以搭配使用。虛擬機所處的區域說明它是屬于新生代收集器還是老年代收集器。
? ? ? ?明確一個觀點:沒有最好的垃圾收集器,更加沒有萬能的收集器,只能選擇對具體應用最合適的收集器。這也是HotSpot為什么要實現這么多收集器的原因。
(1)、Serial收集器
? ? ? ?最基本、發展歷史最久的收集器,這個收集器是一個采用復制算法的單線程的收集器,單線程一方面意味著它只會使用一個CPU或一條線程去完成垃圾收集工作,另一方面也意味著它進行垃圾收集時必須暫停其他線程的所有工作,直到它收集結束為止。后者意味著,在用戶不可見的情況下要把用戶正常工作的線程全部停掉,這對很多應用是難以接受的。不過實際上到目前為止,Serial收集器依然是虛擬機運行在Client模式下的默認新生代收集器,因為它簡單而高效。用戶桌面應用場景中,分配給虛擬機管理的內存一般來說不會很大,收集幾十兆甚至一兩百兆的新生代停頓時間在幾十毫秒最多一百毫秒,只要不是頻繁發生,這點停頓是完全可以接受的。
? ? ? ?說明:1. 需要STW(Stop The World),停頓時間長。2. 簡單高效,對于單個CPU環境而言,Serial收集器由于沒有線程交互開銷,可以獲取最高的單線程收集效率。
(2)、ParNew收集器
? ? ? ?ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集外,其余行為和Serial收集器完全一樣,包括使用的也是復制算法。ParNew收集器除了多線程以外和Serial收集器并沒有太多創新的地方,但是它卻是Server模式下的虛擬機首選的新生代收集器,其中有一個很重要的和性能無關的原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作(看圖)。CMS收集器是一款幾乎可以認為有劃時代意義的垃圾收集器,因為它第一次實現了讓垃圾收集線程與用戶線程基本上同時工作。ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,甚至由于線程交互的開銷,該收集器在兩個CPU的環境中都不能百分之百保證可以超越Serial收集器。當然,隨著可用CPU數量的增加,它對于GC時系統資源的有效利用還是很有好處的。它默認開啟的收集線程數與CPU數量相同,在CPU數量非常多的情況下,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。
(3)、Parallel Scavenge收集器
? ? ? ?Parallel Scavenge收集器也是一個新生代收集器,也是用復制算法的收集器,也是并行的多線程收集器,但是它的特點是它的關注點和其他收集器不同。介紹這個收集器主要還是介紹吞吐量的概念。CMS等收集器的關注點是盡可能縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是打到一個可控制的吞吐量。所謂吞吐量的意思就是CPU用于運行用戶代碼時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總運行100分鐘,垃圾收集1分鐘,那吞吐量就是99%。另外,Parallel Scavenge收集器是虛擬機運行在Server模式下的默認垃圾收集器。
? ? ? ?停頓時間短適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗;高吞吐量則可以高效率利用CPU時間,盡快完成運算任務,主要適合在后臺運算而不需要太多交互的任務。
? ? ? ?虛擬機提供了-XX:MaxGCPauseMillis和-XX:GCTimeRatio兩個參數來精確控制最大垃圾收集停頓時間和吞吐量大小。不過不要以為前者越小越好,GC停頓時間的縮短是以犧牲吞吐量和新生代空間換取的。由于與吞吐量關系密切,Parallel Scavenge收集器也被稱為“吞吐量優先收集器”。Parallel Scavenge收集器有一個-XX:+UseAdaptiveSizePolicy參數,這是一個開關參數,這個參數打開之后,就不需要手動指定新生代大小、Eden區和Survivor參數等細節參數了,虛擬機會根據當前系統的運行情況手機性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量。如果對于垃圾收集器運作原理不太了解,以至于在優化比較困難的時候,使用Parallel Scavenge收集器配合自適應調節策略,把內存管理的調優任務交給虛擬機去完成將是一個不錯的選擇。
(4)、Serial Old收集器
? ? ? ?Serial收集器的老年代版本,同樣是一個單線程收集器,使用“標記-整理算法”,這個收集器的主要意義也是在于給Client模式下的虛擬機使用。
(5)、Parallel Old收集器
? ? ? ?Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。這個收集器在JDK 1.6之后的出現,“吞吐量優先收集器”終于有了比較名副其實的應用組合,在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge收集器+Parallel Old收集器的組合。運行過程如下圖所示:
(6)、CMS收集器
? ? ? ?CMS(Conrrurent Mark Sweep)收集器是以獲取最短回收停頓時間為目標的收集器。使用標記 - 清除算法,收集過程分為如下四步:
? ? ? ? ? ? ? 1、初始標記,標記GCRoots能直接關聯到的對象,時間很短。
? ? ? ? ? ? ? 2、并發標記,進行GCRoots Tracing(可達性分析)過程,時間很長。
? ? ? ? ? ? ? 3、重新標記,修正并發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,時間較長。
? ? ? ? ? ? ? 4、并發清除,回收內存空間,時間很長。
? ? ? ?其中,并發標記與并發清除兩個階段耗時最長,但是可以與用戶線程并發執行。
? ? ? ?在G1之前的垃圾收集器,收集的范圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的內存布局與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分(可以不連續)Region的集合。
2.4、GC日志分析
[GC [DefNew: 310K->194K(2368K), 0.0269163 secs] 310K->194K(7680K), 0.0269513 secs] [Times: user=0.00 sys=0.00, real=0.03 secs] [GC [DefNew: 2242K->0K(2368K), 0.0018814 secs] 2242K->2241K(7680K), 0.0019172 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (System) [Tenured: 2241K->193K(5312K), 0.0056517 secs] 4289K->193K(7680K), [Perm : 2950K->2950K(21248K)], 0.0057094 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heapdef new generation total 2432K, used 43K [0x00000000052a0000, 0x0000000005540000, 0x0000000006ea0000)eden space 2176K, 2% used [0x00000000052a0000, 0x00000000052aaeb8, 0x00000000054c0000)from space 256K, 0% used [0x00000000054c0000, 0x00000000054c0000, 0x0000000005500000)to space 256K, 0% used [0x0000000005500000, 0x0000000005500000, 0x0000000005540000)tenured generation total 5312K, used 193K [0x0000000006ea0000, 0x00000000073d0000, 0x000000000a6a0000)the space 5312K, 3% used [0x0000000006ea0000, 0x0000000006ed0730, 0x0000000006ed0800, 0x00000000073d0000)compacting perm gen total 21248K, used 2982K [0x000000000a6a0000, 0x000000000bb60000, 0x000000000faa0000)the space 21248K, 14% used [0x000000000a6a0000, 0x000000000a989980, 0x000000000a989a00, 0x000000000bb60000) No shared spaces configured.(1)、日志的開頭“GC”、“Full GC”表示這次垃圾收集的停頓類型,而不是用來區分新生代GC還是老年代GC的。如果有Full,則說明本次GC停止了其他所有工作線程(Stop-The-World)。看到Full GC的寫法是“Full GC(System)”,這說明是調用System.gc()方法所觸發的GC。
(2)、“GC”中接下來的“[DefNew”表示GC發生的區域,這里顯示的區域名稱與使用的GC收集器是密切相關的,例如上面樣例所使用的Serial收集器中的新生代名為“Default New Generation”,所以顯示的是“[DefNew”。如果是ParNew收集器,新生代名稱就會變為“[ParNew”,意為“Parallel New Generation”。如果采用Parallel Scavenge收集器,那它配套的新生代稱為“PSYoungGen”,老年代和永久代同理,名稱也是由收集器決定的。
(3)、后面方括號內部的“310K->194K(2368K)”、“2242K->0K(2368K)”,指的是該區域已使用的容量->GC后該內存區域已使用的容量(該內存區總容量)。方括號外面的“310K->194K(7680K)”、“2242K->2241K(7680K)”則指的是GC前Java堆已使用的容量->GC后Java堆已使用的容量(Java堆總容量)。
(4)、再往后“0.0269163 secs”表示該內存區域GC所占用的時間,單位是秒。最后的“[Times: user=0.00 sys=0.00 real=0.03 secs]”則更具體了,user表示用戶態消耗的CPU時間、內核態消耗的CPU時間、操作從開始到結束經過的墻鐘時間。后面兩個的區別是,墻鐘時間包括各種非運算的等待消耗,比如等待磁盤I/O、等待線程阻塞,而CPU時間不包括這些耗時,但當系統有多CPU或者多核的話,多線程操作會疊加這些CPU時間,所以如果看到user或sys時間超過real時間是完全正常的。
(5)、“Heap”后面就列舉出堆內存目前各個年代的區域的內存情況。
?
?
?
總結
- 上一篇: 论文浅尝 | 神经符号推理综述(下)
- 下一篇: CCKS 2019 | 百度 CTO 王