? 最近開始讀周志明老師《深入理解Java虛擬機》,已經將第二部分的自動內存管理機制詳細的讀了一遍,對java虛擬機的內存模型,垃圾回收有了初步的了解。
一、JVM內存區域
? Java虛擬機在運行時,會把內存空間分為若干個區域,根據《Java虛擬機規范(Java SE 7 版)》的規定,Java虛擬機所管理的內存區域分為如下部分:方法區、堆內存、虛擬機棧、本地方法棧、程序計數器。
1.程序計數器
? 程序計數器(Program CounterRegister)是一塊較小的內存,它可以看做是當前線程所執行字節碼的行號指示器。為了線程切換后能恢復到正確的執行位置,每個線程都需要有一個單獨的程序計數器,各線程之間計數器互不影響,獨立存儲,這類內存區域可以稱為“線程私有”的內存。此內存區域是唯一一個在Java虛擬機規范中沒有規定任何OutOfMemoryError情況的區域。
2.Java虛擬機棧
? 與程序計數器一樣,Java虛擬機棧(Java Virtual MachineStacks)也是線程私有的,它的生命周期與線程相同。虛擬機會為每個線程分配一個虛擬機棧,每個虛擬機棧中都有若干個棧幀,每個棧幀中存儲了局部變量表、操作數棧、動態鏈接、返回地址等。一個棧幀就對應Java代碼中的一個方法,當線程執行到一個方法時,就代表這個方法對應的棧幀已經進入虛擬機棧并且處于棧頂的位置,每一個Java方法從被調用到執行結束,就對應了一個棧幀從入棧到出棧的過程。
3.本地方法棧
? 本地方法棧(Native MethodStacks)與虛擬機棧所發揮的作用是非常相似的。他們的區別不過是虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則為虛擬機執行Native方法服務。
4.java堆
? 對大多數應用來說,Java堆是Java虛擬機所管理的內存中最大的一塊。Java堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建,此內存區域的唯一目的就是存放實例對象,幾乎所有的對象實例都在這里分配內存。從內存回收的角度看,由于現在收集器基本都采用分代收集算法,所以Java堆可以細分為:新生代和老年代;再細致一點的有Eden空間、From Survivor空間、To Survivor空間等。從內存分配的角度看,線程共享的Java堆中可能劃分出多個線程私有的分配緩沖區(Thread? Local Allocation Buffer,TLAB)。
5.方法區
? 方法區(Method Area)與Java堆一樣是各線程共享的內存區域,它用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。雖然Java虛擬機把方法區描述為堆的一個邏輯部分,但它卻有一個別名叫做Non-heap(非堆),目的應該是與Java堆區分開來。從jdk1.7已經開始準備“去永久代”的規劃,jdk1.7的HotSpot中,已經把原本放在方法區中的靜態變量、字符串常量池等移到堆內存中,(常量池除字符串常量池還有class常量池等),這里只是把字符串常量池移到堆內存中;在jdk1.8中,方法區已經不存在,原方法區中存儲的類信息、編譯后的代碼數據等已經移動到了元空間(MetaSpace)中,元空間并沒有處于堆內存上,而是直接占用的本地內存(NativeMemory)。
二、JVM垃圾回收
? 垃圾回收,就是通過垃圾收集器把內存中沒用的對象清理掉。垃圾回收涉及到的內容有:1、判斷對象是否存活;2、垃圾收集算法;3.垃圾回收器。
1 判斷對象是否存活
(1)引用計數法
? 引用計數法是指給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時候計數器值為0就表示不會再被任何對象使用。引用計數法(Reference Counting)的實現簡單,判斷效率也很高,在大部分情況下都是一個不錯的算法。但在主流的Java虛擬機里面沒有使用引用計數法來管理內存,主要原因是它很難解決對象之間相互循環引用的問題。
? 如圖:當引用1和引用2斷開后,對象1和對象2之間彼此引用,這時候兩個對象的引用計數器不為0,無法判斷他們是死對象,垃圾回收器也就無法回收。
(2)可達性分析法
? 基本思路是通過一系列稱為GC Roots的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈(就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。
? 在Java語言中,可作為GC Roots的對象包括下面幾種:
??? ①虛擬機棧(棧幀中的本地變量表)中引用的對象。
??? ②方法區中類靜態屬性引用的對象。
??? ③方法區中常量引用的對象。
? ? ④本地方法棧中JNI(即Native方法)引用的對象。
? 進行可達性分析后對象和GC Roots之間沒有引用鏈相連時,對象將會被進行一次標記,接著會判斷如果對象沒有覆蓋Object的finalize()方法或者finalize()方法已經被虛擬機調用過,那么它們就會被行刑(清除);如果對象覆蓋了finalize()方法且還沒有被調用,則會執行finalize()方法中的內容,所以在finalize()方法中如果重新與GC Roots引用鏈上的對象關聯就可以拯救自己。但是周志明老師也建議盡量不用使用這個方法,因為它運行代價高昂,不確定性大,無法保證各個對象的調用順序,并不適合做“關閉外部資源”之類的工作。
(3)對象引用
? 在JDK1.2之后,Java對引用的概念做了擴充,將引用分為強引用(StrongReference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(PhantomReference)4種,這4種引用的強度依次減弱。
2 垃圾收集算法
2.1 標記-清除算法
? “標記-清除”(Mark-Sweep)算法是最基礎的收集算法,這個算法分為“標記”和“清除”兩個階段:首先標記出需要回收的所有對象,在標記完成后統一回收標記的對象,標記過程就是之前講的通過引用計數法和可達性分析法進行判定。之所以說它是最基礎的算法,是因為后面的算法都是在它基礎上進行的改進,它的主要不足有兩個:一個是效率問題,標記和清除的效率都不高,還有一個是空間問題,標記清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致在需要分配較大對象時,無法找到連續的內存空間而不得不提前觸發另一次垃圾收集動作。如圖:
2.2 復制算法
? 把內存分為大小相等的兩塊,每次存儲只用其中一塊,當這一塊用完了,就把存活的對象全部復制到另一塊上,同時把使用過的這塊內存空間全部清理掉,往復循環。現在商用虛擬機都采用這種算法來回收新生代,因為新生代中98%的對象都是朝生夕死的,所以并不需要1:1的比例來劃分內存空間,而是將內存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和一塊Survivor空間。當回收時,將Eden和Survivor中開存活的對象一次性復制到另一塊Survivor空間上,最后清理掉Eden和剛才使用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小是8:1,也就是每次新生代可用空間是整個新生代容量的90%(80%+10%),只有10%的內存會被“浪費”。當然,98%可回收只是一般的情況的數據,我們沒有辦法保證每次回收都只有不多于10%的對象存活,當Survivor空間不夠用時,需要依賴其他內存(這里指老年代)進行分配擔保(Handle Promotion)。
2.3 標記-整理算法
? 標記過程任然與“標記-清除”算法的標記過程相同,但后續步驟不是對可回收對象進行直接清理,而是讓所有活的對象都移動到另一端,然后直接清理掉端邊界以外的內存,這種算法適合于老年代。
2.4 分代收集算法
? 把堆內存分為新生代和老年代,新生代又分為Eden區、From Survivor和To Survivor。一般新生代中的對象基本上都是朝生夕滅的,每次只有少量對象存活,因此采用復制算法,只需要復制那些少量存活的對象就可以完成垃圾收集;老年代中的對象存活率較高,就采用標記-清除和標記-整理算法來進行回收。
3 垃圾收集器
? 現在常見的垃圾收集器有如下幾種
? 新生代收集器:Serial、ParNew、Parallel Scavenge
? 老年代收集器:Serial Old、CMS、Parallel Old
? 堆內存垃圾收集器:G1
3.1 Serial收集器
? Serial收集器是最基本、發展歷史最悠久的收集器。這個收集器是單線程收集器,采用復制算法進行垃圾收集,它在完成垃圾收集工作時,必須暫停其他所有的工作線程,直到它收集結束。就好比“在打掃房間時你必須老老實實的在椅子上或者房間外待著,如果一邊打掃,你一邊亂扔紙屑,那么房間就打掃不完了?!?,這確實是一個合乎情理的矛盾,雖然垃圾收集這項工作聽起來和打掃房間屬于一個性質,但實際上肯定是要比打掃房間復雜的多。
3.2 ParNew收集器
? ParNew收集器實際上就是Serial收集器的多線程版本,但它卻是許多運行在Server模式下虛擬機的首選新生代收集器,其中有一個與性能無關但很重要的原因是,除了Serial收集器之外,目前只有ParNew收集器能與CMS收集器配合工作。
3.3 ParalleScavenge收集器
? Paralle Scavenge收集器是一個新生代收集器,它也是使用復制算法的收集器,又是并行的多線程收集器。他的特點是它的關注點與其他收集器不同,CMS收集器的關注點是盡可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目的是達到一個可控制的吞吐量(Throughout)。所謂吞吐量就是CPU運行用戶代碼的時間與CPU總消耗時間的比,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉了1分鐘,那吞吐量就是99%。
3.4 SerialOld收集器
? Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器,使用“標記-整理”算法。這個收集器的主要意義也是給Client模式的虛擬機使用。如果在Server模式下,那么它有兩大用途:一種用途是在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用,另一種用途是作為CMS收集器的后備方案,在并發收集發生Concurrent Mode Failure時使用。
3.5 ParallelOld收集器
? Parallel Old是Parallel Scavenge收集器的老年代版,使用多線程和“標記-整理”算法。
3.6 CMS收集器
? CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤其注重服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就非常符合這類應用的需求。
? 整個垃圾收集過程分為4個步驟:
? ① 初始標記:標記一下GC Roots能直接關聯到的對象,速度較快
? ② 并發標記:進行GC Roots Tracing,標記出全部的垃圾對象,耗時較長
? ③ 重新標記:修正并發標記階段引用戶程序繼續運行而導致變化的對象的標記記錄,耗時較短
? ④ 并發清除:用標記-清除算法清除垃圾對象,耗時較長
3.7 G1收集器
G1是一款面向服務器的垃圾收集器。HotSpot開發團隊賦予它的使命是未來可以替換掉JDK1.5中發布的CMS收集器