java 虛擬機 && Java內存結構 && JVM垃圾回收機制算法

什麼是HotSpot VM

提起HotSpot VM,相信所有Java程序員都知道,它是Sun JDK和OpenJDK中所帶的虛擬機,也是目前使用範圍最廣的Java虛擬機。

但不一定所有人都知道的是,這個目前看起來“血統純正”的虛擬機在最初並非由Sun公司開發,而是由一家名爲“Longview Technologies”的小公司設計的,甚至這個虛擬機最初並非是爲Java語言而開發的,它來源於Strongtalk VM,而這款虛擬機中相當多的技術又是來源於一款支持Self語言實現“達到C語言50%以上的執行效率”的目標而設計的虛擬機。

Sun公司注意到了這款虛擬機在JIT編譯上有許多優秀的理念和實際效果,在1997年收購了Longview Technologies公司,從而獲得了HotSpot VM。

HotSpot VM既繼承了Sun之前兩款商用虛擬機的優點(如前面提到的準確式內存管理),也有許多自己新的技術優勢,如它名稱中的HotSpot指的就是它的熱點代碼探測技術(其實兩個VM基本上是同時期的獨立產品,HotSpot還稍早一些,HotSpot一開始就是準確式GC,而Exact VM之中也有與HotSpot幾乎一樣的熱點探測。

爲了Exact VM和HotSpot VM哪個成爲Sun主要支持的VM產品,在Sun公司內部還有過爭論,HotSpot打敗Exact並不能算技術上的勝利。

HotSpot VM的熱點代碼探測能力可以通過執行計數器找出最具有編譯價值的代碼,然後通知JIT編譯器以方法爲單位進行編譯。

如果一個方法被頻繁調用,或方法中有效循環次數很多,將會分別觸發標準編譯和OSR(棧上替換)編譯動作。

通過編譯器與解釋器恰當地協同工作,可以在最優化的程序響應時間與最佳執行性能中取得平衡,而且無須等待本地代碼輸出才能執行程序,

即時編譯的時間壓力也相對減小,這樣有助於引入更多的代碼優化技術,輸出質量更高的本地代碼。

在2006年的JavaOne大會上,Sun公司宣佈最終會把Java開源,並在隨後的一年,陸續將JDK的各個部分(其中當然也包括了HotSpot VM)在GPL協議下公開了源碼,

並在此基礎上建立了OpenJDK。這樣,HotSpot VM便成爲了Sun JDK和OpenJDK兩個實現極度接近的JDK項目的共同虛擬機。

在2008年和2009年,Oracle公司分別收購了BEA公司和Sun公司,這樣Oracle就同時擁有了兩款優秀的Java虛擬機:JRockit VM和HotSpot VM。

Oracle公司宣佈在不久的將來(大約應在發佈JDK 8的時候)會完成這兩款虛擬機的整合工作,使之優勢互補。

整合的方式大致上是在HotSpot的基礎上,移植JRockit的優秀特性,譬如使用JRockit的垃圾回收器與MissionControl服務,使用HotSpot的JIT編譯器與混合的運行時系統。

     

Java內存結構

Java堆(Java Heap)

java堆是java虛擬機所管理的內存中最大的一塊,是被所有線程共享的一塊內存區域,在虛擬機啓動時創建。此內存區域的唯一目的就是存放對象實例,這一點在Java虛擬機規範中的描述是:所有的對象實例以及數組都要在堆上分配

java堆是垃圾收集器管理的主要區域,因此也被成爲GC堆”(Garbage Collected Heap)。從內存回收角度來看java堆可分爲:新生代和老生代(當然還有更細緻的劃分,在下一章會講到)。從內存分配的角度看,線程共享的Java堆中可能劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer,TLAB)。無論怎麼劃分,都與存放內容無關,無論哪個區域,存儲的都是對象實例,進一步的劃分都是爲了更好的回收內存,或者更快的分配內存。

根據Java虛擬機規範的規定,java堆可以處於物理上不連續的內存空間中。當前主流的虛擬機都是可擴展的(通過 -Xmx -Xms 控制)。如果堆中沒有內存完成實例分配,並且堆也無法再擴展時,將會拋出OutOfMemoryError異常。

Java虛擬機棧(Java Virtual Machine Stacks

java虛擬機也是線程私有的,它的生命週期和線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀(Stack Frame)用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。

咱們常說的堆內存、棧內存中,棧內存指的就是虛擬機棧。局部變量表存放了編譯期可知的各種基本數據類型(8個基本數據類型)、對象引用(地址指針)、returnAddress類型。

局部變量表所需的內存空間在編譯期間完成分配。在運行期間不會改變局部變量表的大小。

這個區域規定了兩種異常狀態:

  1. 如果線程請求的棧深度大於虛擬機所允許的深度,則拋出StackOverflowError異常;
  2. 如果虛擬機棧可以動態擴展,在擴展是無法申請到足夠的內存,就會拋出OutOfMemoryError異常。

本地方法棧(Native Method Stack)

本地方法棧與虛擬機棧所發揮作用非常相似,區別是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的native方法服務。本地方法棧也是拋出兩個異常。

方法區(Method Area)

方法區與java堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。它有個別命叫Non-Heap(非堆)。當方法區無法滿足內存分配需求時,拋出OutOfMemoryError異常。

直接內存(Direct Memory)

直接內存不是虛擬機運行時數據區的一部分,也不是java虛擬機規範中定義的內存區域。但這部分區域也會頻繁使用,而且也可能導致OutOfMemoryError異常

在JDK1.4中新加入的NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可以使用Native函數庫直接分配堆外內存,然後通過一個存儲在java堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作。

運行時常量池(Runtime Constant Pool)

運行時常量池是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在加載後進入方法區的運行時常量池中存放。

程序計數器(Program Counter Register)

程序計數器是一塊較小的內存空間,它可以看作是當前線程所執行的字節碼的行號指示器。

由於Java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,一個處理器都只會執行一條線程中的指令。因此,爲了線程切換後能恢復到正確的執行位置,每條線程都有一個獨立的程序計數器,各個線程之間計數器互不影響,獨立存儲。稱之爲“線程私有”的內存。程序計數器內存區域是虛擬機中唯一沒有規定OutOfMemoryError情況的區域。

執行引擎

虛擬機核心的組件就是執行引擎,它負責執行虛擬機的字節碼,一般會先進行編譯成機器碼後執行。

垃圾收集系統

垃圾收集系統是Java的核心,也是不可少的,Java有一套自己進行垃圾清理的機制,開發人員無需手工清理

垃圾回收機制算法分析

什麼是垃圾回收機制

不定時去堆內存中清理不可達對象。不可達的對象並不會馬上就會直接回收, 垃圾收集器在一個Java程序中的執行是自動的,不能強制執行,即使程序員能明確地判斷出有一塊內存已經無用了,是應該回收的,程序員也不能強制垃圾收集器回收該內存塊。程序員唯一能做的就是通過調用System.gc 方法來"建議"執行垃圾收集器,但其是否可以執行,什麼時候執行卻都是不可知的。這也是垃圾收集器的最主要的缺點。當然相對於它給程序員帶來的巨大方便性而言,這個缺點是瑕不掩瑜的。

finalize方法作用

Java技術使用finalize()方法在垃圾收集器將對象從內存中清除出去前,做必要的清理工作。這個方法是由垃圾收集器在確定這個對象沒有被引用時對這個對象調用的。它是在Object類中定義的,因此所有的類都繼承了它。子類覆蓋finalize()方法以整理系統資源或者執行其他清理工作。finalize()方法是在垃圾收集器刪除對象之前對這個對象調用的。

新生代與老年代

Java 中的堆是 JVM 所管理的最大的一塊內存空間,主要用於存放各種類的實例對象。

在 Java 中,根據垃圾回收機制的不同,Java堆有可能擁有不同的結構,最爲常見的就是將整個Java堆分爲兩個不同的區域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被劃分爲三個區域:Eden、From Survivor、To Survivor。From Survivor、To Survivor 也可以稱爲s0和s1區域,他們是兩塊大小相等並且可以互相角色的空間。絕大多數情況下,對象首先分配在eden區,在新生代回收後,如果對象還存活,則進入s0或s1區,之後每經過一次新生代回收,如果對象存活則它的年齡就加1,對象達到一定的年齡後,則進入老年代。

這樣劃分的目的是爲了使 JVM 能夠更好的管理堆內存中的對象,包括內存的分配以及回收。

堆的內存模型大致大致如下:

    

以下涉及的 JVM 默認值均以JDK1.6版本爲準。
默認的,新生代 ( Young ) 與老年代 ( Old ) 的比例的值爲 1:2 ( 該值可以通過參數 –XX:NewRatio 來指定 ),即:新生代 ( Young ) = 1/3 的堆空間大小。老年代 ( Old ) = 2/3 的堆空間大小。其中,新生代 ( Young ) 被細分爲 Eden 和 兩個 Survivor 區域,這兩個 Survivor 區域分別被命名爲 from 和 to,以示區分。
默認的,Edem : from : to = 8 : 1 : 1 ( 可以通過參數 –XX:SurvivorRatio 來設定 ),即: Eden = 8/10 的新生代空間大小,from = to = 1/10 的新生代空間大小。

如何判斷對象是否存活

引用計數法

引用計數法就是如果一個對象沒有被任何引用指向,則可視之爲垃圾。這種方法的缺點就是不能檢測到環的存在。

首先需要聲明,至少主流的Java虛擬機裏面都沒有選用引用計數算法來管理內存。 那爲什麼主流的Java虛擬機裏面都沒有選用這種算法呢?其中最主要的原因是它很難解決對象之間相互循環引用的問題。

什麼是引用計數算法:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值加1;當引用失效時,計數器值減1.任何時刻計數器值爲0的對象會被認爲是不可達對象。

根搜索算法

根搜索算法的基本思路就是通過一系列名爲”GC Roots”的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可達的。

那麼問題又來了,如何選取GCRoots對象呢?在Java語言中,可以作爲GCRoots的對象包括下面幾種:

(1). 虛擬機棧(棧幀中的局部變量區,也叫做局部變量表)中引用的對象。

(2). 方法區中的類靜態屬性引用的對象。

(3). 方法區中常量引用的對象。

(4). 本地方法棧中JNI(Native方法)引用的對象。https://images2015.cnblogs.com/blog/249993/201703/249993-20170302205315766-1323892362.png

         
下面給出一個GCRoots的例子,如下圖,爲GCRoots的引用鏈。
根搜索算法的基本思路就是通過一系列名爲”GC Roots”的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。

從上圖,reference1、reference2、reference3都是GC Roots,可以看出: 
reference1-> 對象實例1; 
reference2-> 對象實例2; 
reference3-> 對象實例4; 
reference3-> 對象實例4 -> 對象實例6; 
可以得出對象實例1、2、4、6都具有GC Roots可達性,也就是存活對象,不能被GC回收的對象。 
而對於對象實例3、5直接雖然連通,但並沒有任何一個GC Roots與之相連,這便是GC Roots不可達的對象,這就是GC需要回收的垃圾對象。

垃圾回收機制策略

標記清除算法

該算法有兩個階段。

1. 標記階段:找到所有可訪問的對象,做個標記

2. 清除階段:遍歷堆,把未被標記的對象回收

應用場景

該算法一般應用於老年代,因爲老年代的對象生命週期比較長。

優缺點

標記清除算法的優點和缺點

1. 優點

- 是可以解決循環引用的問題

- 必要時纔回收(內存不足時)

2. 缺點:

- 回收時,應用需要掛起,也就是stop the world。

- 標記和清除的效率不高,尤其是要掃描的對象比較多的時候,因爲是通過遍歷逐一清理,效率慢且易產生內存碎片。

- 會造成內存碎片(會導致明明有內存空間,但是由於不連續,申請稍微大一些的對象無法做到)。

複製算法

概念

如果jvm使用了coping算法,一開始就會將可用內存分爲兩塊,from域和to域, 每次只是使用from域,to域則空閒着。當from域內存不夠了,開始執行GC操作,這個時候,會把from域存活的對象拷貝到to域,然後直接把from域進行內存清理。

應用場景

coping算法一般是使用在新生代中,因爲新生代中的對象一般都是朝生夕死的,存活對象的數量並不多,這樣使用coping算法進行拷貝時效率比較高。jvm將Heap 內存劃分爲新生代與老年代,又將新生代劃分爲Eden(伊甸園) 與2塊Survivor Space(倖存者區) ,然後在Eden –>Survivor Space 以及From Survivor Space 與To Survivor Space 之間實行Copying 算法。 不過jvm在應用coping算法時,並不是把內存按照1:1來劃分的,這樣太浪費內存空間了。一般的jvm都是8:1。也即是說,Eden區:From區:To區域的比例是始終有90%的空間是可以用來創建對象的,而剩下的10%用來存放回收後存活的對象。

1、當Eden區滿的時候,會觸發第一次young gc,把還活着的對象拷貝到Survivor From區;當Eden區再次觸發young gc的時候,會掃描Eden區和From區域,對兩個區域進行垃圾回收,經過這次回收後還存活的對象,則直接複製到To區域,並將Eden和From區域清空。

2、當後續Eden又發生young gc的時候,會對Eden和To區域進行垃圾回收,存活的對象複製到From區域,並將Eden和To區域清空。

3、可見部分對象會在From和To區域中複製來複制去,如此交換15次(由JVM參數MaxTenuringThreshold決定,這個參數默認是15),最終如果還是存活,就存入到老年代

注意: 萬一存活對象數量比較多,那麼To域的內存可能不夠存放,這個時候會藉助老年代的空間

優缺點

優點:在存活對象不多的情況下,性能高,能解決內存碎片和java垃圾回收算法之-標記清除 中導致的引用更新問題。

缺點: 會造成一部分的內存浪費。不過可以根據實際情況,將內存塊大小比例適當調整;如果存活對象的數量比較大,coping的性能會變得很差。

標記壓縮算法

標記清除算法和標記壓縮算法非常相同,但是標記壓縮算法在標記清除算法之上解決內存碎片
概念

    

壓縮算法簡單介紹

任意順序 : 即不考慮原先對象的排列順序,也不考慮對象之間的引用關係,隨意移動對象;

線性順序 : 考慮對象的引用關係,例如a對象引用了b對象,則儘可能將a和b移動到一塊;

滑動順序 : 按照對象原來在堆中的順序滑動到堆的一端。

優點:解決內存碎片問題,缺點壓縮階段,由於移動了可用對象,需要去更新引用。

Minor GC和Full GC區別

概念:

新生代 GC(Minor GC):指發生在新生代的垃圾收集動作,因爲 Java 對象大多都具備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快。

老年代 GC(Major GC  / Full GC):指發生在老年代的 GC,出現了 Major GC,經常會伴隨至少一次的 Minor GC(但非絕對的,在 ParallelScavenge 收集器的收集策略裏就有直接進行 Major GC 的策略選擇過程) 。MajorGC 的速度一般會比 Minor GC 慢 10倍以上。

Minor GC觸發機制:

當年輕代滿時就會觸發Minor GC,這裏的年輕代滿指的是Eden代滿,Survivor滿不會引發GCFull GC觸發機制:

當年老代滿時會引發Full GC,Full GC將會同時回收年輕代、年老代,當永久代滿時也會引發Full GC,會導致Class、Method元信息的卸載其中Minor GC如下圖所示


虛擬機給每個對象定義了一個對象年齡(Age)計數器。如果對象在 Eden 出生並經過第一次 Minor GC 後仍然存活,並且能被 Survivor 容納的話,將被移動到 Survivor 空間中,並將對象年齡設爲 1。對象在 Survivor 區中每熬過一次 Minor GC,年齡就增加 1 歲,當它的年齡增加到一定程度(默認爲 15 歲)時,就會被晉升到老年代中。對象晉升老年代的年齡閾值,可以通過參數 -XX:MaxTenuringThreshold (閾值)來設置。

JVM的永久代中會發生垃圾回收麼?

垃圾回收不會發生在永久代,如果永久代滿了或者是超過了臨界值,會觸發完全垃圾回收(Full GC)。如果你仔細查看垃圾收集器的輸出信息,就會發現永久代也是被回收的。這就是爲什麼正確的永久代大小對避免Full GC是非常重要的原因。請參考下Java8:從永久代到元數據區
(注:Java8中已經移除了永久代,新加了一個叫做元數據區的native內存區)

分代算法

概述

這種算法,根據對象的存活週期的不同將內存劃分成幾塊,新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。可以用抓重點的思路來理解這個算法。

新生代對象朝生夕死,對象數量多,只要重點掃描這個區域,那麼就可以大大提高垃圾收集的效率。另外老年代對象存儲久,無需經常掃描老年代,避免掃描導致的開銷。

新生代

在新生代,每次垃圾收集器都發現有大批對象死去,只有少量存活,採用複製算法,只需要付出少量存活對象的複製成本就可以完成收集;可以參看我之前寫的java垃圾回收算法之-coping複製

老年代

而老年代中因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須“標記-清除-壓縮”算法進行回收。參看java垃圾回收算法之-標記_清除壓縮

新創建的對象被分配在新生代,如果對象經過幾次回收後仍然存活,那麼就把這個對象劃分到老年代。

老年代區存放Young區Survivor滿後觸發minor GC後仍然存活的對象,當Eden區滿後會將存活的對象放入Survivor區域,如果Survivor區存不下這些對象,GC收集器就會將這些對象直接存放到Old區中,如果Survivor區中的對象足夠老,也直接存放到Old區中。如果Old區滿了,將會觸發Full GC回收整個堆內存。

JVM參數配置

JVM提供了諸多的參數進行JVM各個方面內存大小的設置,爲Java應用進行優化提供了諸多的工具,本文將會詳細分析各個參數的功能與使用。

常見參數配置

-XX:+PrintGC                  每次觸發GC的時候打印相關日誌

-XX:+UseSerialGC         串行回收

-XX:+PrintGCDetails       更詳細的GC日誌

-Xms                    堆初始值

-Xmx                    堆最大可用值

-Xmn                    新生代堆最大可用值

-XX:SurvivorRatio         用來設置新生代中eden空間和from/to空間的比例.

-XX:NewRatio               配置新生代與老年代佔比 1:2

含以-XX:SurvivorRatio=eden/from=den/to

總結:在實際工作中,我們可以直接將初始的堆大小與最大堆大小相等,

這樣的好處是可以減少程序運行時垃圾回收次數,從而提高效率。

-XX:SurvivorRatio     用來設置新生代中eden空間和from/to空間的比例.

堆內存大小配置

使用示例:  -Xmx20m -Xms5m 

說明: 當下Java應用最大可用內存爲20M, 初始內存爲5M

// byte[] b = new byte[4 * 1024 * 1024];

// System.out.println("分配了4M空間給數組");

System.out.print("最大內存");

System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");

System.out.print("可用內存");

System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");

System.out.print("已經使用內存");

System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");

設置新生代比例參數

使用示例:-Xms20m -Xmx20m -Xmn1m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC

說明:堆內存初始化值20m,堆內存最大值20m,新生代最大值可用1m,eden空間和from/to空間的比例爲2/1

byte[] b = null;

for (int i = 0; i < 10; i++) {

b = new byte[1 * 1024 * 1024];

}

設置新生代與老年代比例參數

使用示例: -Xms20m -Xmx20m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC

-XX:NewRatio=2

說明:堆內存初始化值20m,堆內存最大值20m,新生代最大值可用1m,eden空間和from/to空間的比例爲2/1

新生代和老年代的佔比爲1/2

實戰OutOfMemoryError異常

Java堆溢出

錯誤原因: java.lang.OutOfMemoryError: Java heap space 堆內存溢出

解決辦法:設置堆內存大小 // -Xms1m -Xmx10m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError

// -Xms1m -Xmx10m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError

List<Object> listObject = new ArrayList<>();

for (int i = 0; i < 10; i++) {

System.out.println("i:" + i);

Byte[] bytes = new Byte[1 * 1024 * 1024];

listObject.add(bytes);

}

System.out.println("添加成功...");

虛擬機棧溢出

錯誤原因: java.lang.StackOverflowError  棧內存溢出

棧溢出 產生於遞歸調用,循環遍歷是不會的,但是循環方法裏面產生遞歸調用, 也會發生棧溢出。

解決辦法:設置線程最大調用深度

-Xss5m 設置最大調用深度

public class JvmDemo04 {

 private static int count;

 public static void count(){

       try {

             count++;

             count();

       } catch (Throwable e) {

            System.out.println("最大深度:"+count);

            e.printStackTrace();

       }

 }

 public static void main(String[] args) {

        count();

}

}

內存溢出與內存泄漏區別

Java內存泄漏與內存溢出的關係:使用過的內存空間沒有被及時釋放,且被長時間佔用,沒有及時清理內存垃圾,導致系統無法再給你提供內存資源(內存資源耗盡),最終導致內存空間不足,而出現內存溢出。

而Java內存溢出就是你要求分配的內存超出了系統能給你的,系統不能滿足需求,於是產生溢出,說明存儲空間不夠大。就像倒水倒多了,從杯子上面溢出了來了一樣。

內存泄漏,原理是,使用過的內存空間沒有被及時釋放,長時間佔用內存,最終導致內存空間不足,而出現內存溢出。

垃圾收集器

串行與並行收集器

串行回收: JDK1.5前的默認算法 缺點是隻有一個線程,執行垃圾回收時程序停止的時間比較長

並行回收: 多個線程執行垃圾回收適合於吞吐量的系統,回收時系統會停止運行

serial收集器

串行收集器是最古老,最穩定以及效率高的收集器,可能會產生較長的停頓。新生代、老年代使用串行回收;新生代複製算法、老年代標記-壓縮算法。

一個單線程的收集器,在進行垃圾收集時候,必須暫停其他所有的工作線程直到它收集結束。即垃圾收集的過程中會Stop The World(服務暫停)

特點:CPU利用率最高,停頓時間即用戶等待時間比較長。

適用場景:小型應用

通過JVM參數-XX:+UseSerialGC可以使用串行垃圾回收器。

ParNew收集器

ParNew收集器其實就是Serial收集器的多線程版本。新生代並行,老年代串行;新生代複製算法、老年代標記-壓縮算法

參數控制:-XX:+UseParNewGC  ParNew收集器

-XX:ParallelGCThreads 限制線程數量

parallel 收集器

Parallel Scavenge收集器類似ParNew收集器,Parallel收集器更關注系統的吞吐量。可以通過參數來打開自適應調節策略,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或最大的吞吐量;也可以通過參數控制GC的時間不大於多少毫秒或者比例;新生代複製算法、老年代標記-壓縮

採用多線程來通過掃描並壓縮堆
特點:停頓時間短,回收效率高,對吞吐量要求高。
適用場景:大型應用,科學計算,大規模數據採集等。
通過JVM參數 XX:+USeParNewGC 打開併發標記掃描垃圾回收器。

cms收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。目前很大一部分的Java應用都集中在互聯網站或B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。

從名字(包含“Mark Sweep”)上就可以看出CMS收集器是基於“標記-清除”算法實現的,它的運作過程相對於前面幾種收集器來說要更復雜一些,整個過程分爲4個步驟,包括:

初始標記(CMS initial mark)

併發標記(CMS concurrent mark)

重新標記(CMS remark)

併發清除(CMS concurrent sweep)

其中初始標記、重新標記這兩個步驟仍然需要“Stop The World”。初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快,併發標記階段就是進行GC Roots Tracing的過程,而重新標記階段則是爲了修正併發標記期間,因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短。

由於整個過程中耗時最長的併發標記和併發清除過程中,收集器線程都可以與用戶線程一起工作,所以總體上來說,CMS收集器的內存回收過程是與用戶線程一起併發地執行。老年代收集器(新生代使用ParNew)

優點:併發收集、低停頓

缺點:產生大量空間碎片、併發階段會降低吞吐量

採用“標記-清除”算法實現,使用多線程的算法去掃描堆,對發現未使用的對象進行回收。

(1)初始標記

(2)併發標記

(3)併發預處理

(4)重新標記

(5)併發清除

(6)併發重置

特點:響應時間優先,減少垃圾收集停頓時間

適應場景:大型服務器等。

通過JVM參數 -XX:+UseConcMarkSweepGC設置

g1收集器

在G1中,堆被劃分成許多個連續的區域(region)。採用G1算法進行回收,吸收了CMS收集器特點。

特點:支持很大的堆,高吞吐量

  -- 支持多CPU和垃圾回收線程

  -- 在主線程暫停的情況下,使用並行收集

  -- 在主線程運行的情況下,使用併發收集

實時目標:可配置在N毫秒內最多隻佔用M毫秒的時間進行垃圾回收

通過JVM參數 -XX:+UseG1GC 使用G1垃圾回收器

注意: 併發是指一個處理器同時處理多個任務。 
並行是指多個處理器或者是多核的處理器同時處理多個不同的任務。 
併發是邏輯上的同時發生(simultaneous),而並行是物理上的同時發生。 
來個比喻:併發是一個人同時喫三個饅頭,而並行是三個人同時喫三個饅頭。 

Tomcat配置調優測試

Jmeter壓力測試工具

JMeter是一款在國外非常流行和受歡迎的開源性能測試工具,像LoadRunner 一樣,它也提供了一個利用本地Proxy Server(代理服務器)來錄製生成測試腳本的功能,但是這個功能並不好用。所以在本文中介紹一個更爲常用的方法——使用Badboy錄製生成 JMeter 腳本。

簡單的介紹一下BadboyBadboy是一款不錯的Web自動化測試工具,如果你將它用於非商業用途,或者用於商業用途但是安裝Badboy 的機器數量不超過5臺,你是不需要爲它支付任何費用的。也許是一種推廣策略,Badboy提供了將Web測試腳本直接導出生成JMeter 腳本的功能,並且這個功能非常好用,也非常簡單。你可以跟着下面的試驗步驟來邁出你在開源世界的第一步。

1. 通過Badboy的官方網站下載Badboy的最新版本;

2. 安裝Badboy。安裝過程同一般的Windows 應用程序沒有什麼區別,安裝完成後你可以在桌面和Windows開始菜單中看到相應的快捷方式——如果找不到,可以找一下Badboy安裝目錄下的Badboy.exe 文件,直接雙擊啓動Badboy

3. 啓動Badboy,你可以看到下面的界面。

在地址欄(圖中紅色方框標註的部分)中輸入你需要錄製的Web應用的URL——這裏我們以http://www.yahoo.com 爲例,並點擊GO 按鈕開始錄製。如果你用過LoadRunner之類的商業工具,對於這個操作一定不會陌生吧 ^_^

4. 開始錄製後,你可以直接在Badboy內嵌的瀏覽器(主界面的右側)中對被測應用進行操作,所有的操作都會被記錄在主界面左側的編輯窗口中——在這個試驗中,我們在Yahoo的搜索引擎中輸入 JMeter 進行搜索。不過你將看到,錄製下來的腳本並不是一行行的代碼,而是一個個Web對象——這就有點像LoadRunner的VuGen中的Tree View視圖;

5. 錄製完成後,點擊工具欄中的“停止錄製”按鈕,完成腳本的錄製;

6. 選擇“File -> Export to JMeter”菜單,填寫文件名“login_mantis.jmx”,將錄製好腳本導出爲JMeter腳本格式。也可以選擇“File -> Save”菜單保存爲Badboy腳本;

7. 啓動JMeter並打開剛剛生成的測試腳本。

也許你已經急不可待的準備開始嘗試着用JMeter處理你手頭的工作了^_^ 在下面的幾節,我將繼續爲大家介紹如何在 JMeter 中完成一個測試場景的設置和JMeter測試結果分析入門,以及如何參數化JMeter腳本。

當然,如果你的動手能力很強,幾分鐘你就可以熟悉這些內容。不過還是請允許我一點點由淺入深的來幫大家完成“JMeter從入門到精通”的過程。我相信在這個過程中你將會了解到更多有關性能測試的知識和經驗,甚至包括一些LoadRunner等商業測試工具所無法提供給你的經驗。

什麼是吞吐量

QPS:Queries Per Second意思是“每秒查詢率”,是一臺服務器每秒能夠相應的查詢次數,是對一個特定的查詢服務器在規定時間內所處理流量多少的衡量標準。

測試串行吞吐量

-XX:+PrintGCDetails -Xmx32M -Xms1M

-XX:+HeapDumpOnOutOfMemoryError

-XX:+UseSerialGC

-XX:PermSize=32M

GC 回收次數25次 吞吐量4662

堆的初始值和堆的最大一致

加大初始堆內存大小-Xms1M 修改爲32m

GC 回收次數7次 吞吐量5144

擴大堆的內存

-XX:+PrintGCDetails -Xmx512M -Xms32M

-XX:+HeapDumpOnOutOfMemoryError

-XX:+UseSerialGC

-XX:PermSize=32M

 

GC 回收次數6次 吞吐量5141

結論:垃圾回收次數和設置最大堆內存大小無關,只和初始內存有關係。

初始內存會影響吞吐量。

調整初始堆

-XX:+PrintGCDetails -Xmx512M –Xms512M

-XX:+HeapDumpOnOutOfMemoryError

-XX:+UseSerialGC

-XX:PermSize=32M

GC回收次數0次 吞吐量6561次

結論:堆的初始值和最大堆內存一致,並且初始堆越大就會高。

並行回收(UseParNewGC)

-XX:+PrintGCDetails -Xmx512M -Xms512M

-XX:+HeapDumpOnOutOfMemoryError

-XX:+UseParNewGC

-XX:PermSize=32M

GC回收0次 吞吐量6800

CMS收集器

-XX:+PrintGCDetails -Xmx512M -Xms512M

-XX:+HeapDumpOnOutOfMemoryError

-XX:+UseConcMarkSweepGC

-XX:PermSize=32M

G1回收方式

-XX:+PrintGCDetails -Xmx512M -Xms512M

-XX:+HeapDumpOnOutOfMemoryError

-XX:+UseG1GC

-XX:PermSize=32M

調優總結

初始堆值和最大堆內存內存越大,吞吐量就越高。

最好使用並行收集器,因爲並行收集器速度比串行吞吐量高,速度快。

設置堆內存新生代的比例和老年代的比例最好爲1:2或者1:3

減少GC對老年代的回收。

理解gc日誌

Minor GC和Full GC區別

概念:

新生代 GC(Minor GC):指發生在新生代的垃圾收集動作,因爲 Java 對象大多都具備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快。

老年代 GC(Major GC  / Full GC):指發生在老年代的 GC,出現了 Major GC,經常會伴隨至少一次的 Minor GC(但非絕對的,在 ParallelScavenge 收集器的收集策略裏就有直接進行 Major GC 的策略選擇過程) 。MajorGC 的速度一般會比 Minor GC 慢 10倍以上。

Minor GC觸發機制:

當年輕代滿時就會觸發Minor GC,這裏的年輕代滿指的是Eden代滿,Survivor滿不會引發GCFull GC觸發機制。

當年老代滿時會引發Full GC,Full GC將會同時回收年輕代、年老代,當永久代滿時也會引發Full GC,會導致Class、Method元信息的卸載其Minor。

如下圖所示,虛擬機給每個對象定義了一個對象年齡(Age)計數器。如果對象在 Eden 出生並經過第一次 Minor GC 後仍然存活,並且能被 Survivor 容納的話,將被移動到 Survivor 空間中,並將對象年齡設爲 1。對象在 Survivor 區中每熬過一次 Minor GC,年齡就增加 1 歲,當它的年齡增加到一定程度(默認爲 15 歲)時,就會被晉升到老年代中。對象晉升老年代的年齡閾值,可以通過參數 -XX:MaxTenuringThreshold (閾值)來設置。

JVM的永久代中會發生垃圾回收麼?

垃圾回收不會發生在永久代,如果永久代滿了或者是超過了臨界值,會觸發完全垃圾回收(Full GC)。如果你仔細查看垃圾收集器的輸出信息,就會發現永久代也是被回收的。這就是爲什麼正確的永久代大小對避免Full GC是非常重要的原因。請參考下Java8:從永久代到元數據區

(注:Java8中已經移除了永久代,新加了一個叫做元數據區的native內存區)

對象優先在eden分配

大多數情況下,對象在新生代Eden區中分配。當Eden區沒有足夠的空間進行分配時,虛擬機將發起一次Minor GC。

虛擬機提供了-XX:+PrintGCDetails這個收集器日誌參數,告訴虛擬機在發生垃圾收集行爲時打印內存回收日誌,並且在進程退出的時候輸出當前內存各區域的分配情況。在實際應用中,內存回收日誌一般是打印到文件後通過日誌工具進行分析,不過本實驗的日誌並不多,直接閱讀就能看得很清楚。

public class Test00010 {

  private static final int _1MB = 1024 * 1024;

  // -Xms20m -Xmx20m -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails

  // -XX:+UseSerialGC

  public static void main(String[] args) {

          testAllocation();

  }

 

  public static void testAllocation() {

          byte[] allocation1, allocation2, allocation3, allocation4;

          allocation1 = new byte[2 * _1MB];

          allocation2 = new byte[2 * _1MB];

          allocation3 = new byte[2 * _1MB];

          allocation4 = new byte[4 * _1MB]; // 出現一次Minor GC

  }

}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章