1、前言
對於Java虛擬機在內存分配與回收的學習,若是讀者大學時代沒有偷懶的話,操做系統和計算機組成原理這兩門功課學的比較好的話,理解起來JVM是比較容易的,只要底子還在,不少東西均可以舉一反三。javascript
1.1 計算機==>操做系統==>JVM
JVM全稱爲Java Virtual Machine,譯爲Java虛擬機,讀者會問,虛擬機虛擬的是誰呢?即虛擬是對什麼東西的虛擬,即實體是什麼,是如何虛擬的?下面讓咱們來看看「虛擬與實體」。java
一圖解析計算機、操做系統、JVM三者關係linux
1.1.1 虛擬與實體(對上圖的結構層次分析)
JVM之因此稱爲之虛擬機,是由於它是實現了計算機的虛擬化。下表展現JVM位於操做系統堆內存中,分別實現的了對操做系統和計算機的虛擬化。程序員
操做系統棧對應JVM棧,操做系統堆對應JVM堆,計算機磁盤對應JVM方法區,存放字節碼對象,計算機PC寄存器對應JVM程序計數器(注意:計算機PC寄存器是下一條指令地址,JVM程序計數器是當前指令的地址),惟一不一樣的是,整個計算機(內存(操做系統棧+操做系統堆) +磁盤+PC計數器)對應JVM佔用的整個內存(JVM棧+JVM堆+JVM方法區+JVM程序計數器)。web
1.1.2 Java程序執行(對上圖的箭頭流程分析)
上圖中不只是結構圖,展現JVM的虛擬和實體的關係,也是一個流程圖,上圖中的箭頭展現JVM對一個對象的編譯執行。算法
程序員寫好的類加載到虛擬機執行的過程是:當一個classLoder啓動的時候,classLoader的生存地點在JVM中的堆,首先它會去主機硬盤上將Test.class裝載到JVM的方法區,方法區中的這個字節文件會被虛擬機拿來new Test字節碼(),而後在堆內存生成了一個Test字節碼的對象,最後Test字節碼這個內存文件有兩個引用一個指向Test的class對象,一個指向加載本身的classLoader。整個過程上圖用箭頭表示,這裏作說明。數組
就像本文開始時說過的,有了計算機組成原理和操做系統兩門課的底子,學起JVM的時候會容易許多,由於JVM本質上就是對計算機和操做系統的虛擬,就是一個虛擬機。tomcat
Java正是有了這一套虛擬機的支持,才成就了跨平臺(一次編譯,永久運行)的優點。安全
這樣一來,前言部分咱們成功引入JVM,接下來,本文要講述的重點是JVM自動內存管理,先給出總述:服務器
JVM自動內存管理=分配內存(指給對象分配內存)+回收內存(回收分配給對象的內存)
上面公式告訴咱們,JVM自動內存管理分爲兩塊,分配內存和回收內存
2、JVM內存空間與參數設置
2.1 運行時數據區
JVM在執行Java程序的過程當中會把它所管理的內存劃分爲若干個不一樣的運行時數據區域。這些運行時數據區包括方法區、堆、虛擬棧、本地方法棧、程序計數器,如圖:
讓咱們一步步介紹,對於運行時數據區,不少博客都是使用順序介紹的方式,不利於讀者對比比較學習,這裏筆者以表格的方式呈現:
讓咱們對上表繼續深刻,講述上表中的StackOverflowError和OutOfMemoryError。
2.2 關於StackOverflowError和OutOfMemoryError
2.2.1 StackOverflowError
運行時數據區中,拋出棧溢出的就是虛擬機棧和本地方法棧,
產生緣由:線程請求的棧深度大於虛擬機所容許的深度。由於JVM棧深度是有限的而不是無限的,可是通常的方法調用都不會超過JVM的棧深度,若是出現棧溢出,基本上都是代碼層面的緣由,如遞歸調用沒有設置出口或者無限循環調用。
解決方法:程序員檢查代碼是否有無限循環便可。
2.2.2 OutOfMemoryError
容易發生OutOfMemoryError內存溢出問題的內存空間包括:Permanent Generation space和Heap space。
一、第一種java.lang.OutOfMemoryError:PermGen space(方法區拋出)
產生緣由:發生這種問題的原意是程序中使用了大量的jar或class,使java虛擬機裝載類的空間不夠,與Permanent Generation space有關。因此,根本緣由在於jar或class太多,方法區堆溢出,則解決方法有兩個種,要麼增大方法區,要麼減小jar、class文件,且看解決方法。
解決方法:
1. 從增大方法區方面入手:
增長java虛擬機中的XX:PermSize和XX:MaxPermSize參數的大小,其中XX:PermSize是初始永久保存區域大小,XX:MaxPermSize是最大永久保存區域大小。
如web應用中,針對tomcat應用服務器,在catalina.sh 或catalina.bat文件中一系列環境變量名說明結束處增長一行:
JAVA_OPTS=" -XX:PermSize=64M -XX:MaxPermSize=128m"
可有效解決web項目的tomcat服務器常常宕機的問題。
2. 從減小jar、class文件入手:
清理應用程序中web-inf/lib下的jar,若是tomcat部署了多個應用,不少應用都使用了相同的jar,能夠將共同的jar移到tomcat共同的lib下,減小類的重複加載。
二、第二種OutOfMemoryError:Java heap space(堆拋出)
產生緣由:發生這種問題的緣由是java虛擬機建立的對象太多,在進行垃圾回收之間,虛擬機分配的到堆內存空間已經用滿了,與Heap space有關。因此,根本緣由在於對象實例太多,Java堆溢出,則解決方法有兩個種,要麼增大堆內存,要麼減小對象示例,且看解決方法。
解決方法:
1.從增大堆內存方面入手:
增長Java虛擬機中Xms(初始堆大小)和Xmx(最大堆大小)參數的大小。如:set JAVA_OPTS= -Xms256m -Xmx1024m
2.從減小對象實例入手:
通常來講,正常程序的對象,堆內存時絕對夠用的,出現堆內存溢出通常是死循環中建立大量對象,檢查程序,看是否有死循環或沒必要要地重複建立大量對象。找到緣由後,修改程序和算法。
三、第三種OutOfMemoryError:unable to create new native thread(Java虛擬機棧、本地方法棧拋出)
產生緣由:這個異常問題本質緣由是咱們建立了太多的線程,而能建立的線程數是有限制的,致使了異常的發生。能建立的線程數的具體計算公式以下:
(MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Number of threads
注意:MaxProcessMemory 表示一個進程的最大內存,JVMMemory 表示JVM內存, ReservedOsMemory 表示保留的操做系統內存,ThreadStackSize 表示線程棧的大小。
在java語言裏, 當你建立一個線程的時候,虛擬機會在JVM內存建立一個Thread對象同時建立一個操做系統線程,而這個系統線程的內存用的不是JVMMemory,而是系統中剩下的內存(MaxProcessMemory - JVMMemory - ReservedOsMemory)。由公式得出結論:你給JVM內存越多,那麼你能建立的線程越少,越容易發生 java.lang.OutOfMemoryError: unable to create new native thread。
解決方法:
1.若是程序中有bug,致使建立大量不須要的線程或者線程沒有及時回收,那麼必須解決這個bug,修改參數是不能解決問題的。
2.若是程序確實須要大量的線程,現有的設置不能達到要求,那麼能夠經過修改MaxProcessMemory,JVMMemory,ThreadStackSize這三個因素,來增長能建立的線程數:MaxProcessMemory 表示使用64位操做系統,VMMemory 表示減小 JVMMemory 的分配
ThreadStackSize 表示減少單個線程的棧大小。
2.3 JVM堆內存和非堆內存
2.3.1 堆內存和非堆內存
JVM內存劃分爲堆內存和非堆內存,堆內存分爲年輕代(Young Generation)、老年代(Old Generation),非堆內存就一個永久代(Permanent Generation)。
年輕代又分爲Eden和Survivor區。Survivor區由FromSpace和ToSpace組成。Eden區佔大容量,Survivor兩個區佔小容量,默認比例是8:1:1。
堆內存用途:存放的是對象,垃圾收集器就是收集這些對象,而後根據GC算法回收。
非堆內存用途:永久代,也稱爲方法區,存儲程序運行時長期存活的對象,好比類的元數據、方法、常量、屬性等。
在JDK1.8版本廢棄了永久代,替代的是元空間(MetaSpace),元空間與永久代上相似,都是方法區的實現,他們最大區別是:永久代使用的是JVM的堆內存空間,而元空間使用的是物理內存,直接受到本機的物理內存限制。在後面的實踐中,由於筆者使用的是JDK8,因此打印出的GC日誌裏面就有MetaSpace。
2.3.2 JVM堆內部構型(新生代和老年代)
Jdk8中已經去掉永久區,這裏爲了與時俱進,再也不贅餘。
上圖演示Java堆內存空間,分爲新生代和老年代,分別佔Java堆1/3和2/3的空間,新生代中又分爲Eden區、Survivor0區、Survivor1區,分別佔新生代8/十、1/十、1/10空間。
問題1:什麼是Java堆?
回答1:JVM規範中說到:」全部的對象實例以及數組都要在堆上分配」。Java堆是垃圾回收器管理的主要區域,百分之九十九的垃圾回收發生在Java堆,另外百分之一發生在方法區,所以又稱之爲」GC堆」。根據JVM規範規定的內容,Java堆能夠處於物理上不連續的內存空間中。
問題2:爲何Java堆要分爲新生代和老年代?
回答2:當前JVM對於堆的垃圾回收,採用分代收集的策略。根據堆中對象的存活週期將堆內存分爲新生代和老年代。在新生代中,每次垃圾回收都有大批對象死去,只有少許存活。而老年代中存放的對象存活率高。這樣劃分的目的是爲了使 JVM 可以更好的管理堆內存中的對象,包括內存的分配以及回收。
問題3:爲何新生代要分爲Eden區、Survivor0區、Survivor1區?
回答3:這是結構與策略相適應的原則,新生代垃圾收集使用的是複製算法(一種垃圾收集算法,Serial收集器、ParNew收集器、Parallel scavenge收集器都是用這種算法),複製算法能夠很好的解決垃圾收集的內存碎片問題,可是有一個自然的缺陷,就是要犧牲一半的內存(即任意時刻只有一半內存用於工做),這對於寶貴的內存資源來講是極度奢侈的。新生代在使用複製算法做爲其垃圾收集算法的時候,對其作了優化,拿出2/10的新生代的內存做爲交換區,稱爲Survivor0區和Survivor1區。
值得注意的是,有的博客上稱爲From Survivor Space和To Survivor Space,這樣闡述也是對的,可是容易對初學者造成誤導,由於在複製算法中,複製是雙向的,沒有固定的From和To,這一次是由這一邊到另外一邊,下次就是從另外一邊到這一邊,使用From Survivor Space和To Survivor Space容易讓後來學習者誤覺得複製只能從一邊到另外一邊,固然有的博客中會附加無論從哪邊到哪邊,起始就是From,終點就是To,即From Survivor Space和To Survivor Space所對應的區循環對調,可是讀者不必定想的明白。因此筆者這裏使用Survivor0、Survivor1,減小誤解。
因此說,新生代在結構上分爲Eden區、Survivor0區、Survivor1區,是與其使用的垃圾收集算法(複製算法)相適應的結果。
問題4:關於永久區Permanent Space?
回答4:因爲Jdk8中取消了永久區Permanent Space,本文爲與時俱進,再也不講述Permanent Space。
2.4 JVM堆參數設置
這些都是和堆內存分配有關的參數,因此咱們放在第二部分了,和垃圾收集器有關的參數放在第四部分。
舉例:java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m
2.4.1 JVM重要參數
由於整個堆大小=年輕代大小(新生代大小) + 年老代大小 + 持久代大小,
-Xmn2g:表示年輕代大小爲2G。持久代通常固定大小爲64m,因此增大年輕代後,將會減少年老代大小。此值對系統性能影響較大,Sun官方推薦配置爲整個堆的3/8。
-XX:NewRatio=4:設置年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代)。這裏設置爲4,表示年輕代與年老代所佔比值爲1:4,又由於上面設置年輕代爲2G,則老年代大小爲8G
-XX:SurvivorRatio=8:設置年輕代中Eden區與Survivor區的大小比值。這裏設置爲8,則兩個Survivor區與一個Eden區的比值爲2:8,一個Survivor區佔整個年輕代的1/10
則Eden:Survivor0:Survivor1=8:1:1
-XX:MaxPermSize=16m:設置持久代大小爲16m。
全部整個堆大小=年輕代大小 + 年老代大小 + 持久代大小= 2G+ 8G+ 16M=10G+6M=10246MB
2.4.2 JVM其餘參數
-Xmx3550m:設置JVM最大可用內存爲3550M。
-Xms3550m:設置JVM促使內存爲3550m,此值能夠設置與-Xmx相同。
-Xss128k:設置每一個線程的堆棧大小。JDK5.0之後每一個線程堆棧大小爲1M,之前每一個線程堆棧大小爲256K。更具應用的線程所需內存大小進行調整。在相同物理內存下,減少這個值能生成更多的線程。可是操做系統對一個進程內的線程數仍是有限制的,不能無限生成,經驗值在3000~5000左右。
關於爲何-xmx與-xms的大小設置爲同樣的?
首先,在Java堆內存分配中,-xmx用於指定JVM最大分配的內存,-xms用於指定JVM初始分配的內存,因此,-xmx與-xms相等表示JVM初次分配的內存的時候就把全部能夠分配的最大內存分配給它(指JVM),這樣的作的好處是:
1. 避免JVM在運行過程當中、每次垃圾回收完成後向OS申請內存:由於全部的能夠分配的最大內存第一個就給它(JVM)了。
2. 延後啓動後首次GC的發生時機、減小啓動初期的GC次數:由於第一次給它分配了最大的;
3. 儘量避免使用swap space:swap space爲交換空間,當web項目部署到linux上時,有一條調優原則就是「儘量使用內存而不是交換空間」
4.設置堆內存爲不可擴展和收縮,避免在每次GC 後調整堆的大小
影響堆內存擴展與收縮的兩個參數
由上表可知,堆內存默認是自動擴展和收縮的,可是有一個前提條件,就是到xmx比xms大的時候,當咱們將xms設置爲和xmx同樣大,堆內存就不可擴展和收縮了,即整個堆內存被設置爲一個固定值,避免在每次GC 後調整堆的大小。
附加:在Java非堆內存分配中,通常是用永久區內存分配:
JVM 使用 -XX:PermSize 設置非堆內存初始值,由 -XX:MaxPermSize 設置最大非堆內存的大小。
2.5 從日誌看JVM(開發實踐)
這裏了設置GC日誌關聯的類和將GC日誌打印
如程序所述,申請了10MB的空間,allocation1 2MB+allocation2 2MB+allocation3 2MB+allocation4 4MB=10MB
接下來咱們開始閱讀GC日誌,這裏筆者以本身電腦上打印的GC日誌爲例,講述閱讀GC日誌的方法:
heap表示堆,即下面的日誌是對JVM堆內存的打印;
由於使用的是jdk8,因此默認使用ParallelGC收集器,也就是在新生代使用Parallel Scavenge收集器,老年代使用ParallelOld收集器
PSYoungGen 表示使用Parallel scavenge收集器做爲年輕代收集器,ParOldGen表示使用Parallel old收集器做爲老年代收集器,即筆者電腦上默認是使用Parallel scavenge+Parallel old收集器組合。
其中,PSYoungGen總共38400K(37.5MB),被使用了13568K(13.25MB),PSYoungGen又分爲Eden Space 33280K(32.5MB) 被使用了40% 13MB,from space 5120K(5MB)和to space 5120K(5MB),這就是一個eden區和兩個survivor區。
此處注意,由於使用的是jdk8,因此沒有永久區了,只有MetaSpace,見上圖。
3、HotSpot VM
3.1 HotSpot VM相關知識
問題一:什麼是HotSpot虛擬機?HotSpot VM的前世此生?
回答一:HotSpot VM是由一家名爲「Longview Technologies」的公司設計的一款虛擬機,Sun公司收購Longview Technologies公司後,HotSpot VM成爲Sun主要支持的VM產品,Oracle公司收購Sun公司後,即在HotSpot的基礎上,移植JRockit的優秀特性,將HotSpot VM與JRockit VM整合到一塊兒。
問題二:HotSpot VM有何優勢?
回答二:HotSpot VM的熱點代碼探測能力能夠經過執行計數器找出最具備編譯價值的代碼,而後通知JIT編譯器以方法爲單位進行編譯。若是一個方法被頻繁調用,或方法中有效循環次數不少,將會分別觸發標準編譯和OSR(棧上替換)編譯動做。經過編譯器與解釋器恰當地協同工做,能夠在最優化的程序響應時間與最佳執行性能中取得平衡,並且無須等待本地代碼輸出才能執行程序,即時編譯的時間壓力也相對減少,這樣有助於引入更多的代碼優化技術,輸出質量更高的本地代碼。
問題三:HotSpot VM與JVM是什麼關係?
回答三:今天的HotSpot VM,是Sun JDK和OpenJDK中所帶的虛擬機,也是目前使用範圍最廣的Java虛擬機。
3.2 HotSpot VM的兩個實現與查看本機HotSpot
HotSpot VM包括兩個實現,不一樣的實現適合不一樣的場景:
Java HotSpot Client VM:經過減小應用程序啓動時間和內存佔用,在客戶端環境中運行應用程序時能夠得到最佳性能。此通過專門調整,可縮短應用程序啓動時間和內存佔用,使其特別適合客戶端環境。此jvm實現比較適合咱們平時用做本地開發,平時的開發不須要很大的內存。
Java HotSpot Server VM:旨在最大程度地提升服務器環境中運行的應用程序的執行速度。此jvm實現通過專門調整,多是特別調整堆大小、垃圾回收器、編譯器那些。用於長時間運行的服務器程序,這些服務器程序須要儘量快的運行速度,而不是快速啓動時間。
只要電腦上安裝jdk,咱們就能夠看到hotspot的具體實現:
4、JVM內存回收
咱們知道,Java中是沒有析構函數的,既然沒有析構函數,那麼如何回收對象呢,答案是自動垃圾回收。Java語言的自動回收機制可使程序員不用再操心對象回收問題,一切都交給JVM就行了。那麼JVM又是如何作到自動回收垃圾的呢,且看本節,本節分爲兩個部分——垃圾收集算法和垃圾收集器,其中,收集算法是內存回收的理論,而垃圾回收器是內存回收的實踐。
4.1 垃圾收集算法(內存回收理論)
4.1.1 標記-清除算法
標記-清除算法分爲兩個階段,「標記」和「清除」,
標記:首先標記出全部須要回收的對象;
清除:在標記完成後統一回收全部被標記的對象。
「標記-清除」算法的不足:第一,效率問題,標記和清除兩個過程的效率都不會過高;第二,空間問題,標記清除後產生大量不連續的內存碎片,這些內存空間碎片可能會致使之後程序運行過程當中須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發一次垃圾收集動做,若是很容易出現這樣的空間碎片多、沒法找到大的連續空間的狀況,垃圾收集就會較爲頻繁。
4.1.2 複製算法
爲了解決「標記-清除算法」的效率問題,一種複製算法產生了,它將當前可用內存按容量劃分爲大小相等的兩塊,每次只使用其中一塊。當一塊的內存用完了,就將還活着的對象複製到另外一塊上面,而後再把已使用的內存空間一次清除掉。這樣使得每次都對整個半區進行內存回收,內存分配時就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可。
這種算法處理內存碎片的核心在於將整個半塊中活的的對象複製到另外一整個半塊上面去,因此稱爲複製算法。
附:關於複製算法的改進
複製算法合理的解決了內存碎片問題,可是卻要以犧牲一半的寶貴內存爲代價,這是很是讓人心疼的。使人愉快地是,現代虛擬機中,早就有了關於複製算法的改進:
對於Java堆中新生代中的對象來講,99%的對象都是「朝升夕死」的,就是說不少的對象在建立出來後不久就會死掉了,全部咱們能夠大膽一點,不須要按照1:1的比例來劃份內存空間,而是將新生代的內存劃分爲一塊較大的Eden區(通常佔新生代8/10的大小)和兩塊較小的Survivor區(用於複製,通常每塊佔新生代1/10的大小,兩塊佔新生代2/10的大小)。當回收時,將Eden區和Survivor裏面當前還活着的對象所有都複製到另外一塊Survivor中(關於另外一個塊Survivor是否會溢出的問題,答案是不會,這裏將新生代90%的容量裏的對象複製到10%的容量裏面,確實是有風險的,可是JVM有一種內存的分配擔保機制,即當目的Survivor空間不夠,會將多出來的對象放到老年代中,由於老年代是足夠大的),最後清理Eden區和源Survivor區的空間。這樣一來,每次新生代可用內存空間爲整個新生代90%,只有10%的內存被浪費掉,
正是由於這一特性,現代虛擬機中採用複製算法來回收新生代,如Serial收集器、ParNew收集器、Parallel scavenge收集器均是如此。
4.1.3 標誌-整理算法(複製算法變動後在老年代的應用)
對於新生代來講,因爲具備「99%的對象都是朝生夕死的」這一特色,因此咱們能夠大膽的使用10%的內存去存放90%的內存中活着的對象,即便是目的Survivor的容量不夠,也能夠將多餘的存放到老年代中(擔保機制),全部對於新生代,咱們使用複製算法是比較好的(Serial收集器、ParNew收集器、Parallel scavenge收集器)。
可是對於老年代,沒有大多數對象朝生夕死這一特色,若是使用複製算法就要浪費一半的寶貴內存,全部咱們用另外一種辦法來處理它(指老年代)——標誌-整理算法。
標記-整理算法分爲兩個階段,「標記」和「整理」,
標記:首先標記出全部須要回收的對象(和標記-清除算法同樣);
整理:在標記完成後讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存(向一端移動相似複製算法)。
4.1.4 分代收集算法
當前商業虛擬機都是的垃圾收集都使用「分代收集」算法,這種算法並無什麼新的思想,只是根據對象存活週期的不一樣將內存劃分爲幾塊。通常是把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採起最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許對象存活,就是使用複製算法,這樣只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象的存活率高、沒有額外空間對其分配擔保(新生代複製算法若是目的Survivor容量不夠會將多餘對象放到老年代中,這就是老年代對新生代的分配擔保),必須使用「標記-清除算法」或「標記-整理算法」來回收。
四種經常使用算法優缺點比較、用途比較
4.2 垃圾收集器(內存回收實踐)
有了上面的垃圾回收算法,就有了不少的垃圾回收器。對於垃圾回收器,不多有表格對比,筆者以表格對比的方式呈現:
注意:G1收集器的收集算法加粗了,這裏作出說明,G1收集器從總體上來看是基於「標記-整理」算法實現的收集器,從局部(兩個region之間)上看來是基於「複製」算法實現的。
從上表能夠獲得的收集經常使用組合包括:
經常使用組合1:Serial + serial old 新生代和老年代都是單線程,簡單
經常使用組合2:ParNew+ serial old 新生代多線程,老年代單線程,簡單
經常使用組合3:Parallel scavenge + Parallel old 該組合完成吞吐量優先虛擬機,適用於後臺計算
經常使用組合4:cms收集器 完成響應時間短虛擬機,適用於用戶交互
經常使用組合5:G1收集器 面向服務端的垃圾回收器
4.2.1 經常使用組合1:Serial + serial old 新生代和老年代都是單線程,簡單
附:圖上有一個safepoint,譯爲安全點(有的博客上寫成了savepoint,是錯誤的,至少是不許確的),這個safepoint幹什麼的呢?如何肯定這個safepoint的位置?
這個safepoint是幹什麼的?
safepoint的定義是「A point in program where the state of execution is known by the VM」,譯爲程序中一個點就是虛擬機所知道的一個執行狀態。
JVM中safepoint有兩種,分別爲GC safepoint、Deoptimization safepoint:
GC safepoint:用在垃圾收集操做中,若是要執行一次GC,那麼JVM裏全部須要執行GC的Java線程都要在到達GC safepoint以後才能夠開始GC;
Deoptimization safepoint:若是要執行一次deoptimization
,那麼JVM裏全部須要執行deoptimization的Java線程都要在到達deoptimization safepoint以後才能夠開始deoptimize
咱們上圖中的safepoint天然是GC safepoint,因此上圖中的兩個safepoint都是指執行GC線程前的狀態。
對於上圖的理解是(不少博客上都有這種運行示意圖,可是沒有加上解釋,筆者這裏加上):
一、多個用戶線程(圖中是四個)要開始執行新生代GC操做,因此都要達到GC safepoint點,先到的要等待晚到的,圖中都達到了;
二、四個線程都執行新生代的GC操做,由於使用的是Serial收集器,因此是基於複製算法的單線程GC,並且要Stop the world,因此只有GC線程在執行,四個用戶線程都中止了。
三、新生代GC操做完成,四個線程繼續執行,過了一下子,要開始執行老年代的GC操做了,因此四個線程都要再次達到GC safepoint點,先到的要等待晚到的,圖中都達到了;
四、四個線程都執行老年代的GC操做,由於使用的是Serial Old收集器,因此是基於標誌-整理算法的單線程GC,並且要Stop the world,因此只有GC線程在執行,四個用戶線程都中止了。
五、老年代GC操做完成,四個線程繼續執行。
4.2.2 經常使用組合2:ParNew+ serial old 新生代多線程,老年代單線程,簡單
該組合中新生代ParNew收集器僅僅是Serial收集器的多線程版本,全部該組合相對於Serial + serial old 只是新生代是多線程而已,其他不變
對於上圖的理解是(不少博客上都有這種運行示意圖,可是沒有加上解釋,筆者這裏加上):
一、多個用戶線程(圖中是四個)要開始執行新生代GC操做,因此都要達到GC safepoint點,先到的要等待晚到的,圖中都達到了;
二、四個線程都執行新生代的GC操做,由於使用的是Parnew收集器,因此是基於複製算法的多線程GC(注意:這裏的多線程GC,是指多個GC線程併發,用戶線程仍是要中止的)因此仍是要Stop the world,因此只有GC線程在執行,四個用戶線程都中止了。
三、新生代GC操做完成,四個線程繼續執行,過了一下子,要開始執行老年代的GC操做了,因此四個線程都要再次達到GC safepoint點,先到的要等待晚到的,圖中都達到了;
四、四個線程都執行老年代的GC操做,由於使用的是Serial Old收集器,因此是基於標誌-整理算法的單線程GC,並且要Stop the world,因此只有GC線程在執行,四個用戶線程都中止了。
五、老年代GC操做完成,四個線程繼續執行。
4.2.3 經常使用組合3:Parallel scavenge + Parallel old 新生代和老年代都是多線程,該組合完成吞吐量優先虛擬機,適用於後臺計算
對於上圖的理解是:
一、多個用戶線程(圖中是四個)要開始執行新生代GC操做,因此都要達到GC safepoint點,先到的要等待晚到的,圖中都達到了;
二、四個線程都執行新生代的GC操做,由於使用的是Parallel scavenge收集器,因此是基於複製算法的多線程GC(注意,這裏的多線程GC,是指多個GC線程併發,用戶線程仍是要中止的)因此只有GC線程在執行,四個用戶線程都中止了。
三、新生代GC操做完成,四個線程繼續執行,過了一下子,要開始執行老年代的GC操做了,因此四個線程都要再次達到GC safepoint點,先到的要等待晚到的,圖中都達到了;
四、四個線程都執行老年代的GC操做,由於使用的是Parallel Old收集器,因此是基於標誌-整理算法的多線程GC,(注意,這裏的多線程GC,是指多個GC線程併發,用戶線程仍是要中止的)因此只有GC線程在執行,四個用戶線程都中止了。
五、老年代GC操做完成,四個線程繼續執行。
4.2.4 經常使用組合4:cms收集器 多線程,完成響應時間短虛擬機,適用於用戶交互
對於上圖的理解是:
CMS收集包括四個步驟:初始標記、併發標記、從新標記、併發清除(CMS做爲標記-清除收集器,三個標記一個清除)
|
是否須要stop the world,中止用戶線程 |
單個GC線程運行or多個GC線程運行 |
初始標記 |
須要 |
單個GC線程運行 |
併發標記 |
不須要 |
多個GC線程運行 |
從新標記 |
須要 |
多個GC線程運行 |
併發清除 |
不須要 |
多個GC線程運行 |
一、多個用戶線程(圖中是四個)要開始執行新生代GC操做,因此都要達到GC safepoint點,先到的要等待晚到的,圖中都達到了;
二、四個線程都執行GC操做,由於使用的是CMS收集器,第一步驟是初始標記,初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,GC的標記階段須要stop the world,讓全部Java線程掛起,這樣JVM才能夠安全地來標記對象。因此只有「初始標記」在執行,四個用戶線程都中止了。初始標記完成後,達到第二個GC safepoint,圖中達到了;
三、開始執行併發標記,併發標記是GCRoot開始對堆中的對象進行可達性分析,找出存活的對象,併發標記能夠與用戶線程一塊兒執行,併發標記完成後,全部線程達到下一個GC safepoint,圖中達到了;
四、開始執行從新標記,從新標記是爲了修正在併發標記期間因用戶程序繼續運做而致使標記產生變更的那部分標記記錄,
從新標記完成後,全部線程達到下一個GC safepoint,圖中達到了;
五、開始執行併發清理,併發清理能夠與用戶線程一塊兒執行,併發清理完成後,全部線程達到下一個GC safepoint,圖中達到了;
六、開始重置線程,就是對剛纔併發標記操做的對象,圖中是線程3(注意:重置線程針對的是併發標記的線程,沒有被併發標記的線程不須要重置線程操做),重置操做線程3的時候,與其餘三個用戶線程無關,它們能夠一塊兒執行。
CMS爲何是多線程收集器?
由於CMS收集器整個過程當中耗時最長的第二併發標記和第四併發清除過程當中,GC線程均可以與用戶線程一塊兒工做,初始標記和從新標記時間忽略不計,因此,從整體上來講,cms收集器的內存回收過程與用戶線程是併發執行的,因此上表中CMS爲多線程收集器。
4.2.5 經常使用組合5:G1收集器 多線程,面向服務端的垃圾回收器
一、什麼是G1?
G1就是Gabage-First,它將整個Java堆劃分爲多個大小相等的獨立區域,即Region,雖然還保留新生代和老年代的概念,但新生代和老年代已再也不物理隔離,它們都是一部分Region的集合。
G1收集器的底層原理:G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收所得到的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次依據容許的收集時間,優先收集回收價值最大的Region。正是這種使用Region劃份內存空間以及有優先級的區域回收方式,保證了G1收集器在有限時間內能夠獲取儘量高的效率。
G1收集器運行示意圖以下:
對於上圖的理解是:
G1收集包括四個步驟:初始標記、併發標記、最終篩選、篩選回收
一、多個用戶線程(圖中是四個)要開始執行新生代GC操做,因此都要達到GC safepoint點,先到的要等待晚到的,圖中都達到了;
二、開始執行初始標記,初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,而且修改TAMS(Next Top at Mark Start)的值,讓下一個階段用戶程序併發標記時,能在正確可用的Region上建立新對象,整個標記階段須要stop the world,讓全部Java線程掛起,這樣JVM才能夠安全地來標記對象。因此只有「初始標記」在執行,四個用戶線程都中止了。初始標記完成後,達到第二個GC safepoint,圖中達到了;
三、開始執行併發標記,併發標記是GCRoot開始對堆中的對象進行可達性分析,找出存活的對象,併發標記能夠與用戶線程一塊兒執行,併發標記完成後,全部線程(GC線程、用戶線程)達到下一個GC safepoint,圖中達到了;
四、開始執行最終標記,最終標記是爲了修正在併發標記期間因用戶程序繼續運做而致使標記產生變更的那部分標記記錄,最終標記完成後,全部線程達到下一個GC safepoint,圖中達到了;
五、開始執行篩選回收,篩選迴歸首先對各個Region的回收價值和成本排序, 根據用戶期待的GC停頓時間來制定回收計劃,篩選回收過程當中,由於停頓用戶線程將大幅提升收集效率,因此通常篩選迴歸是中止用戶線程的,篩選迴歸完成後,全部線程達到下一個GC safepoint,圖中達到了;
六、G1收集器收集結束,繼續併發執行用戶線程。
4.3 垃圾收集器經常使用參數
(筆者這裏加上idea上如何使用這些參數,這些是垃圾收集器的參數,因此這裏放到第四部分,在本文第五部份內存分配咱們會用到)
參數 |
idea中使用方式 |
描述 |
UseSerialGC |
VM Options: -XX:+UseSerialGC |
虛擬機運行在Client模式下的默認值,打開此開關以後,使用Serial+Serial Old的收集器組合進行內存回收 |
UseParNewGC |
VM Options: -XX:+UseParNewGC |
打開此開關以後,使用ParNew+ Serial Old的收集器組合進行內存回收 |
UseConcMarkSweepGC |
VM Options: -XX:+UseConcMarkSweepGC |
打開此開關以後,使用ParNew + CMS+ Serial Old的收集器組合進行內存回收。Serial Old收集器將做爲CMS收集器出現Concurrent Mode Failure失敗後的後備收集器使用 |
UseParallelGC |
VM Options: -XX:+UseParallelGC |
虛擬機運行在Server模式下的默認值,打開此開關以後,使用Parallel Scavenge + Serial Old(PS MarkSweep)的收集器組合進行內存回收 |
UseParallelOldGC |
VM Options: -XX:UseParallelOldGC |
打開此開關後,使用Parallel Scavenge + Parallel Old 的收集器組合進行內存回收 |
SurvivorRatio |
VM Options: -XX:SurvivorRatio=8 |
新生代中Eden區域與Survivor區域的容量比值,默認爲8,表明Eden:Survivor=8:1 |
PretenureSizeThreshold |
VM Options: -XX:PretenureSizeThreshold=3145728 表示大於3MB都到老年代中去 |
直接晉升到老年代的對象大小,設置這個參數後,這個參數以字節B爲單位大於這個參數的對象將直接在老年代中分配 |
MaxTenuringThreshold |
VM Options: -XX:MaxTenuringThreshold=2 表示經歷兩次Minor GC,就到老年代中去 |
晉升到老年代的對象年齡,每一個對象在堅持過一次Minor GC以後,年齡就增長1,當超過這個參數值就進入到老年代 |
UseAdaptiveSizePolicy |
VM Options: -XX:+UseAdaptiveSizePolicy |
動態調整Java堆中各個區域的大小以及進入老年代的年齡 |
HandlePromotionFailure |
jdk1.8下,HandlePromotionFailure會報錯,Unrecongnized VM option |
是否容許分配擔保失敗,即老年代的剩餘空間不足應應對新生代的整個Eden區和Survivor區的全部對象存活的極端狀況 |
ParallelGCThreads |
VM Options: -XX:ParallelGCThreads=10 |
設置並行GC時進入內存回收線程數 |
GCTimeRadio |
VM Options: -XX:GCTimeRadio=99 |
GC佔總時間的比率,默認值是99,即容許1%的GC時間,僅在使用Parallel Scavenge收集器時生效 |
MaxGCPauseMillis |
VM Options: -XX:MaxGCPauseMillis=100 |
設置GC的最大停頓時間,僅在使用Parallel Scavenge收集器時生效 |
CMSInitiatingOccupanyFraction |
VM Options: -XX:CMSInitiatingOccupanyFraction=68 |
設置CMS收集器在老年代空間被使用多少後觸發垃圾收集,默認值68%,僅在使用CMS收集器時生效 |
UseCMSCompactAtFullCollection |
VM Options: -XX:+UseCMSCompactAtFullCollection |
設置CMS收集器在完成垃圾收集後是否要進行一次內存碎片的整理,僅在使用CMS收集器時生效 |
CMSFullGCsBeforeCompaction |
VM Options: -XX:CCMSFullGCsBeforeCompaction=10 |
設置CMS收集在進行若干次垃圾收集後再啓動一次內存碎片整理,僅在使用CMS收集器時生效 |
5、JVM內存分配
這部分的內容借鑑《深刻理解Java虛擬機》一書,有改動。
附:What is minorGC? What is Major GC(Full GC)?
新生代GC(Minor GC):發生在新生代的垃圾收集動做,由於Java對象大多具備朝生夕滅的特性,全部Minor GC很是頻繁,通常回收速度較快。
老年代GC(Major GC/Full GC):發生在老年代的GC,出現了major GC,常常會伴隨一個MinorGC(可是不絕對),Major GC速度通常比Minor GC慢10倍。
5.1 對象優先在Eden上分配
5.1.1 設置VM Options
-XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+UseSerialGC
5.1.2 程序輸出(給出附加解釋)
第一步:能夠看到,當分配6M內存時,所有都在Eden區,沒有任何問題,說明JVM優先在Eden區上分配對象
第二步:由於年輕代只有9M,剩下1M是給To Survivor用的,已經使用了6M,如今申請4M, 就會觸發Minor GC,將6M的存活的對象放到目的survivor中去,可是放不下,由於目的survivor只有1M空間,因此分配擔保到老年代中去,而後將4M對象放到Eden區中。因此,最後的結果是 Eden區域使用了4096KB 4M 老年代中使用了6M 這裏form space佔用57%能夠忽略不計。
5.2 大對象直接進入老年代(使用-XX:PretenureSizeThreshold參數設置)
5.2.1 設置VM Options
-XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:PretenureSizeThreshold=3145728
5.2.2 程序輸出(給出附加解釋)
5.3 長期存活的對象應該進入老年代(使用-XX:MaxTenuringThreshold參數設置)
5.3.1 設置VM Options
-XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:MaxTenuringThreshold=1
5.3.2 程序輸出(給出附加解釋)
第一步驟:只分配allocation1 allocation2,不會產生任何Minor GC,對象都在Eden區中
第二步驟:分配allocation3,產生Minor GC,allocation2移入老年區
第三步驟:allocation3再次分配,allocation1也被送入老年區,老年區裏有allocation1 allocation2
6、尾聲
本文講述JVM自動內存管理(包括內存回收和內存),前言部分從操做系統引入JVM,第二部分介紹JVM空間結構(運行時數據區、堆內存和非堆內存),第三部分介紹HotSpot虛擬機,第四部分和第五部分分別介紹自動內存回收和自動內存分配的原理實現。
每天打碼,每天進步!