JVM分析(基於JDK1.8):類加載過程、堆的新生代與老年代

一、什麼是JVM

1.引言

jvm即Java Virtual Machine(java虛擬機)的簡寫,如果你在面試的時候被面試官問到什麼是JVM時候,你回答這句話,那麼恭喜你,你就GG了,面試官問你JVM時候,作爲一個面試者,至少得和麪試官談15分鐘左右,這裏將通過六章來闡述JVM以及分析。

2.爲什麼需要JVM

當我們第一天學習java的時候,就聽說這個詞語了,jvm主要是作爲一個翻譯官的角色,Java虛擬機會將字節碼,即class文件加載到JVM中。由JVM進行解釋和執行。除了 Java 外,Scala、Clojure、Groovy,以及時下熱門的 Kotlin,這些語言都可以運行在 Java 虛擬機之上。

3.JVM運行流程

在這裏插入圖片描述

4.常見的JVM

1.HotSpot VM(目前應用最廣的JVM),有點是熱點代碼探測技術
2.Sun Classis VM(世界上第一款商用虛擬機,已淘汰)
3.Jrockit(IBM公司開發,曾廣泛應用於IBM公司系統內部及IBM小型機上)

二、JVM內存體系

在這裏插入圖片描述

1.類加載器與類加載過程

a.類加載器的作用

當我們將java文件編譯爲字節碼文件(class File)之後,就需要類加載器將字節碼文件加載到JVM中,類加載器(即ClassLoader),它負責加載class文件,class文件在文件開頭有特定的文件標示,並且ClassLoader只負責class文件的加載,至於它是否可以運行,則由Execution Engine決定。

b.類加載器分類

那麼,類加在器有哪些呢:

1.啓動類加載器(Bootstrap):由C/C++實現,加載jre中的最爲基礎、最爲重要的類,如rt.jar
2.擴展類加載器(Extension):由Java代碼實現,用於加載相對次要、但又通用的類,如ext 目錄下 jar 包中的類
3.應用程序類加載器(AppClassLoader):由Java代碼實現, 它負責加載應用程序路徑下的類,即用戶自己寫的類由應用程序類加載器加載
4.用戶自定義類加載器:用戶可以定製類的加載方式,例如可以對 class 文件進行加密,加載時再利用自定義的類加載器對其解密。

c.類的加載過程

類的加載主要分爲加載–>鏈接–>初始化
1.加載:將(硬盤/網絡/數據庫中的)字節碼文件加載到JVM內存中。
2.鏈接:鏈接主要分爲3步:
①驗證:並不是所有的字節碼文件都會被加載,加載前會先進行驗證,例如:字節碼是否是合法的字節碼文件(例如是否以cafe babe開頭),當前JVM運行的JDK版本是都可以運行該字節碼文件的數據(例如JDK1.8可以運行JDK1.7編譯的版本,反過來不行,即向前兼容)
②準備:給成員變量(類變量/靜態變量)默認初始值,把常量(final)等值在方法區準備好
③虛擬機常量池內的符號引用(常量名)替換爲直接引用(地址)的過程(把類中的對應的類型名替換爲該類型的Class對象地址)
3.初始化

1.類初始化調用clinit方法(靜態變量的顯式賦值代碼,靜態代碼塊代碼)
2.當一個類初始化時,發現他的父類沒有初始化,會先初始化父類
3.每個類只會初始化一次,類初始化過程是線程安全的[如果有一個線程在初始化類,其他類則會等着]
4.類的加載不一定會發生類的初始化,可能是1.2.3一起完成(大多數時候),也可能是先完成1.2,某個時候纔開始初始化

d.哪些情況會導致初始化?

1.main方法所在的類加載時,直接就先初初始化
2.new一個對象,一定會先完成類的初始化
3.調用該類的靜態變量(除了final外)或靜態方法(即在A類調用B類的靜態方法,則也會初始化B類)
4.使用java.lang.reflect包的方法對類進行反射調用
5.當一個類初始化時,發現他的父類沒有初始化,會先初始化父類

*e.哪些情況不會導致初始化?

 1.調用靜態常量(final)時,不會初始化。
 2.當訪問一個靜態域時,只有真正申明這個域的類纔會被初始化(子類訪問父類的靜態域(例如靜態屬性)時,只會初始化父類,不會初始化子類)
 3.創建某個類的數組對象時,不會初始化該類(例如A[] arr=new A[];不會初始化A類,並且arr的類型是A[]類型,或者說是[L類型)
* 對於不會導致類的初始化,下面將通過三遍代碼來解釋 

1.類型一 代碼演示:


public class Test01 {
    public static void main(String[] args) {
        System.out.println(User.NAME);//只輸出 zhangsan
        //System.out.println(User.id);//輸出 靜態代碼塊1 zhangsan
    }
}

class User{
    public static final String NAME="zhangsan";
    public static String id="1001";
    static {
        System.out.println("靜態代碼塊1");
    }
    public User(){
        System.out.println("構造方法1");
    }
}

2.類型二代碼演示

public class Test02 {
    public static void main(String[] args) {
        /*
        如果子類調用父類的靜態屬性,只會初始化父類,不會初始化子類
        */
        System.out.println(B.name); //輸出: 父類A的靜態代碼塊 AAAAA
    }
}


class A{
    public static String name="AAAAA";
    static {
        System.out.println("父類A的靜態代碼塊");
    }
}

class B extends A{
    static {
        System.out.println("子類B的靜態代碼塊");
    }
}

3.類型三代碼演示

public class Test03 {
    public static void main(String[] args) {
        /*
        * 創建某個類的數組對象時,不會初始化該類
        * */
        Stu[] stus=new Stu[10];
    }
}

class Stu{
    public static String name="stu";
    static {
        System.out.println("stu的靜態代碼塊");
    }
}

f.類加載結果

在方法區會有一個唯一的的Class對象,每一種類型都有一個Class對象,包含基本數據類型與void

2.棧

一個線程的每個方法在調用時都會在棧上劃分一塊區域(線程私有),用於存儲方法所需要的變量等信息,這塊區域稱之爲棧幀(stack frame)。棧由多個棧幀構成,一個方法調用幾個方法就會有幾個棧幀,棧中的數據都是以棧幀(Stack Frame)爲載體存在。在棧中,方法的調用順序遵循“先進後出”/“後進先出”原則。

3.堆

堆(java虛擬機棧)是java虛擬機所管理的內存中最大的一塊,是被所有線程共享的一塊內存區域,在虛擬機啓動時創建。堆內存的大小是可以調節的(通過 -Xmx 和 -Xms 控制)。幾乎所有的對象實例以及數組都要在堆上分配。如果堆中沒有內存完成實例分配,並且堆也無法再擴展時,將會拋出OutOfMemoryError異常。java堆是垃圾收集器管理的主要區域,因此也被成爲“GC堆”(Garbage Collected Heap)。

那麼,所有的對象一定是在堆中嗎?
不一定,有些時候,對象也可能存在棧中,這種現象稱爲棧上分配,哪些對象可以棧上分配,那麼就需要逃逸分析。
爲什麼需要棧上分配?
有些時候,我們對象可能只用到了一次,然後就不用了,如果在堆中,如果一個對象沒有引用被指向時,就會成爲垃圾,等待垃圾回收器的回收,但是垃圾回收器並不會立即回收,這樣就造成了內存浪費,但是棧就能很好解決這個問題,因爲棧中方法執行完畢會自動彈棧,那麼將對象放在棧中,用完之後,就會被彈棧,釋放內存。
逃逸分析?
逃逸分析的作用就是分析對象的作用域是否會逃逸出方法之外。

堆的分類:新生代與老年代

1.新生代(佔1/3的堆空間),新生代又分爲:
   a. 伊甸區(Eden) 此區域佔新生代的8/10:
      1.剛new出來的對象會放在伊甸區
      2.伊甸區空間塊滿時,會觸發垃圾回收,當一個對象在沒有堆外的引用指向時,就會被判定時垃圾
      3.觸發垃圾回收後,沒有引用的對象將被垃圾回收,有引用的對象將放置到倖存者0區
      4.當伊甸區在此被放滿時,需要再次垃圾回收
      5.此時將回收整個新生代(也就是伊甸區和倖存者1區)
      6.此時將回收整個新生代(也就是伊甸區和倖存者1區(from區))中沒有被引用的對象
   b.倖存者0區(from 區)  此區域佔新生代的1/10:
      1.倖存者0區就會放置很多有引用的對象
      2.此時,倖存者1區(form區)中沒有被引用的對象將會被回收,有引用的對象將會複製倖存者2區(to區)
      3.此時,整個from區將整個被清空,然後和倖存者1區(to區)交換指針,此時from區就變成了to區,to區就變成了from區
   c.倖存者1區(to 區) 此區域佔堆內存的1/10:
      1.交換指針之後,from區就變成了to區,to區就變成了from區,所以to區依然爲空,這也就解釋了爲什麼to區永遠是空的(相當於一個過渡區,中間區)。
      2.注意:倖存者1區(to區)永遠是空的,將不會放置任何對象,0區與1區大小完全一樣,將輪流變換爲from區與to區(通過交換指針變化)。誰是from區,誰是to區,取決於裏面有沒有東西,有就是from區,否則就是to區
      注意:新生代發生的GC叫MiniGC 
 2.老年代(佔2/3的堆空間)
      1.如果一個對象年齡爲15歲,則會到老年代(即經歷了15次GC,仍然存活下來)
      2.如果新創建的對象伊甸區放不下,而老年代可以放下,則會放在老年代
      3.如果from區空間已放滿,而伊甸區經過垃圾回收之後又有新的倖存者對象需要放到from區,則JVM會將from區中年齡較大的對象放到老年代(即使沒到15歲,但是年齡是最大的)
      注意:經常引用的對象就在老年代,老年代發生的GC叫FullG

4.方法區

方法區在1.8也稱之爲元空間,放置類、接口的元數據,也就是類信息,被裝載進此區域的數據不會輕易的被垃圾回收器回收,關閉JVM時纔會釋放此區域所佔用的內存,如果出現OutOfMemoryError:meta space,說明永久代/元空間內存設置不夠,字符串常量池也在元空間

5.程序計數器

當前線程所執行的字節碼行號指示器

6.本地方法棧

爲本地方法棧爲虛擬機使用到的native方法服務

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