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

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

GC和JVM调优实战

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


轉(zhuǎn)載

目錄


  • JVM簡(jiǎn)介

  • JVM結(jié)構(gòu)
    2.1 方法區(qū)

    2.1.1 常量池2.1.1.1 Class文件中的常量池2.1.1.2 運(yùn)行時(shí)常量池2.1.1.3 常量池的好處2.1.1.4 基本類型的包裝類和常量池

    2.2 堆
    2.3 Java棧

    2.3.1 棧幀2.3.1.1 局部變量區(qū)2.3.1.2 操作數(shù)棧2.3.1.3 棧數(shù)據(jù)區(qū)

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

    2.6.1 堆與棧里存什么2.6.2 堆內(nèi)存與棧內(nèi)存的區(qū)別
  • JIT編譯器

  • 類加載機(jī)制
    4.1 類加載的時(shí)機(jī)
    4.2 類加載過程

  • 垃圾回收
    5.1 按代實(shí)現(xiàn)垃圾回收
    5.2 怎樣判斷對(duì)象是否已經(jīng)死亡
    5.3 java中的引用
    5.4 finalize方法什么作用
    5.5 垃圾收集算法
    5.6 Hotspot實(shí)現(xiàn)垃圾回收細(xì)節(jié)
    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參數(shù)
    6.1 典型配置

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

    6.2 參數(shù)詳細(xì)說明

  • JVM性能調(diào)優(yōu)
    7.1 堆設(shè)置調(diào)優(yōu)
    7.2 GC策略調(diào)優(yōu)
    7.3 JIT調(diào)優(yōu)
    7.4 JVM線程調(diào)優(yōu)
    7.5 典型案例

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


  • 1.JVM簡(jiǎn)介

    JVM是java的核心和基礎(chǔ),在java編譯器和os平臺(tái)之間的虛擬處理器。它是一種利用軟件方法實(shí)現(xiàn)的抽象的計(jì)算機(jī)基于下層的操作系統(tǒng)和硬件平臺(tái),可以在上面執(zhí)行java的字節(jié)碼程序。

    java編譯器只要面向JVM,生成JVM能理解的代碼或字節(jié)碼文件。Java源文件經(jīng)編譯成字節(jié)碼程序,通過JVM將每一條指令翻譯成不同平臺(tái)機(jī)器碼,通過特定平臺(tái)運(yùn)行。

    運(yùn)行過程

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

    C++以及Fortran這類編譯型語言都會(huì)通過一個(gè)靜態(tài)的編譯器將程序編譯成CPU相關(guān)的二進(jìn)制代碼。

    PHP以及Perl這列語言則是解釋型語言,只需要安裝正確的解釋器,它們就能運(yùn)行在任何CPU之上。當(dāng)程序被執(zhí)行的時(shí)候,程序代碼會(huì)被逐行解釋并執(zhí)行。


  • 編譯型語言的優(yōu)缺點(diǎn):

    • 速度快:因?yàn)樵诰幾g的時(shí)候它們能夠獲取到更多的有關(guān)程序結(jié)構(gòu)的信息,從而有機(jī)會(huì)對(duì)它們進(jìn)行優(yōu)化。

    • 適用性差:它們編譯得到的二進(jìn)制代碼往往是CPU相關(guān)的,在需要適配多種CPU時(shí),可能需要編譯多次。

    解釋型語言的優(yōu)缺點(diǎn):

    • 適應(yīng)性強(qiáng):只需要安裝正確的解釋器,程序在任何CPU上都能夠被運(yùn)行

    • 速度慢:因?yàn)槌绦蛐枰恢鹦蟹g,導(dǎo)致速度變慢。同時(shí)因?yàn)槿狈幾g這一過程,執(zhí)行代碼不能通過編譯器進(jìn)行優(yōu)化。

    Java的做法是找到編譯型語言和解釋性語言的一個(gè)中間點(diǎn):

    • Java代碼會(huì)被編譯:被編譯成Java字節(jié)碼,而不是針對(duì)某種CPU的二進(jìn)制代碼。

    • Java代碼會(huì)被解釋:Java字節(jié)碼需要被java程序解釋執(zhí)行,此時(shí),Java字節(jié)碼被翻譯成CPU相關(guān)的二進(jìn)制代碼。

    • JIT編譯器的作用:在程序運(yùn)行期間,將Java字節(jié)碼編譯成平臺(tái)相關(guān)的二進(jìn)制代碼。正因?yàn)榇司幾g行為發(fā)生在程序運(yùn)行期間,所以該編譯器被稱為Just-In-Time編譯器。



    image.png


    image.png

    2.JVM結(jié)構(gòu)


    image.png

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

    2.1 方法區(qū)

    方法區(qū),Method Area, 對(duì)于習(xí)慣在HotSpot虛擬機(jī)上開發(fā)和部署程序的開發(fā)者來說,很多人愿意把方法區(qū)稱為“永久代”(Permanent Generation),本質(zhì)上兩者并不等價(jià),僅僅是因?yàn)镠otSpot虛擬機(jī)的設(shè)計(jì)團(tuán)隊(duì)選擇把GC分代收集擴(kuò)展至方法區(qū),或者說使用永久代來實(shí)現(xiàn)方法區(qū)而已。對(duì)于其他虛擬機(jī)(如BEA JRockit、IBM J9等)來說是不存在永久代的概念的。

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

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

    異常

    當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時(shí),將拋出OutOfMemoryError。
    運(yùn)行時(shí)常量池溢出:比如一直往常量池加入數(shù)據(jù),就會(huì)引起OutOfMemoryError異常。

    類信息

  • 類型全限定名。

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

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

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

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

  • 類型的常量池。

  • 字段信息。

  • 方法信息。

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

  • 一個(gè)到類ClassLoader的引用。

  • 一個(gè)到Class類的引用。

  • 2.1.1 常量池

    2.1.1.1 Class文件中的常量池

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

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

    • 類和接口的全限定名

    • 字段名稱和描述符

    • 方法名稱和描述符

    2.1.1.2 運(yùn)行時(shí)常量池

    CLass文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池,用于存放編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。

    運(yùn)行時(shí)常量池相對(duì)于CLass文件常量池的另外一個(gè)重要特征是具備動(dòng)態(tài)性,Java語言并不要求常量一定只有編譯期才能產(chǎn)生,也就是并非預(yù)置入CLass文件中常量池的內(nèi)容才能進(jìn)入方法區(qū)運(yùn)行時(shí)常量池,運(yùn)行期間也可能將新的常量放入池中,這種特性被開發(fā)人員利用比較多的就是String類的intern()方法。

    2.1.1.3 常量池的好處

    常量池是為了避免頻繁的創(chuàng)建和銷毀對(duì)象而影響系統(tǒng)性能,其實(shí)現(xiàn)了對(duì)象的共享。

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

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

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

    雙等號(hào)==的含義

    • 基本數(shù)據(jù)類型之間應(yīng)用雙等號(hào),比較的是他們的數(shù)值。

    • 復(fù)合數(shù)據(jù)類型(類)之間應(yīng)用雙等號(hào),比較的是他們?cè)趦?nèi)存中的存放地址。

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

    java中基本類型的包裝類的大部分都實(shí)現(xiàn)了常量池技術(shù),即Byte,Short,Integer,Long,Character,Boolean。

    這5種包裝類默認(rèn)創(chuàng)建了數(shù)值[-128,127]的相應(yīng)類型的緩存數(shù)據(jù),但是超出此范圍仍然會(huì)去創(chuàng)建新的對(duì)象。 兩種浮點(diǎn)數(shù)類型的包裝類Float,Double并沒有實(shí)現(xiàn)常量池技術(shù)。

    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在編譯的時(shí)候會(huì)直接將代碼封裝成Integer i1=Integer.valueOf(40);,從而使用常量池中的對(duì)象。

    • (2)Integer i1 = new Integer(40);這種情況下會(huì)創(chuàng)建新的對(duì)象。

    • (3)語句i4 == i5 + i6,因?yàn)?#43;這個(gè)操作符不適用于Integer對(duì)象,首先i5和i6進(jìn)行自動(dòng)拆箱操作,進(jìn)行數(shù)值相加,即i4 == 40。然后Integer對(duì)象無法與數(shù)值進(jìn)行直接比較,所以i4自動(dòng)拆箱轉(zhuǎn)為int值40,最終這條語句轉(zhuǎn)為40 == 40進(jìn)行數(shù)值比較。

    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")是在常量池中拿對(duì)象,"abcd"是直接在堆內(nèi)存空間創(chuàng)建一個(gè)新的對(duì)象。只要使用new方法,便需要?jiǎng)?chuàng)建新的對(duì)象。

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

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

    解釋:

    s不等于t,它們不是同一個(gè)對(duì)象。

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

    String s1 = new String("xyz"); //創(chuàng)建了幾個(gè)對(duì)象?

    解釋:

    考慮類加載階段和實(shí)際執(zhí)行時(shí)。

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

    • (2)在這段代碼后續(xù)被運(yùn)行的時(shí)候,”xyz”字面量對(duì)應(yīng)的String實(shí)例已經(jīng)固定了,不會(huì)再被重復(fù)創(chuàng)建。所以這段代碼將常量池中的對(duì)象復(fù)制一份放到heap中,并且把heap中的這個(gè)對(duì)象的引用交給s1 持有。

    這條語句創(chuàng)建了2個(gè)對(duì)象。

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

    解釋:

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

    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對(duì)象.

    在同包不同類下,引用自同一String對(duì)象.

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

    在編譯成.class時(shí)能夠識(shí)別為同一字符串的,自動(dòng)優(yōu)化成常量,引用自同一String對(duì)象.

    在運(yùn)行時(shí)創(chuàng)建的字符串具有獨(dú)立的內(nèi)存地址,所以不引用自同一String對(duì)象.

    2.2 堆

    Heap(堆)是JVM的內(nèi)存數(shù)據(jù)區(qū)。

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

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

    異常

    堆中沒有足夠的內(nèi)存進(jìn)行對(duì)象實(shí)例分配時(shí),并且堆也無法擴(kuò)展時(shí),會(huì)拋出OutOfMemoryError異常。


    image.png

    2.3 Java棧

    Stack(棧)是JVM的內(nèi)存指令區(qū)。

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

    虛擬機(jī)只會(huì)對(duì)棧進(jìn)行兩種操作,以幀為單位的入棧和出棧。Java棧中的每個(gè)幀都保存一個(gè)方法調(diào)用的局部變量、操作數(shù)棧、指向常量池的指針等,且每一次方法調(diào)用都會(huì)創(chuàng)建一個(gè)幀,并壓棧。

    異常

    • 如果一個(gè)線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常, 比如遞歸調(diào)用。

    • 如果線程生成數(shù)量過多,無法申請(qǐng)足夠多的內(nèi)存時(shí),則會(huì)拋出OutOfMemoryError異常。比如tomcat請(qǐng)求數(shù)量非常多時(shí),設(shè)置最大請(qǐng)求數(shù)。

    2.3.1 棧幀

    棧幀由三部分組成:局部變量區(qū)、操作數(shù)棧、幀數(shù)據(jù)區(qū)。

    2.3.1.1 局部變量區(qū)

    包含方法的參數(shù)和局部變量。

    以一個(gè)靜態(tài)方法為例

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

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

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

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

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

    實(shí)例方法的局部變量表和靜態(tài)方法基本一樣,唯一區(qū)別就是實(shí)例方法在Java棧幀的局部變量表里第一個(gè)槽位(0位置)存的是一個(gè)this引用(當(dāng)前對(duì)象的引用),后面就和靜態(tài)方法的一樣了。

    2.3.1.2 操作數(shù)棧

    Java沒有寄存器,故所有參數(shù)傳遞使用Java棧幀里的操作數(shù)棧,操作數(shù)棧被組織成一個(gè)以字長(zhǎng)為單位的數(shù)組,它是通過標(biāo)準(zhǔn)的棧操作-入棧和出棧來進(jìn)行訪問,而不是通過索引訪問。

    看一個(gè)例子:


    image.png

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

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

    發(fā)現(xiàn),整個(gè)計(jì)算過程的參數(shù)傳遞和操作數(shù)棧密切相關(guān)!如圖:


    image.png

    2.3.1.3 棧數(shù)據(jù)區(qū)

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

    2.4 本地方法棧

    Native Method Stack

    訪問本地方式時(shí)使用到的棧,為本地方法服務(wù), 也就是調(diào)用虛擬機(jī)使用到的Native方法服務(wù)。也會(huì)拋出StackOverflowError和OutOfMemoryError異常。

    2.5 PC寄存器

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

    2.6 堆與棧

    2.6.1 堆與棧里存什么

    • 1)堆中存的是對(duì)象。棧中存的是基本數(shù)據(jù)類型和堆中對(duì)象的引用。一個(gè)對(duì)象的大小是不可估計(jì)的,或者說是可以動(dòng)態(tài)變化的,但是在棧中,一個(gè)對(duì)象只對(duì)應(yīng)了一個(gè)4btye的引用。

    • 2)為什么不把基本類型放堆中呢?因?yàn)槠湔加玫目臻g一般是1~8個(gè)字節(jié)——需要空間比較少,而且因?yàn)槭腔绢愋?#xff0c;所以不會(huì)出現(xiàn)動(dòng)態(tài)增長(zhǎng)的情況——長(zhǎng)度固定,因此棧中存儲(chǔ)就夠了,如果把他存在堆中是沒有什么意義的。可以這么說,基本類型和對(duì)象的引用都是存放在棧中,而且都是幾個(gè)字節(jié)的一個(gè)數(shù),因此在程序運(yùn)行時(shí),他們的處理方式是統(tǒng)一的。但是基本類型、對(duì)象引用和對(duì)象本身就有所區(qū)別了,因?yàn)橐粋€(gè)是棧中的數(shù)據(jù)一個(gè)是堆中的數(shù)據(jù)。最常見的一個(gè)問題就是,Java中參數(shù)傳遞時(shí)的問題。

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

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

    2.6.2 堆內(nèi)存與棧內(nèi)存的區(qū)別

    • 申請(qǐng)和回收方式不同:棧上的空間是自動(dòng)分配自動(dòng)回收的,所以棧上的數(shù)據(jù)的生存周期只是在函數(shù)的運(yùn)行過程中,運(yùn)行后就釋放掉,不可以再訪問。而堆上的數(shù)據(jù)只要程序員不釋放空間,就一直可以訪問到,不過缺點(diǎn)是一旦忘記釋放會(huì)造成內(nèi)存泄露。

    • 碎片問題:對(duì)于棧,不會(huì)產(chǎn)生不連續(xù)的內(nèi)存塊;但是對(duì)于堆來說,不斷的new、delete勢(shì)必會(huì)產(chǎn)生上面所述的內(nèi)部碎片和外部碎片。

    • 申請(qǐng)大小的限制:棧是向低地址擴(kuò)展的數(shù)據(jù)結(jié)構(gòu),是一塊連續(xù)的內(nèi)存的區(qū)域。棧頂?shù)牡刂泛蜅5淖畲笕萘渴窍到y(tǒng)預(yù)先規(guī)定好的,如果申請(qǐng)的空間超過棧的剩余空間,就會(huì)產(chǎn)生棧溢出;對(duì)于堆,是向高地址擴(kuò)展的數(shù)據(jù)結(jié)構(gòu),是不連續(xù)的內(nèi)存區(qū)域。堆的大小受限于計(jì)算機(jī)系統(tǒng)中有效的虛擬內(nèi)存。由此可見,堆獲得的空間比較靈活,也比較大。

    • 申請(qǐng)效率的比較:棧由系統(tǒng)自動(dòng)分配,速度較快。但程序員是無法控制的;堆:是由new分配的內(nèi)存,一般速度比較慢,而且容易產(chǎn)生內(nèi)存碎片,不過用起來最方便。

    3.JIT編譯器

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

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

  • C++以及Fortran這類編譯型語言都會(huì)通過一個(gè)靜態(tài)的編譯器將程序編譯成CPU相關(guān)的二進(jìn)制代碼。

  • PHP以及Perl這列語言則是解釋型語言,只需要安裝正確的解釋器,它們就能運(yùn)行在任何CPU之上。當(dāng)程序被執(zhí)行的時(shí)候,程序代碼會(huì)被逐行解釋并執(zhí)行。

  • 編譯型語言的優(yōu)缺點(diǎn):

    • 速度快:因?yàn)樵诰幾g的時(shí)候它們能夠獲取到更多的有關(guān)程序結(jié)構(gòu)的信息,從而有機(jī)會(huì)對(duì)它們進(jìn)行優(yōu)化。

    • 適用性差:它們編譯得到的二進(jìn)制代碼往往是CPU相關(guān)的,在需要適配多種CPU時(shí),可能需要編譯多次。

    解釋型語言的優(yōu)缺點(diǎn):

    • 適應(yīng)性強(qiáng):只需要安裝正確的解釋器,程序在任何CPU上都能夠被運(yùn)行

    • 速度慢:因?yàn)槌绦蛐枰恢鹦蟹g,導(dǎo)致速度變慢。同時(shí)因?yàn)槿狈幾g這一過程,執(zhí)行代碼不能通過編譯器進(jìn)行優(yōu)化。

    Java的做法是找到編譯型語言和解釋性語言的一個(gè)中間點(diǎn):

    • Java代碼會(huì)被編譯:被編譯成Java字節(jié)碼,而不是針對(duì)某種CPU的二進(jìn)制代碼。

    • Java代碼會(huì)被解釋:Java字節(jié)碼需要被java程序解釋執(zhí)行,此時(shí),Java字節(jié)碼被翻譯成CPU相關(guān)的二進(jìn)制代碼。

    • JIT編譯器的作用:在程序運(yùn)行期間,將Java字節(jié)碼編譯成平臺(tái)相關(guān)的二進(jìn)制代碼。正因?yàn)榇司幾g行為發(fā)生在程序運(yùn)行期間,所以該編譯器被稱為Just-In-Time編譯器。

    HotSpot 編譯

    HotSpot VM名字也體現(xiàn)了JIT編譯器的工作方式。在VM開始運(yùn)行一段代碼時(shí),并不會(huì)立即對(duì)它們進(jìn)行編譯。在程序中,總有那么一些“熱點(diǎn)”區(qū)域,該區(qū)域的代碼會(huì)被反復(fù)的執(zhí)行。而JIT編譯器只會(huì)編譯這些“熱點(diǎn)”區(qū)域的代碼。

    這么做的原因在于:

    * 編譯那些只會(huì)被運(yùn)行一次的代碼性價(jià)比太低,直接解釋執(zhí)行Java字節(jié)碼反而更快。* JVM在執(zhí)行這些代碼的時(shí)候,能獲取到這些代碼的信息,一段代碼被執(zhí)行的次數(shù)越多,JVM也對(duì)它們愈加熟悉,因此能夠在對(duì)它們進(jìn)行編譯的時(shí)候做出一些優(yōu)化。

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

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

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

    總結(jié)

  • Java綜合了編譯型語言和解釋性語言的優(yōu)勢(shì)。

  • Java會(huì)將類文件編譯成為Java字節(jié)碼,然后Java字節(jié)碼會(huì)被JIT編譯器選擇性地編譯成為CPU能夠直接運(yùn)行的二進(jìn)制代碼。

  • 將Java字節(jié)碼編譯成二進(jìn)制代碼后,性能會(huì)被大幅度提升。

  • 4.類加載機(jī)制

    Java虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型,這就是虛擬機(jī)的加載機(jī)制。

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


    image.png

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

    類的生命周期的每一個(gè)階段通常都是互相交叉混合式進(jìn)行的,通常會(huì)在一個(gè)階段執(zhí)行的過程中調(diào)用或激活另外一個(gè)階段。

    4.1 類加載的時(shí)機(jī)

    主動(dòng)引用

    一個(gè)類被主動(dòng)引用之后會(huì)觸發(fā)初始化過程(加載,驗(yàn)證,準(zhǔn)備需再此之前開始)

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

    • 2)使用java.lang.reflect包的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候,如果類沒有進(jìn)行過初始化,則需要先觸發(fā)其初始化。

    • 3)當(dāng)初始化一個(gè)類的時(shí)候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化,則需要觸發(fā)父類的初始化。

    • 4)當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要指定一個(gè)執(zhí)行的主類(包含main()方法的類),虛擬機(jī)會(huì)先初始化這個(gè)類。

    • 5)當(dāng)使用jdk7+的動(dòng)態(tài)語言支持時(shí),如果java.lang.invoke.MethodHandle實(shí)例最后的解析結(jié)果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個(gè)方法句柄所對(duì)應(yīng)的類沒有進(jìn)行過初始化,則需要先觸發(fā)器 初始化。

    被動(dòng)引用

    一個(gè)類如果是被動(dòng)引用的話,該類不會(huì)觸發(fā)初始化過程

    • 1)通過子類引用父類的靜態(tài)字段,不會(huì)導(dǎo)致子類初始化。對(duì)于靜態(tài)字段,只有直接定義該字段的類才會(huì)被初始化,因此當(dāng)我們通過子類來引用父類中定義的靜態(tài)字段時(shí),只會(huì)觸發(fā)父類的初始化,而不會(huì)觸發(fā)子類的初始化。

    • 2)通過數(shù)組定義來引用類,不會(huì)觸發(fā)此類的初始化。

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

    4.2 類加載過程

    1、加載

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

    • 1)通過一個(gè)類的全限定名稱來獲取定義此類的二進(jìn)制字節(jié)流。

    • 2)將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。

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

    2、驗(yàn)證

    驗(yàn)證的目的是為了確保Class文件中的字節(jié)流包含的信息符合當(dāng)前虛擬機(jī)的要求,而且不會(huì)危害虛擬機(jī)自身的安全。不同的虛擬機(jī)對(duì)類驗(yàn)證的實(shí)現(xiàn)可能會(huì)有所不同,但大致都會(huì)完成以下四個(gè)階段的驗(yàn)證:文件格式的驗(yàn)證、元數(shù)據(jù)的驗(yàn)證、字節(jié)碼驗(yàn)證和符號(hào)引用驗(yàn)證。

    • 1)文件格式的驗(yàn)證:驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理,該驗(yàn)證的主要目的是保證輸入的字節(jié)流能正確地解析并存儲(chǔ)
      于方法區(qū)之內(nèi)。經(jīng)過該階段的驗(yàn)證后,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法區(qū)中進(jìn)行存儲(chǔ),后面的三個(gè)驗(yàn)證都是基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)進(jìn)行的。

    • 2)元數(shù)據(jù)驗(yàn)證:對(duì)類的元數(shù)據(jù)信息進(jìn)行語義校驗(yàn)(其實(shí)就是對(duì)類中的各數(shù)據(jù)類型進(jìn)行語法校驗(yàn)),保證不存在不符合Java語法規(guī)范的元數(shù)據(jù)信息。

    • 3)字節(jié)碼驗(yàn)證:該階段驗(yàn)證的主要工作是進(jìn)行數(shù)據(jù)流和控制流分析,對(duì)類的方法體進(jìn)行校驗(yàn)分析,以保證被校驗(yàn)的類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的行為。

    • 4)符號(hào)引用驗(yàn)證:這是最后一個(gè)階段的驗(yàn)證,它發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候(解析階段中發(fā)生該轉(zhuǎn)化,后面會(huì)有講解),主要是對(duì)類自身以外的信息(常量池中的各種符號(hào)引用)進(jìn)行匹配性的校驗(yàn)。

    3、準(zhǔn)備

    準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些內(nèi)存都將在方法區(qū)中進(jìn)行分配。

    注:

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

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

    4、解析

    解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過程

    符號(hào)引用(Symbolic Reference):

    符號(hào)引用以一組符號(hào)來描述所引用的目標(biāo),符號(hào)引用可以是任何形式的字面量,符號(hào)引用與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局無關(guān),引用的目標(biāo)并不一定已經(jīng)在內(nèi)存中。

    直接引用(Direct Reference):

    直接引用可以是直接指向目標(biāo)的指針、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。直接引用是與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局相關(guān)的,同一個(gè)符號(hào)引用在不同的虛擬機(jī)實(shí)例上翻譯出來的直接引用一般都不相同,如果有了直接引用,那引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在。

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

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

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

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

    5、初始化

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

    初始化階段是執(zhí)行類構(gòu)造器<clinit>方法的過程。

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

    • 2)<clinit>方法與類的構(gòu)造函數(shù)不同,它不需要顯式地調(diào)用父類構(gòu)造器,虛擬機(jī)會(huì)保證在子類的<clinit>方法執(zhí)行之前,父類的<clinit>方法已經(jīng)執(zhí)行完畢,因此在虛擬機(jī)中第一個(gè)執(zhí)行的<clinit>方法的類一定是java.lang.Object。

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

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

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

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






    總結(jié)

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

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。