第一章 走進JAVA
java的優點:擺脫了硬件平臺的束縛,實現了“一次編寫,到處運行”;它提供了一個相對安全的內存管理和訪問機制,避免了絕大部分的內存泄漏和指針越界問題;它實現了熱點代碼檢測和運行時編譯及優化,這使得java應用能隨著運行時間的增加而獲得更高的性能。
1 java虛擬機發展史
1.1 Sun Classic/Exact VM(jdk1.0~jdk1.2)
世界上第一款商用java虛擬機,它只能使用純解釋器方式來執行Java代碼,如果要使用JIT編譯器,就必須進行外掛,但是外掛JIT后,JIT編譯器就會完全接管虛擬機的執行系統,解釋器就不工作了。由于Classic VM不能和JIT配合工作,這就意味著如果要使用編譯器執行,編譯器就不得不對每一個方法、每一行代碼都進行編譯,而無論他們執行的頻率是否具有編譯的價值。基于程序響應時間的壓力,這些編譯器不敢用編譯耗時稍高的優化技術 ,即使用了JIT,其執行效率也和C++有很大差距。
1.2 Sun HotSpot VM
HotSpot VM繼承了Sun之前兩款商用虛擬機優點,HotSpot指是的它的熱點代碼探測技術。HotSpot VM的熱點代碼探測技術可以通過執行計數器找出最有編譯價值的代碼,然后通知JIT編譯器以方法為單位進行編譯,如果一個方法被頻繁調用,或方法中的有效循環次數很多,將會分別觸發標準編譯和OSR編譯動作。通過編譯與解釋器恰當的協同工作,可以在最優化的程序響應時間和最佳執行性能取得平衡,而且無需等待本地代碼輸出才能執行程序,即時編譯的時間壓力也相對減小,這樣有助于更多的代碼優化技術秘輸出質量更高的本地代碼。
2 模塊化
它是解決應用系統與技術平臺越來越復雜、越來越龐大 的一個重要途徑。
3 64位虛擬機
幾年之前,java程序運行在64位虛擬機上需要付出比較大的額外代價:首先是內存問題,由于指針和各種數據類型對齊補白的原因,運行于64位系統上的java應用程序需要耗費更多的內存,通常要比32位系統額外增加10%~30%的內存消耗;其次,64位虛擬機性能也全面落后于32位。
第二章 Java內存區域與內存溢出異常
2.1 概述
對于java程序員來說,在虛擬機自動內存管理機制下,不再需要為每一個new操作,去寫配對的delete/free代碼,不容易出現內存泄漏和內存溢出問題,由java虛擬機管理內存。但是也正是java程序員把內存控制權交給了java虛擬機,一旦出現內存泄漏和溢出方面的問題,不了解虛擬機是怎樣實用內存的,那么排查錯誤會是一項艱難的工作。
2.2運行時數據區域
程序計數器(線程私有內存區域): 程序計數器(Program Counter Register) 是一塊較小的內存空間,它可以看作是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型里,字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條執行字節碼指令。每條線程都有一個獨立的程序計數器,獨立存儲,互不影響,因此這類內存區域被稱為線程私有的內存。 如果執行的是java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令地址。如果是native方法,計數器為空。此內存區域是唯一一個在java虛擬機規范中沒有規定任何OutOfMemoryError情況的區域。
Java虛擬機棧(線程私有內存區域):與程序計數器一樣,它也是線程私有的,它的生命周期和線程相同。同樣是線程私有,描述Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀(Stack Frame)用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。一個方法對應一個棧幀。在java虛擬機規范中,對這個區域規定了兩種異常情況:如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError異常;如果虛擬機可以動態擴展,如果擴展時無法申請到足夠的內存,則會拋出OutOfMemoryError異常。
本地方法棧(Native Method Stack):和Java虛擬機棧很類似,虛擬機棧為虛擬機執行java方法,不同的是本地方法棧為Native方法服務。與虛擬機棧一樣,本地方法棧也會拋出StackOverflowError異常和OutOfMemoryError異常。
Java堆:是Java虛擬機所管理的內存中最大的一塊。由所有線程共享,在虛擬機啟動時創建。堆區唯一目的就是存放對象實例。堆中可細分為新生代和老年代,再細分可分為Eden空間、From Survivor空間、To Survivor空間。 堆無法擴展時,拋出OutOfMemoryError異常。
方法區(Non-Heap):所有線程共享,存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。當方法區無法滿足內存分配需求時,拋出OutOfMemoryError.
運行時常量池:它是方法區的一部分,Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項是常量池(Const Pool Table),用于存放編譯期生成的各種字面量和符號引用。并非預置入Class文件中常量池的內容才進入方法運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的便是String類的intern()方法。當方法區無法滿足內存分配需求時,拋出OutOfMemoryError。
直接內存:并不是虛擬機運行時數據區的一部分,也不是Java虛擬機規范中定義的內存區域。JDK1.4加入了NIO,引入一種基于通道與緩沖區的I/O方式,它可以使用Native函數庫直接分配堆外內存,然后通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內存的引用進行操作。因為避免了在Java堆和Native堆中來回復制數據,提高了性能。 當各個內存區域總和大于物理內存限制,拋出OutOfMemoryError異常。
2.3 Hotspot虛擬機
2.3.1 對象的創建
new 對象-------》常量池定位類引用---------》檢查類引用和引用的類是否被加載、解析和初始化---------》沒有,就先執行類加載。----》創建對象分配內存
類加載通過后----》虛擬機從java堆中分配內存---》內存規整(已分配的內存和未分配的內存區域由一個指針劃分開):分配內存時把該指針移動一個對象大小的距離,成為指針碰撞;內存不規整時:從一個大的空閑列表區域分配內存。
分配內存時存在并發問題----》兩種解決方法:1 對分配內存空間的動作進行同步處理(實際上虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性);2 把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在java堆中預先分配一小塊內存,成為本地線程分配緩沖。
new指令執行時,對象的所有字段仍為零,需要等init方法執行后,字段才會賦予新值。
2.3.2 對象的內存區域
在HotSpot虛擬機中,對象在內存中存儲的布局可以分為3塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。
HotSpot虛擬機的對象頭包括兩部分信息,第一部分用于存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等。另一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
2.3.3 對象的訪問定位
對象的訪問定位也取決于具體的虛擬機實現。當我們在堆上創建一個對象實例后,就要通過虛擬機棧中的reference類型數據來操作堆上的對象。現在主流的訪問方式有兩種(HotSpot虛擬機采用的是第二種):
使用句柄訪問對象。即reference中存儲的是對象句柄的地址,而句柄中包含了對象實例數據與類型數據的具體地址信息,相當于二級指針。
直接指針訪問對象。即reference中存儲的就是對象地址,相當于一級指針。
兩種方式有各自的優缺點。當垃圾回收移動對象時,對于方式一而言,reference中存儲的地址是穩定的地址,不需要修改,僅需要修改對象句柄的地址;而對于方式二,則需要修改reference中存儲的地址。從訪問效率上看,方式二優于方式一,因為方式二只進行了一次指針定位,節省了時間開銷,而這也是HotSpot采用的實現方式
2.4 OutOfMemoryError異常
2.4.1 java堆溢出
2.4.2 虛擬機棧溢出和本地方法棧溢出
2.4.3 方法區和運行時常量池區
2.4.4 本機直接內存溢出
第三章 垃圾收集器與內存分配策略
3.1 概述
程序計數器、虛擬機棧、本地方法棧等3個區域隨線程而生,隨線程而滅,因為方法結束或線程結束時,內存自然就跟著回收了。而Java堆和方法區則不一樣,一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存也可能不一樣,我們只有在程序出于運行期間才能知道會創建哪些對象,這部分內存的分配和回收都是動態的,垃圾收集器所關注的是這部分內存。
3.2 判斷對象是否已死
3.2.1 引用計數算法
算法:給對象添加一個引用計數器,每當有一個地方引用它時,計數器就加1,當失效時,計數器就減1,任何時刻計數器為0的對象就是不可能再被用的。缺點:難以解決對象之間循環相互引用的問題。
3.2.2 可達性分析算法
在Java中,是通過可達性分析(Reachability Analysis)來判定對象是否存活的。該算法的基本思路就是通過一些被稱為(GC Roots)的對象作為起點,從這些節點開始向下搜索,搜索走過的路徑被稱為(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時(即從GC Roots節點到該節點不可達),則證明該對象是不可用的。
在Java中,可作為GC Root的對象包括以下幾種:
虛擬機棧(棧幀中的本地變量表)中引用的對象
方法區中類靜態屬性引用的對象
方法區中常量引用的對象
本地方法棧中JNI(即一般說的Native方法)引用的對象
3.2.3 引用
在JDK1.2后,java對引用進行了擴充,將引用分為強引用、軟引用、弱引用和虛引用。這四種引用強度依次減弱。
強引用:是指創建一個對象并把這個對象賦給一個引用變量,比如:Object object =new Object(),強引用有引用變量指向時永遠不會被垃圾回收,JVM寧愿拋出OutOfMemory錯誤也不會回收這種對象。
軟引用:用來描述一些還有用但并非必須的對象。對于軟引用關聯著的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收范圍之中進行第二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。在jdk1.2后,提供了SoftReference類來實現軟引用。
弱引用:它是用來描述非必須對象的,但是他的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在JDK1.2后,提供了WeakReference類來實現弱引用。
虛引用(也稱幽靈引用或者幻影引用):它是最弱的一種引用關系。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關聯的唯一目的就是在這個對象被垃圾回收器回收時收到一個系統通知。在JDK 1.2之后,提供了PhantomReference類來實現虛引用。
3.2.4 生存還是死亡
即使在可達性分析算法中不可達的對象,也并非是非死不可的,這時候它們暫時處于緩刑階段,要真正宣告一個對象的死亡,至少要經過兩次標記階段:
如果對象在進行可達性分析后發現沒有與GC Roots相連的引用鏈,那么它將會被第一次標記并且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。
當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視為沒有必要執行。
如果這個對想法被判定為有必要執行finalize()方法,那么這個對象將會放置在一個叫作F-Queue的隊列中,并且稍后由一個由虛擬機自動建立的、低優先級的Finalizer線程去執行它。這里的‘執行’是指虛擬機會觸發這個方法,但并不承諾會等待它運行結束,這樣做的原因是,如果一個對象在finalize()方法中執行緩慢,或者發生了死循環,將可能導致F-Queue隊列中其他對象永久處于等待,甚至導致整個內存回收系統崩潰。finalize()方法是對象逃脫死亡的最后一次機會,稍后GC將對F-Queue中的對象進行第二次小規模標記,如果對象要在finalize()中成功拯救自己----------只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移除出‘即將回收’的集合;如果對象這時候還沒有逃脫,那么它就真的被回收了。例如:
public class FinalizeEscape { public static FinalizeEscape SAVE_HOOK = null; public void isAlive(){ System.out.println(" i am alive"); } @Override protected void finalize() throws Throwable { // TODO Auto-generated method stub super.finalize(); System.out.println("finallize method exec"); FinalizeEscape.SAVE_HOOK = this; } public static void main(String[] args) throws InterruptedException { SAVE_HOOK = new FinalizeEscape(); SAVE_HOOK = null; System.gc(); Thread.sleep(5000); if(SAVE_HOOK != null){ SAVE_HOOK.isAlive(); }else{ System.out.println(" i am dead"); } SAVE_HOOK = null; System.gc(); Thread.sleep(5000); if(SAVE_HOOK != null){ SAVE_HOOK.isAlive(); }else{ System.out.println(" i am dead"); } } }
輸出:
3.2.5 回收方法區
方法區是所有線程共享的一片內存區域。它存儲的是已被JVM加載的類信息,常量,靜態變量,編譯器編譯后的代碼等數據。在JDK1.8以前的HotSpot虛擬機中,方法區也被稱為永久代,1.8后被元空間取代。方法區稱為永久代并不意味這進入方法區就永久存在,方法區也會發生內存回收,此區域的內存回收主要是針對常量池的回收以及對類型的卸載。我們已經知道,GC在進行垃圾回收之前,先要進行回收對象的判斷(回收對象判斷算法:引用計數法,可達性分析算法),然后再進行垃圾回收。
很多人認為方法區(或者Hotspot中的永久代)是沒有垃圾收集的,jvm規范中確實說過可以不要求在方法區實現垃圾收集,而且在方法區進行垃圾收集的性價比比較低:在堆中,新生代的常規應用進行一次垃圾收集一般可以回收70%~95%的空間,而永久代的垃圾收集效率遠低于此。
永久代的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。回收廢棄常量和回收java堆中的對象非常相似。以常量池中字面量的回收為例,假如一個字符串“abc"已經進入了常量池中,但是當前系統沒有任何一個String對象是叫做“abc”的,換句話說,就是沒有任何String對象引用常量池中的“abc”常量,也沒有其他地方引用了這個字面量,如果這是發生內存回收,而且必要的話,這個“abc"常量就會被系統清理出常量池。常量池中的其他類(接口)、方法、字段的符號引用也與此類似。
判斷一個常量是否是廢棄常量比較簡單,而要判定一個類是否是無用的類的條件相對苛刻很多。類需要同時滿足下面三個條件才能算是無用的類:
該類的所有的實例都已經被回收,也就是java堆中不存在該類的任何實例;
加載該類的ClassLoader被回收;
該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
3.3 垃圾收集算法
JVM中的堆,一般分為三大部分:新生代、老年代、永久代。
當前商業虛擬機的垃圾收集都采用分代收集算法,這種算法是根據各個年代的特點采用最適當的收集算法。一般是把java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大量對象死去,只有少量存活,那就選用復制算法,只需要付出少量的復制成本就可以完成收集。而老年代中因為對象存活率高,沒有額外空間對它進行分配擔保,就必須使用標記-清除或者標記-整理算法進行回收。
新生代主要是用來存放新生的對象。一般占據堆的1/3空間。由于頻繁創建對象,所以新生代會頻繁觸發MinorGC進行垃圾回收。新生代又分為 Eden區、ServivorFrom、ServivorTo三個區。當JVM無法為新建對象分配內存空間的時候(Eden滿了),Minor GC被觸發。因此新生代空間占用率越高,Minor GC越頻繁。
MinorGC的過程:采用復制算法。
首先,把Eden和ServivorFrom區域中存活的對象復制到ServicorTo區域(如果有對象的年齡以及達到了老年的標準,一般是15,則賦值到老年代區)
同時把這些對象的年齡+1(如果ServicorTo不夠位置了就放到老年區)
然后,清空Eden和ServicorFrom中的對象;最后,ServicorTo和ServicorFrom互換,原ServicorTo成為下一次GC時的ServicorFrom區。
老年代的對象比較穩定,所以MajorGC不會頻繁執行。
在進行MajorGC前一般都先進行了一次MinorGC,使得有新生代的對象晉身入老年代,導致空間不夠用時才觸發。當無法找到足夠大的連續空間分配給新創建的較大對象時也會提前觸發一次MajorGC進行垃圾回收騰出空間。
MajorGC采用標記—清除算法:
首先掃描一次所有老年代,標記出存活的對象
然后回收沒有標記的對象。
MajorGC的耗時比較長,因為要掃描再回收。MajorGC會產生內存碎片,為了減少內存損耗,我們一般需要進行合并或者標記出來方便下次直接分配。
當老年代也滿了裝不下的時候,就會拋出OOM(Out of Memory)異常。
永久代指內存的永久保存區域,主要存放Class和Meta(元數據)的信息。
Class在被加載的時候被放入永久區域。它和和存放實例的區域不同,GC不會在主程序運行期對永久區域進行清理。所以這也導致了永久代的區域會隨著加載的Class的增多而脹滿,最終拋出OOM異常。
在Java8中,永久代已經被移除,被一個稱為“元數據區”(元空間)的區域所取代。
元空間的本質和永久代類似,都是對JVM規范中方法區的實現。不過元空間與永久代之間最大的區別在于:元空間并不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制。類的元數據放入 native memory, 字符串池和類的靜態變量放入java堆中. 這樣可以加載多少類的元數據就不再由MaxPermSize控制, 而由系統的實際可用空間來控制。
Major GC和Full GC區別
Full GC:收集young gen、old gen、perm gen
Major GC:有時又叫old gc,只收集old gen
3.3.1 標記-清除算法(老年代回收算法)
標記-清除算法(最基礎的算法)分為標記和清除兩個階段:首先標記處所有需要回收的對象,在標記完成之后統一回收所有被標記的對象,它的標記過程其實在前一節講述對象標記判定時已經介紹過了。
缺點:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之后會產生大量的不連續的內存碎片,空間碎片較多時會導致以后在程序運行過程中分配較大對象時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
3.3.2 復制算法(新生代回收算法)
為了解決效率問題,一種稱為復制的收集算法出現了,它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活的對象復制到另一塊上面,然后再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等復雜情況,只要移動堆頂指針,按順序分配內存即可。
優點:實現簡單,運行高效。
缺點:要犧牲內存為代價。
3.3.3 標記-整理算法(老年代回收算法)
由于復制算法在對象存活率較高時存在大量的復制操作,效率降低,而且浪費空間,所以老年代一般不能選擇這種算法。
根據老年代的特點,提出了標記-整理算法,標記過程與標記-清除算法一致,整理過程是先將存活的對象移到一端,然后將存活的對象邊界外的內存清理。
3.4 HotSpot的算法實現
3.5 垃圾收集器
3.5.1 Serial收集器
Serial收集器是最基本、發展歷史最悠久的收集器,曾經(jdk1.3.1之前)是虛擬機新生代收集的唯一選擇。它是一個單線程的收集器,它使用一個CPU或一條收集線程完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。
直到現在為止,它依然是虛擬機運行在CLient模式下的默認新生代垃圾收集器。
優點:簡單而高效,由于運行在單個CPU的環境中,沒有線程交互的開銷,可以專心做垃圾收集從而獲得最高的單線程收集效率。
缺點:有停頓時間,需要停止所有當前工作線程。
3.5.2 ParNew收集器
ParNew收集器是Serial收集器的多線程版本,除了使用多線程進行垃圾收集外,其余行為包括Serial收集器可用的所有控制參數、收集算法和回收策略等和Serial收集器完全一樣。
ParNew收集器是許多運行在Server模式下的虛擬機首選的新生代收集器,其中一個與性能無關的原因是,它能和CMS收集器配合工作。在JDK1.5時期,HotSpot推出了一款在強交互應用中幾乎可認為有劃時代意義的垃圾收集器-----------CMS收集器,這款收集器是HotSpot中第一款真正意義上的并發收集器,它第一次實現了讓垃圾收集線程與用戶線程同時工作。
ParNew收集器在Cpu環境中不會比Serial收集器有更好的效果,甚至由于存在線程交互的開銷,該收集器在通過超線程技術實現的兩個CPU的環境中都不能百分之百的保證可以超越Serial收集器。當然隨著可以使用的CPU的數量的增加,它對于GC時系統資源的有效利用還是很有好處的。它默認開啟的收集線程數與CPU的數量相同,在CPU非常多的環境下可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。
3.5.3 Parallel Scavenge收集器
Parallel Scavenge收集器是一個新生代收集器,它也是使用復制算法的收集器,又是并行的多線程收集器。
Parallel Scavenge收集器的特點是它的關注點與其它收集器不同,CMS等收集器的關注點是盡可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量。所謂吞吐量就是CPU用于運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了100分鐘,垃圾收集1分鐘,那吞吐量就是99%。
Parallel Scavenge收集器擁有自適應的調節策略(GC Ergonomics),它會根據當前系統的運行狀況動態調整停頓時間和吞吐量,這也是和ParNew收集器不同之處。
3.5.4 Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本,它同樣是一個單線程收集器,使用‘標記-整理’算法。這個收集器的主要意義是在于給Client模式下的虛擬機使用。如果在Server模式下,那么它還有兩大用途:一種是在JDK1.5以及之前版本中與Parallel Scavenge收集器搭配使用,另一種是作為CMS收集器的后備預案,在并發收集發生Concurrent Mode Failure時使用。
3.5.5 Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和標記整理算法。這個收集器是在JDK1.6才開始提供的,在此之前,新生代的Parallel Scavenge收集器處于比較尷尬的狀態,因為新生代選擇了Parallel Scavenge收集器,老年代必須選擇Serial Old(因為Parallel Scavenge收集器無法與CMS收集器搭配使用)。Serial Old由于是單線程,在服務端性能是拖累,因此即使使用了Parallel Scavenge收集器也未必能獲得吞吐量最大化的效果,而且單線程的老年代收集中無法充分利用服務器多CPU的處理能力,在老年代很大而且硬件比較高級的環境中,這種組合的吞吐量甚至還不一定有ParNew加CMS組合的能力。
3.5.6 CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。主要應用在服務端,以提供較短的響應時間。它是基于標記-清除算法實現。整個過程分為4個步驟:
初始標記(CMS initial mark)
并發標記(CMS concurrent mark)
重新標記(CMS remark)
并發清除(CMS concurrent sweep)
其中初始標記和重新標記依然會有停頓時間(Stop the world)。初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快,并發標記就是進行GC Roots Tracing的過程,而重新標記則是為了修正并發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比并發標記的時間短。
CMS優點:并發收集,低停頓。
缺點:
CMS收集器對CPU資源非常敏感。在CMS中并發階段,它依賴CPU資源,若CPU資源較少時,CMS會消耗掉比較多的一部分CPU資源,使得程序的執行速度變慢。
CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。由于CMS并發清理階段用戶程序還在運行,就會產生新的垃圾,這一部分垃圾出現在標記過程后,只好留到下一次GC時清理,這一部分垃圾成為
"浮動垃圾"。
CMS收集器使用的時標記-清除算法,所以會產生內存碎片,導致大對象申請內存時,無法找到足夠大的連續空間來分配對象,不得不提前觸發Full GC。
3.5.7 G1收集器
為解決CMS算法產生空間碎片和其它一系列的問題缺陷,HotSpot提供了另外一種垃圾回收策略,G1(Garbage First)算法,通過參數-XX:+UseG1GC來啟用,該算法在JDK 7u4版本被正式推出,官網對此描述如下:
G1垃圾收集算法主要應用在多CPU大內存的服務中,在滿足高吞吐量的同時,竟可能的滿足垃圾回收時的暫停時間,該設計主要針對如下應用場景:
垃圾收集線程和應用線程并發執行,和CMS一樣
空閑內存壓縮時避免冗長的暫停時間
應用需要更多可預測的GC暫停時間
不希望犧牲太多的吞吐性能
不需要很大的Java堆 (翻譯的有點虛,多大才算大?)
第四章 虛擬機性能監控與故障處理工具
4.1 概述
了解了虛擬機內存分配與回收技術的介紹,再根據從實踐的角度去了解虛擬機內存管理的世界。
給一個系統定位問題的時候,知識、經驗是關鍵基礎,數據是依據,工具是運用處理數據的手段。這里說的數據包括:運行日志、異常堆棧、GC日志、線程快照(threaddump/javacore文件)、堆轉儲快照(heapdump/hprof文件)等。經常使用適當的虛擬機監控和分析的工具可以加快我們分析數據、定位解決問題的速度,但在學習工具前,也應當意識到工具永遠都是知識技能的一層包裝,沒有什么工具是秘密武器。
4.2 JDK的命令行工具
Sun jdk監控和故障處理工具
4.2.1 jps:虛擬機進程狀況工具
jps [option] [hostid]
-q 只輸出LVMID,省略主類的名稱
-m 輸出虛擬機進程啟動時傳遞給主類main()函數的參數
-l 輸出主類的全名,如果進程執行的是Jar包,輸出Jar路徑
-v 輸出虛擬機進程啟動JVM參數
4.2.2 jstat:虛擬機統計信息監視工具
jstat [option vmid [interval[s|ms] [count] ]
-class 監視類裝載、卸載數量、總空間以及類裝載所耗費的時間
-gc 監視java堆狀況,包括Eden區、兩個suivivor區、老年代、永久代等的容量、已用空間、GC時間合計等信息
-gccapacity 監視內容與gc基本相同,但輸出主要關注Java堆各個區域使用到的最大、最小空間
-gcutil 監視內容與gc基本相同,但輸出主要關注已使用空間占總空間的百分比
-gccause 與-gcutil功能一樣,但是會額外輸出導致上一次GC產生的原因
-gcnew 監視新生代GC狀況
4.2.3 jinfo:Java配置信息工具
jinfo [option] pid
4.2.4 jmap:Java內存映像工具
jmap [option] vmid
————————————————
版權聲明:本文為CSDN博主「24koby」的原創文章,遵循CC 4.0 by-sa版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/qq_31583183/article/details/95059493