GC和JVM调优实战
轉(zhuǎn)載
目錄
JVM簡(jiǎn)介
JVM結(jié)構(gòu)
2.1 方法區(qū)
2.2 堆
2.3 Java棧
2.4 本地方法棧
2.5 PC寄存器
2.6 堆與棧
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 垃圾收集器
JVM參數(shù)
6.1 典型配置
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ì)被加入字符串池中。
解釋:
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ì)象本身。
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)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深度学习:向人工智能迈进
- 下一篇: Oracle 12C -- sequen