日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

GC和JVM调优实战

發布時間:2024/4/14 编程问答 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 GC和JVM调优实战 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.


轉載

目錄


  • JVM簡介

  • JVM結構
    2.1 方法區

    2.1.1 常量池2.1.1.1 Class文件中的常量池2.1.1.2 運行時常量池2.1.1.3 常量池的好處2.1.1.4 基本類型的包裝類和常量池

    2.2 堆
    2.3 Java棧

    2.3.1 棧幀2.3.1.1 局部變量區2.3.1.2 操作數棧2.3.1.3 棧數據區

    2.4 本地方法棧
    2.5 PC寄存器
    2.6 堆與棧

    2.6.1 堆與棧里存什么2.6.2 堆內存與棧內存的區別
  • JIT編譯器

  • 類加載機制
    4.1 類加載的時機
    4.2 類加載過程

  • 垃圾回收
    5.1 按代實現垃圾回收
    5.2 怎樣判斷對象是否已經死亡
    5.3 java中的引用
    5.4 finalize方法什么作用
    5.5 垃圾收集算法
    5.6 Hotspot實現垃圾回收細節
    5.7 垃圾收集器

    5.7.1 Serial收集器5.7.2 ParNew收集器5.7.3 Parallel Scavenge收集器5.7.4 Serial Old收集器5.7.5 Parallel Old收集器5.7.6 CMS收集器5.7.7 G1收集器
  • JVM參數
    6.1 典型配置

    6.1.1 堆大小設置6.1.2 回收器選擇6.1.3 輔助信息

    6.2 參數詳細說明

  • JVM性能調優
    7.1 堆設置調優
    7.2 GC策略調優
    7.3 JIT調優
    7.4 JVM線程調優
    7.5 典型案例

  • 常見問題
    8.1 內存泄漏及解決方法
    8.2 年老代堆空間被占滿
    8.3 持久代被占滿
    8.4 堆棧溢出
    8.5 線程堆棧滿
    8.6 系統內存被占滿


  • 1.JVM簡介

    JVM是java的核心和基礎,在java編譯器和os平臺之間的虛擬處理器。它是一種利用軟件方法實現的抽象的計算機基于下層的操作系統和硬件平臺,可以在上面執行java的字節碼程序。

    java編譯器只要面向JVM,生成JVM能理解的代碼或字節碼文件。Java源文件經編譯成字節碼程序,通過JVM將每一條指令翻譯成不同平臺機器碼,通過特定平臺運行。

    運行過程

    Java語言寫的源程序通過Java編譯器,編譯成與平臺無關的‘字節碼程序’(.class文件,也就是0,1二進制程序),然后在OS之上的Java解釋器中解釋執行。

    C++以及Fortran這類編譯型語言都會通過一個靜態的編譯器將程序編譯成CPU相關的二進制代碼。

    PHP以及Perl這列語言則是解釋型語言,只需要安裝正確的解釋器,它們就能運行在任何CPU之上。當程序被執行的時候,程序代碼會被逐行解釋并執行。


  • 編譯型語言的優缺點:

    • 速度快:因為在編譯的時候它們能夠獲取到更多的有關程序結構的信息,從而有機會對它們進行優化。

    • 適用性差:它們編譯得到的二進制代碼往往是CPU相關的,在需要適配多種CPU時,可能需要編譯多次。

    解釋型語言的優缺點:

    • 適應性強:只需要安裝正確的解釋器,程序在任何CPU上都能夠被運行

    • 速度慢:因為程序需要被逐行翻譯,導致速度變慢。同時因為缺乏編譯這一過程,執行代碼不能通過編譯器進行優化。

    Java的做法是找到編譯型語言和解釋性語言的一個中間點:

    • Java代碼會被編譯:被編譯成Java字節碼,而不是針對某種CPU的二進制代碼。

    • Java代碼會被解釋:Java字節碼需要被java程序解釋執行,此時,Java字節碼被翻譯成CPU相關的二進制代碼。

    • JIT編譯器的作用:在程序運行期間,將Java字節碼編譯成平臺相關的二進制代碼。正因為此編譯行為發生在程序運行期間,所以該編譯器被稱為Just-In-Time編譯器。



    image.png


    image.png

    2.JVM結構


    image.png

    java是基于一門虛擬機的語言,所以了解并且熟知虛擬機運行原理非常重要。

    2.1 方法區

    方法區,Method Area, 對于習慣在HotSpot虛擬機上開發和部署程序的開發者來說,很多人愿意把方法區稱為“永久代”(Permanent Generation),本質上兩者并不等價,僅僅是因為HotSpot虛擬機的設計團隊選擇把GC分代收集擴展至方法區,或者說使用永久代來實現方法區而已。對于其他虛擬機(如BEA JRockit、IBM J9等)來說是不存在永久代的概念的。

    主要存放已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據(比如spring 使用IOC或者AOP創建bean時,或者使用cglib,反射的形式動態生成class信息等)。

    注意:JDK 6 時,String等字符串常量的信息是置于方法區中的,但是到了JDK 7 時,已經移動到了Java堆。所以,方法區也好,Java堆也罷,到底詳細的保存了什么,其實沒有具體定論,要結合不同的JVM版本來分析。

    異常

    當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError。
    運行時常量池溢出:比如一直往常量池加入數據,就會引起OutOfMemoryError異常。

    類信息

  • 類型全限定名。

  • 類型的直接超類的全限定名(除非這個類型是java.lang.Object,它沒有超類)。

  • 類型是類類型還是接口類型。

  • 類型的訪問修飾符(public、abstract或final的某個子集)。

  • 任何直接超接口的全限定名的有序列表。

  • 類型的常量池。

  • 字段信息。

  • 方法信息。

  • 除了常量意外的所有類(靜態)變量。

  • 一個到類ClassLoader的引用。

  • 一個到Class類的引用。

  • 2.1.1 常量池

    2.1.1.1 Class文件中的常量池

    在Class文件結構中,最頭的4個字節用于存儲Megic Number,用于確定一個文件是否能被JVM接受,再接著4個字節用于存儲版本號,前2個字節存儲次版本號,后2個存儲主版本號,再接著是用于存放常量的常量池,由于常量的數量是不固定的,所以常量池的入口放置一個U2類型的數據(constant_pool_count)存儲常量池容量計數值。

    常量池主要用于存放兩大類常量:字面量(Literal)和符號引用量(Symbolic References),字面量相當于Java語言層面常量的概念,如文本字符串,聲明為final的常量值等,符號引用則屬于編譯原理方面的概念,包括了如下三種類型的常量:

    • 類和接口的全限定名

    • 字段名稱和描述符

    • 方法名稱和描述符

    2.1.1.2 運行時常量池

    CLass文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用于存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區的運行時常量池中存放。

    運行時常量池相對于CLass文件常量池的另外一個重要特征是具備動態性,Java語言并不要求常量一定只有編譯期才能產生,也就是并非預置入CLass文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用比較多的就是String類的intern()方法。

    2.1.1.3 常量池的好處

    常量池是為了避免頻繁的創建和銷毀對象而影響系統性能,其實現了對象的共享。

    例如字符串常量池,在編譯階段就把所有的字符串文字放到一個常量池中。

    • (1)節省內存空間:常量池中所有相同的字符串常量被合并,只占用一個空間。

    • (2)節省運行時間:比較字符串時,\==比equals()快。對于兩個引用變量,只用==判斷引用是否相等,也就可以判斷實際值是否相等。

    雙等號==的含義

    • 基本數據類型之間應用雙等號,比較的是他們的數值。

    • 復合數據類型(類)之間應用雙等號,比較的是他們在內存中的存放地址。

    2.1.1.4 基本類型的包裝類和常量池

    java中基本類型的包裝類的大部分都實現了常量池技術,即Byte,Short,Integer,Long,Character,Boolean。

    這5種包裝類默認創建了數值[-128,127]的相應類型的緩存數據,但是超出此范圍仍然會去創建新的對象。 兩種浮點數類型的包裝類Float,Double并沒有實現常量池技術。

    Integer與常量池

    Integer i1 = 40;Integer i2 = 40;Integer i3 = 0;Integer i4 = new Integer(40);Integer i5 = new Integer(40);Integer i6 = new Integer(0);System.out.println("i1=i2 ? " + (i1 == i2));System.out.println("i1=i2+i3 ? " + (i1 == i2 + i3));System.out.println("i1=i4 ? " + (i1 == i4));System.out.println("i4=i5 ? " + (i4 == i5));System.out.println("i4=i5+i6 ? " + (i4 == i5 + i6)); ?System.out.println("40=i5+i6 ? " + (40 == i5 + i6));i1=i2 ? truei1=i2+i3 ? truei1=i4 ? falsei4=i5 ? falsei4=i5+i6 ? true40=i5+i6 ? true

    解釋:

    • (1)Integer i1=40;Java在編譯的時候會直接將代碼封裝成Integer i1=Integer.valueOf(40);,從而使用常量池中的對象。

    • (2)Integer i1 = new Integer(40);這種情況下會創建新的對象。

    • (3)語句i4 == i5 + i6,因為+這個操作符不適用于Integer對象,首先i5和i6進行自動拆箱操作,進行數值相加,即i4 == 40。然后Integer對象無法與數值進行直接比較,所以i4自動拆箱轉為int值40,最終這條語句轉為40 == 40進行數值比較。

    String與常量池

    String str1 = "abcd";String str2 = new String("abcd");System.out.println(str1==str2);//falseString str1 = "str";String str2 = "ing";String str3 = "str" + "ing";String str4 = str1 + str2;System.out.println(str3 == str4);//falseString str5 = "string";System.out.println(str3 == str5);//true

    解釋:

    • (1)new String("abcd")是在常量池中拿對象,"abcd"是直接在堆內存空間創建一個新的對象。只要使用new方法,便需要創建新的對象。

    • (2)連接表達式 +
      只有使用引號包含文本的方式創建的String對象之間使用“+”連接產生的新對象才會被加入字符串池中。
      對于所有包含new方式新建對象(包括null)的“+”連接表達式,它所產生的新對象都不會被加入字符串池中。

    public static final String A; // 常量Apublic static final String B; ? ?// 常量Bstatic { ?A = "ab"; ?B = "cd"; ? } ?public static void main(String[] args) { ?// 將兩個常量用+連接對s進行初始化 ?String s = A + B; ?String t = "abcd"; ?if (s == t) { ?System.out.println("s等于t,它們是同一個對象"); ?} else { ?System.out.println("s不等于t,它們不是同一個對象"); ?} ? }

    解釋:

    s不等于t,它們不是同一個對象。

    A和B雖然被定義為常量,但是它們都沒有馬上被賦值。在運算出s的值之前,他們何時被賦值,以及被賦予什么樣的值,都是個變數。因此A和B在被賦值之前,性質類似于一個變量。那么s就不能在編譯期被確定,而只能在運行時被創建了。

    String s1 = new String("xyz"); //創建了幾個對象?

    解釋:

    考慮類加載階段和實際執行時。

    • (1)類加載對一個類只會進行一次。”xyz”在類加載時就已經創建并駐留了(如果該類被加載之前已經有”xyz”字符串被駐留過則不需要重復創建用于駐留的”xyz”實例)。駐留的字符串是放在全局共享的字符串常量池中的。

    • (2)在這段代碼后續被運行的時候,”xyz”字面量對應的String實例已經固定了,不會再被重復創建。所以這段代碼將常量池中的對象復制一份放到heap中,并且把heap中的這個對象的引用交給s1 持有。

    這條語句創建了2個對象。

    public static void main(String[] args) { String s1 = new String("計算機");String s2 = s1.intern();String s3 = "計算機";System.out.println("s1 == s2? " + (s1 == s2));System.out.println("s3 == s2? " + (s3 == s2));}s1 == s2? falses3 == s2? true

    解釋:

    String的intern()方法會查找在常量池中是否存在一份equal相等的字符串,如果有則返回該字符串的引用,如果沒有則添加自己的字符串進入常量池。

    public class Test {public static void main(String[] args) { String hello = "Hello", lo = "lo";System.out.println((hello == "Hello") + " "); //trueSystem.out.println((Other.hello == hello) + " "); //trueSystem.out.println((other.Other.hello == hello) + " "); //trueSystem.out.println((hello == ("Hel"+"lo")) + " "); //trueSystem.out.println((hello == ("Hel"+lo)) + " "); //falseSystem.out.println(hello == ("Hel"+lo).intern()); //true} }class Other { static String hello = "Hello"; }package other;public class Other { public static String hello = "Hello"; }

    解釋:

    在同包同類下,引用自同一String對象.

    在同包不同類下,引用自同一String對象.

    在不同包不同類下,依然引用自同一String對象.

    在編譯成.class時能夠識別為同一字符串的,自動優化成常量,引用自同一String對象.

    在運行時創建的字符串具有獨立的內存地址,所以不引用自同一String對象.

    2.2 堆

    Heap(堆)是JVM的內存數據區。

    一個虛擬機實例只對應一個堆空間,堆是線程共享的。堆空間是存放對象實例的地方,幾乎所有對象實例都在這里分配。堆也是垃圾收集器管理的主要區域(也被稱為GC堆)。堆可以處于物理上不連續的內存空間中,只要邏輯上相連就行。

    Heap 的管理很復雜,每次分配不定長的內存空間,專門用來保存對象的實例。在Heap 中分配一定的內存來保存對象實例,實際上也只是保存對象實例的屬性值,屬性的類型和對象本身的類型標記等,并不保存對象的方法(方法是指令,保存在Stack中)。而對象實例在Heap中分配好以后,需要在Stack中保存一個4字節的Heap 內存地址,用來定位該對象實例在Heap 中的位置,便于找到該對象實例。

    異常

    堆中沒有足夠的內存進行對象實例分配時,并且堆也無法擴展時,會拋出OutOfMemoryError異常。


    image.png

    2.3 Java棧

    Stack(棧)是JVM的內存指令區。

    描述的是java方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀,用于存放局部變量表(基本類型、對象引用)、操作數棧、方法返回、常量池指針等信息。 由編譯器自動分配釋放, 內存的分配是連續的。Stack的速度很快,管理很簡單,并且每次操作的數據或者指令字節長度是已知的。所以Java 基本數據類型,Java 指令代碼,常量都保存在Stack中。

    虛擬機只會對棧進行兩種操作,以幀為單位的入棧和出棧。Java棧中的每個幀都保存一個方法調用的局部變量、操作數棧、指向常量池的指針等,且每一次方法調用都會創建一個幀,并壓棧。

    異常

    • 如果一個線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError異常, 比如遞歸調用。

    • 如果線程生成數量過多,無法申請足夠多的內存時,則會拋出OutOfMemoryError異常。比如tomcat請求數量非常多時,設置最大請求數。

    2.3.1 棧幀

    棧幀由三部分組成:局部變量區、操作數棧、幀數據區。

    2.3.1.1 局部變量區

    包含方法的參數和局部變量。

    以一個靜態方法為例

    public class Demo { ? ? public static int doStaticMethod(int i, long l, float f, Object o, byte b) { ? ? ? ? return 0;}}

    編譯之后的具備變量表字節碼如下:

    LOCALVARIABLEiIL0L10 LOCALVARIABLElJL0L11 LOCALVARIABLEfFL0L13 LOCALVARIABLEoLjava/lang/Object;L0L14 LOCALVARIABLEbBL0L15 MAXSTACK=1 ? ?//該方法操作棧的最大深度MAXLOCALS=6 ?//確定了該方法所需要分配的最大局部變量表的容量

    可以認為Java棧幀里的局部變量表有很多的槽位組成,每個槽最大可以容納32位的數據類型,故方法參數里的int i 參數占據了一個槽位,而long l 參數就占據了兩個槽(1和2),Object對象類型的參數其實是一個引用,o相當于一個指針,也就是32位大小。byte類型升為int,也是32位大小。如下:

    0 int int i1 long long l3 float float f4 reference Object o5 int byte b

    實例方法的局部變量表和靜態方法基本一樣,唯一區別就是實例方法在Java棧幀的局部變量表里第一個槽位(0位置)存的是一個this引用(當前對象的引用),后面就和靜態方法的一樣了。

    2.3.1.2 操作數棧

    Java沒有寄存器,故所有參數傳遞使用Java棧幀里的操作數棧,操作數棧被組織成一個以字長為單位的數組,它是通過標準的棧操作-入棧和出棧來進行訪問,而不是通過索引訪問。

    看一個例子:


    image.png

    注意,對于局部變量表的槽位,按照從0開始的順序,依次是方法參數,之后是方法內的局部變量,局部變量0就是a,1就是b,2就是c…… 編譯之后的字節碼為:

    // access flags 0x9public static add(II)IL0LINENUMBER 18 L0 // 對應源代碼第18行,以此類推ICONST_0 // 把常量0 push 到Java棧幀的操作數棧里ISTORE 2 // 將0從操作數棧pop到局部變量表槽2里(c),完成賦值L1LINENUMBER 19 L1ILOAD 0 // 將局部變量槽位0(a)push 到Java棧幀的操作數棧里ILOAD 1 // 把局部變量槽1(b)push到操作數棧 IADD // pop出a和b兩個變量,求和,把結果push到操作數棧ISTORE 2 // 把結果從操作數棧pop到局部變量2(a+b的和給c賦值)L2LINENUMBER 21 L2ILOAD 2 // 局部變量2(c)push 到操作數棧IRETURN // 返回結果L3LOCALVARIABLE a I L0 L3 0LOCALVARIABLE b I L0 L3 1LOCALVARIABLE c I L1 L3 2MAXSTACK = 2MAXLOCALS = 3

    發現,整個計算過程的參數傳遞和操作數棧密切相關!如圖:


    image.png

    2.3.1.3 棧數據區

    存放一些用于支持常量池解析(常量池指針)、正常方法返回以及異常派發機制的信息。即將常量池的符號引用轉化為直接地址引用、恢復發起調用的方法的幀進行正常返回,發生異常時轉交異常表進行處理。

    2.4 本地方法棧

    Native Method Stack

    訪問本地方式時使用到的棧,為本地方法服務, 也就是調用虛擬機使用到的Native方法服務。也會拋出StackOverflowError和OutOfMemoryError異常。

    2.5 PC寄存器

    每個線程都擁有一個PC寄存器,線程私有的。
    PC寄存器的內容總是下一條將被執行指令的"地址",這里的"地址"可以是一個本地指針,也可以是在方法字節碼中相對于該方法起始指令的偏移量。如果該線程正在執行一個本地方法,則程序計數器內容為undefined,區域在Java虛擬機規范中沒有規定任何OutOfMemoryError情況的區域。

    2.6 堆與棧

    2.6.1 堆與棧里存什么

    • 1)堆中存的是對象。棧中存的是基本數據類型和堆中對象的引用。一個對象的大小是不可估計的,或者說是可以動態變化的,但是在棧中,一個對象只對應了一個4btye的引用。

    • 2)為什么不把基本類型放堆中呢?因為其占用的空間一般是1~8個字節——需要空間比較少,而且因為是基本類型,所以不會出現動態增長的情況——長度固定,因此棧中存儲就夠了,如果把他存在堆中是沒有什么意義的。可以這么說,基本類型和對象的引用都是存放在棧中,而且都是幾個字節的一個數,因此在程序運行時,他們的處理方式是統一的。但是基本類型、對象引用和對象本身就有所區別了,因為一個是棧中的數據一個是堆中的數據。最常見的一個問題就是,Java中參數傳遞時的問題。

    • 3)Java中的參數傳遞時傳值呢?還是傳引用?程序運行永遠都是在棧中進行的,因而參數傳遞時,只存在傳遞基本類型和對象引用的問題。不會直接傳對象本身。

    int a = 0; //全局初始化區char p1; //全局未初始化區main(){ ?int b; //棧char s[] = "abc"; //棧char p2; //棧char p3 = "123456"; //123456\0在常量區,p3在棧上。static int c =0//全局(靜態)初始化區p1 = (char *)malloc(10); //堆p2 = (char *)malloc(20); //堆}

    2.6.2 堆內存與棧內存的區別

    • 申請和回收方式不同:棧上的空間是自動分配自動回收的,所以棧上的數據的生存周期只是在函數的運行過程中,運行后就釋放掉,不可以再訪問。而堆上的數據只要程序員不釋放空間,就一直可以訪問到,不過缺點是一旦忘記釋放會造成內存泄露。

    • 碎片問題:對于棧,不會產生不連續的內存塊;但是對于堆來說,不斷的new、delete勢必會產生上面所述的內部碎片和外部碎片。

    • 申請大小的限制:棧是向低地址擴展的數據結構,是一塊連續的內存的區域。棧頂的地址和棧的最大容量是系統預先規定好的,如果申請的空間超過棧的剩余空間,就會產生棧溢出;對于堆,是向高地址擴展的數據結構,是不連續的內存區域。堆的大小受限于計算機系統中有效的虛擬內存。由此可見,堆獲得的空間比較靈活,也比較大。

    • 申請效率的比較:棧由系統自動分配,速度較快。但程序員是無法控制的;堆:是由new分配的內存,一般速度比較慢,而且容易產生內存碎片,不過用起來最方便。

    3.JIT編譯器

  • JIT編譯器是JVM的核心。它對于程序性能的影響最大。

  • CPU只能執行匯編代碼或者二進制代碼,所有程序都需要被翻譯成它們,然后才能被CPU執行。

  • C++以及Fortran這類編譯型語言都會通過一個靜態的編譯器將程序編譯成CPU相關的二進制代碼。

  • PHP以及Perl這列語言則是解釋型語言,只需要安裝正確的解釋器,它們就能運行在任何CPU之上。當程序被執行的時候,程序代碼會被逐行解釋并執行。

  • 編譯型語言的優缺點:

    • 速度快:因為在編譯的時候它們能夠獲取到更多的有關程序結構的信息,從而有機會對它們進行優化。

    • 適用性差:它們編譯得到的二進制代碼往往是CPU相關的,在需要適配多種CPU時,可能需要編譯多次。

    解釋型語言的優缺點:

    • 適應性強:只需要安裝正確的解釋器,程序在任何CPU上都能夠被運行

    • 速度慢:因為程序需要被逐行翻譯,導致速度變慢。同時因為缺乏編譯這一過程,執行代碼不能通過編譯器進行優化。

    Java的做法是找到編譯型語言和解釋性語言的一個中間點:

    • Java代碼會被編譯:被編譯成Java字節碼,而不是針對某種CPU的二進制代碼。

    • Java代碼會被解釋:Java字節碼需要被java程序解釋執行,此時,Java字節碼被翻譯成CPU相關的二進制代碼。

    • JIT編譯器的作用:在程序運行期間,將Java字節碼編譯成平臺相關的二進制代碼。正因為此編譯行為發生在程序運行期間,所以該編譯器被稱為Just-In-Time編譯器。

    HotSpot 編譯

    HotSpot VM名字也體現了JIT編譯器的工作方式。在VM開始運行一段代碼時,并不會立即對它們進行編譯。在程序中,總有那么一些“熱點”區域,該區域的代碼會被反復的執行。而JIT編譯器只會編譯這些“熱點”區域的代碼。

    這么做的原因在于:

    * 編譯那些只會被運行一次的代碼性價比太低,直接解釋執行Java字節碼反而更快。* JVM在執行這些代碼的時候,能獲取到這些代碼的信息,一段代碼被執行的次數越多,JVM也對它們愈加熟悉,因此能夠在對它們進行編譯的時候做出一些優化。

    在HotSpot VM中內嵌有兩個JIT編譯器,分別為Client Compiler和Server Compiler,但大多數情況下我們簡稱為C1編譯器和C2編譯器。開發人員可以通過如下命令顯式指定Java虛擬機在運行時到底使用哪一種即時編譯器,如下所示:

    -client:指定Java虛擬機運行在Client模式下,并使用C1編譯器;-server:指定Java虛擬機運行在Server模式下,并使用C2編譯器。

    除了可以顯式指定Java虛擬機在運行時到底使用哪一種即時編譯器外,默認情況下HotSpot VM則會根據操作系統版本與物理機器的硬件性能自動選擇運行在哪一種模式下,以及采用哪一種即時編譯器。簡單來說,C1編譯器會對字節碼進行簡單和可靠的優化,以達到更快的編譯速度;而C2編譯器會啟動一些編譯耗時更長的優化,以獲取更好的編譯質量。不過在Java7版本之后,一旦開發人員在程序中顯式指定命令“-server”時,缺省將會開啟分層編譯(Tiered Compilation)策略,由C1編譯器和C2編譯器相互協作共同來執行編譯任務。不過在早期版本中,開發人員則只能夠通過命令“-XX:+TieredCompilation”手動開啟分層編譯策略。

    總結

  • Java綜合了編譯型語言和解釋性語言的優勢。

  • Java會將類文件編譯成為Java字節碼,然后Java字節碼會被JIT編譯器選擇性地編譯成為CPU能夠直接運行的二進制代碼。

  • 將Java字節碼編譯成二進制代碼后,性能會被大幅度提升。

  • 4.類加載機制

    Java虛擬機把描述類的數據從Class文件加載到內存,并對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的加載機制。

    類從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期包括了:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(using)、和卸載(Unloading)七個階段。其中驗證、準備和解析三個部分統稱為連接(Linking),這七個階段的發生順序如下圖所示:


    image.png

    如上圖所示,加載、驗證、準備、初始化和卸載這五個階段的順序是確定的,類的加載過程必須按照這個順序來按部就班地開始,而解析階段則不一定,它在某些情況下可以在初始化階段后再開始。

    類的生命周期的每一個階段通常都是互相交叉混合式進行的,通常會在一個階段執行的過程中調用或激活另外一個階段。

    4.1 類加載的時機

    主動引用

    一個類被主動引用之后會觸發初始化過程(加載,驗證,準備需再此之前開始)

    • 1)遇到new、get static、put static或invoke static這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令最常見的Java代碼場景是:使用new關鍵字實例化對象時、讀取或者設置一個類的靜態字段(被final修飾、已在編譯器把結果放入常量池的靜態字段除外)時、以及調用一個類的靜態方法的時候。

    • 2)使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。

    • 3)當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要觸發父類的初始化。

    • 4)當虛擬機啟動時,用戶需要指定一個執行的主類(包含main()方法的類),虛擬機會先初始化這個類。

    • 5)當使用jdk7+的動態語言支持時,如果java.lang.invoke.MethodHandle實例最后的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發器 初始化。

    被動引用

    一個類如果是被動引用的話,該類不會觸發初始化過程

    • 1)通過子類引用父類的靜態字段,不會導致子類初始化。對于靜態字段,只有直接定義該字段的類才會被初始化,因此當我們通過子類來引用父類中定義的靜態字段時,只會觸發父類的初始化,而不會觸發子類的初始化。

    • 2)通過數組定義來引用類,不會觸發此類的初始化。

    • 3)常量在編譯階段會存入調用類的常量池中,本質上沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。

    4.2 類加載過程

    1、加載

    在加載階段,虛擬機需要完成以下三件事情:

    • 1)通過一個類的全限定名稱來獲取定義此類的二進制字節流。

    • 2)將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。

    • 3)在java堆中生成一個代表這個類的java.lang.Class對象,作為方法區這些數據的訪問入口。
      相對于類加載過程的其他階段,加載階段是開發期相對來說可控性比較強,該階段既可以使用系統提供的類加載器完成,也可以由用戶自定義的類加載器來完成,開發人員可以通過定義自己的類加載器去控制字節流的獲取方式。

    2、驗證

    驗證的目的是為了確保Class文件中的字節流包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。不同的虛擬機對類驗證的實現可能會有所不同,但大致都會完成以下四個階段的驗證:文件格式的驗證、元數據的驗證、字節碼驗證和符號引用驗證。

    • 1)文件格式的驗證:驗證字節流是否符合Class文件格式的規范,并且能被當前版本的虛擬機處理,該驗證的主要目的是保證輸入的字節流能正確地解析并存儲
      于方法區之內。經過該階段的驗證后,字節流才會進入內存的方法區中進行存儲,后面的三個驗證都是基于方法區的存儲結構進行的。

    • 2)元數據驗證:對類的元數據信息進行語義校驗(其實就是對類中的各數據類型進行語法校驗),保證不存在不符合Java語法規范的元數據信息。

    • 3)字節碼驗證:該階段驗證的主要工作是進行數據流和控制流分析,對類的方法體進行校驗分析,以保證被校驗的類的方法在運行時不會做出危害虛擬機安全的行為。

    • 4)符號引用驗證:這是最后一個階段的驗證,它發生在虛擬機將符號引用轉化為直接引用的時候(解析階段中發生該轉化,后面會有講解),主要是對類自身以外的信息(常量池中的各種符號引用)進行匹配性的校驗。

    3、準備

    準備階段是正式為類變量分配內存并設置類變量初始值的階段,這些內存都將在方法區中進行分配。

    注:

    • 1)這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨著對象一塊分配在Java堆中。

    • 2)這里所設置的初始值通常情況下是數據類型默認的零值(如0、0L、、false等),而不是被在Java代碼中被顯式地賦予的值。

    4、解析

    解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程

    符號引用(Symbolic Reference):

    符號引用以一組符號來描述所引用的目標,符號引用可以是任何形式的字面量,符號引用與虛擬機實現的內存布局無關,引用的目標并不一定已經在內存中。

    直接引用(Direct Reference):

    直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現的內存布局相關的,同一個符號引用在不同的虛擬機實例上翻譯出來的直接引用一般都不相同,如果有了直接引用,那引用的目標必定已經在內存中存在。

    • 1)類或接口的解析:判斷所要轉化成的直接引用是對數組類型,還是普通的對象類型的引用,從而進行不同的解析。

    • 2)字段解析:對字段進行解析時,會先在本類中查找是否包含有簡單名稱和字段描述符都與目標相匹配的字段,如果有,則查找結束;如果沒有,則會按照繼承關系從上往下遞歸搜索該類所實現的各個接口和它們的父接口,還沒有,則按照繼承關系從上往下遞歸搜索其父類,直至查找結束。

    • 3)類方法解析:對類方法的解析與對字段解析的搜索步驟差不多,只是多了判斷該方法所處的是類還是接口的步驟,而且對類方法的匹配搜索,是先搜索父類,再搜索接口。

    • 4)接口方法解析:與類方法解析步驟類似,只是接口不會有父類,因此,只遞歸向上搜索父接口就行了。

    5、初始化

    類初始化階段是類加載過程的最后一步,前面的類加載過程中,除了加載(Loading)階段用戶應用程序可以通過自定義類加載器參與之外,其余動作完全由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的Java程序代碼。

    初始化階段是執行類構造器<clinit>方法的過程。

    • 1)<clinit>方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{}塊)中的語句合并產生的,編譯器收集的順序由語句在源文件中出現的順序所決定。

    • 2)<clinit>方法與類的構造函數不同,它不需要顯式地調用父類構造器,虛擬機會保證在子類的<clinit>方法執行之前,父類的<clinit>方法已經執行完畢,因此在虛擬機中第一個執行的<clinit>方法的類一定是java.lang.Object。

    • 3)由于父類的<clinit>方法先執行,也就意味著父類中定義的靜態語句塊要優先于子類的變量賦值操作。

    • 4)<clinit>方法對于類或者接口來說并不是必需的,如果一個類中沒有靜態語句塊也沒有對變量的賦值操作,那么編譯器可以不為這個類生成<clinit>方法。

    • 5)接口中可能會有變量賦值操作,因此接口也會生成<clinit>方法。但是接口與類不同,執行接口的<clinit>方法不需要先執行父接口的<clinit>方法。只有當父接口中定義的變量被使用時,父接口才會被初始化。另外,接口的實現類在初始化時也不會執行接口的<clinit>方法。

    • 6)虛擬機會保證一個類的<clinit>方法在多線程環境中被正確地加鎖和同步。如果有多個線程去同時初始化一個類,那么只會有一個線程去執行這個類的<clinit>方法,其它線程都需要阻塞等待,直到活動線程執行<clinit>方法完畢。如果在一個類的<clinit>方法中有耗時很長的操作,那么就可能造成多個進程阻塞。






    總結

    以上是生活随笔為你收集整理的GC和JVM调优实战的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。