JVM
JVM 工作流程
運行時數(shù)據(jù)區(qū)(Runtime Data Area)
程序計數(shù)器
程序計數(shù)器(Program Counter Register) 是一塊較小的內(nèi)存空間,它可以看作是當前線程所執(zhí)行的字節(jié)碼的行號指示器。
字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令娃胆,分支鳍咱、循環(huán)、跳轉(zhuǎn)葵袭、異常處理纯露、線程恢復等基礎功能都需要依賴這個計數(shù)器來完成欠窒。
由于 Java 虛擬機的多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方式來實現(xiàn)的,在任何一個確定的時刻铸屉,一個處理器(對于多核處理器來說是一個內(nèi)核)都只會執(zhí)行一條線程中的指令钉蒲。
因此,為了線程切換后能恢復到正確的執(zhí)行位置彻坛,每條線程都需要有一個獨立的程序計數(shù)器顷啼,各條線程之間計數(shù)器互不影響,獨立存儲昌屉,我們稱這類內(nèi)存區(qū)域為“線程私有”的內(nèi)存钙蒙。
- 如果線程正在執(zhí)行的是一個 Java 方法,這個計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址怠益。
- 如果線程正在執(zhí)行的是一個 Native 方法仪搔,這個計數(shù)器值則為空(Undefined)。
此內(nèi)存區(qū)域是唯一一個在Java虛擬機規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域蜻牢。
Java 虛擬機棧
Java 虛擬機棧(Java Virtual Machine Stacks)也是線程私有的烤咧,它的生命周期與線程相同。虛擬機棧描述的是 Java 方法執(zhí)行的內(nèi)存模型抢呆,每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀(Stack Frame) 用于存儲局部變量表煮嫌、操作數(shù)棧、動態(tài)鏈接抱虐、方法出口等消息昌阿。每一個方法從調(diào)用直至執(zhí)行完成的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程。
局部變量表存放了編譯器可知的各種基本數(shù)據(jù)類型(boolean懦冰、byte灶轰、char、short刷钢、int笋颤、float、long内地、double)伴澄、對象引用(reference類型,它不等同于對象本身阱缓,可能是一個指向?qū)ο笃鹗嫉刂返囊弥羔樂橇瑁部赡苁侵赶蛞粋€代表對象的句柄或其他與此對象相關(guān)的位置)和 returnAddress 類型(指向了一條字節(jié)碼指令的地址)。
其中 64 位長度的 long 和 double 類型的數(shù)據(jù)會占用兩個局部變量空間(Slot)荆针,其余的數(shù)據(jù)類型只占用一個敞嗡。局部變量表所需的內(nèi)存空間在編譯期間完成分配,當進入一個方法時祭犯,這個方法需要在幀中分配多大的局部變量空間是完全確定的秸妥,在方法運行期間不會改變局部變量表的大小。
在 Java 虛擬機規(guī)范中沃粗,對這個區(qū)域規(guī)定了兩種異常狀態(tài):
- 如果線程請求的棧深度大于虛擬機所允許的的深度粥惧,將拋出 StackOverflowError 異常。
- 如果虛擬機椬钪眩可以動態(tài)擴展(當前大部分的Java虛擬機都可動態(tài)擴展突雪,只不過Java虛擬機規(guī)范中也允許固定長度的虛擬機棧),如果擴展時無法申請到足夠的內(nèi)存涡贱,就會拋出 OutOfMemoryError 異常咏删。
本地方法棧
本地方法棧(Native Method Stack) 與虛擬機棧所發(fā)揮的作用是非常相似的,它們之間的區(qū)別不過是虛擬機棧為虛擬機執(zhí)行Java方法(也就是字節(jié)碼)服務问词,而本地方法棧則為虛擬機使用到的Native方法服務督函。
在虛擬機規(guī)范中對本地方法棧中方法使用的語言、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒有強制規(guī)定激挪,因此具體的虛擬機可以自由實現(xiàn)它辰狡。甚至有的虛擬機(例如:Sun HotSpot虛擬機)直接就把虛擬機棧和本地方法棧合二為一。與虛擬機棧一樣垄分,本地方法棧區(qū)域也會拋出 StackOverflowError 和 OutOfMemoryError 異常宛篇。
Java 堆
對于大多數(shù)應用來說,Java 堆(Java Heap) 是 Java 虛擬機所管理的的內(nèi)存中最大的一塊薄湿。Java 堆是被所有線程共享的一塊內(nèi)存區(qū)域叫倍,在虛擬機啟動時創(chuàng)建偷卧。此內(nèi)存區(qū)域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內(nèi)存吆倦。
Java堆是垃圾收集器管理的主要區(qū)域听诸,從內(nèi)存回收的角度來看,由于現(xiàn)在收集器基本采用分代收集算法逼庞,所以Java堆中還可以細分為:新生代和老年代蛇更;再細致一點的有 Eden 空間瞻赶、From Survivor 空間赛糟、To Survivor 空間等。
從內(nèi)存分配的角度來看砸逊,線程共享的Java堆中可能劃分出多個線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer璧南,TLAB)。不過無論如何劃分师逸,都與存放內(nèi)容無關(guān)司倚,無論哪個區(qū)域,存儲的仍然是對象實例篓像,進一步劃分的目的是為了更好地回收內(nèi)存动知,或者更快地分配內(nèi)存。
方法區(qū)
方法區(qū)(Method Area)與 Java 堆一樣员辩,是各個線程共享的內(nèi)存區(qū)域盒粮,它用于存儲已被虛擬機加載的類信息、常量奠滑、靜態(tài)變量丹皱、即時編譯器編譯后的代碼等數(shù)據(jù)。
運行時常量池(Runtime Constant Pool) 是方法區(qū)的一部分宋税。Class 文件中除了有類的版本摊崭、字段、方法杰赛、接口等描述信息外呢簸,還有一項信息是常量池(Constant Pool Table),用于存放編譯器生成的各種字面量和符號引用乏屯,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池中存放根时。
既然運行時常量池是方法區(qū)的一部分,自然受到方法區(qū)內(nèi)存的限制瓶珊,當常量池無法再申請到內(nèi)存時就會拋出 OutOfMemoryError 異常啸箫。
方法指令
指令 | 說明 |
---|---|
invokeinterface | 用以調(diào)用接口方法 |
invokevirtual | 指令用于調(diào)用對象的實例方法 |
invokestatic | 用以調(diào)用類/靜態(tài)方法 |
invokespecial | 用于調(diào)用一些需要特殊處理的實例方法,包括實例初始化方法伞芹、私有方法和父類方法 |
類加載器
類加載器 | 說明 |
---|---|
BootstrapClassLoader | Bootstrap 類加載器負責加載 rt.jar 中的 JDK 類文件忘苛,它是所有類加載器的父加載器蝉娜。Bootstrap 類加載器沒有任何父類加載器,如果你調(diào)用 String.class.getClassLoader()扎唾,會返回 null召川,任何基于此的代碼會拋出 NUllPointerException 異常。Bootstrap 加載器被稱為初始類加載器 |
ExtClassLoader | 而 Extension 將加載類的請求先委托給它的父加載器胸遇,也就是Bootstrap荧呐,如果沒有成功加載的話,再從 jre/lib/ext 目錄下或者 java.ext.dirs 系統(tǒng)屬性定義的目錄下加載類纸镊。Extension 加載器由 sun.misc.Launcher$ExtClassLoader 實現(xiàn) |
AppClassLoader | 第三種默認的加載器就是 System 類加載器(又叫作 Application 類加載器)了倍阐。它負責從 classpath 環(huán)境變量中加載某些應用相關(guān)的類,classpath 環(huán)境變量通常由 -classpath 或 -cp 命令行選項來定義逗威,或者是 JAR 中的 Manifest 的 classpath 屬性峰搪。Application 類加載器是 Extension 類加載器的子加載器 |
工作原理 | 說明 |
---|---|
委托機制 | 加載任務委托交給父類加載器,如果不行就向下傳遞委托任務凯旭,由其子類加載器加載概耻,保證 java 核心庫的安全性 |
可見性機制 | 子類加載器可以看到父類加載器加載的類,而反之則不行 |
單一性機制 | 父加載器加載過的類不能被子加載器加載第二次 |
垃圾回收 gc
對象存活判斷
- 引用計數(shù)
每個對象有一個引用計數(shù)屬性罐呼,新增一個引用時計數(shù)加1鞠柄,引用釋放時計數(shù)減1,計數(shù)為0時可以回收嫉柴。此方法簡單厌杜,無法解決對象相互循環(huán)引用的問題。
- 可達性分析
從 GC Roots 開始向下搜索差凹,搜索所走過的路徑稱為引用鏈期奔。當一個對象到 GC Roots 沒有任何引用鏈相連時,則證明此對象是不可用的危尿。不可達對象呐萌。
在Java語言中,GC Roots包括:
- 虛擬機棧中引用的對象谊娇。
- 方法區(qū)中類靜態(tài)屬性實體引用的對象肺孤。
- 方法區(qū)中常量引用的對象。
- 本地方法棧中 JNI 引用的對象济欢。
垃圾收集算法
- 標記 -清除算法
“標記-清除”(Mark-Sweep)算法赠堵,如它的名字一樣,算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象法褥,在標記完成后統(tǒng)一回收掉所有被標記的對象茫叭。之所以說它是最基礎的收集算法,是因為后續(xù)的收集算法都是基于這種思路并對其缺點進行改進而得到的半等。
它的主要缺點有兩個:一個是效率問題揍愁,標記和清除過程的效率都不高呐萨;另外一個是空間問題,標記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片莽囤,空間碎片太多可能會導致谬擦,當程序在以后的運行過程中需要分配較大對象時無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作尿这。
- 復制算法
“復制”(Copying)的收集算法牺蹄,它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊孵奶。當這一塊的內(nèi)存用完了话肖,就將還存活著的對象復制到另外一塊上面北秽,然后再把已使用過的內(nèi)存空間一次清理掉。
這樣使得每次都是對其中的一塊進行內(nèi)存回收狼牺,內(nèi)存分配時也就不用考慮內(nèi)存碎片等復雜情況羡儿,只要移動堆頂指針,按順序分配內(nèi)存即可是钥,實現(xiàn)簡單,運行高效缅叠。只是這種算法的代價是將內(nèi)存縮小為原來的一半悄泥,持續(xù)復制長生存期的對象則導致效率降低。
- 標記-整理算法
復制收集算法在對象存活率較高時就要執(zhí)行較多的復制操作肤粱,效率將會變低弹囚。更關(guān)鍵的是,如果不想浪費50%的空間领曼,就需要有額外的空間進行分配擔保鸥鹉,以應對被使用的內(nèi)存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法庶骄。
根據(jù)老年代的特點毁渗,有人提出了另外一種“標記-整理”(Mark-Compact)算法,標記過程仍然與“標記-清除”算法一樣单刁,但后續(xù)步驟不是直接對可回收對象進行清理灸异,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內(nèi)存羔飞。
- 分代收集算法
GC 分代的基本假設:絕大部分對象的生命周期都非常短暫肺樟,存活時間短。
“分代收集”(Generational Collection)算法逻淌,把Java堆分為新生代和老年代么伯,這樣就可以根據(jù)各個年代的特點采用最適當?shù)氖占惴āT谛律锌ㄈ澹看卫占瘯r都發(fā)現(xiàn)有大批對象死去田柔,只有少量存活誓篱,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集凯楔。而老年代中因為對象存活率高窜骄、沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或“標記-整理”算法來進行回收摆屯。
垃圾收集器
- CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器邻遏。目前很大一部分的 Java 應用都集中在互聯(lián)網(wǎng)站或B/S系統(tǒng)的服務端上,這類應用尤其重視服務的響應速度虐骑,希望系統(tǒng)停頓時間最短准验,以給用戶帶來較好的體驗。
從名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“標記-清除”算法實現(xiàn)的廷没,它的運作過程相對于前面幾種收集器來說要更復雜一些糊饱,整個過程分為4個步驟,包括:
- 初始標記(CMS initial mark)
- 并發(fā)標記(CMS concurrent mark)
- 重新標記(CMS remark)
- 并發(fā)清除(CMS concurrent sweep)
其中初始標記颠黎、重新標記這兩個步驟仍然需要“Stop The World”另锋。初始標記僅僅只是標記一下GC Roots能直接關(guān)聯(lián)到的對象,速度很快狭归,并發(fā)標記階段就是進行GC Roots Tracing的過程夭坪,而重新標記階段則是為了修正并發(fā)標記期間,因用戶程序繼續(xù)運作而導致標記產(chǎn)生變動的那一部分對象的標記記錄过椎,這個階段的停頓時間一般會比初始標記階段稍長一些室梅,但遠比并發(fā)標記的時間短。
由于整個過程中耗時最長的并發(fā)標記和并發(fā)清除過程中疚宇,收集器線程都可以與用戶線程一起工作亡鼠,所以總體上來說,CMS收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)地執(zhí)行敷待。老年代收集器(新生代使用ParNew)
- G1收集器
與CMS收集器相比G1收集器有以下特點:
1间涵、空間整合,G1收集器采用標記整理算法讼撒,不會產(chǎn)生內(nèi)存空間碎片浑厚。分配大對象時不會因為無法找到連續(xù)空間而提前觸發(fā)下一次GC。
2根盒、可預測停頓钳幅,這是G1的另一大優(yōu)勢,降低停頓時間是G1和CMS的共同關(guān)注點炎滞,但G1除了追求低停頓外敢艰,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為N毫秒的時間片段內(nèi)册赛,消耗在垃圾收集上的時間不得超過N毫秒钠导,這幾乎已經(jīng)是實時 Java(RTSJ)的垃圾收集器的特征了震嫉。
使用G1收集器時,Java堆的內(nèi)存布局與其他收集器有很大差別牡属,它將整個Java堆劃分為多個大小相等的獨立區(qū)域(Region)票堵,雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔閡了逮栅,它們都是一部分(可以不連續(xù))Region 的集合悴势。
G1的新生代收集跟 ParNew 類似,當新生代占用達到一定比例的時候措伐,開始出發(fā)收集特纤。和 CMS 類似,G1 收集器收集老年代對象會有短暫停頓侥加。
內(nèi)存模型與回收策略
Java 堆(Java Heap)是JVM所管理的內(nèi)存中最大的一塊捧存,堆又是垃圾收集器管理的主要區(qū)域,Java 堆主要分為2個區(qū)域-年輕代與老年代担败,其中年輕代又分 Eden 區(qū)和 Survivor 區(qū)昔穴,其中 Survivor 區(qū)又分 From 和 To 2個區(qū)。
- Eden 區(qū)
大多數(shù)情況下氢架,對象會在新生代 Eden 區(qū)中進行分配傻咖,當 Eden 區(qū)沒有足夠空間進行分配時,虛擬機會發(fā)起一次 Minor GC岖研,Minor GC 相比 Major GC 更頻繁,回收速度也更快警检。 通過 Minor GC 之后孙援,Eden 會被清空,Eden 區(qū)中絕大部分對象會被回收扇雕,而那些無需回收的存活對象拓售,將會進到 Survivor 的 From 區(qū)(若 From 區(qū)不夠,則直接進入 Old 區(qū))镶奉。
- Survivor 區(qū)
Survivor 區(qū)相當于是 Eden 區(qū)和 Old 區(qū)的一個緩沖础淤,類似于我們交通燈中的黃燈。Survivor 又分為2個區(qū)哨苛,一個是 From 區(qū)鸽凶,一個是 To 區(qū)。每次執(zhí)行 Minor GC建峭,會將 Eden 區(qū)和 From 存活的對象放到 Survivor 的 To 區(qū)(如果 To 區(qū)不夠玻侥,則直接進入 Old 區(qū))。Survivor 的存在意義就是減少被送到老年代的對象亿蒸,進而減少 Major GC 的發(fā)生凑兰。Survivor 的預篩選保證掌桩,只有經(jīng)歷16次 Minor GC 還能在新生代中存活的對象,才會被送到老年代姑食。
- Old 區(qū)
老年代占據(jù)著2/3的堆內(nèi)存空間波岛,只有在 Major GC 的時候才會進行清理,每次 GC 都會觸發(fā)“Stop-The-World”音半。內(nèi)存越大则拷,STW 的時間也越長,所以內(nèi)存也不僅僅是越大就越好祟剔。由于復制算法在對象存活率較高的老年代會進行很多次的復制操作隔躲,效率很低,所以老年代這里采用的是標記——整理算法物延。