JVM系列之:內存與垃圾回收篇(三)

JVM系列之:內存與垃圾回收篇(三)

##本篇內容概述:
1、執行引擎
2、StringTable
3、垃圾回收

一、執行引擎

##一、執行引擎概述
如果想讓一個java程序運行起來,執行引擎的任務就是將字節碼指令解釋/編譯爲對應平臺上的本地機器指令纔可以。
簡單來說,JVM中的執行引擎充當了將改機語言翻譯爲機器語言的譯者。

##二、執行引擎的工作過程
1)執行引擎在執行的過程中究竟需要執行什麼樣的字節碼指令完全依賴於PC寄存器

2)每當執行完一項操作後,PC寄存器就會更新下一條需要被執行的指令地址

3)當方法在執行的過程中,執行引擎有可能會通過存儲在局部變量表中的
對象引用準確定位到存儲在Java堆區中的對象實例信息,以及通過對象頭
中的元數據指針定位到目標對象的類型信息。

##三、什麼是解釋器,什麼是JIT編譯器?
解釋器:當java虛擬機啓動時會根據預定義的規範對字節碼採用逐行解釋
的方式執行,將每條字節碼文件中的內容翻譯爲對應平臺的本地機器指令
執行。(解釋執行)

JIT編譯器:就是虛擬機將源碼直接編譯成和本地機器平臺相關的語言。(先翻譯好,放在方法區等待執行)


##四、解釋器
JVM設計者初衷僅僅只是單純地爲了滿足java程序實現跨平臺的特性,因
此避免了採用靜態編譯的方式直接生成本地機器指令,從而誕生了實現解
釋器在運行時採用逐行解釋字節碼執行程序的想法。

解釋器真正意義上所承擔的角色是一個運行時翻譯者,將字節碼文件中的
內容翻譯爲對應平臺的本地機器指令執行。

當一條字節碼指令被解釋執行完成後,接着在根據PC寄存器中記錄的下一
條需要被執行的字節碼指令執行解釋操作

##五、JIT編譯器
基於解釋器執行已經淪爲低效的代名詞,爲了解決這個問題,JVM平臺支持一種叫做即時編譯器的技術。即使編譯的目的是避免函數被解釋執行,而是將整個函數體編譯成爲機器碼,每次函數執行時,只執行編譯後的機器碼即可,這種方式可以使執行效率大幅提升。


##六、HotSpot爲何解釋器和JIT編譯器共存?
首先,在程序啓動後,解釋器可以馬上發揮作用,省去編譯的時間,立即執行。
編譯器要想發揮作用,把代碼編譯成本地代碼,需要一定的執行時間。但編譯爲本地代碼後,執行效率高。

所以,當JAVA虛擬機啓動時,解釋器可以首先發揮作用,而不必等待即時
編譯器全部編譯完成後再執行,這樣可以省去許多不必要的編譯時間。隨
着時間的推移,編譯器發揮作用,把越來越多的代碼編譯成本地代碼,獲
得更高的執行效率。

同時,解釋執行在編譯器進行激進優化不成立的時候,作爲編譯器的逃生門。



##七、熱點代碼及探測方式
代碼是否需要啓動JIT編譯器將字節碼直接便以爲對應平臺的本地機器指令則需要根據代碼被調用的頻率而定。哪些需要被編譯爲本地代碼的字節碼被稱爲熱點代碼。

目前HotSpot VM所採用的的熱點探測方式是基於計數器的熱點探測:
方法調用計數器,client模式下是1500次,server模式下是
10000次纔會觸發JIT編譯(不是按調用絕對次數統計的,調用次數
有熱度衰減)。
回邊計數器,統計一個方法中循環體代碼執行的次數,在字節碼中遇到控制流向後跳轉的指令成爲回邊。

##八、設置HotSpot模式
-Xint完全採用解釋器模式執行程序

-Xcomp完全採用JIT編譯器模式執行程序。如果即時編譯器出現問題,解釋器會介入執行。

-Xmixed採用解釋器+即時編譯器的混合模式共同執行程序。



二、StringTable

##一、String的基本特性
·String字符創:實例化方式
  String s1 = "aaa";//字面量的定義方式
  String s2 = new String("bbb");

·String是被聲明爲final的,不可被繼承

·String實現了Serializable接口,表示字符串是支持序列化的   String實現了Comparable接口,表示String可以比較大小

·String在JDK8及之前內部定義了final char[] value用於存儲字符串數據
 String在JDK9改爲了final byte[] value

·String代表不可變的字符序列。簡稱不可變性
>當對字符串重新賦值時,需要重新制定內存區域賦值,不能使用原有的value進行賦值。
>當對現有的字符串進行連接操作時,也需要重新制定內存區域賦值,不能使用原有的value進行賦值。
>當調用String的replace()方法修改指定字符或字符串是,也需要重新指定內存區域賦值,不能使用原有的value進行復制。

·通過字面量的方式(區別於new)給一個字符串賦值,此時的字符串值聲明在字符串常量池中。
【字符串常量池中是不可以放相同的字符串的】

eg:
public class StringExer{
  String str = "good";
  char[] ch = {'t','e','s','t'};
  
  public void change(String str,char ch[]){
    str = "test ok";
    ch[0] = 'b';
  }

  public static void main(String[] args){
    StringExer ex = new String Exer();
    ex.change(str,ch);
    System.out.println(ex.str);//結果是good
    System.out.println(ex.ch);//結果是best
  }
}


##二、StringTable底層Hashtable結構的說明

·字符串常量池中是不會存儲相同內容的字符串的
>String的String Pool是一個固定大小的Hashtable,默認值大小長度是
1009.如果放進String Pool的String非常多,就會造成Hash衝突嚴重,
從而導致鏈表會很長,而鏈表長了之後直接回造成的影響就是當調用
String.intern的性能會大幅下降

>使用-XX:StringTableSize可以設置StringTable的長度

>jdk6中StringTable是固定的,就是1009的長度,所以如果常量池中的字
符串過多就會導致效率下降很快,StringTableSize設置沒有要求

>jdk7中,StringTable的長度默認值爲60013

>jdk8中,StringTabledSize 1009是可設置的最小值


##三、String的內存分配

·在Java中有8中基本數據類型和一種比較特殊的類型String。這些類型爲了使它們在運行過程中速度更快‘更節省內存,都提供了一種常量池的概念

·常量池就類似一個Java系統級別提供的緩存。8中基本數據類型的常量池都是系統協調的,String類型的常量池比較特殊。它的主要使用方法有兩種。
>直接使用""聲明出來的String對象會直接存儲在常量池中
String info = "Hebei";
>如果不是使用""聲明的String對象,可以使用String提供的intern()方法。


·JDK6及以前,字符串常量池存放在永久代
jdk7字符串常量池的位置調整到了java堆中
JDK8永久代變爲了元空間,字符串常量池還在堆中


##四、String的拼接操作

·常量與常量的拼接結果在常量池,原理是編譯期優化
·常量池中不會存在相同內容的常量
·拼接操作中只要其中一個是變量,結果就在堆中(不是常量池中)。
 變量拼接的原理是StringBuilder(相當於新new了一個對象)
·拼接符號左右兩邊都是字符串常量或常量引用,則仍然使用編譯期優化,即非StringBuilder的方式
·如果拼接的結果調用intern()方法,則主動將常量池中還沒有的字符串對象放入池中,並返回此對象地址,有的話則直接返回地址。

說明:
equals判斷兩個變量或者實例指向同一個內存空間的"值"是不是相同
而==是判斷兩個變量或者實例是不是指向同一個內存空間地址

eg:
public void test1(){
  String s1 = "a"+"b"+"c";//也放在常量池中
  String s2 = "abc";//放在字符串常量池中,並將此地址賦值給s2

  System.out.println(s1==s2);//true
  System.out.println(s1.equals(s2));//true
}


public void test2(){
  String s1 = "javaEE";
  String s2 = "hadoop";

  String s3 = "javaEEhadoop";
  String s4 = "javaEE"+"hadoop";
  String s5 = s1 + "hadoop";
  String s6 = "javaEE" + s2;
  String s7 = s1 + s2;

  System.out.println(s3==s4);//true
  System.out.println(s3==s5);//false s1爲變量,結果在堆

  //如果拼接符號的前後出現了變量
  //相當於在堆空間中new String()
  //具體的內容爲拼接的結果
  System.out.println(s3==s6);//false 結果在堆
  System.out.println(s3==s7);//false 結果在堆
  System.out.println(s5==s6);//false 結果在堆
  System.out.println(s5==s7);//false 結果在堆
  System.out.println(s6==s7);//false 結果在堆

  //intern():判斷字符串常量池中費否存在javaEEhadoop
  //如果存在,則返回常量池中JavaEEHadoop的地址
  //如果字符串常量池中不存在JavaEEHadoop,則在常量池中加載一份
  //並返回此對象的地址
  String s8 = s6.intern();
  System.out.println(s3==s8);//true  結果在字符串常量池中
}


public void test3(){
  String s1 = "a";
  String s2 = "b";
  String s3 = "ab";

  //先StringBuilder s = new StringBuilder()
  //然後 s.append("a")
  //然後 s.append("b")
  //然後 s.toString() -->  約等於new String("ab")
  //注:jdk5之後使用StringBuilder,jdk5之前使用StringBuffer
  String s4 = s1 + s2;//
  System.out.println(s3==s4);//false s4結果在堆
}


public void test4(){
  final String s1 = "a";//常量
  final String s2 = "b";//常量
  String s3 = "ab";
  String s4 = s1 + s2;
  System.out.println(s3==s4);//true
}


##五、String拼接和StringBuilder append的效率對比

string拼接:
for(int i=0;i<10000;i++){
  s = s + "a"; //每次循環都會創建一個StringBuilder,然後再轉成String   }
                          
StringBuilder append:
for(int i=0;i<10000;i++){
  s.append("a");                        
}

結論:通過StringBuilder的append()的方式拼接字符串的效率遠高於String的字符串拼接方式。
①、StringBuilder自始至終只創建了一個StringBuilder的對象
②、使用String拼接方式,每次for循環都會創建一次StringBuilder和String對象,因此內存佔用也更大,GC還需要花費額外的時間。

StringBuilder在實際開發中,如果基本確定字符串的總長度不會高於highlevel的情
況下,也減一使用構造器new StringBuilder(highLevel)//指定長度,防止在拼接
的過程中StringBuilder擴容(擴容的過程中原有的StringBuilder會廢棄然後重新創
建StringBuilder)。


##六、intern()的使用
如果不是""聲明的String對象,可以使用String提供的intern方法:
intern方法會從字符串常量池中查詢當前字符串是否存在,若不存在就會將當前字符串放
入常量池中,然後返回地址;若存在則直接返回地址。
eg:String info = new String("i love you").intern();

也就是說,如果任意字符串上調用String.intern()方法,name其返回結果所指向的那
個實力,必須和直接以常量形式出現的字符串實力完全相擁。因此下列表達式的值必定是true:
eg: ("a"+"b"+"c").intern() == "abc";

通俗點將,intern就是確保字符串在內存裏只有一份拷貝,這樣可以節約內存空間,加快
字符串操作任務的執行速度。
注意:這個值會被存放子啊字符串常量池中。


##七、new String("ab")創建了幾個對象?
 String str = new String("ab")
 創建了兩個對象,一個new關鍵字在堆空間中創建的,
 然後在字符串常量池中創建了一個ab
 
 String str = new String("a")+new String("b");
 創建了3個對象,
 對象1:new StringBuilder(),用以拼接
 對象2: new String("a")
 對象3:字符串常量池中 a
 對象4 new String("b")
 對象5 字符串常量池中b
 [stringbuilder的tostring方法是沒有在字符串常量池中創建ab的]

eg:
public static void main(String[] args) {
    String s1 = new String("77");//s1指向堆中的地址,且常量池中有77
    s1.intern();//指向了常量池中已有的77,但沒有把 這個引用賦值給s1,它是不同於s1=s1.intern()的
    String s2 = new String("77").intern();//s2指向了s1在常量池中創建的77的地址
    String s3 = "77";//s3也指向了s1在常量池中創建的77的地址
    System.out.println(s1==s2);//false
    System.out.println(s1==s3);//false
    System.out.println(s2==s3);//true

    String s4 = new String("1")+new String("2");//s4指向了自己在堆中的地址,且在常量池中創建了1和2,但沒有創建12
    s4.intern();//在常量池中創建了77,常量池中的77指向了s4在堆中的地址
    String s5 = "12";//s5指向了常量池中77,相當於指向了s4的堆中的引用
    System.out.println(s4==s5);//true

    String s6 = new String("5")+new String("6");//s6指向了自己在堆中的地址,且在常量池中創建了5和6,但沒有創建56
    String s7 = "56";//在常量池中創建了56
    String s8 = s6.intern();//intern由於56已在常量池中創建,因此s8指向了s7在常量池中創建的56
    System.out.println(s6==s7);//false
    System.out.println(s6==s8);//false
    System.out.println(s7==s8);//true
}

intern()總結:
·jdk1.6:
>如果串池中有,則不會放入。返回已有的串池中的對象的地址。
>如果串池沒有,則會把此對象複製一份,放入串池,並返回串池中的對象地址。
·jdk1.7之後:
>如果串池中有,則不會放入。返回已有的串池中的對象的地址。
>如果串池沒有,則會把此對象的引用地址複製一份,放入串池,並返回串池中的引用地址


[String str = new String("abc");]
[以上動作常量池中會存在abc,可以拆解開理解:常量池中創建abc,在堆中new一個String("abc"),棧中會保存str,並將堆中的地址賦值給str]

三、垃圾回收

1、垃圾回收概述

##一、什麼是垃圾?
·在運行程序中沒有任何指針指向的對象,這個對象就是需要被回收的垃圾

##二、爲什麼需要GC?
如果不及時對內存中的垃圾進行清理,那麼,這些垃圾對象所佔的內存空間會一致保留到應
用程序結束,被保留的空間無法被其它對象使用。甚至可能導致內存溢出。

##三、內存溢出和內存泄漏

·指程序申請內存時,沒有足夠的內存供申請者使用,或者說,給了你一塊存儲int類型數據
的存儲空間,但是你卻存儲long類型的數據,那麼結果就是內存不夠用,此時就會報錯OOM,
即所謂的內存溢出。

·是指程序在申請內存後,無法釋放已申請的內存空間,一次內存泄漏似乎不會有大的影響,
但內存泄漏堆積後的後果就是內存溢出。

【GC的主要作用區域:方法區+堆】
【Java堆是GC的工作重點】
【頻繁回收 新生代,較少回收 老年代,基本不動 元空間或永久代】

2、垃圾回收相關算法(重)

2.1、垃圾標記階段的算法:

##1、垃圾標記階段:對象存活判斷
·在堆裏存放着幾乎所有的java對象實例,在GC執行垃圾回收之前,首先需要區分出內存中哪些是存活對
象,哪些是已經死亡的對象。只有被標記爲已經死亡的對象,GC纔會在執行垃圾回收時,釋放掉其所佔用
的內存空間,因此這個過程我們可以成爲垃圾標記階段。

·在JVM中如何標記一個死亡對象?簡單來說,當一個對象已經不再被任何的存活對象繼續引用時,就可以
宣判爲已經死亡。

·判斷對象存活一般有兩種方式:【"引用計數算法"】 和 【"可達性分析算法"】



##2、引用計數算法Refrence Counting--->HotPot沒有使用
·引用計數算法,爲每個對象保存一個整型的"引用計數器"屬性們,用於記錄對象被引用的情況。

·對於一個對象A,只要有任何一個對象引用了A,則A的引用計數器就加1;當引用失效時,引用計數器就
減1.只要對象A的引用計數器的值爲0,即表示對象A不可能再被使用,可進行回收。

·優點:實現簡單,垃圾對象便於辨識;判定效率高,回收沒有延遲性
·缺點:
>需要單獨的字段存儲計數器,這樣的做法增加了存儲空間的開銷
>每次賦值都需要更新計數器,伴隨着加法和減法操作,增加了時間開銷
>引用計數器有一個嚴重的問題,即 "無法處理循環引用" 的情況。這是一條致命的缺陷,導致在java的垃圾回收器中沒有使用這類算法。

##3、可達性分析算法(也叫根搜索算法、追蹤性垃圾收集)

·相對於引用計數算法而言,可達性分析算法不僅同樣具備實現簡單和執行高效等特點,更重要的是該算法可以有效地解決在引用計數算法中循環引用的問題,防止內存泄漏的發生。

·可達性分析被java c#選擇。這種類型的垃圾收集也叫做"追蹤性垃圾收集"

·所謂GC Roots跟集合就是一組必須活躍的引用

·基本思路:
>可達性分析算法是以根對象集合(GC Roots)爲起始點,按照從上至下的方式搜索被根對象集合所連接的目標對象是否可達。

>使用可達性分析算法後,內存中的存活對象都會被根對象集合直接或間接連接着,搜索鎖走過的路徑被稱爲引用鏈(Refrence Chain)

>如果目標對象沒有被任何引用鏈相連,則是不可達的,就意味着該對象已經死亡,可以標記爲垃圾對象

>在可達性分析算法中,只有能夠被根對象集合直接或間接連接的對象纔是存活對象

·在java中,GC Roots包括以下幾類元素:
>虛擬機棧中的引用對象
	eg:各個線程被調用的方法中使用到的參數、局部變量
>本地方法棧內JNI引用的對象
>方法區中類靜態屬性引用的對象
	eg:Java類的引用類型靜態變量
>方法區中常量引用的對象
	eg:字符串常量池裏的引用
>所有被同步鎖synchronized持有的對象
>Java虛擬機內部的引用
	eg:基本數據類型對象的class對象,一些常駐的異常對象,系統類加載器
>反映java虛擬機內部情況的JMXBean、JVMTI中註冊的回調、本地代碼緩存等

缺點:
·如果使用可達性分析算法判斷內存是否回收,需要分析工作必須在一個能保障一致性的快照中進行。這點不滿足的話分析結果的準確性就無法保證,這也是GC時必須"STW(stop the world)"的一個重要原因。

2.2、對象的finalization機制

·Java語言提供了對象終止(finalization)機制來允許開發人員提供對象被銷燬前的自定義處理邏輯

·當垃圾回收器發現沒有引用指向一個對象,即:垃圾回收此對象前,總會先調用這個對象的finalize()方法

·finalize()方法允許在子類中被重寫,用於在對象被回收時進行資源釋放,通常在這個方法中進行一些資源
釋放和清理工作,比如關閉文件、套接字和數據庫連接等

·永遠不要主動的調用某個對象的finalize()方法,應交給垃圾回收機制調用。
原因如下:
>在finalize()是可能會導致對象復活
>finalize()方法的執行時間是沒有保障的,它完全由GC線程決定,極端情況下,若不發生GC,則finalize()方法沒有執行幾回
>一個糟糕的finalize()會嚴重影響GC的性能

·由於finalize()方法的存在,虛擬機中的對象一般處於三種可能的狀態:如果從根節點都無法訪問到某個對
象,說明對象已經不再使用了。一般來說此對象需要被回收。但事實上,也並非是非死不可的,這時候它們暫時
處於緩刑階段。一個無法觸及的對象可能在某一個條件下復活自己,如果這樣,那麼對它的回收就是不合理的,
爲此,定義虛擬機中的對象可能的三種狀態如下:
>可觸及的:從根節點開始,可以到達這個對象
>可復活的:對象的所有引用都被釋放,但是對象有可能在finalize()中復活
>不可觸及的:對象的finalize()被調用,並且沒有復活,那麼就會進入不可觸及狀態。不可觸及的對象不可
能被複活,因爲finalize()只能被調用一次,類似servlet的destory()。

以上三種狀態中,是由於finalize()方法的存在,進行的區分。只有在對象不可觸及時纔可以被回收。



##判定一個對象ObjA是否可回收的具體過程:
·如果對象ObjA到GC Roots沒有引用鏈,則進行第一次標記

·進行篩選,判斷次對象是否有必要執行finalize()方法:
>如果obja沒有重寫finalize()方法,或者finalize()方法已經被虛擬機調用過,則虛擬機視爲沒有必要執
行,obja被判定爲不可觸及的
>如果對象obja重寫了finalize()方法,且還未執行過,那麼obja會被插入到F-Queue隊列中沒有一個虛擬
機自動創建的、低優先級的finalizer線程觸發器finalize()方法執行
>finalize()方法時對象逃脫死亡的最後機會,稍後GC會對F-Queue隊列中的對象進行第二次標記。如果
obja在finalize()方法中與引用鏈上的任何一個對象建立了聯繫,那麼在第二次標記時,obja會被溢出即將
回收集合。之後,對象會再次出現沒有引用存在的情況。這個情況下finalize方法不會被再次調用,對象會直
接編程不可觸及的狀態,也就是說,一個對象的finalize方法只會被調用一次

2.3、MAT與JProfiler的GC Roots溯源

MAT下載地址 JProfiler下載地址

MAT(memory analyzer)是一款java堆內存分析器,用於查找內存泄漏以及查看內存消耗情況
[MAT是eclipse開發的,免費性能分析工具]


##獲取dump文件
>方式一:命令行使用jmap
>方式二:使用JVisualVM生成
JVisualVM -> 監視 -> 右側堆dump -> 左側樹右擊保存

##MAT打開dump



##JProfiler也可以在idea中安裝插件使用

2.4、垃圾清除階段的算法

##1、垃圾清除

當成功區分出內存中存活對象和死亡對象後,GC接下來的任務就是執行垃圾回收,釋放掉無用對象所佔用的內存
空間,以便有足夠的可用內存空間爲新對象分配內存。

目前JVM中比較常見的三種垃圾清除算法是:
>標記-清除算法(mark-sweep)
>複製算法
>標記-壓縮算法


##2、標記-清除算法(mark-sweep)
執行過程:
·當堆中的有效內存空間被耗盡的時候,就會停止整個程序(STW),然後進行兩項工作,第一項是標記,第二項是清除。
>標記:collector從引用根節點開始遍歷,標記所有被引用的對象。一般是在對象的header中記錄爲可達對象
>清除:collector對堆內存從頭到尾進行現行遍歷,如果發現某個對象在其Header中沒有標記爲
可達對象,則將其回收(內存空間碎片化了),並將其地址記錄在空閒列表中以便之後使用

缺點:
>效率不高(兩次全遍歷)
>在進行GC時,需要停止整個應用程序(STW),用戶體驗差
>這種方式清理出來的空閒內存是不連續的,產生內存碎片,需要維護一個空閒列表(記錄哪個位置是空閒的可以存放對象)

注意:
這裏所謂的清除並不是真的置空,而是把需要清除的對象地址保存在空閒的地址列表裏。下次有新
對象需要加載時,判斷垃圾的位置空間是否夠,如果夠,就存放。【其實是覆蓋】


##3、複製算法(copying)--->適用於新生代
核心思想:
·將活着的內存空間分爲兩塊,每次只使用其中一塊,在垃圾回收時將正在使用的內存中的存活對象
複製到未被使用的內存塊中,之後清除正在使用的內存塊中的所有對象,交換兩個內存的角色,最
後完成垃圾回收。【類似堆區新生代中的Eden區和survivor0區和survivor1區】

優點:
>沒有標記和清除過程,實現簡單,運行高效
>複製過去以後保證空間的連續性,不會出現內存碎片問題

缺點:
>此算法可用內存空間被砍半了
>對於G1這種分拆成爲大量region的gc,複製而不是移動,意味着GC需要維護region之間對象引
用關係,不管是內存佔用或者時間開銷也不小.(移動後 棧對堆中對象的引用地址就會發生變化需要重新設置)

應用場景:
·新生代中的對象大部分是朝生夕死的,回收的性價比高,所以現在的商業虛擬機都是用這種收集算法回收新生代。


##4、標記-壓縮算法(mark-compact)--->適用於老年代
執行過程:
·第一階段和標記-清除算法一樣,從根節點開始標記所有被引用對象
·第二階段將所有的存活對象壓縮到內存的一段,按順序排放
·之後清理邊界外所有空間(整理內存,防止了內存碎片)


標記壓縮算法的最終效果等同於標記-清除算法執行完後,再進行一次內存碎片整理。
二者本質差異在於標記-清除算法是一種非移動式的回收算法,標記-壓縮算法是移動式的。
可以看到,標記存活對象將會被整理,按照內存地址一次排列,而未被標記的內存會被清理掉。如
此一來,當我們需要給新對象分配內存是,JVM只需要持有一個內存的起始地址即可,這比維護一個
空閒列表顯然少了許多開銷。
【同時堆中 可觸及的存活對象地址的移動,需要修改引用者的引用地址】

優點:
>解決了標記-清除算法中內存碎片和JVM必須維護一個可用空閒列表的缺點,此時JVM只需維護一個內存起始地址即可。
>消除了複製算法中內存減半的高額代價

缺點:
>效率上要低於複製算法
>移動對象的同時,還需要調整引用者引用的地址
>移動過程中,需要STW全程暫停用戶應用程序



##5、三種算法的對比
複製算法在效率上講是最快的,但是浪費了一半內存

標記-整理算法效率最低,比複製算法多了一個標記階段,比標記-清除多了個整理內存的階段,但卻有效減少了內存碎片問題。

整體上說:複製算法適用於新生代,標記-清理和標記-整理算法適用於老年代

總結:
·目前幾乎所有的GC都是採用 "分帶收集算法"  執行垃圾回收的。

·新生代:區域相對小,對象生命週期短、存活率低、揮手頻繁,這種情況下適用於複製算法
·老年代:區域較大,對象生命週期長、存活率高,回收不及新生代頻繁,一般由標記-清除或者標記-整理算法的混合實現


##6、增量收集算法
上述算法,在垃圾回收過程中,應用軟件將處於STW狀態,在STW狀態下,應用程序所有線程都會掛起,暫
停一切正常工作。等待垃圾的回收完成,嚴重影響了用戶體驗和系統的穩定性。

·基本思想
如果一次性將所有的垃圾進行處理,需要造成系統長時間的停頓,那麼就可以讓垃圾收集線程和應用程序線
程交替執行。每次,垃圾收集線程只收集一小片區域的內存空間,接着切換到應用程序線程。依次反覆,直
到垃圾收集完成。

總的來說,增量收集算法的基礎仍是傳統的標記-清除算法和複製算法。增量收集算法通過對線程間衝突的
妥善處理,允許垃圾收集線程以分段的方式完成標記、清理或複製工作。

缺點:
使用這種方式,由於在垃圾回收過程中,間斷性地還執行了應用程序代碼,所以能減少系統的停頓時間。
但是,因爲線程切換和上下文轉換的消耗,會是的垃圾回收的總體成本上升,造成系統吞吐量的下降


##7、分區算法
一般來說,相同條件下,堆空間越大,一次GC時所需要的時間越長,有關GC產生的停頓也就越長。
爲了更好滴控制GC產生的停頓時間,將一塊大的內存區域分割成多個小塊,根據目標的停頓時間,每次合理地回收若干個小區間,而不是整個堆空間,從而減少一次GC所產生的的停頓。

分代算法將按照對象的生命週期長短劃分成兩個部分(新生代和老年代),分區算法將整個堆空間劃分成連續的不同小區間。

每個小區間都是獨立使用,獨立回收。這種算法的好處是可以控制一次回收多少個小區間。

3、垃圾回收相關概念

3.1、System.GC

·默認情況下,通過System.gc()或者Runtime.getRuntime().gc()的調用,會顯式觸發Full GC,同時對老年代和新生代進行回收,嘗試釋放被丟棄對象佔用的內存

·然而System.gc()調用"無法保證對垃圾收集器的調用"

·JVM實現者可以通過System.gc()調用來決定JVM的GC行爲。而一般情況下,垃圾回收
應該是自動進行的,無需手動觸發,否則就太過於麻煩了。在一些特殊情況下,如我們正在
編寫一個性能基準,我們可以在運行期間調用System.gc()

·System.runFinalization()強制調用使用引用的對象的finalize()方法
(垃圾回收此對象前,總會先調用這個對象的finalize()方法)

3.2、內存溢出與內存泄漏

##一、內存溢出OOM:

·內存溢出是引發程序崩潰的罪魁禍首之一

·由於GC一直在發展,所有一般情況下,除非應用程序佔用的內存增長速度非常快,造成垃圾
回收已經跟不上內存消耗的速度,否則不太容易出現OOM的情況

·大多數情況下,GC會進行各種年齡段的垃圾回收,實現不行了就來一次獨佔式的Full GC操
作,這時候會回收大量的內存,供應用程序繼續使用

·Javadoc中對OOM的解釋是:沒有空閒內存,並且垃圾收集器也無法提供更多內存

·沒有空閒內存的情況:說明JVM的堆內存不夠,原因有二:
>Java虛擬機的堆內存設置不夠
比如:可能存在內存泄漏問題;也很有可能就是堆的大小不合理,比如我們要處理比較可觀的
數據量,但是沒有顯示指定JVM堆大小或者指定數值偏小。我們可以通過參數-Xms\-Xmx來
調整
>代碼中創建了大量大對象,並且長時間不能被垃圾收集器收集(存在被引用)
java.lang.OutOfMemoryError:PermGen space
java.lang.OutOfMemoryError:Metaspace

·在拋出OOM之前,通常垃圾收集器會被觸發,盡其所能去清理出空間


##二、內存泄漏Memory Leak:
·內存泄漏也可以稱作 "存儲滲漏"。嚴格來說,只有對象不會再被程序用到了,但是GC又不
能回收它們的情況,才叫內存泄漏。

·但實際情況很多時候一些不太好的實踐會導致對象的生命週期變得很長甚至導致OOM,也可
以叫做寬泛意義上的內存泄漏

·儘管內存泄漏並不會立刻引起程序崩潰,但是一旦發生內存泄漏,程序中的可用內存就會被
逐步蠶食,直至耗盡所有內存,最終出現OOM異常,導致程序崩潰。

·注意,這裏的存儲空間並不是指物理內存,而是指虛擬內存大小,這個虛擬內存大小取決於
磁盤交換區設定的大小

eg:
1、單例模式:單例的生命週期和應用程序是一樣長的,所以單例程序中,如果持有對象外部
對象的引用的話,那麼這個外部對象是不能被回收的,則會導致內存泄漏的產生
2、一些提供close的資源未關閉導致的內存泄漏
如:datasource.getConnetction()、網絡連接socket和IO連接必須手動close,否
則是不能被回收的

3.3、STW

·Stop The World(STW),指的是GC事件發生過程中,會產生應用程序的停頓。停頓產生時整個應用程序線程都會被暫停,沒有任何響應,這個停頓稱爲STW

·被STW中斷的應用程序線程會在完成GC之後恢復.

·STW事件和曹勇哪款GC無關,所有的GC都有這個事件

·STW是JVM在後臺自動發起和自動完成的。在用戶不可見的情況下,把用戶正常的工作線程全部停掉

·開發中不要使用System.gc()會導致STW的發生

(JVM在遍歷GC Roots的時候需要在一個能確保一致性的快照中進行,所以會暫停一切用戶程序從而產生STW)

3.4、垃圾回收的併發與並行

##一、併發Concurrent
·在操作系統中,是指一個時間段中有幾個程序都處於已啓動運行到運行完畢之間,且這幾個
程序都在同一個處理器上運行

·併發並不是真真意義上的"同時進行",只是CPU把一個時間段劃分成幾個時間片段,然後在
這幾個時間區間之間來回切換,由於CPU處理速度非常快,只要時間間隔處理得當,即可讓用
戶感覺是多個應用程序同時在進行

##二、並行Parallel
·當系統有一個以上CPU時,當一個CPU執行一個進程時,另一個CPU可以執行另一個進程,兩
個進程互不搶佔CPU資源,可以同時進行,我們稱之爲並行

·其實決定並行的因素不是CPU的數量,而是CPU的核心數量,比如一個CPU多個核也可以並行

·適合科學計算,後臺處理等弱交互場景

##三、併發與並行對比

併發,指的是多個事情,在同一時間段內同時發生
並行,指的是多個事情,在同一時間點上同時發生

併發的多個任務之間是互相搶佔資源的
並行的多個任務之間是不互相搶佔資源的

只有在多CPU或者一個CPU多核的情況下,纔會發生並行。
否則看似同時發生的事情,其實都是併發執行的


##四、垃圾回收中的併發與並行

·並行:指多條垃圾收集線程並行工作,但此時用戶線程仍處於等待狀態
>如ParNew、Parallel Scavenge、Parallel Old


·併發:指用戶線程與垃圾收集線程同時執行,垃圾回收線程在執行時不會停頓用戶程序的運
行(不一定是並行的,可能會交替執行)
>用戶程序在繼續運行,二垃圾收集程序線程運行於另一個CPU上
>如CMS、G1


·串行
>相較於並行的概念,單線程執行
>如果內存不夠,則程序暫停,啓動JVM垃圾回收器進行垃圾回收,回收完,再啓動程序的線程


3.5、安全點與安全區域

##一、安全點safe point
·程序執行時並非在所有地方都能停頓下來開始GC,只有在特定的位置才能停頓下來開始GC,
這些位置稱爲"安全點safepoint"

·safe point的選擇很重要,如果太少可能導致GC等待的時間太長,如果太頻繁可能導致運
行時的性能問題。大部分指令的執行時間都非常短暫,通常會根據"是否具有讓程序長時間執
行的特徵"爲標準。比如:選擇一些執行時間較長的指令作爲safe point,如方法調用、循
環跳轉和異常跳轉等

·如何在GC發生時,檢查所有線程都跑到最近的安全點停頓下來?
>搶先式中斷(目前沒有虛擬機採用了)
首先中斷所有線程,如果線程不再安全點,就恢復線程,讓線程跑到安全點
>主動式中斷
設置一箇中斷標誌,各個線程運行到safe point的時候主動輪訓這個標誌,如果中斷標誌爲真,則將自己進行中斷掛起

##二、安全區域safe ragion
·safepoint機制保證了程序執行時,在不太長的時間內就會遇到可進入GC的safepoint。
但是程序不執行的時候呢?例如線程處於sleep狀態或blocked狀態,這時候線程無法響應
jvm的中斷請求,走到安全點去中斷掛起,JVM也不太可能等待線程被喚醒。杜宇這種情況,
就需要安全區域來解決

·安全區域是指在一段代碼片段中,對象的引用關係不會發生變化,在這個區域中的任何位置
開始GC都是安全的。我們也可以吧safe region看作是被擴展了的safepoint

·實際執行時:
>當線程運行到safe region的代碼時,首先標識已經進入了safe region,如果這段時間
內發生GC,JVM會忽略標識爲Safe Region狀態的線程

>當線程即將離開safe region時,會檢查JVM是否已經完成GC,如果完成了,則繼續運行,否則線程必須等待直到收到可以安全離開safe region的信號爲止


3.6、強引用、軟引用、弱引用、虛引用、終結器引用(重)

我們希望能描述這樣一類對象:當內存空間還足夠時,則能保留在內存中;如果內存空間在進
行垃圾收集後還是很緊張,則可以拋棄這些對象。

java對引用的概念進行了擴充:將引用分爲強引用strong reference、軟引用soft 
reference、弱引用weak reference和虛引用phantom reference四種,這四種引用
強度一次逐漸減弱。

除了強引用外,其它3中引用可以在java.lang.ref包中找到,開發人員可以在應用程序中
直接使用它們。

java.lang.ref中只有終結器引用時包內可見的,其它均爲public,可以在應用程序中直接使用

##一、概念:
>強引用:最傳統的引用定義,是指在代碼中普遍存在的引用賦值,即類似Object obj = new Object()這種引用關係。
無論任何情況下,只要強引用關係還存在,垃圾收集器就永遠不會回收掉被引用的對象
【只要關係存在,無論內存是否充足都不能被回收】

>軟引用:在系統將要發生內存溢出之前,將會把這些對象列入回收範圍之中進行第二次回
收。如果這次回收後還沒有足夠的內存,纔會拋出OOM
【即使關係存在,只要內存不足還是要被回收】

>弱引用:被弱引用關聯的對象只能生存到下一次垃圾收集之前,當垃圾收集器工作時,無論
內存空間是否足夠,都會回收掉被弱引用關聯的對象
【只要垃圾收集,無論關係是否存在,也無論內存是否充足都要被回收】

>虛引用:一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引
用來獲得一個對象的實例。爲一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器
回收時收到一個系統通知
【虛引用對垃圾收集沒影響,只是做對象回收跟蹤的,隨時可能被垃圾回收器回收】

##二、強引用strong reference:不回收
·當java中使用new操作符創建一個新對象,並將其賦值給一個變量的時候,這個變量就成爲指向該對象的一個強引用

·只要強引用的對象時刻觸及的,垃圾收集器就永遠不會回收掉被引用的對象

·對於一個普通對象,如果沒有其它的引用關係,只要超過了引用的作用域或者顯式地將響應引用賦值爲null,就可以當做垃圾被收集了,當然具體回收時機還是要看垃圾收集策略。

·強引用時造成java內存泄漏的主要元兇之一

eg:
StringBuffer s1 = new StringBuffer("yyyyy");
StringBuffer s2 = s1;


##二、軟引用soft reference:內存不足即回收
·軟引用是用來描述一些還有用,但非必須的對象。只被軟引用關聯着的對象在系統將要發生內存溢出前,會把這些對象列進回收範圍之中進行第二次回收,如果這次回收還沒足夠的內存,纔會拋出內存溢出異常。
【第一次回收不可觸及的對象,第二次回收軟引用】

·軟引用通常用來實現內存敏感的緩存。
比如:高速緩存就有用到軟引用,如果還有空閒內存就可以暫時保留緩存,當內存不足時清理掉,這樣就保證了使用緩存的同事,不會耗盡內存

·垃圾回收器在某個時候決定回收軟可達的對象的時候,會清理軟引用,並可選地把引用存放到一個引用隊列reference queue

eg:
User user = new User(1,"lee","male");//聲明一個強引用
SoftReference<User> sf = new SoftReference<User>(user);
user = null;//銷燬強引用
//此時user對應的new User就是一個軟引用
    
或
SoftReference<User> sr = new SoftReference<User>(new User(1,"lee","male"));
    
    
##三、弱引用weak reference:發現即回收
·弱引用也是用來描述那些非必須對象,被弱引用關聯的對象只能生存島下一次垃圾收集發生
爲止。在系統GC時,只要發現弱引用,不管系統堆空間使用是否充足,都會回收掉只被弱引用
關聯的對象。
    
·但是,由於垃圾回收器的線程通常優先級很低,因此,並不一定能很快地發現持有弱引用的
對象。在這種情況下,弱引用對象可以存在較長時間。

eg:
WeakReference<User> wr = new WeakReference<User>(new User(1,"lee","male"));
   
WeakHashMap就是實現了WeakReference    
    
##四、虛引用phantom reference:對象回收跟蹤
·一個對象是否有虛引用的存在,完全不會決定對象的生命週期。如果一個對象僅持有虛引用,
那麼它和沒有引用幾乎是一樣的,隨時可能被垃圾回收器回收
    
·它不能單獨使用,也無法通過虛引用來獲取被引用的對象。當試圖通過虛引用的get()方法取
得對象時,總是NULL
    
·爲一個對象設置虛引用關聯的唯一目的在於跟蹤垃圾回收過程。比如:能在這個對象被收集器
回收時收到一個系統通知
    
·由於虛引用可以跟蹤對象的回收時間,因此,也可以將一些資源釋放操作放置在虛引用中執行和記錄
    
eg:
ReferenceQueue rQueue = new ReferenceQueue();
PhantomReference<User> pr = new PhantomReference<User>(new User(1,"Lee","male"),rQueue);


##五、終結器引用Final Reference:包內可見
·它可以實現對象的finalize()方法,也可以成爲終結器引用

·無需手動編碼,其內部配合引用隊列使用

·在GC是,終結器引用入隊,有Finalizer線程通過終結器引用找到被引用對象並調用它的
finalize()方法,第二次GC時才能回首被引用的對象

4、垃圾回收器

4.1、GC分類與性能指標

##一、垃圾回收分類
1、按照線程數分:(垃圾回收的線程數)

·"串行垃圾回收器" 和 "並行垃圾回收器"

·串行垃圾回收器指的是在同一時段內只允許一個CPU用於執行垃圾回收操作,此時工作線程被暫停,直至垃圾收集工作結束。

·並行收集可以運用多個CPU同時執行垃圾回收,因此提升了應用的吞吐量,不過並行回收仍然和串行回收一樣,採用獨佔式,使用STW機制

2、按照工作模式分

·併發式的垃圾回收器 和 獨佔式的垃圾回收器

·併發式垃圾回收器與應用程序線程交替工作,以儘可能減少應用程序的停頓時間

·獨佔式垃圾回收器一旦運行,就停止應用程序中的所有用戶線程,直到垃圾回收過程完全結束

3、按照碎片處理方式分

·壓縮式垃圾回收器  和  非壓縮式垃圾回收器

·壓縮式垃圾回收器會在回收完成後,對存活對象進行壓縮整理,消除回收後的碎片

·非壓縮式的垃圾回收器不進行這不操作(需額外維護空閒列表)

4、按工作內存區分

·新生代垃圾回收器 和 老年代垃圾回收器



##二、評估GC的性能指標

·吞吐量:運行用戶代碼的時間佔總運行時間的比例(主要)
>運行總時間 = 程序的運行時間 + 內存回收的時間

·暫停時間:執行垃圾收集時,程序的工作線程被暫停的時間(主要)

·內存佔用:Java堆區所佔的內存大小
>內存小GC頻繁,內存大GC的暫停時間會增大

·垃圾收集開銷:吞吐量的補數,垃圾收集所用時間與總運行時間的比例

·收集頻率:相對於應用程序的執行,收集操作發生的頻率

·快速:一個對象從誕生到被回收所經歷的時間


>高吞吐量 和 低暫停時間 是相互矛盾的
>如果選擇吞吐量優先,那麼必然需要降低內存回收的執行頻率,但這樣會導致GC需要更長的暫
停時間來執行內存回收
>如果選擇低延遲優先的原則,那麼爲了降低每次執行內存回收時的暫停時間,只能頻繁地執行
內存回收,但這又引起了新生代內存的所見和導致程序吞吐量的下降

現在標準:在最大吞吐量優先的情況下,降低停頓時間

4.2、垃圾收集器的發展 和 經典的垃圾收集器

垃圾回收:Garbage Collection
垃圾回收器:Garbage Collector

##一、垃圾回收器發展歷史
·JDK1.3: 串行方式Serial GC,它是第一款GC,ParNew垃圾收集器是Serial收集器的多線程版本

·JDK1.4: Parallel GC 和 Concurrent Mark Sweep GC (CMS)

·JDK6: Parallel GC成爲HotSpot默認GC

·JDK7: G1可用

·JDK9:G1成爲默認的垃圾收集器,以替代CMS

·JDK11: 引入Epsilon和ZGC

·JDK12: 引入Shenadoah GC

·JDK13: 增強ZGC

·JDK14: 刪除CMS,擴展ZGC

##二、7款經典的垃圾回收器

·串行回收器: Serial GC  和  Serial Old
·並行回收器: ParNew 和 Parallel Scavenge 和 Parallel Old
·併發回收器: CMS 和 G1

##三、7款經典收集器 與 垃圾分代之間的關係

·新生代收集器:Serial 和 ParNew 和  Parallel Scavenge
·老年代收集器:Serial Old 和 Prallel Old 和 CMS
·整堆收集器:G1

·爲什麼要有這麼多垃圾收集器,因爲java的使用場景不同,如移動端,服務器等。針對不用的
場景,提供不同的垃圾收集器,提高垃圾收集的性能

4.3、如何查看默認的垃圾收集器

·-XX:+PrintCommandLineFlags 查看命令行相關參數(包含使用的垃圾收集器)

·使用命令行指令:jinfo -flag 相關垃圾回收器參數 進程ID

4.4、Serial回收器:串行回收

【Serial回收新生代  Serial Old回收老年代】

·Serial收集器作爲HotSpot中client模式下的默認新生代垃圾收集器

·Serial收集器採用"複製算法"、"串行回收"和"STW機制"的方式執行內存回收

·Serial Old收集器同樣採用了"串行回收"和"STW機制",只不過內存回收算法使用的是 "標記-壓縮算法"

>client模式下 serial old是默認的老年代回收器
>server模式下 ①、與新生代的Parallel scavenge配合使用 ②、作爲CMS收集器的後備垃圾收集方案


·Serial收集器是一個單線程的收集器,但它的單線程的意義並不僅僅說明它只會使用一個
CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其它所
有工作線程,直到它收集結束(STOP THE WORLD)


##優勢:
·簡單而高效(與其它收集器單線程比),對於限定單個CPU的環境來說,Serial收集器由於
沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率
>運行在client模式下的虛擬機是個不錯的選擇

·在用戶的桌面應用場景中,可用內存一般不大(幾十兆至一兩百兆),可以在較短時間內完成
垃圾回收,只要不頻繁發生使用串行回收器是可以接受的。


##配置:
·在HotSpot虛擬機中,使用-XX+UseSerialGC參數可以指定新生代和老年代都使用串行收集器
>等價於 新生代使用Serial GC,老年代使用Serial Old GC


##總結

現在已經不用串行的垃圾回收器拉,而且在限定單核CPU纔可以用,現在都不是單核的了

對於交互較強的應用而言,這種垃圾收集器是不能接受的。一般在java web應用程序中是不
會採用串行垃圾收集器的

4.5、ParNew回收器:並行回收

【JDK9中已被移除】
【ParNew回收新生代】

·ParNew收集器是Serial收集器的多線程版本
>par是Parallel的縮寫,New:表示新生代

·ParNew收集器除了採用並行回收的方式執行內存回收外,兩款垃圾收集器之間幾乎沒有任何
區別。ParNew收集器在新生代中同樣採用"複製算法"、"STW"機制

·ParNew是很多JVM運行在server模式下新生代的默認垃圾收集器

>對於新生代,回收次數頻繁,使用並行方式高效
>對於來年代,回收次數少,使用串行方式節省資源(CPU並行需要切換線程,串行可以省去切換線程的資源)


##由於ParNew收集器是基於並行回收,那麼是否可以判定parNew收集器的回收效率在任何場
景下都會比serial收集器效率更高?
>ParNew收集器運行在多CPU的環境下,由於可以充分利用多CPU、多核心等屋裏硬件資源優
勢,可以更快地完成垃圾收集,提升程序吞吐量

>但是在單個CPU的環境下,ParNew收集器不必Serial收集器更高效。雖然serial收集器是
基於串行回收,但是由於CPU不需要頻繁地做任務切換,因此可以有效避免多線程交互過程中
產生的一些額外開銷

·除了serial外,目前只有parNew GC能與CMS收集器配合工作

##配置:
·-XX:+UserParNewGC手動指定ParNew收集器執行內存回收任務。它表示新生代使用並行收集器,不影響老年代

·-XX:ParallelGCThreads限制線程數量,默認開啓和CPU數據相同的線程數

·-XX:UseConcMarkSweepGC設置老年代使用CMS回收器

4.6、Parallel回收器:吞吐量優先

【JDK8默認回收器】
【Parallel回收新生代  Parallel Old回收老年代】

·Parallel是Parallel Scavenge收集器的簡寫

·Parallel收集器同樣採用"複製算法"、"並行回收" 和 "STW"機制

##有了ParNew收集器,Parallel收集器的出現是否多此一舉?

>和ParNew收集器不同,Parallel收集器的目標則是達到一個可控制的吞吐量,它被稱爲吞
吐量優先的垃圾收集器

>自適應調節策略也是Parallel和ParNew一個重要區別
[可動態調整內存區域分配情況,新生代大小,Eden和Survivor比例,晉升老年代所需年齡等,默認開啓]

·高吞吐量可以高效低利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不需要
太多交互的任務。因此常見在服務器環境中使用。例如,哪些執行批量處理、訂單處理、工資
支付、科學計算的應用程序

·Parallel Old收集老年代垃圾,用來替換來年代的Serial Old收集器

·Parallel Old收集器採用了"標記-壓縮算法",但同樣也是基於"並行回收"和"STW"機制


·在吞吐量優先的應用場景中,Parallel收集器和Parallel Old收集器的組合,在Server
模式下的內存回收性能很不錯

·JDK8中,默認是此垃圾回收器

##參數配置:
·-XX:+UseParallelGC 手動指定新生代使用Parallel並行收集器執行內存回收任務

·-XX:+UseParallelOldGc 手動指定老年代的並行回收收集器
>上面兩個參數,默認開啓一個,另一個也會被開啓(互相激活)

·-XX:ParallelGCThreads 設置新生代並行收集器的線程數,最好與CPU數量相等。
>默認CPU數量小於8個,ParallelGCThreads的值等於CPU數量
>默認CPU數量大於8個時u,ParallelGCThreads的值等於3+[5*CPUCount]/8

·-XX:MaxGCPauseMillis設置垃圾收集器最大停頓時間(即STW時間),單位是ms
>爲儘可能地把停頓時間控制在MaxGcPasuseMillis以內,收集器在工作時會調整java堆大小或者一些其它參數
>對於用戶來講,停頓時間越短體驗越好。但是在服務器端,我們注重高併發,整體吞吐量,所以服務器端適合Parallel,進行控制
>該參數使用需謹慎

·-XX:GCTimeRatio垃圾收集時間棧總時間比例[1/(N+1)]用於很小兔兔量大小
>取值範圍(0,100),默認值99,也就是垃圾回收時間不超過1%
>與前一個-XX:MaxGCPauseMillis參數有一定矛盾性,暫停時間越長,Radio參數就越容易超過設定的比例

·-XX:+UseAdaptiveSizePolicy設置parallel scavenge收集器具有自適應調節策略
【默認開啓】
>在這種模式下,新生代的大小、Eden和Surivivor的比例、晉升老年代的對象年齡等參數會
被自動調整,以達到堆大小、吞吐量和停頓時間之間的平衡點
>在手動調整比較困難的場合,可以直接使用這種自適應的方式,僅指定虛擬機的最大堆、目標
的吞吐量和停段時間,讓虛擬機自己完成調優工作

4.7、CMS回收器:低延遲

【回收老年代】
【JDK14刪除CMS】

·Concurrent-Mark-Sweep收集器,簡稱CMS(標記-清除算法)

·併發收集器,第一次實現了讓垃圾收集線程與用戶線程同時工作

·CMS收集器關注點是儘可能縮短STW時間,STW時間越短就越適合與用戶交互的程序
>目前很大一部分java應用程序在互聯網站或B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,已給用戶帶來較好的體驗,CMS收集器就是非常符合這類應用的要求

·CMS垃圾手機算法採用"標記-清除"算法,並且也會STW

·CMS只能和Serial和ParNew一起配合工作,不能和Parallel配合使用

·在G1之前,CMS使用非常廣泛

##工作過程
·CMS工作的蒸鍋過程分爲4個主要階段,即初始標記階段、併發標記階段、重新標記階段和併發清除階段

>初始標記initial-mark:在這個階段,程序中所有的工作線程都會因爲STW機制而出現短
暫的暫停,這個階段的主要任務僅僅是標記處GC Roots能直接關聯到的對象。一旦標記完成
之後就會恢復之前被暫停的所有應用線程。由於直接關聯對象比較小,所以這裏的速度非常快

>併發標記concurrent-mark:從GC Roots的直接關聯對象開始遍歷整個對象圖的過程,
這個過程耗時較長但是不需要停頓用戶線程,可以與垃圾收集線程一起併發運行

>重新標記remark:由於在併發標記階段,程序的工作線程回合垃圾收集線程同時運行或者交
叉運行,因此爲了修正併發標記期間,因用戶程序繼續運作而導致標記產生變動的那一部分對
象的標記記錄,這個階段的停頓時間通常會比初始標記階段稍微長一些,單頁遠比並發標記階
段的時間短

>併發清除concurrent-sweep:此階段清理刪除掉標記段判斷的已經死亡的對象,釋放內存
空間,由於不需要移動存活對象,所以這個階段也是可以與用戶線程同時併發的

##CMS的特點與弊端:
·儘管CMS收集器採用併發揮收,但是在其初始化標記和重新標記兩個階段仍需執行STW,不過
暫停時間不會太長,因此可以說明目前所有的垃圾收集器都做不到完全避免STW,只是儘可能
地縮短暫停時間

·由於耗費時間和併發標記與併發清除階段都不需要暫停工作,所以整體的回收時低停頓的

·另外由於在垃圾收集階段用戶線程沒有中斷,所以CMS回收過程中,還應該確保應用程序用戶
線程有足夠的內存可用。因此CMS收集器不能像其它收集器那樣等到老年代幾乎完全被填滿了
再進行收集,而是當堆內存使用率達到某一閾值是,便開始進行回收,以確保應用在CMS工作
過程中依然有足夠的空間支持應用程序運行。要是CMS運行期間預留的內存無法滿足程序需
要,就會出現一次Concurrent Mode Failure,這是虛擬機將啓動後背元:臨時啓用
serial old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了

·CMS收集器採用"標記-清除"算反,這意味着每次執行完內存回收後,由於被執行內存回收的
無用對象所佔用的內存空間極有可能不是連續的一些內存塊,不可避免地將會產生一些內存碎
片。那麼CMS在位新對象分配內存空間時,將無法使用指針碰撞技術,而只能夠選擇空閒列表執
行內存分配(維護一個空閒列表)

##Mark-Sweep既然會產生內存碎片,爲什麼CMS不採用Mark-Compact呢?
·CMS是併發執行的,如果要壓縮做碎片整理,就需要停掉所有用戶線程,和CMS的初衷不匹配


##CMS優缺點:
·優點
>併發收集
>低延遲(STW時間非常短)

·缺點
>會產生內存碎片
>CMD收集器堆CPU資源非常敏感
在併發階段,它雖然不會導致用戶線程停頓,但是會因爲佔用了一部分線程而導致應用程序變
慢,總吞吐量降低
>CMS收集器無法處理浮動垃圾
可能出現Concurrent Mode Failure失敗而導致另一次Full GC的產生。在併發標記階段
由於程序的工作線程和垃圾收集線程是同時運行或者交叉運行的,那麼在併發標記階段如果產生
新的垃圾對象,CMS將無法對這些對象進行標記,最終導致這些新產生的垃圾對象沒有被及時回
收,從而只能在下一次執行GC時釋放這些之前未被回收的內存空間
(浮動垃圾:在併發標記的過程中,其它用戶線程產生的新垃圾即浮動垃圾)


##參數設置
·-XX:+UseConcMarkSweepGC 手動指定使用CMS收集器執行垃圾回收任務
>開啓該參數後,會自動將-XX:+UseParNewGC打開,即ParNew(新)+CMS(老)+Serial Old(老)組合

·-XX:CMSInitiatingOccupanyFraction 設置堆內存使用率的閾值,一旦達到該閾值,
變開始進行回收

·-XX:+UseCMSCompactAtFullCollection 用於指定在執行完Full GC後堆內存空間進
行壓縮整理,以此避免內存碎片的產生,不過由於內存壓縮整理過程無法併發執行,所帶來的的
問題就是停頓時間變得更長了

·-XX:CMSFullGCsBeforeCompaction 設置在執行多少次Full GC後堆內存進行壓縮整理

·-XX:ParallelCMSThreads 設置CMS的線程數量
>CMS默認啓動的線程數是(ParallelGCThreads+3)/4,ParallelGCThreads是新生代並
行收集器的線程數。當CPU資源比較緊張時,收到CMS收集器線程的影響,應用程序的性能在垃
圾回首階段可能會非常糟糕

4.8、G1回收器:區域化分代式

【JDK9以後默認使用的】
【JDK8可用,但還不是默認,需-XX:UseG1GC】

·G1:garbage first

·G1是爲了適應不斷擴大的內存和不斷增加的處理器數量,進一步降低暫停時間,同時兼顧良好的吞吐量而產生的

·G1設定的目標是在延遲可控的情況下獲得儘可能高的吞吐量

·G1是一款面向服務器端應用的垃圾收集器,主要針對配備多核CPU及大容量內存的機器,
以極高概率滿足GC停頓時間的同時,還兼具高吞吐量的性能特徵


##一、爲什麼叫做Garbage First?

·G1是一個"並行回收器",它把堆內存分割爲很多不相關的區域region(物理上不連續的),
使用不同的region來表示Eden、倖存者0區、倖存者1區、老年代等

·G1 GC有計劃地避免在整個java堆中進行全區域的垃圾收集。
G1跟蹤各個region裏面的垃圾堆積的價值大小(回收所獲得的的空間大小以及回收需要時間的
經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的region

·由於這種方式的側重點在於回收垃圾最大量的區間region,所以我們給G1一個名字:垃圾優先Garbage First

##二、G1回收器的優勢
(與其它GC收集器相比,G1採用了全新的分區算法)

1·並行與併發兼具
>並行性:G1在回收期間,可以有多個GC線程同時工作,有效利用多核計算能力。此時用戶線程STW
>併發性:G1擁有與應用程序交替執行的能力,部分工作可以和應用程序同時執行,因此,一般
來說,不會再整個回首階段發生完全阻塞應用的情況

2·分代收集
>G1屬於分代性垃圾收集器,它會區分新生代和來年代,新生代依然有Eden區和Surivivor區,但從堆的結構上看,它不要求整個Eden區、新生代或者老年代都是連續的,也不再堅持固定大小和固定數量(這段時間可以是Eden區,下次垃圾回收後可能是Surivivor區)
>將堆空間分爲若干個區域,這些區域中包含了邏輯上的新生代和老年代
>和之前的各類回收器不同,它同時兼顧新生代和老年代。


3·空間整合
>CMS:"標記-清除"算法、內存碎片、若干次GC後進行一次碎片整理
>G1將內存劃分爲一個個的region,內存的回收是以region作爲基本單位的。
region之間是複製算法,但整體上實際可看做是"標記-壓縮"算法,兩種算法都可以避免內存
碎片,這種特性有利於程序長時間運行,分配大對象時不會因爲無法找到連續內存空間而提前觸
發下一次GC。尤其是當Java堆非常大的時候,G1的優勢更加明顯

4·可預測的停頓時間模型(即:軟實時soft real-time)
這是G1相對於CMS的另一大優勢,G1除了追求地停頓外,還建立可預測的停頓時間模型,能讓
使用者明確指定在一個長度爲M毫秒的時間段內,消耗在垃圾收集上的時間不得超過N毫秒
>由於分區的原因,G1可以只選取部分區域進行內存回收,這樣縮小了回收的範圍,因此對於全
局停頓的情況的發生也能得到較好的控制
>G1跟蹤各個region裏面的垃圾堆積的價值大小,在後臺維護一個優先列表,每次根據允許的
收集時間,優先回收價值最大的region,保證了G1收集器在有限的時間內可以獲取儘可能高的
收集效率
>相較於CMS,G1未必能做到CMS在最好情況下的延時停頓,但是最差情況要好很多


##三、G1的缺點
相較於CMS,G1無論是爲垃圾收集產生的內存佔用,還是程序運行時的額外執行負載都要比CMS高。

從經驗上來說,在小內存應用上CMS的表現大概率會優於G1,而G1在大內存應用上則發揮其優勢,平衡點在6-8GB之間


##四、參數設置
·-XX:+UseG1GC 手動指定使用G1收集器執行內存回收任務(JDK9以後是默認的,JDK8可用但須設置)
·-XX:G1HeapRegionSize 設置每個region的大小,值是2的冪,範圍是1MB~32MB之間,目標是根據最小的java堆大小劃分出約2048個區域。默認是堆內存的1/2000
·-XX:MAXGCPauseMillis 設置期望達到的最大GC停頓時間指標,默認值是200ms
·-XX:ParallelGCThread 設置STW時GC線程數值,最多設置爲8
·-XX:ConcGCThreads 設置併發標記的線程數,將n設置爲並行垃圾回收線程數(ParallelGCThreads)的1/4左右
·-XX:InitiatingHeapOccupancyPercent 設置觸發併發GC週期的Java堆佔用率閾值,超過此值,就觸發GC。默認值是45


G1的設計原則就是簡化JVM性能調優,我們只需三步即可完成調優:
1>開啓G1垃圾收集器-XX:+UseG1GC
2>設置堆的最大內存-Xms -Xmn
3>設置最大的停頓時間-XX:ParallelGCThread

##五、G1回收器的使用場景
·面對服務器端,針對具有大內存、多處理器的機器
·需要低GC延遲,具有大堆(6G或者更大時)的應用程序
·下面的一些情況下,使用G1性能比CMS好
>超過50%的java堆被活動數據佔用
>對象分配頻率或年代提升頻率變化很大
>GC停頓時間過長(長於0.5至1秒)

##六、Region的使用介紹

·使用G1收集器時,它將整個Java堆劃分稱謂約2048個大小相同的獨立region塊,每個
region塊大小根據堆空間的實際大小而定,整日被控制在1MB到32MB之間,且爲2的N次冪,可
以通過-XX:G1HeapRegionSize設定。
【所有region的大小相同,且在JVM生命週期內不會改變】

·雖然還保留有新生代和老年代的概念,但新生代和來年代不再是物理隔離的了,它們都是一部分region(不需要連續)的集合。通過region的動態分配方式實現邏輯上的連續

·一個region可能屬於Eden,survivor或者old內存區域。但是一個region只能屬於一個角色

·G1垃圾收集器還增加了一種新的內存區域Humongous內存區域,主要用於存儲大對象,如果超過0.5個region,就放到H

設置H區的原因:
對於堆中的大對象,默認直接會被分配到老年代,但是如果它是一個短期存在的大對象,就會對
垃圾收集器造成負面影響。爲了解決這個問題,G1劃分了一個Humongous區,它用來專門存放
大對象。如果一個H區裝不下一個大對象,那麼G1會尋找連續的H區來存儲,爲了能找到連續的H
區,有時候不得不啓動Full GC,G1的大多數行爲都把H區作爲老年代的一部分來看待


##七、G1回收器垃圾回收過程
G1的垃圾回收主要包括如下三個環節:
>新生代GC(Young GC)
>老年代併發標記過程(Concurrent Marking)
>混合回收(Mixed GC)

(如果需要,單線程、獨佔式、高強度的Full GC還是繼續存在的,它針對GC的平菇失敗提供了一種失敗保護機制,即強力回收)

·應用程序分配內存,當新生代的Eden區用盡時開始新生代回收過程:
G1的新生代收集階段是一個並行的獨佔式收集器。在新生代回收期,G1 GC暫停所有應用程序線
程(STW),啓動多線程執行新生代回收,然後從新生代區間移動存活對象到Survivor區間或
者老年區間,也有可能是兩個區間都會涉及

·當堆內存使用叨叨一定值時(默認45%),開始老年代併發標記過程

·標記完成馬上開始混合回收過程。
對於一個混合回收器,G1 GC從老年區間移動存活對象到空閒區間,這些空閒區間也就成爲了老
年代的一部分。和新生代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整
個老年代被回收,一次只需掃描/回收一小部分老年代的region就可以了。同時,這個老年代
region是和新生代一起被回收的

eg:一個Web服務器,java進程最大堆內存爲4G,每分鐘響應1500個請求,每45秒中會新分配
大約2G的內存。G1會每45秒中進行一次新生代回收,每31個小時整個堆的使用率會達到45%,
會開始老年代併發標記過程,標記完成後開始四到五次的混合回收


##八、記憶集與寫屏障

Remembered Set: R Set

#問題:
·一個對象被不同區域引用的問題
·一個region不可能是孤立的,一個region中的對象可能被其他任意region中的對象引用,判斷對象存活是,是否需要掃描整個java堆才能保證準確?
·在其他的分代收集器,也存在這樣的問題(G1更突出)
·回收新生代也不得不同時掃描老年代?
·這樣的話會降低Minor GC的效率

#解決
·無論G1還是其他分代收集器,JVM都是使用 Remembered Set(記憶集)來避免全局掃描
·每個region都有一個對應的remembered set
·每次reference類型數據寫操作是,都會長生一個write barrier(寫屏障)暫時中斷操作
·然後檢查將要寫入的引用指向的對象是否和該refrences類型數據在不同的region
·如果不用,通過cardtable把相關引用信息記錄到引用指向對象的所在region對應的remembered set中
·當進行垃圾收集時,在GC根節點的枚舉範圍加入remembered set,就可以保證不進行全局掃描,也不會有遺漏

##九、G1垃圾回收過程
1、新生代GC:
JVM啓動時,G1先準備好Eden區,程序在運行過程中不斷創建對象到Eden區,當Eden空間耗盡
時,G1會啓動一次年輕代垃圾回收過程

年輕代垃圾回收只會回收Eden區和Surivivor區

YGC時,首先G1停止應用程序的執行STW,G1創建回收機(Collection Set),回收集是指需
要被回收的內存分段的集合,年輕代回收過程的回收集包括年輕代Eden區和Survivor區所有的
內存分段。
(Eden區滿了會觸發YGC,但Survivor區滿了不會觸發YGC)

過程:
>第一階段:掃描根
根是指static變量指向的對象,正在執行的方法調用鏈條上的局部變量等,根引用連用rset記
錄的外部引用作爲掃描存活對象的入口
>第二階段:更新rset
處理dirty card queue中的card,更新rset,此階段完成後,rset可以準確的反映老年代
對所在的內存分段中的對象引用
>第三階段:處理rset
識別被來年代對象指向的Eden中的對象,這些被指向的Eden中的對象被認爲是存活的對象
>第四階段:複製對象
此階段,對象樹被遍歷,Eden區內存段中存活的對象會被複制到survivor區中空的內存分段,
survivor區中內存段中存活的對象如果年齡未達到閾值,年齡會加1,大道與之會被複制到old
區中空的內存分段,如果survivor空間不夠,Eden空間的部分數據直接晉升到old空間
>第五階段:處理引用
處理soft、weak、phantom、final、JNI Weak等引用,最終Eden空間的數據爲空,GC停
止工作,而目標內存中的對象都是連續存儲的,沒有碎片,所以賦值過程可以達到內存整理的效
果,減少碎片。

2、併發標記過程:
>初始標記階段:
標記從根節點直接可達的對象。這個階段是STW的沒並且會觸發一次年輕代GC
>根區域掃描
G1掃描Survivor區直接可達的老年代區域對象,並標記被引用的對象,這一過程必須在YGC之
前完成
>併發標記
在整個堆中進行併發標記,此過程可能被YGC中斷,在併發標記階段,若發現對象中的所有對象
都是垃圾,那這個區域會被立即回收,同時併發標記過程中,會計算每個區域的對象活性
>再次標記
由於應用程序持續進行,需要修正上一次的標記結果,是STW的,G1中採用了比CMS更快的快照
算法
>獨佔清理
計算各個區域的存活對象和GC回收比例,並進行排序,識別可以混合回收的區域,爲下階段做鋪
墊,是STW的(這個階段並不會實際上去做垃圾的收集)
>併發清理階段
識別並清理完全空閒的區域

3、混合回收
當越來越多的對象晉升到old region時,爲了避免堆內存被耗盡,虛擬機會觸發一個混合的垃
圾收集器,即Mixed GC,該算法並不是一個Old GC,除了回收整個Young Region還會回收一
部分的Old Region,這裏需要注意:是一部分老年代,而不是全部老年代。可以選擇那些old
region進行收集,從而可以對垃圾回收的耗時時間進行控制。也需要注意的是Mixed GC並不
是Full GC

>併發標記結束以後,老年代中百分百爲垃圾的內存分段被回收了,部分爲垃圾的內存分段被計
算了出來。默認情況下,這些老年代的內存分段會分8次被回收
>混合回收的收集器包括八分之一的來年代內存分段,Eden區內存分段,Survivor區內存分
段。混合回收的算法和年輕代回收的算法完全一樣,只是回收集多了老年代的內存分段
>由於來年代中的內存分段默認分8次回收,G1會優先回收垃圾多的內存段,垃圾站村分段比例越
高,越先被回收
>混合回收並不一定要進行8次,有一個閾值-XX:G1HeapWastePercent,默認爲10%,意思
是允許整個堆內存中有10%的空間被浪費,意味着如果發現可以回收的垃圾佔堆內存的比例低於
10%,則不再進行混合回收。因爲GC會花費很多的時間但是會受到的內存卻很少


4、Full GC
G1的初衷是避免Full GC的出現,但是如果上述方式不能正常工作,G1會STW,使用單線程的內存回收算法進行垃圾回收,性能會非常差,應用程序停頓時間會很長

4.9、垃圾回收器總結

·最小化地使用內存和並行開銷:選擇Serial GC + Serial Old
·最大化應用程序的吞吐量:選擇Parallel GC + Parallel Old
·最小化GC的中斷或停頓時間:選擇CMS GC + ParNew + Serial Old

·JDK9 廢棄了CMS  JDK14刪除了CMS

(新生代大部分是複製算法  老年代大部分是標記-整理、標記-清除算法)
垃圾收集器 分類 作用位置 算法 特點 場景
Serial 串行 新生代 複製算法 響應速度 單CPU環境下的Client模式
ParNew 並行 新生代 複製算法 響應速度 多CPU環境Server模式,與CMS配合使用
Parallel 並行 新生代 複製算法 吞吐量 適用於後臺運算且不需要太多的交互場景
Serial Old 串行 老年代 標記-壓縮算法 響應速度 單CPU環境下的Client模式
Parallel Old 並行 老年代 標記-壓縮算法 吞吐量 適用於後臺運算且不需要太多的交互場景
CMS 併發 老年代 標記-清除算法 響應速度 適用於互聯網或B/S業務
G1 併發、並行 新生代、老年代 複製算法、標記-壓縮算法 響應速度 面向服務端應用

5.0、GC日誌分析

##一、日誌參數

-XX:+PrintGC         打印GC日誌
-XX:+PrintGCDetails  打印日誌詳情
-XX:+PrintGCTimeStamps  打印GC的時間戳(以基準時間形式)
-XX:+PrintGCDateStamps  打印GC的時間戳(以日期的形式)
-XX:+PrintHeapAtGC   在進行GC的前後打印出堆的信息

-Xloggc:./logs/gc.log  日誌文件的輸出路徑

##二、GC日誌分析

GC  表示只在"新生代"上進行
Full GC 包括"新生代""老年代""元空間" (會發生STW)

PSYoungen :  Parallel Scavenge收集器新生代的名稱
DefNew : 使用了Serial收集器新生代的名稱  Default New Generation
ParNew :ParNew收集器在新生代的名稱  Parallel New Generation
garbage-first heap : G1收集器
ParOldGen : Parallel Old

Allocation Failure :  GC發生的原因
3745K->1127K(58880K) : 堆在GC前的大小 和  GC後的大小  和  本身總的大小
0.0083899 secs : GC持續時間


##三、常見的日誌分析工具
先使用-Xloggc:./logs/gc.log  日誌文件的輸出路徑
在用工具:
GCViewer、GCEasy、GCHisto、GCLogViewer、Hpjmeter、garbagecat等

5.1、新時期的垃圾回收器

·Epsilon : A No-Op Garbage Collector(無操作)
[只做內存分配,不做垃圾回收(運行完直接退出程序的場景)]


·Shenandoah GC : 低停頓,但吞吐量下降了  (RedHat開發)


·ZGC : A Scalable Low-Latency Garbage Collector(可擴展、低延遲[停頓])
[基於Region的內存佈局、可併發的標記壓縮算法、低延遲爲目標]
[併發標記-併發與被重分配-併發重分配-併發重映射]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章