java程序猿面試系列之jvm專題

前言

因為疫情的影響,現在都變成金五銀六了。為了方便大家,在此開一個程序猿面試系列。總結各大公司所問的問題,希望能夠幫助到大家,適合初中級java程序猿閱讀。

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文件

zipjar等歸檔文件中加載.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調用的一個子方法具有兩個參數,nameresolve,由於resolvefalse。在代碼中並不會調用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 形式調用的,但是因為countDfather的靜態成員變量,所以只初始化Dfather類,而不初始化Dson

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

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

如果是基礎類型(int/float等),則為[Iint類型)、[Ffloat類型)等

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。用於加載javahomejre/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 區、ServivorFromServivorTo 三個區。

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

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

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

minorgc採用複製算法。

minorgc觸發過程:

1edenservicorFrom 複製到 ServicorTo,年齡+1

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

2:清空 edenservicorFrom

然後,清空 Eden ServicorFrom 中的對象;

3ServicorTo ServicorFrom 互換

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

區。

 

老年代

主要存放應用程序中生命周期長的內存對象。

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

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

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

有標記的對象。MajorGC 的耗時比較長,因為要掃描再回收。MajorGC 會產生內存碎片,為了減少內存損耗,我們一般需要進行合併或者標記出來方便下次直接分配。當老年代也滿了裝不下的時候,就會拋出 OOMOut 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 帶來不必要的複雜度,並且回收效率偏低。

4Oracle 可能會將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 failedconcurrent mode failure

對於採用CMS進行老年代GC的程序而言,尤其要注意GC日誌中是否有promotion failedconcurrent 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+的版本中有可能會由於JDKbug29導致CMSremark完畢後很久才觸發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 收集器也是一個新生代垃圾收集器,同樣使用複製算法,也是一個多線程的垃圾收集器,它重點關注的是程序達到一個可控制的吞吐量(ThoughputCPU 用於運行用戶代碼的時間/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. jdk789默認垃圾回收器

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. Javaosgi是什麼

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

動態改變構造

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

模塊化編程與熱插拔

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

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

熱部署就是典型的osgi

Tags: