JVM 內(nèi)存模型

程序計數(shù)器
程序計數(shù)器是一塊較小的內(nèi)存空間,可以看作是當前線程所執(zhí)行的字節(jié)碼的行號指示器。分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復等基礎(chǔ)功能都需要依賴這個計數(shù)器來完成。
由于Java 虛擬機的多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方式來實現(xiàn)的,在任何一個確定的時刻,一個處理器(對于多核處理器來說是一個內(nèi)核)只會執(zhí)行一條線程中的指令。因此,為了線程切換后能恢復到正確的執(zhí)行位置,每條線程都需要有一個獨立的程序計數(shù)器,各條線程之間的計數(shù)器互不影響,獨立存儲,我們稱這類內(nèi)存區(qū)域為“線程私有”的內(nèi)存。
如果線程正在執(zhí)行的是一個Java 方法,這個計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址;如果正在執(zhí)行的是Natvie 方法,這個計數(shù)器值則為空(Undefined)。
此內(nèi)存區(qū)域是唯一一個在Java 虛擬機規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。
虛擬機棧
虛擬機棧(Java Virtual Machine Stacks)是線程隔離的,每創(chuàng)建一個線程時就會對應創(chuàng)建一個Java棧,即每個線程都有自己獨立的虛擬機棧。這個棧中又會對應包含多個棧幀,每調(diào)用一個方法時就會往棧中創(chuàng)建并壓入一個棧幀,棧幀存儲局部變量表、操作棧、動態(tài)鏈接、方法出口等信息,每一個方法從調(diào)用到最終返回結(jié)果的過程,就對應一個棧幀從入棧到出棧的過程。
虛擬機棧是一個后入先出的數(shù)據(jù)結(jié)構(gòu),線程運行過程中,只有處于棧頂?shù)臈攀怯行У?,稱為當前棧幀,與這個棧幀相關(guān)聯(lián)的方法稱為當前方法,當前活動幀棧始終是虛擬機棧的棧頂元素。
- 局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型和對象引用類型。通常我們所說的“棧內(nèi)存”指的就是局部變量表這一部分。
- 局部變量表所需的內(nèi)存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀分配多少內(nèi)存是固定的,運行期間不會改變局部變量表的大小。
- 64位的long和double類型的數(shù)據(jù)會占用2個局部變量空間,其余的數(shù)據(jù)類型只占用1個。
棧的大小可以固定也可以動態(tài)擴展。
- 在固定大小的情況下,JVM會為每個線程的虛擬機棧分配一定的內(nèi)存大?。?Xss參數(shù)),因此虛擬機棧能夠容納的棧幀數(shù)量是有限的,若棧幀不斷進棧而不出棧,最終會導致當前線程虛擬機棧的內(nèi)存空間耗盡,會拋出StackOverflowError異常。
- 在動態(tài)擴展的情況下,當整個虛擬機棧內(nèi)存耗盡,并且無法再申請到新的內(nèi)存時,就會拋出OutOfMemoryError異常。
在概念模型上,典型的棧幀結(jié)構(gòu)如圖所示:

棧特點
- 是一種運算受限的線性表。其限制是僅允許在表的一端進行插入和刪除運算。這一端被稱為棧頂,相對地,把 另一端稱為棧底。其特性是先進后出;
- 棧是線程私有的,生命周期跟線程相同,當創(chuàng)建一個線程時,同時會創(chuàng)建一個棧,棧的大小和深度都是固定的;
- 方法參數(shù)列表中的變量,方法體中的基本數(shù)據(jù)類型的變量和引用數(shù)據(jù)類型的引用都存放在棧中,成員變量和對象本身不存放在棧中。運行時,成員函數(shù)的局部變量引用也存放在棧中;
- 棧的變量隨著變量作用域的結(jié)束而釋放,不需要jvm垃圾回收機制回收;
- 棧不是全局共享的,每個線程創(chuàng)建一個棧,該線程只能訪問其對應的棧數(shù)據(jù);
- 棧內(nèi)存的大小是在編譯期就確定了的;
棧幀
一個棧中可以有多個棧幀,棧幀隨著方法的調(diào)用而創(chuàng)建,隨著方法的結(jié)束而消亡。該棧幀中存儲該方法中的變量,原則上各個棧幀之間的數(shù)據(jù)是不能共享的,但是在方法間調(diào)用時,jvm會將一方法的返回值賦值給調(diào)用它的棧幀中。每一個方法調(diào)用,就是一個壓棧的過程,每個方法的結(jié)束就是一個彈棧的過程。壓棧都將會將該棧幀置于棧頂,每個棧不會同時操作多個棧幀,只會操作棧頂,當棧頂操作結(jié)束時,會將該棧幀彈出,同時會釋放該棧幀內(nèi)存,其下一個棧幀將變?yōu)闂m?。棧?nèi)存歸屬于單個線程,每個線程都會有一個棧內(nèi)存,其存儲的變量只能在其所屬線程中可見,即棧內(nèi)存可以理解成線程的私有內(nèi)存。
棧中的優(yōu)化,其一是當局部變量賦值時,會在??臻g中找其對應的值,當有該值時,將該值指向變量,當沒有該值時,創(chuàng)建一個該值,然后再指向該變量,例如:int a = 1, int b = 1, b = 2; 其二是棧中的變量隨著方法的調(diào)用而創(chuàng)建,當方法執(zhí)行結(jié)束后,jvm會自動釋放內(nèi)存。
棧幀的組成部分
局部變量表:
是一組變量值的存儲空間,用呀存放方法參數(shù)和局部變量,虛擬機通過索引定位的方式使用局部變量表。
操作樹棧:
常稱為操作數(shù)棧,是一個后入先出棧。方法執(zhí)行中進行算術(shù)運算或者是調(diào)用其他的方法進行參數(shù)傳遞的時候是通過操作數(shù)棧進行的。在概念模型中,兩個棧幀是相互獨立的。但是大多數(shù)虛擬機的實現(xiàn)都會進行優(yōu)化,令兩個棧幀出現(xiàn)一部分重疊。令下面的部分操作數(shù)棧與上面的局部變量表重疊在一塊,這樣在方法調(diào)用的時候可以共用一部分數(shù)據(jù),無需進行額外的參數(shù)復制傳遞。
動態(tài)連接:
在說明什么是動態(tài)連接之前先看看方法的大概調(diào)用過程,首先在虛擬機運行的時候,運行時常量池會保存大量的符號引用,這些符號引用可以看成是每個方法的間接引用,如果代表棧幀A的方法想調(diào)用代表棧幀B的方法,那么這個虛擬機的方法調(diào)用指令就會以B方法的符號引用作為參數(shù),但是因為符號引用并不是直接指向代表B方法的內(nèi)存位置,所以在調(diào)用之前還必須要將符號引用轉(zhuǎn)換為直接引用,然后通過直接引用才可以訪問到真正的方法,這時候就有一點需要注意,如果符號引用是在類加載階段或者第一次使用的時候轉(zhuǎn)化為直接應用,那么這種轉(zhuǎn)換成為靜態(tài)解析,如果是在運行期間轉(zhuǎn)換為直接引用,那么這種轉(zhuǎn)換就成為動態(tài)連接。
方法返回地址:
方法的返回分為兩種情況,一種是正常退出,退出后會根據(jù)方法的定義來決定是否要傳返回值給上層的調(diào)用者,一種是異常導致的方法結(jié)束,這種情況是不會傳返回值給上層的調(diào)用方法.不過無論是那種方式的方法結(jié)束,在退出當前方法時都會跳轉(zhuǎn)到當前方法被調(diào)用的位置,如果方法是正常退出的,則調(diào)用者的PC計數(shù)器的值就可以作為返回地址,如果是因為異常退出的,則是需要通過異常處理表來確定.在方法的的一次調(diào)用就對應著棧幀在虛擬機棧中的一次入棧出棧操作,因此方法退出時可能做的事情包括,恢復上層方法的局部變量表以及操作數(shù)棧,如果有返回值的話,就把返回值壓入到調(diào)用者棧幀的操作數(shù)棧中,還會把PC計數(shù)器的值調(diào)整為方法調(diào)用入口的下一條指令。
棧優(yōu)點
棧幀內(nèi)存數(shù)據(jù)共享:
棧幀之間數(shù)據(jù)不能共享,但是同一個棧幀內(nèi)的數(shù)據(jù)是可以共享的,這樣設(shè)計是為了減小內(nèi)存消耗,例如:int a = 1, int b= 1時,前面定義了a=1,a和1都在棧內(nèi)存內(nèi),如果再定義一個b=1,此時將b放入棧內(nèi)存,然后查找棧內(nèi)存中是否有1,如果有則b指向1。如果再給b賦值2,則在棧內(nèi)存中查找是否有2,如果沒有就在棧內(nèi)存中放一個2,然后b指向2。也就是如果常量在棧內(nèi)存中,就將變量指向該常量,如果沒有就在該棧內(nèi)存增加一個該常量,并將變量指向該常量。
存取速度比堆要快,僅次于寄存器:
速度快之一是棧在編譯器就申請好了內(nèi)存空間,所以在運行時不需要申請內(nèi)存大小,節(jié)約了時間,其二是棧是機器系統(tǒng)提供的數(shù)據(jù)結(jié)構(gòu),計算機會在底層對棧提供支持:分配專門的寄存器存放棧的地址,壓棧出棧都有專門的指令執(zhí)行,這就決定了棧的效率比較高。其三是訪問時間,訪問堆的一個具體單元,需要兩次訪問內(nèi)存,第一次得取得指針,第二次才是真正得數(shù)據(jù),而棧只需訪問一次。
棧的缺點
存在棧的數(shù)據(jù)大小和生存期必須是確定的,缺乏靈活性。當棧在運行執(zhí)行程序時,發(fā)現(xiàn)棧內(nèi)存不夠,不會動態(tài)的去申請內(nèi)存,以至于導致程序報錯,所以靈活性較差。
本地方法棧
本地方法棧(Native MethodStacks)與虛擬機棧所發(fā)揮的作用是非常相似的,其區(qū)別不過是虛擬機棧為虛擬機執(zhí)行Java 方法(也就是字節(jié)碼)服務(wù),而本地方法棧則是為虛擬機使用到的Native 方法服務(wù)。虛擬機規(guī)范中對本地方法棧中的方法使用的語言、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒有強制規(guī)定,因此具體的虛擬機可以自由實現(xiàn)它。甚至有的虛擬機(譬如Sun HotSpot 虛擬機)直接就把本地方法棧和虛擬機棧合二為一。
與虛擬機棧一樣,本地方法棧區(qū)域也會拋出StackOverflowError和OutOfMemoryError異常。
堆內(nèi)存

堆是Java 虛擬機所管理的內(nèi)存中最大的一塊。Java 堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機啟動時創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內(nèi)存。但是隨著JIT 編譯器的發(fā)展與逃逸分析技術(shù)的逐漸成熟,棧上分配、標量替換優(yōu)化技術(shù)將會導致一些微妙的變化發(fā)生,所有的對象都分配在堆上也漸漸變得不是那么“絕對”了。
堆是垃圾收集器管理的主要區(qū)域,因此很多時候也被稱做“GC 堆”。
堆的大小可以通過-Xms(最小值)和-Xmx(最大值)參數(shù)設(shè)置,-Xms為JVM啟動時申請的最小內(nèi)存,默認為操作系統(tǒng)物理內(nèi)存的1/64但小于1G,-Xmx為JVM可申請的最大內(nèi)存,默認為物理內(nèi)存的1/4但小于1G,默認當空余堆內(nèi)存小于40%時,JVM會增大Heap到-Xmx指定的大小,可通過-XX:MinHeapFreeRation=來指定這個比列;當空余堆內(nèi)存大于70%時,JVM會減小heap的大小到-Xms指定的大小,可通過XX:MaxHeapFreeRation=來指定這個比列,對于運行系統(tǒng),為避免在運行時頻繁調(diào)整Heap的大小,通常-Xms與-Xmx的值設(shè)成一樣。
如果從內(nèi)存回收的角度看,由于現(xiàn)在收集器基本都是采用的分代收集算法,所以Java 堆中還可以細分為:新生代和老年代;
新生代:程序新創(chuàng)建的對象都是從新生代分配內(nèi)存,新生代由Eden Space和兩塊相同大小的Survivor Space(通常又稱S0和S1或From和To)構(gòu)成,可通過-Xmn參數(shù)來指定新生代的大小,也可以通過-XX:SurvivorRation來調(diào)整Eden Space及SurvivorSpace的大小。
老年代:用于存放經(jīng)過多次新生代GC仍然存活的對象,例如緩存對象,新建的對象也有可能直接進入老年代,主要有兩種情況:1、大對象,可通過啟動參數(shù)設(shè)置-XX:PretenureSizeThreshold=1024(單位為字節(jié),默認為0)來代表超過多大時就不在新生代分配,而是直接在老年代分配。2、大的數(shù)組對象,且數(shù)組中無引用外部對象。
老年代所占的內(nèi)存大小為-Xmx對應的值減去-Xmn對應的值。
方法區(qū)
方法區(qū)在一個jvm實例的內(nèi)部,類型信息被存儲在一個稱為方法區(qū)的內(nèi)存邏輯區(qū)中。類型信息是由類加載器在類加載時從類文件中提取出來的。類(靜態(tài))變量也存儲在方法區(qū)中。
簡單說方法區(qū)用來存儲類型的元數(shù)據(jù)信息,一個.class文件是類被java虛擬機使用之前的表現(xiàn)形式,一旦這個類要被使用,java虛擬機就會對其進行裝載、連接(驗證、準備、解析)和初始化。而裝載(后的結(jié)果就是由.class文件轉(zhuǎn)變?yōu)榉椒▍^(qū)中的一段特定的數(shù)據(jù)結(jié)構(gòu),這個數(shù)據(jù)結(jié)構(gòu)會存儲如下信息:
類型信息
- 這個類型的全限定名
- 這個類型的直接超類的全限定名
- 這個類型是類類型還是接口類型
- 這個類型的訪問修飾符
- 任何直接超接口的全限定名的有序列表
字段信息
- 字段名
- 字段類型
- 字段的修飾符
方法信息
- 方法名
- 方法返回類型
- 方法參數(shù)的數(shù)量和類型(按照順序)
- 方法的修飾符
其他信息
- 除了常量以外的所有類(靜態(tài))變量
- 一個指向ClassLoader的指針
- 一個指向Class對象的指針
- 常量池(常量數(shù)據(jù)以及對其他類型的符號引用)
JVM為每個已加載的類型都維護一個常量池。常量池就是這個類型用到的常量的一個有序集合,包括實際的常量(string,integer,和floating point常量)和對類型,域和方法的符號引用。池中的數(shù)據(jù)項象數(shù)組項一樣,是通過索引訪問的。
每個類的這些元數(shù)據(jù),無論是在構(gòu)建這個類的實例還是調(diào)用這個類某個對象的方法,都會訪問方法區(qū)的這些元數(shù)據(jù)。
構(gòu)建一個對象時,JVM會在堆中給對象分配空間,這些空間用來存儲當前對象實例屬性以及其父類的實例屬性(而這些屬性信息都是從方法區(qū)獲得),注意,這里并不是僅僅為當前對象的實例屬性分配空間,還需要給父類的實例屬性分配,到此其實我們就可以回答第一個問題了,即實例化父類的某個子類時,JVM也會同時構(gòu)建父類的一個對象。從另外一個角度也可以印證這個問題:調(diào)用當前類的構(gòu)造方法時,首先會調(diào)用其父類的構(gòu)造方法直到Object,而構(gòu)造方法的調(diào)用意味著實例的創(chuàng)建,所以子類實例化時,父類肯定也會被實例化。
類變量被類的所有實例共享,即使沒有類實例時你也可以訪問它。這些變量只與類相關(guān),所以在方法區(qū)中,它們成為類數(shù)據(jù)在邏輯上的一部分。在JVM使用一個類之前,它必須在方法區(qū)中為每個non-final類變量分配空間。
方法區(qū)主要有以下幾個特點:
- 方法區(qū)是線程安全的。由于所有的線程都共享方法區(qū),所以,方法區(qū)里的數(shù)據(jù)訪問必須被設(shè)計成線程安全的。例如,假如同時有兩個線程都企圖訪問方法區(qū)中的同一個類,而這個類還沒有被裝入JVM,那么只允許一個線程去裝載它,而其它線程必須等待
- 方法區(qū)的大小不必是固定的,JVM可根據(jù)應用需要動態(tài)調(diào)整。同時,方法區(qū)也不一定是連續(xù)的,方法區(qū)可以在一個堆(甚至是JVM自己的堆)中自由分配。
- 方法區(qū)也可被垃圾收集,當某個類不在被使用(不可觸及)時,JVM將卸載這個類,進行垃圾收集
可以通過-XX:PermSize 和 -XX:MaxPermSize 參數(shù)限制方法區(qū)的大小。
對于習慣在HotSpot 虛擬機上開發(fā)和部署程序的開發(fā)者來說,很多人愿意把方法區(qū)稱為“永久代”(PermanentGeneration),本質(zhì)上兩者并不等價,僅僅是因為HotSpot 虛擬機的設(shè)計團隊選擇把GC 分代收集擴展至方法區(qū),或者說使用永久代來實現(xiàn)方法區(qū)而已。對于其他虛擬機(如BEA JRockit、IBM J9 等)來說是不存在永久代的概念的。
相對而言,垃圾收集行為在這個區(qū)域是比較少出現(xiàn)的,但并非數(shù)據(jù)進入了方法區(qū)就如永久代的名字一樣“永久”存在了。這個區(qū)域的內(nèi)存回收目標主要是針對常量池的回收和對類型的卸載。
堆內(nèi)存和棧內(nèi)存的對比
經(jīng)常有人把Java 內(nèi)存區(qū)分為堆內(nèi)存(Heap)和棧內(nèi)存(Stack),這種分法比較粗糙,Java內(nèi)存區(qū)域的劃分實際上遠比這復雜。這種劃分方式的流行只能說明大多數(shù)程序員最關(guān)注的、與對象內(nèi)存分配關(guān)系最密切的內(nèi)存區(qū)域是這兩塊。
堆很靈活,但是不安全。對于對象,我們要動態(tài)地創(chuàng)建、銷毀,不能說后創(chuàng)建的對象沒有銷毀,先前創(chuàng)建的對象就不能銷毀,那樣的話我們的程序就寸步難行,所以Java中用堆來存儲對象。而一旦堆中的對象被銷毀,我們繼續(xù)引用這個對象的話,就會出現(xiàn)著名的 NullPointerException,這就是堆的缺點——錯誤的引用邏輯只有在運行時才會被發(fā)現(xiàn)。
棧不靈活,但是很嚴格,是安全的,易于管理。因為只要上面的引用沒有銷毀,下面引用就一定還在,在大部分程序中,都是先定義的變量、引用先進棧,后定義的后進棧,同時,區(qū)塊內(nèi)部的變量、引用在進入?yún)^(qū)塊時壓棧,區(qū)塊結(jié)束時出棧,理解了這種機制,我們就可以很方便地理解各種編程語言的作用域的概念了,同時這也是棧的優(yōu)點——錯誤的引用邏輯在編譯時就可以被發(fā)現(xiàn)。
棧--主要存放引用和基本數(shù)據(jù)類型。
堆--用來存放 new 出來的對象實例。
內(nèi)存分配過程
- JVM 會試圖為相關(guān)Java對象在Eden Space中初始化一塊內(nèi)存區(qū)域。
- 當Eden空間足夠時,內(nèi)存申請結(jié)束;否則到下一步。
- JVM 試圖釋放在Eden中所有不活躍的對象(這屬于1或更高級的垃圾回收)。釋放后若Eden空間仍然不足以放入新對象,則試圖將部分Eden中活躍對象放入Survivor區(qū)。
- Survivor區(qū)被用來作為Eden及Old的中間交換區(qū)域,當Old區(qū)空間足夠時,Survivor區(qū)的對象會被移到Old區(qū),否則會被保留在Survivor區(qū)。
- 當Old區(qū)空間不夠時,JVM 會在Old區(qū)進行完全的垃圾收集(0級)。
- 完全垃圾收集后,若Survivor及Old區(qū)仍然無法存放從Eden復制過來的部分對象,導致JVM無法在Eden區(qū)為新對象創(chuàng)建內(nèi)存區(qū)域,則出現(xiàn)“outofmemory”錯誤。
對象訪問
對象訪問在Java 語言中無處不在,是最普通的程序行為,但即使是最簡單的訪問,也會卻涉及Java 棧、Java 堆、方法區(qū)這三個最重要內(nèi)存區(qū)域之間的關(guān)聯(lián)關(guān)系。
如下面的這句代碼:
Object obj = newObject();
假設(shè)這句代碼出現(xiàn)在方法體中,那“Object obj”這部分的語義將會反映到Java 棧的本地變量表中,作為一個reference 類型數(shù)據(jù)出現(xiàn)。而“new Object()”這部分的語義將會反映到Java 堆中,形成一塊存儲了Object 類型所有實例數(shù)據(jù)值(Instance Data,對象中各個實例字段的數(shù)據(jù))的結(jié)構(gòu)化內(nèi)存,根據(jù)具體類型以及虛擬機實現(xiàn)的對象內(nèi)存布局(Object Memory Layout)的不同,這塊內(nèi)存的長度是不固定的。另外,在Java 堆中還必須包含能查找到此對象類型數(shù)據(jù)(如對象類型、父類、實現(xiàn)的接口、方法等)的地址信息,這些類型數(shù)據(jù)則存儲在方法區(qū)中。
由于reference 類型在Java 虛擬機規(guī)范里面只規(guī)定了一個指向?qū)ο蟮囊?,并沒有定義這個引用應該通過哪種方式去定位,以及訪問到Java 堆中的對象的具體位置,因此不同虛擬機實現(xiàn)的對象訪問方式會有所不同,主流的訪問方式有兩種:使用句柄和直接指針。
如果使用句柄訪問方式,Java 堆中將會劃分出一塊內(nèi)存來作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數(shù)據(jù)和類型數(shù)據(jù)各自的具體地址信息。
如圖:

如果使用直接指針訪問方式,reference 中直接存儲的就是對象地址.
如圖:

總結(jié)
| 名稱 | 特征 | 作用 | 配置參數(shù) | 異常 |
|---|---|---|---|---|
| 程序計數(shù)器 | 占用內(nèi)存小,線程私有,生命周期與線程相同 | 大致為字節(jié)碼行號指示器 | 無 | 無 |
| 虛擬機棧 | 線程私有,生命周期與線程相同,使用連續(xù)的內(nèi)存空間 | Java 方法執(zhí)行的內(nèi)存模型,存儲局部變量表、操作棧、動態(tài)鏈接、方法出口等信息 | -Xss | StackOverflowError、OutOfMemoryError |
| java堆 | 線程共享,生命周期與虛擬機相同,可以不使用連續(xù)的內(nèi)存地址 | 保存對象實例,所有對象實例(包括數(shù)組)都要在堆上分配 | -Xms、-Xmx、-Xmn | OutOfMemoryError |
| 方法區(qū) | 線程共享,生命周期與虛擬機相同,可以不使用連續(xù)的內(nèi)存地址 | 存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù) | -XX:PermSize:16M、-XX:MaxPermSize:64M | OutOfMemoryError |
| 運行時常量池 | 方法區(qū)的一部分,具有動態(tài)性 | 存放字面量及符號引用 |
JVM 垃圾回收機制
哪些內(nèi)存需要回收
JVM的內(nèi)存結(jié)構(gòu)包括五大區(qū)域:程序計數(shù)器、虛擬機棧、本地方法棧、堆區(qū)、方法區(qū)。其中程序計數(shù)器、虛擬機棧、本地方法棧3個區(qū)域隨線程而生、隨線程而滅,因此這幾個區(qū)域的內(nèi)存分配和回收都具備確定性,就不需要過多考慮回收的問題,因為方法結(jié)束或者線程結(jié)束時,內(nèi)存自然就跟隨著回收了。而Java堆區(qū)和方法區(qū)則不一樣,這部分內(nèi)存的分配和回收是動態(tài)的,正是垃圾收集器所需關(guān)注的部分。
垃圾收集器在對堆區(qū)和方法區(qū)進行回收前,首先要確定這些區(qū)域的對象哪些可以被回收,哪些暫時還不能回收,這就要用到判斷對象是否存活的算法,常用的算法有“引用計數(shù)算法”、“可達性分析算法”!
引用計數(shù)算法
引用計數(shù)是垃圾收集器中的早期策略。在這種方法中,堆中每個對象實例都有一個引用計數(shù)。當一個對象被創(chuàng)建時,就將該對象實例分配給一個變量,該變量計數(shù)設(shè)置為1。當任何其它變量被賦值為這個對象的引用時,計數(shù)加1(a = b,則b引用的對象實例的計數(shù)器+1),但當一個對象實例的某個引用超過了生命周期或者被設(shè)置為一個新值時,對象實例的引用計數(shù)器減1。任何引用計數(shù)器為0的對象實例可以被當作垃圾收集。當一個對象實例被垃圾收集時,它引用的任何對象實例的引用計數(shù)器減1。
優(yōu)點:引用計數(shù)收集器可以很快的執(zhí)行,交織在程序運行中。對程序需要不被長時間打斷的實時環(huán)境比較有利。
缺點:無法檢測出循環(huán)引用。如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數(shù)永遠不可能為0。
public class ReferenceFindTest {
public static void main(String[] args) {
MyObject object1 = new MyObject();
MyObject object2 = new MyObject();
object1.object = object2;
object2.object = object1;
object1 = null;
object2 = null;
}
}
這段代碼是用來驗證引用計數(shù)算法不能檢測出循環(huán)引用。最后面兩句將object1和object2賦值為null,也就是說object1和object2指向的對象已經(jīng)不可能再被訪問,但是由于它們互相引用對方,導致它們的引用計數(shù)器都不為0,那么垃圾收集器就永遠不會回收它們。
可達性分析算法
可達性分析算法是從離散數(shù)學中的圖論引入的,就是通過一系列名為 “ GC Roots ”的對象為起點,然后開始向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈(在圖里面稱為路徑)時,則證明此對象是不可達的。

在Java語言中,可作為GC Roots的對象包括下面幾種:
- 虛擬機棧中引用的對象(棧幀中的本地變量表);
- 方法區(qū)中類靜態(tài)屬性引用的對象;
- 方法區(qū)中常量引用的對象;
- 本地方法棧中JNI(Native方法)引用的對象。
常用的垃圾回收算法
標記-清除(mark-and-sweep、Tracing)
這是最基礎(chǔ)的垃圾回收算法,之所以說它是最基礎(chǔ)的是因為它最容易實現(xiàn),思想也是最簡單的。標記-清除算法分為兩個階段:標記階段和清除階段。標記階段的任務(wù)是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所占用的空間。
標記-清除算法實現(xiàn)起來比較容易,但是有一個比較嚴重的問題就是容易產(chǎn)生內(nèi)存碎片,碎片太多可能會導致后續(xù)過程中需要為大對象分配空間時無法找到足夠的空間而提前觸發(fā)新的一次垃圾收集動作。
具體過程如下圖所示:

標記-復制(Copying)
為了解決Mark-And-Sweep算法的缺陷,Copying算法就被提了出來。它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內(nèi)存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用的內(nèi)存空間一次清理掉,這樣一來就不容易出現(xiàn)內(nèi)存碎片的問題。
這種算法雖然實現(xiàn)簡單,運行高效且不容易產(chǎn)生內(nèi)存碎片,但是卻對內(nèi)存空間的使用做出了高昂的代價,因為能夠使用的內(nèi)存縮減到原來的一半。很顯然,Copying算法的效率跟存活對象的數(shù)目多少有很大的關(guān)系,如果存活對象很多,那么Copying算法的效率將會大大降低。
在CMS垃圾收集器中,新生代里面分為一個Eden區(qū)和兩個survivor區(qū),默認Eden與survivor區(qū)的占比是8:1:1,也就是說新生代中,內(nèi)存利用的有效率為80%+10%=90%,僅有10%是浪費掉的。當然并不是每次存活的對象會低于10%,如果大于10%,那么這些對象就會通過分配擔保機制進入老年代。在經(jīng)歷一次新生代GC后,后入新到來的對象如果eden區(qū)能夠容納,仍然會放在新生代中。
具體過程如下圖所示:

標記-整理(Compacting)
為了解決Copying算法的缺陷,充分利用內(nèi)存空間,提出了Mark-Compact算法。該算法標記階段和Mark-Sweep一樣,但是在完成標記之后,它不是直接清理可回收對象,而是將存活對象都向一端移動,然后清理掉端邊界以外的內(nèi)存。
具體過程如下圖所示:

分代收集(Generation)
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根據(jù)對象存活的生命周期將內(nèi)存劃分為若干個不同的區(qū)域。一般情況下將堆區(qū)劃分為老年代(Tenured Generation)和新生代(Young Generation),老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那么就可以根據(jù)不同代的特點采取最適合的收集算法。
目前大部分垃圾收集器對于新生代都采取Copying算法,因為新生代中每次垃圾回收都要回收大部分對象,也就是說需要復制的操作次數(shù)較少,但是實際中并不是按照1:1的比例來劃分新生代的空間的,一般來說是將新生代劃分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和其中的一塊Survivor空間,當進行回收時,將Eden和Survivor中還存活的對象復制到另一塊Survivor空間中,然后清理掉Eden和剛才使用過的Survivor空間。
而由于老年代的特點是每次回收都只回收少量對象,一般使用的是Mark-Compact算法。
我們從一個object1來說明其在分代垃圾回收算法中的回收軌跡
-
object1新建,出生于新生代的Eden區(qū)域。
image -
minor GC,object1 還存活,移動到Fromsuvivor空間,此時還在新生代。
image -
minor GC,object1 仍然存活,此時會通過復制算法,將object1移動到ToSuv區(qū)域,此時object1的年齡age+1。
image -
minor GC,object1 仍然存活,此時survivor中和object1同齡的對象并沒有達到survivor的一半,所以此時通過復制算法,將fromSuv和Tosuv 區(qū)域進行互換,存活的對象被移動到了Tosuv。
image -
minor GC,object1 仍然存活,此時survivor中和object1同齡的對象已經(jīng)達到survivor的一半以上(toSuv的區(qū)域已經(jīng)滿了),object1被移動到了老年代區(qū)域。
image -
object1存活一段時間后,發(fā)現(xiàn)此時object1不可達GcRoots,而且此時老年代空間比率已經(jīng)超過了閾值,觸發(fā)了majorGC(也可以認為是fullGC,但具體需要垃圾收集器來聯(lián)系),此時object1被回收了。fullGC會觸發(fā) stop the world。
image
在以上的新生代中,我們有提到對象的age,對象存活于survivor狀態(tài)下,不會立即晉升為老生代對象,以避免給老生代造成過大的影響,它們必須要滿足以下條件才可以晉升:
1, minor gc 之后,存活于survivor 區(qū)域的對象的age會+1,當超過(默認)15的時候,轉(zhuǎn)移到老年代。
2, 動態(tài)對象,如果survivor空間中相同年齡所有的對象大小的綜合和大于survivor空間的一半,年級大于或等于該年級的對象就可以直接進入老年代。
JVM 性能調(diào)優(yōu)
調(diào)優(yōu)目標
GC優(yōu)化的基本方法是:將不同的JVM配置,應用到多個相同的機器環(huán)境中,對比找到可以提高性能、減少GC時間和次數(shù)的配置。
降低進入老年代的對象數(shù)量
除了可以在 JDK7 及更高版本中使用的 G1 收集器以外,其他分代 GC 都是由 Oracle JVM 提供的。關(guān)于分代 GC,就是對象在 Eden 區(qū)被創(chuàng)建,經(jīng)過多次Minor GC、Survivor交換后,任然存活的對象會被轉(zhuǎn)入老年代。也有一些對象由于占用內(nèi)存過大,在 Eden 區(qū)被創(chuàng)建后會直接被傳入老年代。老年代 GC 相對來說會比新生代 GC 更耗時,因此,減少進入老年代的對象數(shù)量可以顯著降低 Full GC 的頻率。減少FULL GC執(zhí)行的時間和次數(shù)
Full GC 的執(zhí)行時間比 Minor GC 要長很多,因此,如果 Full GC 時間過長(超過 1s),將可能出現(xiàn)超時錯誤。如果通過減小老年代內(nèi)存來減少 Full GC 時間,可能會引起 OutOfMemoryError 或者導致 Full GC 的頻率升高。如果通過增加老年代內(nèi)存來降低 Full GC 的頻率,F(xiàn)ull GC 的時間可能會增長。所以你需要通過不斷的實驗對比,找到一個“合適”的值。
下面情況無需進行GC優(yōu)化:
- Minor GC執(zhí)行時間不到50ms;
- Minor GC執(zhí)行不頻繁,約10秒一次;
- Full GC執(zhí)行時間不到1s;
- Full GC執(zhí)行頻率不算頻繁,不低于10分鐘1次;
JVM啟動參數(shù)
| 參數(shù) | 說明 | 實例 |
|---|---|---|
| -Xms | 初始堆大小,默認物理內(nèi)存的1/64 | -Xms512M |
| -Xmx | 最大堆大小,默認物理內(nèi)存的1/4 | -Xms2G |
| -Xmn | 新生代內(nèi)存大小,官方推薦為整個堆的3/8 | -Xmn512M |
| -Xss | 線程堆棧大小,jdk1.5及之后默認1M,之前默認256k | -Xss512k |
| -XX:NewRatio=n | 設(shè)置新生代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代占整個年輕代年老代和的1/4 | -XX:NewRatio=3 |
| -XX:SurvivorRatio=n | 年輕代中Eden區(qū)與兩個Survivor區(qū)的比值。注意Survivor區(qū)有兩個。如:8,表示Eden:Survivor=8:1:1,一個Survivor區(qū)占整個年輕代的1/8 | -XX:SurvivorRatio=8 |
| -XX:PermSize=n | 永久代初始值,默認為物理內(nèi)存的1/64 | -XX:PermSize=128M |
| -XX:MaxPermSize=n | 永久代最大值,默認為物理內(nèi)存的1/4 | -XX:MaxPermSize=256M |
| -verbose:class | 在控制臺打印類加載信息 | - |
| -verbose:gc | 在控制臺打印垃圾回收日志 | - |
| -XX:+PrintGC | 打印GC日志,內(nèi)容簡單 | - |
| -XX:+PrintGCDetails | 打印GC日志,內(nèi)容詳細 | - |
| -XX:+PrintGCDateStamps | 在GC日志中添加時間戳 | - |
| -Xloggc:filename | 指定gc日志路徑 | -Xloggc:/data/jvm/gc.log |
| -XX:+UseSerialGC | 年輕代設(shè)置串行收集器Serial | - |
| -XX:+UseParallelGC | 年輕代設(shè)置并行收集器Parallel Scavenge | - |
| -XX:ParallelGCThreads=n | 設(shè)置Parallel Scavenge收集時使用的CPU數(shù)。并行收集線程數(shù)。 | -XX:ParallelGCThreads=4 |
| -XX:MaxGCPauseMillis=n | 設(shè)置Parallel Scavenge回收的最大時間(毫秒) | -XX:MaxGCPauseMillis=100 |
| -XX:GCTimeRatio=n | 設(shè)置Parallel Scavenge垃圾回收時間占程序運行時間的百分比。公式為1/(1+n) | -XX:GCTimeRatio=19 |
| -XX:+UseParallelOldGC | 設(shè)置老年代為并行收集器ParallelOld收集器 | - |
| -XX:+UseConcMarkSweepGC | 設(shè)置老年代并發(fā)收集器CMS | - |
| -XX:+CMSIncrementalMode | 設(shè)置CMS收集器為增量模式,適用于單CPU情況。 | - |
配置建議
JVM配置方面,一般情況可以先用默認配置(基本的一些初始參數(shù)可以保證一般的應用跑的比較穩(wěn)定了),在測試中根據(jù)系統(tǒng)運行狀況(會話并發(fā)情況、會話時間等),結(jié)合GC日志、內(nèi)存監(jiān)控、使用的垃圾收集器等進行合理的調(diào)整。當老年代內(nèi)存過小時可能引起頻繁Full GC,過大時Full GC時間會特別長。
那么JVM的配置比如新生代、老年代應該配置多大最合適呢?答案是不一定,調(diào)優(yōu)就是找答案的過程。物理內(nèi)存一定的情況下,新生代設(shè)置越大,老年代就越小,F(xiàn)ull GC頻率就越高,但Full GC時間越短;相反新生代設(shè)置越小,老年代就越大,F(xiàn)ull GC頻率就越低,但每次Full GC消耗的時間越大
發(fā)現(xiàn)FullGC頻繁的時候優(yōu)先調(diào)查內(nèi)存泄漏問題
-Xms和-Xmx的值設(shè)置成相等。堆大小默認為-Xms指定的大小,默認空閑堆內(nèi)存小于40%時,JVM會擴大堆到-Xmx指定的大小;空閑堆內(nèi)存大于70%時,JVM會減小堆到-Xms指定的大小。如果在Full GC后滿足不了內(nèi)存需求會動態(tài)調(diào)整,這個階段比較耗費資源,所以設(shè)置成想通知,以避免每次垃圾回收完成后JVM重新分配內(nèi)存。
年輕代的設(shè)置,整個JVM內(nèi)存大小=年輕代大小 + 年老代大小 + 持久代大小。持久代一般固定大小為64m,所以增大年輕代后,將會減小年老代大小。此值對系統(tǒng)性能影響較大,Sun官方推薦配置為整個堆的3/8。
避免創(chuàng)建過大的對象及數(shù)組:過大的對象或數(shù)組在新生代沒有足夠空間容納時會直接進入老年代,如果是短命的大對象,會提前出發(fā)Full GC。
避免同時加載大量數(shù)據(jù),如一次從數(shù)據(jù)庫中取出大量數(shù)據(jù),或者一次從Excel中讀取大量記錄,可以分批讀取,用完盡快清空引用。
當集合中有對象的引用,這些對象使用完之后要盡快把集合中的引用清空,這些無用對象盡快回收避免進入老年代。
盡量避免長時間等待外部資源(數(shù)據(jù)庫、網(wǎng)絡(luò)、設(shè)備資源等)的情況,縮小對象的生命周期,避免進入老年代,如果不能及時返回結(jié)果可以適當采用異步處理的方式等。
調(diào)優(yōu)工具
jmap (Memory Map for Java):
可以生成 java 程序的 dump 文件, 也可以查看堆內(nèi)對象示例的統(tǒng)計信息、查看 ClassLoader 的信息以及 finalizer 隊列
命令:jmap pid
描述:查看進程的內(nèi)存映像信息,類似 Solaris pmap 命令。
命令:jmap -heap pid
描述:顯示Java堆詳細信息
命令:jmap -histo:live pid
描述:顯示堆中對象的統(tǒng)計信息
命令:jmap -clstats pid
描述:打印類加載器信息
命令:jmap -finalizerinfo pid
描述:打印等待終結(jié)的對象信息
命令:jmap -dump:format=b,file=heapdump.phrof pid
描述:生成堆轉(zhuǎn)儲快照dump文件。
jstack:
線程跟蹤工具,用于打印指定Java進程的線程堆棧信息。
jstack -l 5524 > /opt/jstack.txt
jps (JVM process Status):
可以查看虛擬機啟動的所有進程、執(zhí)行主類的全名、JVM啟動參數(shù)。
jstat (JVM Statistics Monitoring Tool):
可以查看堆內(nèi)存各部分的使用量,以及加載類的數(shù)量。
命令:jstat -gc pid
描述:垃圾回收統(tǒng)計
命令:jstat -gccapacity pid
描述:堆內(nèi)存統(tǒng)計
命令:jstat -gcnew pid
描述:新生代垃圾回收統(tǒng)計
命令:jstat -gcnewcapacity pid
描述:新生代內(nèi)存統(tǒng)計
命令:jstat -gcold pid
描述:老年代垃圾回收統(tǒng)計
命令:jstat -gcoldcapacity pid
描述:老年代內(nèi)存統(tǒng)計
命令:jstat -gcmetacapacity pid
描述:元數(shù)據(jù)空間統(tǒng)計
命令:jstat -gcutil pid
描述:總結(jié)垃圾回收統(tǒng)計
jstat -gc pid 500 10 :每500毫秒打印一次Java堆狀況(各個區(qū)的容量、使用容量、gc時間等信息),打印10次
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
12288.0 12800.0 0.0 0.0 642048.0 244137.2 102400.0 33034.0 60080.0 57964.0 7936.0 7472.9 11 0.898 3 1.940 2.838
12288.0 12800.0 0.0 0.0 642048.0 244137.2 102400.0 33034.0 60080.0 57964.0 7936.0 7472.9 11 0.898 3 1.940 2.838
12288.0 12800.0 0.0 0.0 642048.0 244137.2 102400.0 33034.0 60080.0 57964.0 7936.0 7472.9 11 0.898 3 1.940 2.838
12288.0 12800.0 0.0 0.0 642048.0 244137.2 102400.0 33034.0 60080.0 57964.0 7936.0 7472.9 11 0.898 3 1.940 2.838
12288.0 12800.0 0.0 0.0 642048.0 244137.2 102400.0 33034.0 60080.0 57964.0 7936.0 7472.9 11 0.898 3 1.940 2.838
| 名稱 | 描述 |
|---|---|
| S0C | 第一個幸存區(qū)的大小 |
| S1C | 第二個幸存區(qū)的大小 |
| S0U | 第一個幸存區(qū)的使用大小 |
| S1U | 第二個幸存區(qū)的使用大小 |
| EC | 伊甸園區(qū)的大小 |
| EU | 伊甸園區(qū)的使用大小 |
| OC | 老年代大小 |
| OU | 老年代使用大小 |
| MC | 方法區(qū)大小 |
| MU | 方法區(qū)使用大小 |
| CCSC | 壓縮類空間大小 |
| CCSU | 壓縮類空間使用大小 |
| YGC | 年輕代垃圾回收次數(shù) |
| YGCT | 年輕代垃圾回收消耗時間 |
| FGC | 老年代垃圾回收次數(shù) |
| FGCT | 老年代垃圾回收消耗時間 |
| GCT | 垃圾回收消耗總時間 |
jhat:
用來分析java堆的命令,可以將堆中的對象以html的形式顯示出來,包括對象的數(shù)量,大小等等,并支持對象查詢語言。
jinfo:
可以查看運行中jvm的全部參數(shù),還可以設(shè)置部分參數(shù)。
jinfo pid





