Java虛擬機JVM整理

目錄

 

1.虛擬機JVM是什麼?

2.虛擬機執行流程圖

3.類加載系統

3.1類加載系統流程

3.2自定義類加載器

4.運行時數據區

4.1字節碼加載流程

4.2運行時數據區劃分

4.3內存溢出實例

4.3.1堆溢出

4.3.2虛擬機棧和本地方法棧溢出

4.3.3方法區和運行時常量池溢出

4.3.4則拋出OutOfMemoryError異常

4.4內存溢出排查

5.垃圾回收器

5.1堆內存劃分

5.2GC分類

5.3對象生死判斷

5.3.1引用計數算法

5.3.2可達分析算法

5.4如何回收?回收算法?

5.5垃圾回收器分類

5.6堆內存年輕代和老年代的轉換

6.其他

6.1JDK1.8運行時數據區

6.2JDK1.8運行時數據區和JDK1.7運行時數據區區別

6.3. JDK1.8堆統計信息


1.虛擬機JVM是什麼?

虛擬機JVM是運行所有Java程序的抽象計算機,運行所有Java程序的抽象計算機,是Java語言的運行環境,它是Java 最具吸引力的特性之一,Java的跨平臺是必須要有JVM的支持,就是不同平臺支持JVM,然後才能一份Java程序在不同平臺運行。

虛擬機JVM就是一個操作系統中的進程實例,JVM在操作系統中運行,進程是操作系統的執行單位,啓動一個Java的程序,就是一個JVM進程實例,虛擬機進程啓動就緒,然後由虛擬機中的類加載器加載必要的Class文件,包括JDK中的基礎類(如String和Object等),然後由虛擬機進程解釋Class字節碼指令,把這些字節碼指令翻譯成本機cpu能夠識別的指令,才能在cpu上運行。

Java虛擬機內部,有一個叫做類加載器的子系統,這個子系統用來在運行時根據需要加載類;

由虛擬機加載的類,被加載到Java虛擬機內存中之後,虛擬機會讀取並執行它裏面存在的字節碼指令。虛擬機中執行字節碼指令的部分叫做執行引擎。具體說來就是自動釋放沒有用的對象,而不需要程序員編寫代碼來釋放分配的內存。這部分工作由垃圾收集子系統負責。

2.虛擬機執行流程圖

JVM虛擬機主要包含幾部分:類加載系統,內存分配(運行時數據區),垃圾回收系統,執行引擎;

類加載系統:負責加載必要的Class文件,包括JDK中的基礎類(如String和Object等);

內存分配:加載的字節碼,需要一個單獨的內存空間來存放;一個線程的執行,也需要內存空間來維護方法的調用關係,存放方法中的數據和中間計算結果;在執行的過程中,無法避免的要創建對象,創建的對象需要一個專門的內存空間來存放;

垃圾回收系統:具體說來就是自動釋放沒有用的對象,而不需要程序員編寫代碼來釋放分配的內存,這部分工作由垃圾收集子系統負責;

執行引擎:虛擬機中執行字節碼指令的部分,將字節碼處理轉換爲cpu可以執行的機器碼;

3.類加載系統

3.1類加載系統流程

類加載指將類的字節碼文件(.class)中的二進制數據讀入內存,將其放在運行時數據區的方法區內,然後在堆上創建java.lang.Class對象,封裝類在方法區內的數據結構。類加載的最終產品是位於堆中的類對象,類對象封裝了類在方法區內的數據結構,並且向JAVA程序提供了訪問方法區內數據結構的接口。如下是類加載器的層次關係圖。

a.啓動類加載器(BootstrapClassLoader):在JVM運行時被創建,負責加載存放在JDK安裝目錄下的jre\lib的類文件,或者被-Xbootclasspath參數指定的路徑中,並且能被虛擬機識別的類庫(如rt.jar,所有的java.*開頭的類均被Bootstrap ClassLoader加載)。啓動類無法被JAVA程序直接引用。

b.擴展類加載器(Extension ClassLoader):該類加載器負責加載JDK安裝目錄下的\jre\lib\ext的類,或者由java.ext.dirs系統變量指定路徑中的所有類庫,開發者也可以直接使用擴展類加載器。

c.應用程序類加載器(AppClassLoader):負責加載用戶類路徑(Classpath)所指定的類,開發者可以直接使用該類加載器,如果應用程序中沒有定義過自己的類加載器,該類加載器爲默認的類加載器。

d.用戶自定義類加載器(User ClassLoader):JVM自帶的類加載器是從本地文件系統加載標準的java class文件,而自定義的類加載器可以做到在執行非置信代碼之前,自動驗證數字簽名,動態地創建符合用戶特定需要的定製化構建類,從特定的場所(數據庫、網絡中)取得java class。

注意如上的類加載器並不是通過繼承的方式實現的,而是通過組合的方式實現的。而JAVA虛擬機的加載模式是一種委派模式,如上圖中的1-7步所示。下層的加載器能夠看到上層加載器中的類,反之則不行。類加載器可以加載類但是不能卸載類。說了一大堆,還是感覺需要拿點代碼說事。

3.2自定義類加載器

首先我們先定義自己的類加載器MyClassLoader,繼承自ClassLoader,並覆蓋了父類的findClass(String name)方法,如下:

public class MyClassLoader extends ClassLoader{
    private String loaderName;  //類加載器名稱
    private String path = "";   //加載類的路徑
    private final String fileType = ".class";
    public MyClassLoader(String name){
        super();   //應用類加載器爲該類的父類
        this.loaderName = name;
    }
    public MyClassLoader(ClassLoader parent,String name){
        super(parent);
        this.loaderName = name;
    }
    public String getPath(){return this.path;}
    public void setPath(String path){this.path = path;}
    @Override
    public String toString(){return this.loaderName;}
    
    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException{
        byte[] data = loaderClassData(name);
        return this.defineClass(name, data, 0, data.length);
    }
    //讀取.class文件
    private byte[] loaderClassData(String name){
        InputStream is = null;
        byte[] data = null;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            is = new FileInputStream(new File(path + name + fileType));
            int c = 0;
            while(-1 != (c = is.read())){
                baos.write(c);
            }
            data = baos.toByteArray();

        } catch (Exception e) {
            e.printStackTrace();
        } finally{
            try {
                if(is != null)
                    is.close();
                if(baos != null)
                    baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return data;
    }
}

我們如何利用我們定義的類加載器加載指定的字節碼文件(.class)呢?如通過MyClassLoader加載C:\\Users\\Administrator\\下的Test.class字節碼文件,代碼如下所示:

public class Client {
    public static void main(String[] args) {
        // TODO Auto-generated method stub        
        //MyClassLoader的父類加載器爲系統默認的加載器AppClassLoader
        MyClassLoader myCLoader = new MyClassLoader("MyClassLoader");
        //指定MyClassLoader的父類加載器爲ExtClassLoader
        //MyClassLoader myCLoader = new MyClassLoader(ClassLoader.getSystemClassLoader().getParent(),"MyClassLoader");
        myCLoader.setPath("C:\\Users\\Administrator\\");
        Class<?> clazz;
        try {
            clazz = myCLoader.loadClass("Test");
            Field[] filed = clazz.getFields();   //獲取加載類的屬性字段
            Method[] methods = clazz.getMethods();   //獲取加載類的方法字段
            System.out.println("該類的類加載器爲:" + clazz.getClassLoader());
            System.out.println("該類的類加載器的父類爲:" + clazz.getClassLoader().getParent());
            System.out.println("該類的名稱爲:" + clazz.getName());
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

4.運行時數據區

4.1字節碼加載流程

字節碼的加載第一步,其後分別是認證、準備、解析、初始化,那麼這些步驟又具體做了哪些工作,以及他們會對運行時數據區纏身什麼影響呢?如下圖所示:

4.2運行時數據區劃分

以上的圖是基於java7來敘述的,從上面這張圖我們能夠得到如下信息:java虛擬機把內存分爲5個模塊。

(1)程序計數器:程序計數器是線程私有的,主要的作用是通過改變這個計數器的值來選取下一條需要執行的字節碼指令。既然每個線程都有一個,那麼這些線程的計數器是互不影響的。也不會拋出任何異常。

(2)虛擬機棧和本地方法棧:虛擬機棧描述的是java方法執行的內存模型,每個方法在執行的時候都會創建一個棧幀用於存儲局部變量表、操作數棧、動態連接、方法出口等信息。本地方法棧與虛擬機棧的區別是,虛擬機棧爲虛擬機執行java方法服務,而本地方法棧則爲虛擬機提供native方法服務。

在單線程的操作中,無論是由於棧幀太大,還是虛擬機棧空間太小,當棧空間無法分配時,虛擬機拋出的都是StackOverflowError異常,而不會得到OutOfMemoryError異常。而在多線程環境下,則會拋出OutOfMemoryError異常。

(3)Java堆和方法區:java堆區主要存放對象實例和數組等,方法區保存類信息、常量、靜態變量等等。運行時常量池也是方法區的一部分。這兩塊區域是線程共享的區域,只會拋出OutOfMemoryError。

4.3內存溢出實例

4.3.1堆溢出

既然堆是存放實例對象的,那我們就無線創建實例對象。這樣堆區遲早會滿;

因爲我提前設置了堆區內存,所以無限創建就會拋出異常。

4.3.2虛擬機棧和本地方法棧溢出

Java虛擬機規範中描述了兩種異常:

a.如果線程請求的棧深度大於虛擬機鎖允許的最大深度,將拋出StackOverflowError異常;

b.如果虛擬機在擴展棧時無法申請到足夠的內存空間,則拋出OutOfMemoryError異常。第一種我們只需要使用方法遞歸調用即可模擬:

第二種也可以遞歸調用模擬,,但是使用的是類直接調用;

4.3.3方法區和運行時常量池溢出

通過不斷的向方法區添加類,則拋出OutOfMemoryError異常;

4.3.4則拋出OutOfMemoryError異常

DirectMemory容量可通過-XX: MaxDirectMemorySize指定,如果不指定,則默認與Java堆最大值 (-Xmx指定)一樣;

4.4內存溢出排查

排查其實最主要的就是檢查代碼,而且內存溢出往往都是代碼的問題。當然一下幾點都是需要注意的:

(1)內存中加載的數據量過於龐大,如一次從數據庫取出過多數據;

(2)集合類中有對對象的引用,使用完後未清空,使得JVM不能回收;

(3)代碼中存在死循環或循環產生過多重複的對象實體;

(4)使用的第三方軟件中的BUG;

(5)啓動參數內存值設定的過小;

最後就是解決了。

第一步,修改JVM啓動參數,直接增加內存。

第二步,檢查錯誤日誌

第三步,對代碼進行走查和分析,找出可能發生內存溢出的位置。

一般情況下代碼出錯的概率會比較大一些,當然了不同的場景不同錯誤總是複雜多樣的。

5.垃圾回收器

垃圾回收主要回收的是堆內存,基於分代的思想

5.1堆內存劃分

在JDK1.7以及其前期的JDK版本中,堆內存通常被分爲三塊區域:Young Generation、Old Generation、Permanent Generation for VM Matedata;

Young(年輕代)
年輕代分三個區。一個Eden區,兩個 Survivor區。大部分對象在Eden區中生成。當Eden區滿時,還存活的對象將被複制到Survivor區(兩個中的一個),當這個 Survivor區滿時,此區的存活對象將被複制到另外一個Survivor區,當這個Survivor去也滿了的時候,從第一個Survivor區複製 過來的並且此時還存活的對象,將被複制“年老區(Tenured)”。需要注意,Survivor的兩個區是對稱的,沒先後關係,所以同一個區中可能同時 存在從Eden複製過來 對象,和從前一個Survivor複製過來的對象,而複製到年老區的只有從第一個Survivor去過來的對象。而且,Survivor區總有一個是空 的。
Tenured(年老代)
年老代存放從年輕代存活的對象。一般來說年老代存放的都是生命期較長的對象。
Perm(持久代)
用 於存放靜態文件,如今Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者調用一些class,例如Hibernate等, 在這種時候需要設置一個比較大的持久代空間來存放這些運行過程中新增的類。持久代大小通過-XX:MaxPermSize=進行設置。

這個比例在每個 jdk 版本中可能是不一樣的,在 jdk1.7 中 Eden和Survivor (from 或 to)的比例是 8 : 1;在 jdk1.8 是 6 : 1,如下(JDK1.8:年輕代,老年代,元數據(和1.7有區別))

這裏寫圖片描述

5.2GC分類

a.Monior GC,新生代GC,指發生在新生代的垃圾收集動作,因爲Java對象大多都具備朝生夕滅的特點,所以Monior GC很頻繁,速度也很快;

觸發條件:Minor GC觸發條件:當Eden區滿時,觸發Minor GC。

b.Major GC/Full GC,老年代GC,指發生在老年代的垃圾回收動作,一般比Monior GC慢十倍以上;

觸發條件:

System.gc()
1)老年代空間不足
2)永生區空間不足
3)統計得到的Minor GC晉升到舊生代的平均大小大於老年代的剩餘空間
4)堆中分配很大的對象

5.3對象生死判斷

5.3.1引用計數算法

給對象添加一個引用計數器,有一個地方引用它時,計數器值就加一,引用失效時就減一,任何時刻計數值爲0的對象就死了。這個算法雖然簡單但是有一個致命的缺點就是無法解決對象之間相互循環引用的關係。可達性分析算法應運而生。

5.3.2可達分析算法

GC Roots作爲起點向下搜索,若一個對象到GC Roots沒有引用鏈的話,則證明此對象不可用,可以回收。搜索的對象有:

  • 虛擬機棧中引用的對象
  • 方法區中靜態屬性引用的對象
  • 方法區中常量引用的對象
  • 本地方法棧中Native 方法引用的對象

5.4如何回收?回收算法?

標記-清除算法:標記-清除算法將垃圾回收分爲兩個階段:標記階段和清除階段。一種可行的實現是,在標記階段,首先通過根節點,標記所有從根節點開始的可達對象。因此,未被標記的對象就是未被引用的垃圾對象。然後,在清除階段,清除所有未被標記的對象。

複製算法:將原有的內存空間分爲兩塊,每次只使用其中一塊,在垃圾回收時,將正在使用的內存中的存活對象複製到未使用的內存塊中,之後,清除正在使用的內存塊中的所有對象,交換兩個內存中的角色,完成垃圾回收。

標記-壓縮算法:首先從根節點開始,對所有可達的對象做一次標記,但之後,它並不是簡單的清理未標記的對象,而是將所有的存活對象壓縮到內存空間的一端。之後,清理邊界外所有的空間。

分代:將內存區域根據對象的特點分成不同的內存區域,根據每塊區域對象的特徵不同使用不同的回收算法,以提高垃圾回收的效率。

5.5垃圾回收器分類

 

垃圾回收默認配置及互聯網後臺推薦配置

1)在JVM的客戶端模式(Client)下,JVM默認垃圾收集器是串行垃圾收集器(Serial GC + Serial Old,-XX:+USeSerialGC);
2)在JVM服務器模式(Server)下默認垃圾收集器是並行垃圾收集器(ParallelScavaenge +Serial Old,-XX:+UseParallelGC)
3)而適用於Server模式下
4)ParNew + CMS + SerialOld(失敗擔保),-XX:UseConcMarkSweepGC;
5)Parallel scavenge + Parallel,-XX:UseParallelOldGC

5.6堆內存年輕代和老年代的轉換


大對象直接進入老年代
大對象指需要大量連續內存空間的Java對象,如很長的字符串以及數組。直接進入老年代避免頻繁的GC活動。

長期存活的對象將進入老年代
對象在新生代區域每熬過一次Minor GC,年齡就增加一歲(Age Count),超過15歲(默認),就會被晉升到老年代中。

動態年齡判定
如果相同年齡的對象所佔內存大於Survivor空間的一半,年齡大於等於該年齡的對象就可以直接進入老年代。

6.其他

6.1JDK1.8運行時數據區

這裏寫圖片描述

6.2JDK1.8運行時數據區和JDK1.7運行時數據區區別

在JDK1.7以及其前期的JDK版本中,堆內存通常被分爲三塊區域:Young Generation、Old Generation、Permanent Generation for VM Matedata

在JDK1.8中把存放元數據中的永久內存從堆內存中移到了本地內存中,JDK1.8中JVM堆內存結構就變成了如下:

在JDK1.8中,永久代已經不存在,存儲的類信息、編譯後的代碼數據等已經移動到了MetaSpace(元空間)中,元空間並沒有處於堆內存上,而是直接佔用的本地內存(NativeMemory)。

元空間的本質和永久代類似,都是對JVM規範中方法區的實現。

不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存

6.3. JDK1.8堆統計信息

參考:

https://www.jianshu.com/p/c0713884fb12

https://www.cnblogs.com/zhanglei93/p/6590609.html

https://baijiahao.baidu.com/s?id=1652605740506103750&wfr=spider&for=pc

http://cnblogs.com/cjsblog/p/9850300.html

https://blog.csdn.net/codejas/article/details/80466813

https://blog.csdn.net/u013891584/article/details/81113581

http://jianshu.com/p/0bee406113a0

https://my.oschina.net/u/3728166/blog/2873179

https://www.jianshu.com/p/ddc512ad0f02

 

 

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