JVM小結

JVM即Java Virtual Machine(Java虛擬機)的縮寫,身爲一名java開發者,適當瞭解JVM,拓展一下知識面並無壞處,本人結合最近的學習對JVM作了簡單總結,現給你們分享。java

1 JVM結構

JVM體系結構概覽

1.1 Class Loader

class loader顧名思義是類加載器,咱們的類文件(.class)是保存在硬盤上的,若是想要被jvm執行,須要有一箇中間層把它加載到jvm中,這個工做就是由class loader作的,它經過IO流的形式把.class文件載入到虛擬機,類加載器分四種:c++

①啓動類加載器(Bootstrap)

這部分是由c/c++編寫的,屬於最底層的類加載器。他會加載$JAVA_HOME/jre/lib/rt.jar中的全部類,這個jar包中有咱們經常使用的最基本的類,好比java.lang.Object、java.lang.String等,這也就解釋了爲何咱們在使用這些類時不須要導包的緣由,啓動類加載器已經事先加載到jvm中了。編程

②擴展類加載器(Extension)

使用java編寫,它會加載$JAVA_HOME/jre/lib/ext/*.jardom

③應用程序類加載器(AppClassLoader)

也叫系統類加載器,使用java編寫,加載當前應用的$CLASSPATH中的全部類。eclipse

④用戶自定義加載器

Java.lang.ClassLoader的子類,用戶能夠定製類的加載方式。(通常用不到)jvm

雙親委派機制和沙箱機制

提到類加載器,就不得不提這兩個機制,所謂雙親委派是指:當應用類加載器接收到一個加載類的請求時,不會立刻進行加載,而是委託給它的父類加載器——擴展類加載器去加載,而擴展類加載器又委託給啓動類加載器,若是啓動類加載器在它的範圍內沒有找到該類,則會拋一個ClassNotFoundException異常,這時它的子類加載器纔會逐級向下去嘗試加載,直到找個這個類。那麼這有什麼意義呢?設想,假如你建了一個java.lang的包,又在該包下建了一個String類,若是沒有這個雙親委派機制,那麼你本身寫的String類是否是就把jre標準的String給覆蓋了?java爲了保護自身標準的類不會被覆蓋,因而就採用了雙親委派把這些類隔離開來,也就是所謂的「沙箱機制」。編程語言

獲取類加載器

能夠經過java.lang.Class<T>中的getClassLoader方法來獲取當前類加載器。學習

public class JVMTest01 {
    public static void main(String[] args) {
        Object obj = new Object();
        System.out.println(obj.getClass().getClassLoader());
        JVMTest01 test = new JVMTest01();
        System.out.println(test.getClass().getClassLoader());
        System.out.println(test.getClass().getClassLoader().getParent());
        System.out.println(test.getClass().getClassLoader().getParent().getParent());
    }
}

輸出結果:spa

null
sun.misc.Launcher$AppClassLoader@2a139a55
sun.misc.Launcher$ExtClassLoader@7852e922
null

咱們來分析一下這個結果,第二行和第三行的輸出應該容易理解,JVMTest01是一個用戶自定義的類,是由應用類加載器加載的,而它的父類加載器是擴展類加載器。但奇怪的是第一行和第四行的結果,爲何是null?咱們知道Object類是由啓動類加載器加載的,應用類加載器的父類的父類加載器也是啓動類加載器,那爲何獲取不到呢?由於啓動類加載器是jvm最底層的直接跟操做系統打交道的接口,是由c++編寫的,已經很底層了,單靠java已經獲取不到了,因此是null。操作系統

1.2 Execution Engine

執行引擎負責解釋指令,提交給操做系統執行。

1.3 Native Interface

本地接口的做用是融合不一樣的編程語言爲 Java 所用,它的初衷是融合 C/C++程序,Java 誕生之初正是 C/C++橫行的時候,要想立足,必須有調用 C/C++程序,因而就在內存中專門開闢了一塊區域處理標記爲native的代碼,它的具體作法是 Native Method Stack中登記 native方法,在Execution Engine 執行時加載native libraies。
目前該方法使用的愈來愈少了,除非是與硬件有關的應用,好比經過Java程序驅動打印機或者Java系統管理生產設備,在企業級應用中已經比較少見。由於如今的異構領域間的通訊很發達,好比可使用 Socket通訊,也可使用Web Service等等。

1.4 Native Method Stack

它的具體作法是Native Method Stack中登記native方法,在Execution Engine 執行時加載本地方法庫。

1.5 PC寄存器

每一個線程都有一個程序計數器,是線程私有的,就是一個指針,指向方法區中的方法字節碼(用來存儲指向下一條指令的地址,也即將要執行的指令代碼),由執行引擎讀取下一條指令,是一個很是小的內存空間,幾乎能夠忽略不記。

1.6 Method Area

靜態變量+常量+類信息+運行時常量池存在方法區中,該區被全部線程共享。

注:實例變量存在堆內存中,和方法區無關

1.7 Stack

1.7.1 棧是什麼

棧主管Java程序運行,是在線程建立時建立,它的生命期是跟隨線程的生命期,線程結束棧內存也就釋放,對於棧來講不存在垃圾回收問題,只要線程一結束該棧就釋放,生命週期和線程一致,是線程私有的。

1.7.2 棧中存放什麼

棧幀中主要保存3類數據:
本地變量(Local Variables):輸入參數和輸出參數以及方法內的變量。
棧操做(Operand Stack):記錄出棧、入棧的操做。
棧幀數據(Frame Data):包括類文件、方法等等。

1.7.3 棧運行原理

棧中的數據都是以棧幀(Stack Frame)的格式存在,棧幀是一個內存區塊,是一個數據集,是一個有關方法(Method)和運行期數據的數據集,當一個方法A被調用時就產生了一個棧幀 F1,並被壓入到棧中,
A方法又調用了 B方法,因而產生棧幀 F2 也被壓入棧,
B方法又調用了 C方法,因而產生棧幀 F3 也被壓入棧,
……
執行完畢後,先彈出F3棧幀,再彈出F2棧幀,再彈出F1棧幀……
遵循「先進後出」/「後進先出」原則。
如圖:
棧幀結構示意

棧幀 2是最早被調用的方法,先入棧,而後方法 2 又調用了方法1,棧幀 1處於棧頂的位置,棧幀 2 處於棧底,執行完畢後,依次彈出棧幀 1和棧幀 2,線程結束,棧釋放。
設想:若是方法中不斷調用方法,棧幀一幀一幀的往上堆疊,終於超過了棧空間的上限,因而就報了java.lang.StackOverflowError。這就是無限遞歸調用:

public void test() {
        test();
    }

調用這個方法就會產生這個結果:
StackOverflowError

棧+堆+方法區的交互關係

交互關係
圖中表示的關係是這樣的:在棧中,保存了局部變量(基本類型+引用類型),而引用類型指向了堆內存中的一塊對象實例,而這個實例是依據什麼爲藍圖建立的呢?就是存在於方法區中的類信息,它記錄了該類的「DNA」,基於該類的全部實例都以此爲模版進行建立。

注:本地方法存在於本地方法棧中,和普通Java方法不在同一個棧

2 堆體系結構概述

一個JVM實例只存在一個堆內存,堆內存的大小是能夠調節的,堆內存分爲三部分:

  • Young Generation Space 新生區 Young/New
  • Tenure generation space 養老區 Old/Tenure
  • Permanent Space 永久區 Perm
注:JDK1.8開始,永久區替換爲了元空間

新生區又分爲:

  • 伊甸區(Eden Space)
  • 倖存0區(Survivor 0 Space)
  • 倖存1區(Survivor 1 Space)

圖例:
堆heap

全部的對象都是在伊甸區被new出來的,當伊甸園的空間用完時,程序又須要建立對象,JVM的垃圾回收器將對伊甸園區進行垃圾回收(Minor GC),將伊甸園區中的再也不被其餘對象所引用的對象進行銷燬。而後將伊甸園中的剩餘對象移動到倖存 0區。若倖存 0區也滿了,再對該區進行垃圾回收,而後移動到 1 區。那若是1 區也滿了呢?再移動到養老區。若養老區也滿了,那麼這個時候將產生MajorGC(FullGC),進行養老區的內存清理。若養老區執行了Full GC以後發現依然沒法進行對象的保存,就會產生OOM異常java.lang.OutOfMemoryError

永久存儲區是一個常駐內存區域,用於存放JDK自身所攜帶的 Class,Interface 的元數據,也就是說它存儲的是運行環境必須的類信息,被裝載進此區域的數據是不會被垃圾回收器回收掉的,關閉 JVM 纔會釋放此區域所佔用的內存。

若是出現java.lang.OutOfMemoryError: PermGen space,說明是Java虛擬機對永久代Perm內存設置不夠。通常出現這種狀況,都是程序啓動須要加載大量的第三方jar包。例如:在一個Tomcat下部署了太多的應用。或者大量動態反射生成的類不斷被加載,最終致使Perm區被佔滿。

注:
Jdk1.6及以前:有永久代, 常量池1.6在方法區
Jdk1.7:有永久代,但已經逐步「去永久代」,常量池1.7在堆
Jdk1.8及以後:無永久代,常量池1.8在元空間

3 堆參數調優入門

經常使用參數:

  • -Xms 設置初始分配大小,默認爲物理內存的1/64
  • -Xmx 最大分配內存,默認爲物理內存的1/4
  • -XX:PrintGCDetails 輸出詳細GC日誌

Demo01

public static void main(String[] args) {
        long maxMemory = Runtime.getRuntime().maxMemory();    //返回 Java 虛擬機試圖使用的最大內存量
        long totalMemory = Runtime.getRuntime().totalMemory();    //返回 Java 虛擬機中的內存總量
        
        System.out.println("MAX_MEMORY = " + maxMemory + "Byte " + (maxMemory / (double)1024 / 1024) + "MB");
        System.out.println("TOTAL_MEMORY = " + totalMemory + "Byte " + (totalMemory / (double)1024 / 1024) + "MB");
    }

在eclipse中配置jvm參數:
VM args

輸出結果:
result
由圖,咱們利用-Xms和-Xmx參數將初始內存和最大內存都設置爲1024MB(實際結果981.5MB屬於偏差)

注:永久代/元空間 只是JVM邏輯上有這麼一塊區域,但實際物理內存中並不存在,如何證實呢?如圖:新生代+養老代 的內存總和已經等於TOTAL_MEMORY,說明實際內存中只有新生區和養老區,永久代/元空間只是邏輯上存在。

Demo02

public static void main(String[] args) {
        String str = "hello world!";
        while (true) {
            str += str + new Random().nextInt(88888888) + new Random().nextInt(999999999);
        }

    }

參數配置:

-Xms8m -Xmx8m -XX:+PrintGCDetails

運行結果:
result
分析:咱們故意把堆內存調小至8M,而後再不斷地在堆中生成String對象,直到產生OOM異常,從輸出日誌中能夠看到,在拋出異常前JVM不斷進行GC,直到最後一次Full GC以後,堆內存依舊沒有足夠的空間new出新的對象,因而就拋出了OOM異常。通常OOM異常都是在Full GC以後產生的。

-XX:+HeapDumpOnOutOfMemoryError

-XX:+HeapDumpOnOutOfMemoryError這個長參數是比較特別的,因此這裏單獨提一下,它的做用是當JVM產生OOM異常時,生成一個dump文件到你的工程目錄下,能夠配合eclipse的MAT(Eclipse Memory Analyzer)插件分析內存泄漏。

瞭解性參數

  • -XX:PermSize 永久代初始值
  • -XX:MaxPermSize 永久代最大值
  • -Xmn 新生代大小
相關文章
相關標籤/搜索