1. 程式人生 > >一文帶你學會java的jvm精華知識點

一文帶你學會java的jvm精華知識點

前言

本文分為20多個問題,通過問題的方式,來逐漸理解jvm,由淺及深。希望幫助到大家。

1. Java類例項化時,JVM執行順序?

正確的順序如下:

1父類靜態程式碼塊

2父類靜態變數

3子類靜態程式碼塊

3子類靜態變數

4父類成員變數賦值

5父類構造方式開始執行

6子類成員變數賦值

7子類構造方式開始執行

需要注意的地方是靜態變數和靜態程式碼塊誰在前面誰先執行。

 

看一段程式碼示例:

package com.jdk.learn;

 

/**

 * Created by ricky on 2017/8/25.

 *

 * 類載入器載入順序考究

 *

 

 *

 */

public class ClassLoaderTest {

 

    public static void main(String[] args) {

        son sons=new son();

    }

}

 

class parent{

    private static  int a=1;

    private static  int b;

    private   int c=initc();

    static {

        b=1;

        System.out.println("1.父類靜態程式碼塊:賦值b成功");

        System.out.println("1.父類靜態程式碼塊:a的值"+a);

    }

    int initc(){

        System.out.println("3.父類成員變數賦值:---> c的值"+c);

        this.c=12;

        System.out.println("3.父類成員變數賦值:---> c的值"+c);

        return c;

    }

    public parent(){

        System.out.println("4.父類構造方式開始執行---> a:"+a+",b:"+b);

        System.out.println("4.父類構造方式開始執行---> c:"+c);

    }

}

 

class son extends parent{

    private static  int sa=1;

    private static  int sb;

    private   int sc=initc2();

    static {

        sb=1;

        System.out.println("2.子類靜態程式碼塊:賦值sb成功");

        System.out.println("2.子類靜態程式碼塊:sa的值"+sa);

    }

    int initc2(){

        System.out.println("5.子類成員變數賦值--->:sc的值"+sc);

        this.sc=12;

        return sc;

    }

    public son(){

        System.out.println("6.子類構造方式開始執行---> sa:"+sa+",sb:"+sb);

        System.out.println("6.子類構造方式開始執行---> sc:"+sc);

    }

}

 

執行結果如下:

 1.父類靜態程式碼塊:賦值b成功

 1.父類靜態程式碼塊:a的值1

 2.子類靜態程式碼塊:賦值sb成功

 2.子類靜態程式碼塊:sa的值1

 3.父類成員變數賦值:---> c的值0

 3.父類成員變數賦值:---> c的值12

 4.父類構造方式開始執行---> a:1,b:1

 4.父類構造方式開始執行---> c:12

 5.子類成員變數賦值--->:sc的值0

 6.子類構造方式開始執行---> sa:1,sb:1

 6.子類構造方式開始執行---> sc:12

對變數的賦值初始值為0,對於物件來說為null。

2. JVM虛擬機器何時結束生命週期?

執行了System.exit()方法;

程式正常執行結束;

程式在執行過程中遇到了異常或錯誤而異常終止;

由於作業系統出現錯誤而導致Java虛擬機器程序;

3. jvm類的載入機制?

類的載入機制分為如下三個階段:載入,連線,初始化。其中連線又分為三個小階段:驗證,準備,解析。

 

載入階段:

將類的.class檔案中的二進位制資料讀入到記憶體中,將其放在執行時資料區的方法區內,然後再堆內建立一個class物件,用來封裝類在方法區內的資料結構。

載入class檔案的方式:

從本地系統中直接載入

通過網路下載.class檔案

從zip,jar等歸檔檔案中載入.class檔案

從專有資料庫中提取.class檔案

將Java原始檔動態編譯為.class檔案

 

類的載入最終產品是位於堆中的class物件。Class物件封裝了類在方法區內的資料結構,並且向java程式設計師提供了訪問方法區內的資料結構和介面。

類載入並不需要等到某個類被主動使用的時候才載入,jvm規範允許類載入器在預料到某個類要被使用的時候就預先載入。如果預先載入過程中報錯,類載入器必須在首次主動使用的時候才會報錯。如果類一直沒有被使用,就不會報錯。

驗證階段:

此階段驗證的內容如下

類檔案的結構檢查:

確保類檔案遵從java類檔案的固定頭格式,就像平時做檔案上傳驗證檔案頭一樣。還會驗證檔案的主次版本號,確保當前class檔案的版本號被當前的jvm相容。驗證類的位元組流是否完整,根據md5碼進行驗證。

語義檢查:

檢查這個類是否存在父類,父類是否合法,是否存在。

檢查該類是不是final的,是否被繼承了。被final修飾的類是不允許被繼承的。

檢查該類的方法過載是否合法。

檢查類方法翻譯後的位元組碼流是否合法。

引用驗證,驗證當前類使用的其他類和方法是否能夠被順利找到。

準備階段:

通過驗證階段之後,開始給類的靜態變數分配記憶體,設定預設的初始值。類變數的記憶體會被分配到方法區中,例項變數會被分配到堆記憶體中。準備階段的變數會賦予初始值,但是final型別的會被賦予它的值,可以理解為編譯的時候,直接編譯成常量賦給。如果是一個int型別的變數會分配給他4個位元組的記憶體空間,並賦予值為0。如果是long會賦予給8個位元組,並賦予0。

解析階段:

解析階段會把類中的符號引用替換成直接引用。比如Worker類的gotoWork方法會引用car類的run方法。 

在work類的二進位制資料,包含了一個Car類的run的符號引用,由方法的全名和相關描述符組成。解析階段,java虛擬機器會把這個符號引用替換成一個指標,該指標指向car類的run方法在方法區中的記憶體位置,這個指標就是直接引用。

初始化階段:

類的初始化階段就是對壘中所有變數賦予正確的值,靜態變數的賦值和成員變數的賦值都在此完成。初始化的順序參考上方的整理。

初始化有幾點需要注意

如果類還沒有被載入和連線,就先進行載入和連線。如果存在直接的父類,父類沒有被初始化,則先初始化父類。

4. Java類的初始化時機?

類分為主動使用和被動使用。主動使用使類進行初始化,被動使用不會初始化。

主動使用有以下六種情形:

1建立類的例項

2訪問某個類或介面的靜態變數,或者對靜態變數進行賦值

3呼叫類的靜態方法

4反射

5初始化一個類的子類

6具有main方法的java啟動類

需要注意的是:

初始化一個類的時候,要求他的父類都已經被初始化,此條規則不適用於介面。初始化一個類的時候,不會初始化它所實現的介面,在初始化一個介面的時候,並不會初始化他的父介面。

只有到程式訪問的靜態變數或者靜態方法確實在當前類或當前介面中定義的時候,才可以認為是對類或介面的主動使用。

呼叫classloader類的loadclass方法載入一個類,不是對類的主動使用,因為loadclass呼叫的一個子方法具有兩個引數,name和resolve,由於resolve是false。在程式碼中並不會呼叫resolveClass,所以不會對類進行解析。

被動使用的幾種情況:

(1)通過子類引用父類的靜態欄位,為子類的被動使用,不會導致子類初始化。

class Dfather{  

    static int count = 1;  

    static{  

        System.out.println("Initialize class Dfather");  

    }  

}  

  

class Dson extends Dfather{  

    static{  

        System.out.println("Initialize class Dson");  

    }  

}  

  

public class Test4 {  

    public static void main(String[] args) {  

        int x = Dson.count;  

    }  

}

上面這個例子中,雖然是以Dson.count 形式呼叫的,但是因為count是Dfather的靜態成員變數,所以只初始化Dfather類,而不初始化Dson類。

(2)通過陣列定義類引用類,為類的被動使用,不會觸發此類的初始化。

其實陣列已經不是E型別了,E的陣列jvm在執行期,會動態生成一個新的型別,新型別為:如果是一維陣列,則為:[L+元素的類全名;二維陣列,則為[[L+元素的類全名

如果是基礎型別(int/float等),則為[I(int型別)、[F(float型別)等。

class E{  

    static{  

        System.out.println("Initialize class E");  

    }  

}  

  

public class Test5 {  

    public static void main(String[] args) {  

        E[] e = new E[10];  

    }  

}

(3)常量在編譯階段會存入呼叫方法所在類的常量池中。再引用就是直接用常量池中的值了。

class F{  

    static final int count = 1;  

    static{  

        System.out.println("Initialize class F");  

    }  

}  

  

public class Test6 {  

    public static void main(String[] args) {  

        int x = F.count;  

    }  

}

一種特殊的情況如下:

class F{  

    static final String s = UUID.randomUUID().toString();  

    static{  

        System.out.println("Initialize class F");  

    }  

}  

  

public class Test6 {  

    public static void main(String[] args) {  

        String x = F.s;  

    }  

}

則語句 "Initialize class F" 會打印出來,因為UUID.randomUUID().toString()這個方法,是執行期確認的,所以,這不是被動使用。

5. 介紹一下Java的類載入器

類載入器就是用來把類載入到java虛擬機器中的一種東西。對於任意的一個類,由他的類載入器和他的類本身確立其在jvm中的唯一性。

jvm內建三大類載入器:

 

 

根類載入器又叫bootstrap載入器,該類載入器是最頂層的載入器。負責核心類庫的載入。比如java.lang.*等。載入路徑可以通過sun.boot.class.path指定目錄載入。可以通過引數-Xbootclasspath來指定根載入器的路徑。根類載入器實現依賴於底層系統。正常的路徑在於jre/lib下面。

擴充套件類載入器又叫ext classloader。用於載入javahome的jre/lib/ext子目錄的jar包。或者從java.ext.dirs的指定路徑載入。如果把使用者的jar放在這個目錄下也會載入。擴充套件類是純java 的,是classloader的子類。

系統類載入器又叫system classloader。也叫應用類載入器。從環境變數的classpath下面載入類。是classloader的子類。可通過系統屬性的java.class.path進行指定,可通過-classpath指定。平時專案都是通過它載入。

使用者自定義類載入器,使用者可以繼承ClassLoader類,實現其中的findClass方法,來實現自定義的類載入器。

6. 如何實現自定義類載入器?

自定義類載入器必須繼承classloader。需要實現裡面的findClass方法。我們可以傳入路徑,通過二進位制輸出流,將路徑內容讀取為二進位制陣列。通過呼叫defineClass方法定義class。

7. java類的雙親委派機制

當一個類載入器呼叫loadClass之後,並不會直接載入。先去類載入器的名稱空間中查詢這個類是否被載入,如果已經被載入,直接返回。如果沒有被載入。先請求父類載入器載入,父類載入器也沒法載入,就再請求父類,直到根節點,如果找到了就代為載入,放到自己的快取中,沒找到就由自己進行載入,載入不了就報錯。

雙親委派機制的優點是能夠提高軟體系統的安全性,在此機制下,使用者自定義的類載入器不可能載入應該由父類載入器載入的可靠類,從而防止惡意程式碼替代父載入器。

8. java破壞雙親委派機制

可以通過重寫類載入器的loadClass 的方式裡面的邏輯來進行破壞,傳統的是先一層層找。但是破壞的話,改變邏輯,先從自己上面找。

參考java高併發書的 168頁。

9. jvm的類的名稱空間

每一個類載入器例項都有各自的名稱空間,名稱空間是由該類載入器及其所有的父類載入器構成的。在同一個名稱空間中,不會出現類的完整名字相同的兩個類,在不同名稱空間中,可能出現類的完整名字相同的兩個類。

使用同一個類載入器,載入相同類,二者的引用是一直的,class物件相等。

使用不同類載入器或者同一個類載入器的不同例項,去載入一個class,則會產生多個class物件。

參考java高併發書的170頁。

10. jvm的執行時包

由同一類載入器載入的屬於相同包的類組成了執行時包。執行時包是由類載入器的名稱空間和類的全限定名稱共同組成的。這樣做的好處是變使用者自定義的類冒充核心類庫的類,比如java.lang.string類的方法getChar只是包訪問許可權。用於此時偽造了一個java.lang.hackString,用自己的類載入器載入,然後嘗試訪問,這樣是不行的。類載入器不同。

11. JVM載入類的快取機制

每一個類在經過載入之後,在虛擬機器中就會有對應的class例項。類C被類載入器CL載入,CL就是C的初始化類載入器。JVM為每一個類載入器維護了一個類列表。該列表記錄了將類載入器作為初始化類載入器的所有class。在載入一個類的時候,類載入器先在這裡面尋找。在類的載入過程中,只要是參與過類的載入的,再起類載入器的列表中都會有這個類。因此,在自定義的類中是可以訪問String型別的。

12. jvm類的解除安裝

類的最後的生命週期就是解除安裝。滿足以下三個條件類才會被解除安裝,從方法區中解除安裝。

1該類的所有例項已經被gc。

2載入該類的classloader例項被回收。

3該類的class例項沒有在其他地方引用。

 

 

13. java是解釋語言還是編譯語言?

是解釋型的。雖然java程式碼需要編譯成.class檔案。但是編譯後的.class檔案不是二進位制的機器可以直接執行的程式,需要通過java虛擬機器,進行解釋才能正常執行。解釋一句,執行一句。編譯性的定義是編譯過後,機器可以直接執行。也正是因為.class檔案,是的jvm實現跨平臺,一次編譯,處處執行。

14. jvm的記憶體區域

 

 

jvm的記憶體區域主要分為方法區,堆,虛擬機器棧,本地方法棧,程式計數器。

程式計數器:

一塊較小的記憶體區域,是當前執行緒執行位元組碼的行號指示器。每個執行緒都有一個獨立的程式計數器。是執行緒私有的。正是因為程式計數器的存在,多個執行緒來回切換的時候,原來的執行緒才能找到上次執行到哪裡。執行java方法的時候,計數器記錄的是虛擬機器位元組碼指令的地址,如果是native方法,則為空。這個記憶體區域不會產生out of memorry的情況。

虛擬機器棧:

是描述java方法執行的記憶體模型,每個方法在執行的時候都會建立一個棧幀,用於儲存區域性變量表,運算元棧,動態連線,方法出口等。每一個方法從呼叫到執行完成的過程,對應著一個棧幀在虛擬機器中從入棧到出棧的過程。

棧幀用來儲存資料和部分過程結果的資料結構。也被用來處理動態連線,方法返回值,和異常分配。棧幀隨著方法呼叫而建立,隨著方法結束而銷燬。

本地方法棧:

本地方法棧和虛擬機器棧本質一樣,不過是隻儲存本地方法,為本地方法服務。

堆記憶體:

建立的物件和陣列儲存在堆記憶體中,是被執行緒共享的一塊記憶體區域。是垃圾回收的重要區域,是記憶體中最大的一塊區域。存了類的靜態變數和字元常量。

方法區:

又名永久代,用於儲存被jvm載入的類資訊,常量,靜態變數,即時編譯器編譯後的程式碼等資料。hotsoptvm用永久代的方式來實現方法區,這樣垃圾回收器就可以像java堆一樣管理這部分記憶體。

執行時常量池是方法區的一部分。class檔案中除了有類的版本,欄位,方法和介面描述等資訊外,還有就是常量池,用於存放編譯期生成的各種字面量和符號引用。

15. JVM的直接記憶體?

直接記憶體並不是虛擬機器執行時資料區的一部分,也不是Java 虛擬機器規範中定義的記憶體區域。在JDK1.4 中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的 I/O 方式,它可以使用 native 函式庫直接分配堆外記憶體,然後通過一個儲存在Java堆中的 DirectByteBuffer 物件作為這塊記憶體的引用進行操作。這樣能在一些場景中顯著提高效能,因為避免了在 Java 堆和 Native 堆中來回複製資料。

 

本機直接記憶體的分配不會受到Java 堆大小的限制,受到本機總記憶體大小限制

直接記憶體也可以由 -XX:MaxDirectMemorySize 指定

直接記憶體申請空間耗費更高的效能

直接記憶體IO讀寫的效能要優於普通的堆記憶體

當我們的需要頻繁訪問大的記憶體而不是申請和釋放空間時,通過使用直接記憶體可以提高效能。

16. JVM堆的內部結構

jvm的堆從gc角度可以分為新生代和老年代。

 

 

新生代用來存放新生物件,一般佔據堆的三分之一空間。由於頻繁建立物件。所以新生代會頻繁觸發MinorGC 進行垃圾回收。新生代又分為 Eden 區、ServivorFrom、ServivorTo 三個區。

eden區是java新物件的出生地,如果新物件太大,則直接進入老年代。eden區記憶體不夠的時候,觸發一次minorGc,對新生代進行垃圾回收。

servivorfrom區是上一次gc的倖存者,作為這次gc的被掃描者。

servivorto區保留了一次gc的倖存者。

minorgc採用複製演算法。

minorgc觸發過程:

1:eden、servicorFrom 複製到 ServicorTo,年齡+1

首先,把 Eden 和 ServivorFrom 區域中存活的物件複製到 ServicorTo 區域(如果有物件的年齡以及達到了老年的標準,則賦值到老年代區),同時把這些物件的年齡+1(如果 ServicorTo 不夠位置了就放到老年區);

2:清空 eden、servicorFrom

然後,清空 Eden 和 ServicorFrom 中的物件;

3:ServicorTo 和 ServicorFrom 互換

最後,ServicorTo 和 ServicorFrom 互換,原 ServicorTo 成為下一次 GC 時的 ServicorFrom

區。

 

老年代

主要存放應用程式中生命週期長的記憶體物件。

老年代的物件比較穩定,所以 MajorGC 不會頻繁執行。在進行 MajorGC 前一般都先進行

了一次 MinorGC,使得有新生代的物件晉身入老年代,導致空間不夠用時才觸發。當無法找到足夠大的連續空間分配給新建立的較大物件時也會提前觸發一次 MajorGC 進行垃圾回收騰出空間。

MajorGC 採用標記清除演算法:首先掃描一次所有老年代,標記出存活的物件,然後回收沒

有標記的物件。MajorGC 的耗時比較長,因為要掃描再回收。MajorGC 會產生記憶體碎片,為了減少記憶體損耗,我們一般需要進行合併或者標記出來方便下次直接分配。當老年代也滿了裝不下的時候,就會丟擲 OOM(Out of Memory)異常。

 

永久代

指記憶體的永久儲存區域,主要存放 Class 和 Meta(元資料)的資訊,Class 在被載入的時候被放入永久區域,它和和存放例項的區域不同,GC 不會在主程式執行期對永久區域進行清理。所以這也導致了永久代的區域會隨著載入的 Class 的增多而脹滿,最終丟擲 OOM 異常。

 

元資料區

在Java8 中,永久代已經被移除,被一個稱為“元資料區”(元空間)的區域所取代。元空間

的本質和永久代類似,元空間與永久代之間最大的區別在於:元空間並不在虛擬機器中,而是使用本地記憶體。因此,預設情況下,元空間的大小僅受本地記憶體限制。類的元資料放入 native

memory, 字串池和類的靜態變數放入 java 堆中,這樣可以載入多少類的元資料就不再由

MaxPermSize 控制, 而由系統的實際可用空間來控制。

MetaSpace 大小預設沒有限制,一般根據系統記憶體的大小。JVM 會動態改變此值。

可以通過 JVM 引數配置

-XX:MetaspaceSize : 分配給類元資料空間(以位元組計)的初始大小(Oracle 邏輯儲存上的初始高水位,the initial high-water-mark)。此值為估計值,MetaspaceSize 的值設定的過大會延長垃圾回收時間。垃圾回收過後,引起下一次垃圾回收的類元資料空間的大小可能會變大。

-XX:MaxMetaspaceSize :分配給類元資料空間的最大值,超過此值就會觸發Full GC 。此值預設沒有限制,但應取決於系統記憶體的大小,JVM 會動態地改變此值。

 

17. 為什麼移除永久代?

pergenman space,常發生在jsp中。

1、字串存在永久代中,容易出現效能問題和記憶體溢位。

2、類及方法的資訊等比較難確定其大小,因此對於永久代的大小指定比較困難,太小容易出現永久代溢位,太大則容易導致老年代溢位。

3、永久代會為 GC 帶來不必要的複雜度,並且回收效率偏低。

4、Oracle 可能會將HotSpot 與 JRockit 合二為一。jrockit中沒有永久代概念。

 

18. JVM觸發full gc的幾種情況?

System.gc()方法的呼叫

此方法的呼叫是建議JVM進行Full GC,雖然只是建議而非一定,但很多情況下它會觸發 Full GC,從而增加Full GC的頻率,也即增加了間歇性停頓的次數。強烈影響系建議能不使用此方法就別使用,讓虛擬機器自己去管理它的記憶體,可通過通過-XX:+ DisableExplicitGC來禁止RMI呼叫System.gc。

 

老年代空間不足

老年代空間只有在新生代物件轉入及建立為大物件、大陣列時才會出現不足的現象,當執行Full GC後空間仍然不足,則丟擲如下錯誤:

java.lang.OutOfMemoryError: Java heap space

為避免以上兩種狀況引起的Full GC,調優時應儘量做到讓物件在Minor GC階段被回收、讓物件在新生代多存活一段時間及不要建立過大的物件及陣列。

 

永生區空間不足

JVM規範中執行時資料區域中的方法區,在HotSpot虛擬機器中又被習慣稱為永生代或者永生區,Permanet Generation中存放的為一些class的資訊、常量、靜態變數等資料,當系統中要載入的類、反射的類和呼叫的方法較多時,Permanet Generation可能會被佔滿,在未配置為採用CMS GC的情況下也會執行Full GC。如果經過Full GC仍然回收不了,那麼JVM會丟擲如下錯誤資訊:

java.lang.OutOfMemoryError: PermGen space

為避免Perm Gen佔滿造成Full GC現象,可採用的方法為增大Perm Gen空間或轉為使用CMS GC。

 

CMS GC時出現promotion failed和concurrent mode failure

對於採用CMS進行老年代GC的程式而言,尤其要注意GC日誌中是否有promotion failed和concurrent mode failure兩種狀況,當這兩種狀況出現時可能會觸發Full GC。

promotion failed是在進行Minor GC時,survivor space放不下、物件只能放入老年代,而此時老年代也放不下造成的;concurrent mode failure是在執行CMS GC的過程中同時有物件要放入老年代,而此時老年代空間不足造成的(有時候“空間不足”是CMS GC時當前的浮動垃圾過多導致暫時性的空間不足觸發Full GC)。

對措施為:增大survivor space、老年代空間或調低觸發併發GC的比率,但在JDK 5.0+、6.0+的版本中有可能會由於JDK的bug29導致CMS在remark完畢後很久才觸發sweeping動作。對於這種狀況,可通過設定-XX: CMSMaxAbortablePrecleanTime=5(單位為ms)來避免。

 

統計得到的Minor GC晉升到舊生代的平均大小大於老年代的剩餘空間

這是一個較為複雜的觸發情況,Hotspot為了避免由於新生代物件晉升到舊生代導致舊生代空間不足的現象,在進行Minor GC時,做了一個判斷,如果之前統計所得到的Minor GC晉升到舊生代的平均大小大於舊生代的剩餘空間,那麼就直接觸發Full GC。

例如程式第一次觸發Minor GC後,有6MB的物件晉升到舊生代,那麼當下一次Minor GC發生時,首先檢查舊生代的剩餘空間是否大於6MB,如果小於6MB,則執行Full GC。

當新生代採用PS GC時,方式稍有不同,PS GC是在Minor GC後也會檢查,例如上面的例子中第一次Minor GC後,PS GC會檢查此時舊生代的剩餘空間是否大於6MB,如小於,則觸發對舊生代的回收。

除了以上4種狀況外,對於使用RMI來進行RPC或管理的Sun JDK應用而言,預設情況下會一小時執行一次Full GC。可通過在啟動時通過- java -Dsun.rmi.dgc.client.gcInterval=3600000來設定Full GC執行的間隔時間或通過-XX:+ DisableExplicitGC來禁止RMI呼叫System.gc。

 

堆中分配很大的物件

所謂大物件,是指需要大量連續記憶體空間的java物件,例如很長的陣列,此種物件會直接進入老年代,而老年代雖然有很大的剩餘空間,但是無法找到足夠大的連續空間來分配給當前物件,此種情況就會觸發JVM進行Full GC。

 

為了解決這個問題,CMS垃圾收集器提供了一個可配置的引數,即-XX:+UseCMSCompactAtFullCollection開關引數,用於在“享受”完Full GC服務之後額外免費贈送一個碎片整理的過程,記憶體整理的過程無法併發的,空間碎片問題沒有了,但提頓時間不得不變長了,JVM設計者們還提供了另外一個引數 -XX:CMSFullGCsBeforeCompaction,這個引數用於設定在執行多少次不壓縮的Full GC後,跟著來一次帶壓縮的。

 

19. jvm判斷物件是否可被回收?

引用計數法

在 Java 中,引用和物件是有關聯的。如果要操作物件則必須用引用進行。因此,很顯然一個簡單的辦法是通過引用計數來判斷一個物件是否可以回收。簡單說,即一個物件如果沒有任何與之關聯的引用,即他們的引用計數都不為 0,則說明物件不太可能再被用到,那麼這個物件就是可回收物件。

引用計數法,判斷不出迴圈引用的情況。所以沒有采用這種方式。例如

objecta.name = objectb  objectb.name = objecta

可達性分析

為了解決引用計數法的迴圈引用問題,Java 使用了可達性分析的方法。通過一系列的“GC roots”物件作為起點搜尋。如果在“GC roots”和一個物件之間沒有可達路徑,則稱該物件是不可達的。

要注意的是,不可達物件不等價於可回收物件,不可達物件變為可回收物件至少要經過兩次標記過程。兩次標記後仍然是可回收物件,則將面臨回收。

可作為gc root的物件有

1.Java虛擬機器棧(棧幀的本地變量表)中引用的物件

2.本地方法棧 中 JNI引用物件

3.方法區 中常量、類靜態屬性引用的物件。

 

20. jvm垃圾回收演算法

標記清除演算法

標記-清除(Mark-Sweep)演算法,是現代垃圾回收演算法的思想基礎。

標記-清除演算法將垃圾回收分為兩個階段:標記階段和清除階段。

一種可行的實現是,在標記階段,首先通過根節點,標記所有從根節點開始的可達物件。因此,未被標記的物件就是未被引用的垃圾物件(好多資料說標記出要回收的物件,其實明白大概意思就可以了)。然後,在清除階段,清除所有未被標記的物件。

 

 

缺點:

1、效率問題,標記和清除兩個過程的效率都不高。

2、空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後在程式執行過程中需要分配較大的物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。

標記-整理演算法

標記整理演算法,類似與標記清除演算法,不過它標記完物件後,不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉邊界以外的記憶體。

 

 

優點:

1、相對標記清除演算法,解決了記憶體碎片問題。

2、沒有記憶體碎片後,物件建立記憶體分配也更快速了(可以使用TLAB進行分配)。

缺點:

1、效率問題,(同標記清除演算法)標記和整理兩個過程的效率都不高。

複製演算法:

複製演算法,可以解決效率問題,它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊,當這一塊記憶體用完了,就將還存活著的物件複製到另一塊上面,然後再把已經使用過的記憶體空間一次清理掉,這樣使得每次都是對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要移動堆頂指標,按順序分配記憶體即可(還可使用TLAB進行高效分配記憶體)。

 

 

圖的上半部分是未回收前的記憶體區域,圖的下半部分是回收後的記憶體區域。通過圖,我們發現不管回收前還是回收後都有一半的空間未被利用。

優點:

1、效率高,沒有記憶體碎片。

缺點:

1、浪費一半的記憶體空間。

2、複製收集演算法在物件存活率較高時就要進行較多的複製操作,效率將會變低。

分代收集演算法:

當前商業虛擬機器都是採用分代收集演算法,它根據物件存活週期的不同將記憶體劃分為幾塊,一般是把 Java 堆分為新生代和老年代,然後根據各個年代的特點採用最適當的收集演算法。

 

在新生代中,每次垃圾收集都發現有大批物件死去,只有少量存活,就選用複製演算法。

而老年代中,因為物件存活率高,沒有額外空間對它進行分配擔保,就必須使用“標記清理”或者“標記整理”演算法來進行回收。

 

 

圖的左半部分是未回收前的記憶體區域,右半部分是回收後的記憶體區域。

物件分配策略:

物件優先在 Eden 區域分配,如果物件過大直接分配到 Old 區域。

長時間存活的物件進入到 Old 區域。

改進自複製演算法

現在的商業虛擬機器都採用這種收集演算法來回收新生代,IBM 公司的專門研究表明,新生代中的物件 98% 是“朝生夕死”的,所以並不需要按照 1:1 的比例來劃分記憶體空間,而是將記憶體分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor 。當回收時,將 Eden 和 Survivor 中還存活著的物件一次性地複製到另外一塊 Survivor 空間上,最後清理掉 Eden 和剛才用過的 Survivor 空間。

HotSpot 虛擬機器預設 Eden 和 2 塊 Survivor 的大小比例是 8:1:1,也就是每次新生代中可用記憶體空間為整個新生代容量的 90%(80%+10%),只有 10% 的記憶體會被“浪費”。當然,98% 的物件可回收只是一般場景下的資料,我們沒有辦法保證每次回收都只有不多於 10% 的物件存活,當 Survivor 空間不夠用時,需要依賴其他記憶體(這裡指老年代)進行分配擔保(Handle Promotion)。

21. java的引用型別?

強引用

平時使用的new就是強引用,把一個物件賦給一個引用變數。它處於可達狀態的時候,是不會被垃圾回收的。強引用是造成記憶體洩漏的主要原因。

軟引用

軟引用配合softreference使用,當系統中有足夠的記憶體的時候,它不會被回收,當系統中記憶體空間不足的時候會被回收,軟引用存在於對記憶體敏感的程式中。

弱引用

弱引用配合weakreference類來實現。比軟引用的生存期更短,對於弱引用物件來說,只要垃圾回收機制一回收,不管記憶體空間是否充足就直接回收掉了。

虛引用

虛引用需要phantomreference來實現,不能單獨使用,必須配合引用佇列。虛引用主要作用是跟蹤物件的被垃圾回收的狀態。

引用佇列

使用軟引用,弱引用和虛引用的時候都可以關聯這個引用佇列。程式通過判斷引用佇列裡面是不是有這個物件來判斷,物件是否已經被回收了。

軟引用,弱引用和虛引用用來解決oom問題,用來儲存圖片的路徑。主要用於快取。

22. JVM垃圾收集器

Java 堆記憶體被劃分為新生代和年老代兩部分,新生代主要使用複製和標記-清除垃圾回收演算法;

年老代主要使用標記-整理垃圾回收演算法,因此 java 虛擬中針對新生代和年老代分別提供了多種不

同的垃圾收集器,JDK1.6 中 Sun HotSpot 虛擬機器的垃圾收集器如下:

serial垃圾收集器(單執行緒、複製演算法)

Serial(英文連續)是最基本垃圾收集器,使用複製演算法,曾經是JDK1.3.1 之前新生代唯一的垃圾收集器。Serial 是一個單執行緒的收集器,它不但只會使用一個 CPU 或一條執行緒去完成垃圾收集工作,並且在進行垃圾收集的同時,必須暫停其他所有的工作執行緒,直到垃圾收集結束。

Serial 垃圾收集器雖然在收集垃圾過程中需要暫停所有其他的工作執行緒,但是它簡單高效,對於限定單個 CPU 環境來說,沒有執行緒互動的開銷,可以獲得最高的單執行緒垃圾收集效率,因此 Serial垃圾收集器依然是 java 虛擬機器執行在 Client 模式下預設的新生代垃圾收集器。

ParNew 垃圾收集器(Serial+多執行緒)

ParNew 垃圾收集器其實是 Serial 收集器的多執行緒版本,也使用複製演算法,除了使用多執行緒進行垃圾收集之外,其餘的行為和 Serial 收集器完全一樣,ParNew 垃圾收集器在垃圾收集過程中同樣也要暫停所有其他的工作執行緒。

ParNew 收集器預設開啟和 CPU 數目相同的執行緒數,可以通過-XX:ParallelGCThreads 引數來限制垃圾收集器的執行緒數。【Parallel:平行的】

ParNew雖然是除了多執行緒外和Serial 收集器幾乎完全一樣,但是ParNew垃圾收集器是很多 java虛擬機器執行在 Server 模式下新生代的預設垃圾收集器。

Parallel Scavenge 收集器(多執行緒複製演算法、高效)

Parallel Scavenge 收集器也是一個新生代垃圾收集器,同樣使用複製演算法,也是一個多執行緒的垃圾收集器,它重點關注的是程式達到一個可控制的吞吐量(Thoughput,CPU 用於執行使用者程式碼的時間/CPU 總消耗時間,即吞吐量=執行使用者程式碼時間/(執行使用者程式碼時間+垃圾收集時間)),高吞吐量可以最高效率地利用 CPU 時間,儘快地完成程式的運算任務,主要適用於在後臺運算而不需要太多互動的任務。自適應調節策略也是 ParallelScavenge 收集器與 ParNew 收集器的一個重要區別。

下面為年老代的收集器

Serial Old 收集器(單執行緒標記整理演算法 )

Serial Old 是 Serial 垃圾收集器年老代版本,它同樣是個單執行緒的收集器,使用標記-整理演算法,這個收集器也主要是執行在 Client 預設的 java 虛擬機器預設的年老代垃圾收集器。 在 Server 模式下,主要有兩個用途:

1. 在 JDK1.5 之前版本中與新生代的 Parallel Scavenge 收集器搭配使用。

2. 作為年老代中使用 CMS 收集器的後備垃圾收集方案。

Parallel Old 收集器(多執行緒標記整理演算法)

Parallel Old 收集器是Parallel Scavenge的年老代版本,使用多執行緒的標記-整理演算法,在 JDK1.6才開始提供。

在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保證新生代的吞吐量優先,無法保證整體的吞吐量,Parallel Old 正是為了在年老代同樣提供吞吐量優先的垃圾收集器,如果系統對吞吐量要求比較高,可以優先考慮新生代 Parallel Scavenge和年老代 Parallel Old 收集器的搭配策略。

CMS收集器

Concurrent mark sweep(CMS)收集器是一種年老代垃圾收集器,其最主要目標是獲取最短垃圾回收停頓時間,和其他年老代使用標記-整理演算法不同,它使用多執行緒的標記-清除演算法。

最短的垃圾收集停頓時間可以為互動比較高的程式提高使用者體驗。

CMS 工作機制相比其他的垃圾收集器來說更復雜,整個過程分為以下 4 個階段:

初始標記

只是標記一下 GC Roots 能直接關聯的物件,速度很快,仍然需要暫停所有的工作執行緒。

併發標記

進行 GC Roots 跟蹤的過程,和使用者執行緒一起工作,不需要暫停工作執行緒。

重新標記

為了修正在併發標記期間,因使用者程式繼續執行而導致標記產生變動的那一部分物件的標記

記錄,仍然需要暫停所有的工作執行緒。

併發清除

清除 GC Roots 不可達物件,和使用者執行緒一起工作,不需要暫停工作執行緒。由於耗時最長的併發標記和併發清除過程中,垃圾收集執行緒可以和使用者現在一起併發工作,所以總體上來看

CMS 收集器的記憶體回收和使用者執行緒是一起併發地執行。

G1收集器

Garbage first 垃圾收集器是目前垃圾收集器理論發展的最前沿成果,相比與 CMS 收集器,G1 收集器兩個最突出的改進是:

1. 基於標記-整理演算法,不產生記憶體碎片。

2. 可以非常精確控制停頓時間,在不犧牲吞吐量前提下,實現低停頓垃圾回收。

G1 收集器避免全區域垃圾收集,它把堆記憶體劃分為大小固定的幾個獨立區域,並且跟蹤這些區域的垃圾收集進度,同時在後臺維護一個優先順序列表,每次根據所允許的收集時間,優先回收垃圾最多的區域。區域劃分和優先順序區域回收機制,確保 G1 收集器可以在有限時間獲得最高的垃圾收集效率。

23. jdk7、8、9預設垃圾回收器

jdk1.7 預設垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

jdk1.8 預設垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

jdk1.9 預設垃圾收集器G1

 -XX:+PrintCommandLineFlagsjvm引數可檢視預設設定收集器型別

-XX:+PrintGCDetails亦可通過列印的GC日誌的新生代、老年代名稱判斷

24. jdk的命令列有哪些可以監控虛擬機器

jhat :虛擬機器堆轉儲快照分析工具

jstack :Java 堆疊跟蹤工具

JConsole :Java 監視與管理控制檯

25. JVM調優的配置

26. Java的osgi是什麼

OSGi(Open Service Gateway Initiative),是面向 Java 的動態模型系統,是 Java 動態化模組化系統的一系列規範。

動態改變構造

OSGi 服務平臺提供在多種網路裝置上無需重啟的動態改變構造的功能。為了最小化耦合度和促使這些耦合度可管理,OSGi 技術提供一種面向服務的架構,它能使這些元件動態地發現對方。

模組化程式設計與熱插拔

OSGi 旨在為實現 Java 程式的模組化程式設計提供基礎條件,基於 OSGi 的程式很可能可以實現模組級的熱插拔功能,當程序升級更新時,可以只停用、重新安裝然後啟動程式的其中一部分,這對企業級程式開發來說是非常具有誘惑力的特性。

OSGi 描繪了一個很美好的模組化開發目標,而且定義了實現這個目標的所需要服務與架構,同時也有成熟的框架進行實現支援。但並非所有的應用都適合採用 OSGi 作為基礎架構,它在提供強大功能同時,也引入了額外的複雜度,因為它不遵守了類載入的雙親委託模型。

熱部署就是典型的osg