阿里P8架构师谈:JVM的内存分配、运行原理、回收算法机制
不管是BAT面試,還是工作實(shí)踐中的JVM調(diào)優(yōu)以及參數(shù)設(shè)置,或者內(nèi)存溢出檢測(cè)等,都需要涉及到Java虛擬機(jī)的內(nèi)存模型、內(nèi)存分配,以及回收算法機(jī)制等,這些都是必考、必會(huì)技能。
JVM內(nèi)存模型
JVM內(nèi)存模型可以分為兩個(gè)部分,如下圖所示,堆和方法區(qū)是所有線程共有的,而虛擬機(jī)棧,本地方法棧和程序計(jì)數(shù)器則是線程私有的。
1. 堆(Heap)
堆內(nèi)存是所有線程共有的,可以分為兩個(gè)部分:年輕代和老年代。下圖中的Perm代表的是永久代,但是注意永久代并不屬于堆內(nèi)存中的一部分,同時(shí)jdk1.8之后永久代也將被移除。
堆是java虛擬機(jī)所管理的內(nèi)存中最大的一塊內(nèi)存區(qū)域,也是被各個(gè)線程共享的內(nèi)存區(qū)域,該內(nèi)存區(qū)域存放了對(duì)象實(shí)例及數(shù)組(但不是所有的對(duì)象實(shí)例都在堆中)。其大小通過(guò)-Xms(最小值)和-Xmx(最大值)參數(shù)設(shè)置(最大最小值都要小于1G),前者為啟動(dòng)時(shí)申請(qǐng)的最小內(nèi)存,默認(rèn)為操作系統(tǒng)物理內(nèi)存的1/64,后者為JVM可申請(qǐng)的最大內(nèi)存,默認(rèn)為物理內(nèi)存的1/4,默認(rèn)當(dāng)空余堆內(nèi)存小于40%時(shí),JVM會(huì)增大堆內(nèi)存到-Xmx指定的大小,可通過(guò)-XX:MinHeapFreeRation=來(lái)指定這個(gè)比列;當(dāng)空余堆內(nèi)存大于70%時(shí),JVM會(huì)減小堆內(nèi)存的大小到-Xms指定的大小,可通過(guò)XX:MaxHeapFreeRation=來(lái)指定這個(gè)比列,當(dāng)然為了避免在運(yùn)行時(shí)頻繁調(diào)整Heap的大小,通常-Xms與-Xmx的值設(shè)成一樣。堆內(nèi)存 = 新生代+老生代+持久代。在我們垃圾回收的時(shí)候,我們往往將堆內(nèi)存分成新生代和老生代(大小比例1:2),新生代中由Eden和Survivor0,Survivor1組成,三者的比例是8:1:1,新生代的回收機(jī)制采用復(fù)制算法,在Minor GC的時(shí)候,我們都留一個(gè)存活區(qū)用來(lái)存放存活的對(duì)象,真正進(jìn)行的區(qū)域是Eden+其中一個(gè)存活區(qū),當(dāng)我們的對(duì)象時(shí)長(zhǎng)超過(guò)一定年齡時(shí)(默認(rèn)15,可以通過(guò)參數(shù)設(shè)置),將會(huì)把對(duì)象放入老生代,當(dāng)然大的對(duì)象會(huì)直接進(jìn)入老生代。老生代采用的回收算法是標(biāo)記整理算法。
2. 方法區(qū)(Method Area)
方法區(qū)也稱”永久代“,它用于存儲(chǔ)虛擬機(jī)加載的類信息、常量、靜態(tài)變量、是各個(gè)線程共享的內(nèi)存區(qū)域。默認(rèn)最小值為16MB,最大值為64MB(64位JVM由于指針膨脹,默認(rèn)是85M),可以通過(guò)-XX:PermSize 和 -XX:MaxPermSize 參數(shù)限制方法區(qū)的大小。它是一片連續(xù)的堆空間,永久代的垃圾收集是和老年代(old generation)捆綁在一起的,因此無(wú)論誰(shuí)滿了,都會(huì)觸發(fā)永久代和老年代的垃圾收集。不過(guò),一個(gè)明顯的問(wèn)題是,當(dāng)JVM加載的類信息容量超過(guò)了參數(shù)-XX:MaxPermSize設(shè)定的值時(shí),應(yīng)用將會(huì)報(bào)OOM的錯(cuò)誤。參數(shù)是通過(guò)-XX:PermSize和-XX:MaxPermSize來(lái)設(shè)定的。
3.虛擬機(jī)棧(JVM Stack)
描述的是java方法執(zhí)行的內(nèi)存模型:每個(gè)方法被執(zhí)行的時(shí)候都會(huì)創(chuàng)建一個(gè)”棧幀”,用于存儲(chǔ)局部變量表(包括參數(shù))、操作棧、方法出口等信息。每個(gè)方法被調(diào)用到執(zhí)行完的過(guò)程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過(guò)程。聲明周期與線程相同,是線程私有的。棧幀由三部分組成:局部變量區(qū)、操作數(shù)棧、幀數(shù)據(jù)區(qū)。局部變量區(qū)被組織為以一個(gè)字長(zhǎng)為單位、從0開始計(jì)數(shù)的數(shù)組,和局部變量區(qū)一樣,操作數(shù)棧也被組織成一個(gè)以字長(zhǎng)為單位的數(shù)組。但和前者不同的是,它不是通過(guò)索引來(lái)訪問(wèn)的,而是通過(guò)入棧和出棧來(lái)訪問(wèn)的,可以看作為臨時(shí)數(shù)據(jù)的存儲(chǔ)區(qū)域。除了局部變量區(qū)和操作數(shù)棧外,java棧幀還需要一些數(shù)據(jù)來(lái)支持常量池解析、正常方法返回以及異常派發(fā)機(jī)制。這些數(shù)據(jù)都保存在java棧幀的幀數(shù)據(jù)區(qū)中。
局部變量表: 存放了編譯器可知的各種基本數(shù)據(jù)類型、對(duì)象引用(引用指針,并非對(duì)象本身),其中64位長(zhǎng)度的long和double類型的數(shù)據(jù)會(huì)占用2個(gè)局部變量的空間,其余數(shù)據(jù)類型只占1個(gè)。局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進(jìn)入一個(gè)方法時(shí),這個(gè)方法需要在棧幀中分配多大的局部變量是完全確定的,在運(yùn)行期間棧幀不會(huì)改變局部變量表的大小空間。
4.本地方法棧(Native Stack)
與虛擬機(jī)棧基本類似,區(qū)別在于虛擬機(jī)棧為虛擬機(jī)執(zhí)行的java方法服務(wù),而本地方法棧則是為Native方法服務(wù)。(棧的空間大小遠(yuǎn)遠(yuǎn)小于堆)
5.程序計(jì)數(shù)器(PC Register)
是最小的一塊內(nèi)存區(qū)域,它的作用是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器,在虛擬機(jī)的模型里,字節(jié)碼解釋器工作時(shí)就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴計(jì)數(shù)器完成。
6.直接內(nèi)存
直接內(nèi)存并不是虛擬機(jī)內(nèi)存的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域。jdk1.4中新加入的NIO,引入了通道與緩沖區(qū)的IO方式,它可以調(diào)用Native方法直接分配堆外內(nèi)存,這個(gè)堆外內(nèi)存就是本機(jī)內(nèi)存,不會(huì)影響到堆內(nèi)存的大小.
JVM垃圾回收算法
1.標(biāo)記清除
原理:
- 從根集合節(jié)點(diǎn)進(jìn)行掃描,標(biāo)記出所有的存活對(duì)象,最后掃描整個(gè)內(nèi)存空間并清除沒有標(biāo)記的對(duì)象(即死亡對(duì)象)
適用場(chǎng)合:
- 存活對(duì)象較多的情況下比較高效
- 適用于年老代(即舊生代)
缺點(diǎn):
- 標(biāo)記清除算法帶來(lái)的一個(gè)問(wèn)題是會(huì)存在大量的空間碎片,因?yàn)榛厥蘸蟮目臻g是不連續(xù)的,這樣給大對(duì)象分配內(nèi)存的時(shí)候可能會(huì)提前觸發(fā)full gc。
2.復(fù)制算法
原理:
- 從根集合節(jié)點(diǎn)進(jìn)行掃描,標(biāo)記出所有的存活對(duì)象,并將這些存活的對(duì)象復(fù)制到一塊兒新的內(nèi)存(圖中下邊的那一塊兒內(nèi)存)上去,之后將原來(lái)的那一塊兒內(nèi)存(圖中上邊的那一塊兒內(nèi)存)全部回收掉
適用場(chǎng)合:
- 存活對(duì)象較少的情況下比較高效
- 掃描了整個(gè)空間一次(標(biāo)記存活對(duì)象并復(fù)制移動(dòng))
- 適用于年輕代(即新生代):基本上98%的對(duì)象是”朝生夕死”的,存活下來(lái)的會(huì)很少
缺點(diǎn):
- 需要一塊兒空的內(nèi)存空間
- 需要復(fù)制移動(dòng)對(duì)象
3.標(biāo)記整理
原理:
- 從根集合節(jié)點(diǎn)進(jìn)行掃描,標(biāo)記出所有的存活對(duì)象,最后掃描整個(gè)內(nèi)存空間并清除沒有標(biāo)記的對(duì)象(即死亡對(duì)象)(可以發(fā)現(xiàn)前邊這些就是標(biāo)記-清除算法的原理),清除完之后,將所有的存活對(duì)象左移到一起。
適用場(chǎng)合:
- 用于年老代(即舊生代)
缺點(diǎn):
- 需要移動(dòng)對(duì)象,若對(duì)象非常多而且標(biāo)記回收后的內(nèi)存非常不完整,可能移動(dòng)這個(gè)動(dòng)作也會(huì)耗費(fèi)一定時(shí)間
- 掃描了整個(gè)空間兩次(第一次:標(biāo)記存活對(duì)象;第二次:清除沒有標(biāo)記的對(duì)象)
優(yōu)點(diǎn):
- 不會(huì)產(chǎn)生內(nèi)存碎片
4.增量算法
增量算法的基本思想是,如果一次性將所有的垃圾進(jìn)行處理,需要造成系統(tǒng)長(zhǎng)時(shí)間的停頓,那么就可以讓垃圾收集線程和應(yīng)用程序線程交替執(zhí)行。每次,垃圾收集線程只收集一小片區(qū)域的內(nèi)存空間,接著切換到應(yīng)用程序線程。依次反復(fù),直到垃圾收集完成。使用這種方式,由于在垃圾回收過(guò)程中,間斷性地還執(zhí)行了應(yīng)用程序代碼,所以能減少系統(tǒng)的停頓時(shí)間。但是,因?yàn)榫€程切換和上下文轉(zhuǎn)換的消耗,會(huì)使得垃圾回收的總體成本上升,造成系統(tǒng)吞吐量的下降。
垃圾回收器
1.Serial收集器
Serial收集器是最古老的收集器,它的缺點(diǎn)是當(dāng)Serial收集器想進(jìn)行垃圾回收的時(shí)候,必須暫停用戶的所有進(jìn)程,即stop the world。到現(xiàn)在為止,它依然是虛擬機(jī)運(yùn)行在client模式下的默認(rèn)新生代收集器,與其他收集器相比,對(duì)于限定在單個(gè)CPU的運(yùn)行環(huán)境來(lái)說(shuō),Serial收集器由于沒有線程交互的開銷,專心做垃圾回收自然可以獲得最高的單線程收集效率。
2.ParNew收集器
ParNew收集器是Serial收集器新生代的多線程實(shí)現(xiàn),注意在進(jìn)行垃圾回收的時(shí)候依然會(huì)stop the world,只是相比較Serial收集器而言它會(huì)運(yùn)行多條進(jìn)程進(jìn)行垃圾回收。
ParNew收集器在單CPU的環(huán)境中絕對(duì)不會(huì)有比Serial收集器更好的效果,甚至由于存在線程交互的開銷,該收集器在通過(guò)超線程技術(shù)實(shí)現(xiàn)的兩個(gè)CPU的環(huán)境中都不能百分之百的保證能超越Serial收集器。當(dāng)然,隨著可以使用的CPU的數(shù)量增加,它對(duì)于GC時(shí)系統(tǒng)資源的利用還是很有好處的。它默認(rèn)開啟的收集線程數(shù)與CPU的數(shù)量相同,在CPU非常多(譬如32個(gè),現(xiàn)在CPU動(dòng)輒4核加超線程,服務(wù)器超過(guò)32個(gè)邏輯CPU的情況越來(lái)越多了)的環(huán)境下,可以使用-XX:ParallelGCThreads參數(shù)來(lái)限制垃圾收集的線程數(shù)。
3.Parallel Scavenge收集器
Parallel是采用復(fù)制算法的多線程新生代垃圾回收器,似乎和ParNew收集器有很多的相似的地方。但是Parallel Scanvenge收集器的一個(gè)特點(diǎn)是它所關(guān)注的目標(biāo)是吞吐量(Throughput)。所謂吞吐量就是CPU用于運(yùn)行用戶代碼的時(shí)間與CPU總消耗時(shí)間的比值,即吞吐量=運(yùn)行用戶代碼時(shí)間 / (運(yùn)行用戶代碼時(shí)間 + 垃圾收集時(shí)間)。停頓時(shí)間越短就越適合需要與用戶交互的程序,良好的響應(yīng)速度能夠提升用戶的體驗(yàn);而高吞吐量則可以最高效率地利用CPU時(shí)間,盡快地完成程序的運(yùn)算任務(wù),主要適合在后臺(tái)運(yùn)算而不需要太多交互的任務(wù)。
4.CMS收集器
CMS(Concurrent Mark Swep)收集器是一個(gè)比較重要的回收器,現(xiàn)在應(yīng)用非常廣泛,我們重點(diǎn)來(lái)看一下,CMS一種獲取最短回收停頓時(shí)間為目標(biāo)的收集器,這使得它很適合用于和用戶交互的業(yè)務(wù)。從名字(Mark Swep)就可以看出,CMS收集器是基于標(biāo)記清除算法實(shí)現(xiàn)的。它的收集過(guò)程分為四個(gè)步驟:
注意初始標(biāo)記和重新標(biāo)記還是會(huì)stop the world,但是在耗費(fèi)時(shí)間更長(zhǎng)的并發(fā)標(biāo)記和并發(fā)清除兩個(gè)階段都可以和用戶進(jìn)程同時(shí)工作。
不過(guò)由于CMS收集器是基于標(biāo)記清除算法實(shí)現(xiàn)的,會(huì)導(dǎo)致有大量的空間碎片產(chǎn)生,在為大對(duì)象分配內(nèi)存的時(shí)候,往往會(huì)出現(xiàn)老年代還有很大的空間剩余,但是無(wú)法找到足夠大的連續(xù)空間來(lái)分配當(dāng)前對(duì)象,不得不提前開啟一次Full GC。
為了解決這個(gè)問(wèn)題,CMS收集器默認(rèn)提供了一個(gè)-XX:+UseCMSCompactAtFullCollection收集開關(guān)參數(shù)(默認(rèn)就是開啟的),用于在CMS收集器進(jìn)行FullGC完開啟內(nèi)存碎片的合并整理過(guò)程,內(nèi)存整理的過(guò)程是無(wú)法并發(fā)的,這樣內(nèi)存碎片問(wèn)題倒是沒有了,不過(guò)停頓時(shí)間不得不變長(zhǎng)。虛擬機(jī)設(shè)計(jì)者還提供了另外一個(gè)參數(shù)-XX:CMSFullGCsBeforeCompaction參數(shù)用于設(shè)置執(zhí)行多少次不壓縮的FULL GC后跟著來(lái)一次帶壓縮的(默認(rèn)值為0,表示每次進(jìn)入Full GC時(shí)都進(jìn)行碎片整理)。
不幸的是,它作為老年代的收集器,卻無(wú)法與jdk1.4中已經(jīng)存在的新生代收集器Parallel Scavenge配合工作,所以在jdk1.5中使用cms來(lái)收集老年代的時(shí)候,新生代只能選擇ParNew或Serial收集器中的一個(gè)。ParNew收集器是使用-XX:+UseConcMarkSweepGC選項(xiàng)啟用CMS收集器之后的默認(rèn)新生代收集器,也可以使用-XX:+UseParNewGC選項(xiàng)來(lái)強(qiáng)制指定它。
5.G1收集器
G1收集器是一款面向服務(wù)端應(yīng)用的垃圾收集器。HotSpot團(tuán)隊(duì)賦予它的使命是在未來(lái)替換掉JDK1.5中發(fā)布的CMS收集器。與其他GC收集器相比,G1具備如下特點(diǎn):
在使用G1收集器時(shí),Java堆的內(nèi)存布局和其他收集器有很大的差別,它將這個(gè)Java堆分為多個(gè)大小相等的獨(dú)立區(qū)域,雖然還保留新生代和老年代的概念,但是新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續(xù))的集合。
雖然G1看起來(lái)有很多優(yōu)點(diǎn),實(shí)際上CMS還是主流。
與GC相關(guān)的常用參數(shù)
除了上面提及的一些參數(shù),下面補(bǔ)充一些和GC相關(guān)的常用參數(shù):
- -Xmx: 設(shè)置堆內(nèi)存的最大值。
- -Xms: 設(shè)置堆內(nèi)存的初始值。
- -Xmn: 設(shè)置新生代的大小。
- -Xss: 設(shè)置棧的大小。
- -PretenureSizeThreshold: 直接晉升到老年代的對(duì)象大小,設(shè)置這個(gè)參數(shù)后,大于這個(gè)參數(shù)的對(duì)象將直接在老年代分配。
- -MaxTenuringThrehold: 晉升到老年代的對(duì)象年齡。每個(gè)對(duì)象在堅(jiān)持過(guò)一次Minor GC之后,年齡就會(huì)加1,當(dāng)超過(guò)這個(gè)參數(shù)值時(shí)就進(jìn)入老年代。
- -UseAdaptiveSizePolicy: 在這種模式下,新生代的大小、eden 和 survivor 的比例、晉升老年代的對(duì)象年齡等參數(shù)會(huì)被自動(dòng)調(diào)整,以達(dá)到在堆大小、吞吐量和停頓時(shí)間之間的平衡點(diǎn)。在手工調(diào)優(yōu)比較困難的場(chǎng)合,可以直接使用這種自適應(yīng)的方式,僅指定虛擬機(jī)的最大堆、目標(biāo)的吞吐量 (GCTimeRatio) 和停頓時(shí)間 (MaxGCPauseMills),讓虛擬機(jī)自己完成調(diào)優(yōu)工作。
- -SurvivorRattio: 新生代Eden區(qū)域與Survivor區(qū)域的容量比值,默認(rèn)為8,代表Eden: Suvivor= 8: 1。
- -XX:ParallelGCThreads:設(shè)置用于垃圾回收的線程數(shù)。通常情況下可以和 CPU 數(shù)量相等。但在 CPU 數(shù)量比較多的情況下,設(shè)置相對(duì)較小的數(shù)值也是合理的。
- -XX:MaxGCPauseMills:設(shè)置最大垃圾收集停頓時(shí)間。它的值是一個(gè)大于 0 的整數(shù)。收集器在工作時(shí),會(huì)調(diào)整 Java 堆大小或者其他一些參數(shù),盡可能地把停頓時(shí)間控制在 MaxGCPauseMills 以內(nèi)。
- -XX:GCTimeRatio:設(shè)置吞吐量大小,它的值是一個(gè) 0-100 之間的整數(shù)。假設(shè) GCTimeRatio 的值為 n,那么系統(tǒng)將花費(fèi)不超過(guò) 1/(1+n) 的時(shí)間用于垃圾收集。
你可能也喜歡:
總結(jié)
以上是生活随笔為你收集整理的阿里P8架构师谈:JVM的内存分配、运行原理、回收算法机制的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 机器学习从理论到工程的第二步-开发环境与
- 下一篇: iOS 覆盖率检测原理与增量代码测试覆盖