聲明:本文基于HotSpot JVM 1.7版本
本文垃圾回收器部分不具體介紹G1
文中部分圖片來源于網(wǎng)絡(luò),權(quán)侵刪
1.JVM規(guī)范規(guī)定的運(yùn)行時(shí)數(shù)據(jù)區(qū)域
Java虛擬機(jī)規(guī)范將Java內(nèi)存劃分為程序計(jì)數(shù)器、Java虛擬機(jī)棧、本地方法棧、方法區(qū)、運(yùn)行時(shí)常量池和直接內(nèi)存,下面我們就簡(jiǎn)單介紹一下這些區(qū)域:
1.1 程序計(jì)數(shù)器
程序計(jì)數(shù)器(Program Counter Register)可以看作是當(dāng)前線程所執(zhí)行字節(jié)碼的行號(hào)指示器,字節(jié)碼解釋器工作時(shí)就是通過這個(gè)計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)程序計(jì)數(shù)器來完成。如果線程運(yùn)行的是一個(gè)Java方法,它記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果運(yùn)行的是一個(gè)Native方法,這個(gè)計(jì)數(shù)器的值則為空(Undefined)。
1.2 Java虛擬機(jī)棧
Java虛擬機(jī)棧(Java Vitual Machine Stacks)也是線程私有的,生命周期與線程相同。虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行的同時(shí)會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。虛擬機(jī)棧中的局部變量表存放了編譯器可知的各種基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)、對(duì)象引用(可以是指向?qū)ο笃鹗嫉刂返囊弥羔?,也可能是指向一個(gè)代表對(duì)象的句柄或其他與此對(duì)象相關(guān)的位置)和returnAdress類型(指向了一條字節(jié)碼指令的地址)。這部分區(qū)域規(guī)定了兩種異常狀況:如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverFlowError異常;如果虛擬機(jī)棧可以動(dòng)態(tài)擴(kuò)展,如果無法申請(qǐng)到足夠的內(nèi)存,就會(huì)拋出OutOfMemoeyError異常。
1.3 本地方法棧
本地方法棧(Native Method Stack)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,他們之間的區(qū)別是虛擬機(jī)棧執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則為Native方法服務(wù)。與虛擬機(jī)棧一樣本地方法??赡軖伋鯯tackOverFlowError和OutOfMemoryError異常。
1.4 Java堆
Java堆是所有線程共享的一塊區(qū)域,也是Java虛擬機(jī)所管理最大的一塊內(nèi)存區(qū)域。此內(nèi)存區(qū)域唯一的目的就是存放對(duì)象實(shí)例,幾乎所有的對(duì)象、數(shù)組都在這里分配內(nèi)存,但隨著JIT編譯器的發(fā)展與逃逸分析技術(shù)逐漸成熟,棧上分配、標(biāo)量替換優(yōu)化技術(shù)將會(huì)導(dǎo)致一些微妙的變化,所有對(duì)象都分配在堆上也漸漸變得不是那么絕對(duì)。如果創(chuàng)建對(duì)象時(shí)無法申請(qǐng)到足夠的內(nèi)存就會(huì)拋出OutOfMemoryError異常。
1.5 方法區(qū)
方法區(qū)(Method Area)也是各個(gè)線程共享區(qū)域,它用來存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時(shí),將拋出OutOfMemoryError異常。
1.6 運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池(Constant Pool Table),用于存放編譯器生成的各種字面量和符號(hào)引用,這部分內(nèi)容會(huì)在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。作為方法區(qū)的一部分,當(dāng)常量池?zé)o法申請(qǐng)到內(nèi)存時(shí)會(huì)拋出OutOfMemoryError異常。
1.7 直接內(nèi)存
直接內(nèi)存(Direct Memory)并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)域的一部分,也不是java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域。在JDK1.4新加入的NIO(New Input/Output)類,引入了一種基于通道(Chnnel)與緩沖區(qū)(Buffer)的I/O方式,他可以使用Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個(gè)存儲(chǔ)在Java堆中的DirectByteBuffer對(duì)象作為這塊內(nèi)存區(qū)域 的引用進(jìn)行操作。這樣就避免了在Java堆和Native堆中來回復(fù)制數(shù)據(jù)。雖然直接內(nèi)存不受Java堆大小的限制,但既然是內(nèi)存就會(huì)受到本機(jī)總內(nèi)存(包括RAM以及SWAP區(qū)或者分頁文件)大小以及處理器尋址空間的限制。

從線程的角度來說,Java內(nèi)存可以分為兩類:
所有線程共享區(qū)域
方法區(qū)、運(yùn)行時(shí)常量池、Java堆;這些區(qū)域是所有線程共享的;在Java虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建的,只有當(dāng)Java虛擬機(jī)退出時(shí)才會(huì)銷毀。
線程間隔離的數(shù)據(jù)區(qū)
程序計(jì)數(shù)器、Java虛擬機(jī)棧、本地方法棧;這些數(shù)據(jù)區(qū)域是每個(gè)線程私有的,不與其他線程共享;每個(gè)線程的數(shù)據(jù)區(qū)隨線程創(chuàng)建而創(chuàng)建,并在線程退出時(shí)銷毀。
2.HotSpot 1.7對(duì)JVM規(guī)范中運(yùn)行時(shí)數(shù)據(jù)區(qū)域的實(shí)現(xiàn)
2.1 Java虛擬機(jī)棧和本地方法棧
HotSpot將Java虛擬機(jī)棧和本地方法棧合二為一,統(tǒng)稱為棧內(nèi)存。
JVM棧內(nèi)存設(shè)置參數(shù):
- -Xss 設(shè)置棧內(nèi)存大小
2.2 堆內(nèi)存和方法區(qū)
HotSpot1.7使用分代回收的思想,將堆內(nèi)存劃分為新生代和老年代,而方法區(qū)通過永久代實(shí)現(xiàn)。

JVM堆內(nèi)存相關(guān)的參數(shù)如下:
- -Xms 控制Java堆的初始化值
- -Xmx 堆的最大值
- -Xmn 控制年輕代的值
- -Xss 設(shè)置java線程棧的大小
- -XX:NewRatio 年輕代與老年代的比值
- -XX:SurvivorRatio Eden區(qū)與兩個(gè)Survivor區(qū)域的比值, 設(shè)置為8,則兩個(gè)Survivor區(qū)與一個(gè)Eden區(qū)的比值為2:8,一個(gè)Survivor區(qū)占整個(gè)年輕代的1/10
JVM永久代(方法區(qū))內(nèi)存參數(shù):
- -XX:PermSize 永久代的初始大小
- -XX:MaxPermSize 永久代的最大值
3.判斷對(duì)象是否有可以被回收
3.1 引用計(jì)數(shù)器算法
思路就是給對(duì)象添加一個(gè)引用計(jì)數(shù)器:當(dāng)對(duì)象被引用一次時(shí),計(jì)數(shù)器值就增加1;當(dāng)引用失效時(shí),引用器的值就減1;任何引用計(jì)數(shù)器值為零的對(duì)象就是沒有被使用的對(duì)象。這種判斷對(duì)象是否被使用的方式實(shí)現(xiàn)簡(jiǎn)單,但是存在一個(gè)問題:當(dāng)A、B兩個(gè)對(duì)象只是循環(huán)引用,沒有其他任何引用,這時(shí)候其實(shí)這兩個(gè)對(duì)象已經(jīng)不能被訪問到了,但是他們還是被對(duì)方所引用。所以Java沒有采用這種方式,而是采用可達(dá)性分析算法。
3.2 可達(dá)性分析算法
這個(gè)算法以一系列的“GC Roots”對(duì)象作為起點(diǎn),從這些起點(diǎn)開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當(dāng)一個(gè)對(duì)象從引用鏈不可達(dá)時(shí),證明此對(duì)象是不可用的。

實(shí)際上Java1.2以后就對(duì)引用的概念進(jìn)行了擴(kuò)充,將引用分為強(qiáng)引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference),具體GC對(duì)幾種引用的回收機(jī)制可以參考這篇文章:JVM 引用計(jì)數(shù)、強(qiáng)引用、弱引用、軟引用、虛引用
4.垃圾回收算法
4.1 標(biāo)記-清理算法(Mark-Sweep)
標(biāo)記清理算法分為兩步--“標(biāo)記”和“清理”:首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收所有標(biāo)記的對(duì)象。這種算法有兩個(gè)明顯的不足:一個(gè)是效率問題,標(biāo)記和清理兩個(gè)過程的效率都不高;另一個(gè)是空間問題,標(biāo)記清除后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,內(nèi)存碎片太多可能會(huì)導(dǎo)致后續(xù)分配大對(duì)象時(shí),無法找到足夠的連續(xù)內(nèi)存,而不得提前觸發(fā)另一次垃圾回收動(dòng)作。

4.2 復(fù)制算法
復(fù)制算法的思路就是,將內(nèi)存劃分為兩塊,每次只是用其中的一塊。當(dāng)其中的一塊內(nèi)存用完了,就將還存活的對(duì)象復(fù)制到另一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。這樣每次清理只需要移動(dòng)堆頂指針,按順序分配即可,運(yùn)行效率高,且不會(huì)產(chǎn)生內(nèi)存碎片。但是他的代價(jià)是將內(nèi)存縮小為原來的一半。。。

實(shí)際上在真正垃圾回收器實(shí)現(xiàn)時(shí),沒有必要將內(nèi)存劃分為等量的兩塊。例如,HotSpot就是將新生代劃分為Eden區(qū)和兩個(gè)Survivor區(qū)(From 和To區(qū)),默認(rèn)比例為8:1:1,創(chuàng)建新對(duì)象是在Eden區(qū)域分配內(nèi)存,每次垃圾回收時(shí),將Eden區(qū)和From區(qū)中存活的對(duì)象復(fù)制到To區(qū),然后將Eden和From區(qū)清空,下一次垃圾回收時(shí)同上操作。這樣只浪費(fèi)了1/10的新生代區(qū)域。
4.3 標(biāo)記-整理算法
復(fù)制算法在對(duì)象存活率高時(shí)就要進(jìn)行較多的復(fù)制操作,效率就會(huì)大打折扣,對(duì)于對(duì)象存活的老年區(qū)就不太適用了,所以標(biāo)記整理算法就應(yīng)運(yùn)而生了。標(biāo)記整理算法的“標(biāo)記過程”和“標(biāo)記清除”的一樣,但后續(xù)的整理階段是將存活的對(duì)象移動(dòng)到一端,然后清理掉端邊界以外的內(nèi)存。

5.垃圾回收器
5.1 Serial收集器
Serial是一個(gè)單線程新生代的收集器,它在進(jìn)行垃圾回收時(shí)會(huì)暫停其他所有的工作線程(Stop The World),直到它收集結(jié)束。這是一個(gè)古老的垃圾收集器,一般不會(huì)用在企業(yè)級(jí)應(yīng)用中,但對(duì)于一些內(nèi)存區(qū)域很小的桌面程序,倒是可以采用這種垃圾回收器??梢允褂脜?shù)-XX:+UseSerialGC使用Serial收集器。
5.2 ParNew收集器
ParNew收集器其實(shí)就是Serial的多線程版,采用多線程的方式回收垃圾,同樣回收過程中,也需要暫停其他所有用戶線程。可以使用JVM參數(shù)-XX:UsePartNewGC指定新生代使用ParNew收集器。
5.3 Parallel Scavenge收集器
新生代垃圾回收器,采用復(fù)制算法,是一個(gè)吞吐量(運(yùn)行用戶代碼的時(shí)間、(運(yùn)行用戶代碼的時(shí)間+垃圾回收的時(shí)間))優(yōu)先的并行垃圾回收器。他提供了控制最大垃圾收集時(shí)間的參數(shù)-XX:MaxGCOauseMillis和直接設(shè)置吞吐量的參數(shù)-XX:GCTimeRatio,其中-XX:MaxGCOauseMillis是以毫秒為單位的正數(shù),-XX:GCTimeRatio是大于0小于100的整數(shù)。
Parallel Scavenge收集器還有一個(gè)參數(shù)-XX:+UseAdaptiveSizePolicy,它是一個(gè)開關(guān),當(dāng)它打開后就不需要手動(dòng)設(shè)置新生代的大?。?Xmn)、Eden與Survicor區(qū)的比例(-XX:Survivor)、晉升老年代對(duì)象年齡(-XX:PretenureSizeThreshold)等詳細(xì)參數(shù)了,虛擬機(jī)會(huì)根據(jù)當(dāng)前系統(tǒng)的運(yùn)行情況、收集性能監(jiān)控信息,動(dòng)態(tài)調(diào)整這些參數(shù),以提供最合適的停頓時(shí)間或者最大吞吐量,這種調(diào)節(jié)方式成為GC自適應(yīng)策略(GC Ergonomics)。
可以使用參數(shù)-XX:+UseParallelGC指定新生代使用Parallel Scavenge收集器。
5.4 Serial Old收集器
Serial Old收集器是Serial的老年代版本,通同樣使用一個(gè)線程進(jìn)行垃圾回收,采用“標(biāo)記整理”算法。這種垃圾收集器主要給Client模式下的虛擬機(jī)使用。但是Server模式下也有兩種使用場(chǎng)景:JDK1.5及以前的版本中與Parallel Scavenge收集器搭配使用,另一個(gè)場(chǎng)景是作為CMS收集器后備方案,在并發(fā)收集發(fā)生Concurrent Mode Failure時(shí)使用??梢允褂脜?shù)-XX:+UseSerialGC指定老年代使用Serial old收集器。
5.5 Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年版本,使用多線程和“標(biāo)記-整理”算法。這個(gè)收集器的出現(xiàn)很大程度上解決了新生代的Parallel Scavenge收集器的尷尬地位。原因是在JDK1.6以前,新生代采用Parallel Scavenge收集器,老年代的收集器只能采用Serial Old收集器,這樣的組合很大程度上還不如ParNew加CMS給力。但是1.6出現(xiàn)的Parallel Old收集器和新生代的Parallel Scavenge收集器是一個(gè)吞吐量?jī)?yōu)先的最佳搭檔。使用參數(shù)-XX:+UseParallelGC指定老年代使用Parallel Old收集器。
5.6 CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的老年代垃圾收集器。從名字(包括“Mark Sweep”)上就可以看出,CMS收集器是基于“標(biāo)記-清除”算法實(shí)現(xiàn)的,它的運(yùn)作過程分為4個(gè)步驟:
- 初始標(biāo)記(CMS initial mark)
- 并發(fā)標(biāo)記(CMS concurrent mark)
- 重新標(biāo)記(CMS remark)
- 并發(fā)清除(CMS concurrent sweep)
其中,初始標(biāo)記、重新標(biāo)記這兩個(gè)步驟仍需要“Stop The World”,可以使用參數(shù)-XX:+UseConcMarkSweepGC指定老年代使用CMS回收器。CMS還有一個(gè)重要的參數(shù)-XX:CMSInitiatingOccupancyFraction,它是0-100的數(shù),是觸發(fā)CMS回收老年代的閾值,即:當(dāng)老年代使用率大于改參數(shù)指定值時(shí)候,就會(huì)使用CMS回收器對(duì)老年代的垃圾進(jìn)行回收。
6.JVM參數(shù)
HotSpot JVM選項(xiàng)分為三類:
6.1 標(biāo)準(zhǔn)選項(xiàng)
這類選項(xiàng)的功能很穩(wěn)定,在后續(xù)的版本中也不太會(huì)發(fā)生變化。運(yùn)行java -help可以看到所有的標(biāo)準(zhǔn)選項(xiàng)。標(biāo)準(zhǔn)選項(xiàng)都是以-開頭的,比如-version,-server等。
6.2 X選項(xiàng)
比如-Xms。這類選項(xiàng)都是以-X開頭的,也被稱為X選項(xiàng)。運(yùn)行java -X命令可以查看所有的X選項(xiàng)。
6.3 XX選項(xiàng)
這類選項(xiàng)屬于實(shí)驗(yàn)性的,主要是給JVM開發(fā)者用于開發(fā)和調(diào)試JVM的,在后續(xù)的行為中可能會(huì)發(fā)生改變。XX選項(xiàng)的語法
- 如果是布爾類型的選項(xiàng),它的格式為-XX:+flag或者-XX:-flag,分別表示開啟和關(guān)閉該選項(xiàng)。
- 針對(duì)非布爾類型選項(xiàng),它的格式為-XX:flag=value。
對(duì)于HotSpot建議將最大堆和最小堆設(shè)置為相同值
- -Xms 控制Java堆的初始化值
- -Xmx 堆的最大值
- -Xmn 控制年輕代的值
- -Xss 設(shè)置java線程棧的大小
- -XX:PermSize 永久代的初始大小
- -XX:MaxPermSize 永久代的最大值
- -XX:NewRatio 年輕代與老年代的比值
- -XX:SurvivorRatio Eden區(qū)與兩個(gè)Survivor區(qū)域的比值, 設(shè)置為8,則兩個(gè)Survivor區(qū)與一個(gè)Eden區(qū)的比值為2:8,一個(gè)Survivor區(qū)占整個(gè)年輕代的1/10
- -XX:+PrintGCDetails 打印GC詳情
更多JVM參數(shù)請(qǐng)參閱:JVM參數(shù)設(shè)置與分析
7.參考文獻(xiàn)
Java中的垃圾回收機(jī)制
內(nèi)存區(qū)域 JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)
雜談GC
JVM系列三:JVM參數(shù)設(shè)置、分析
JVM 垃圾回收器工作原理及使用實(shí)例介紹