柚子快報激活碼778899分享:JVM進階(2)
一)方法區(qū):
?java虛擬機中有一個方法區(qū),該區(qū)域被所有的java線程都是共享,虛擬機一啟動,運行時數(shù)據(jù)區(qū)就被開辟好了,官網(wǎng)上說了方法區(qū)可以不壓縮還可以不進行GC,JAVA虛擬機就相當(dāng)于是接口,具體的HotSpot就是虛擬機的實現(xiàn),因為永久代還是使用的是JAVA虛擬機的內(nèi)存,
方法區(qū)域可以是固定大小的,也可以根據(jù)計算的需要擴展,如果不需要更大的方法區(qū)域,則可以收縮,物理上是不連續(xù)的,在邏輯上是連續(xù)的;
1)方法區(qū)和JAVA堆一樣,是各個線程共享的內(nèi)存區(qū)域
2)方法區(qū)在JVM啟動的時候就被創(chuàng)建,并且它的實際的物理內(nèi)存空間和JAVA隊去一樣都可以是不連續(xù)的
3)方法區(qū)的大小和堆空間一樣,可以選擇固定大小或者是可擴展
4)方法去的大小決定了系統(tǒng)可以保存多少各類,如果系統(tǒng)定義了太多的類,導(dǎo)致方法去溢出,虛擬機同樣也會拋出內(nèi)存溢出錯誤,java.lang.OutOfMemory:perm space(永久代空間溢出)或者是java.lang.OutOfMemeory:MeatSpace(元空間空間溢出),比如說大量加載第三方j(luò)ar包,Tomact部署的工程過多,大量的動態(tài)生成反射類,加載大量第三方j(luò)ar包,Tomact部署的應(yīng)用程序太多,一個簡單的代碼可能要加載很多類,動態(tài)生成反射類,比如說動態(tài)代理,元空間不在虛擬機設(shè)置的內(nèi)存中,而是使用本地內(nèi)存物理內(nèi)存,字符串常量池和靜態(tài)變量也會變化,但是如果超過本地內(nèi)存上限,也會發(fā)生OOM metaSpace,因為方法區(qū)是不共享的,所以說只能有一個類可以調(diào)用類加載器實現(xiàn)類加載;
5)關(guān)閉JVM就會釋放這個區(qū)域的內(nèi)存,JDK7以前,習(xí)慣上把方法區(qū)稱之為是永久代,JDK8開始使用元空間替代了永久代,本質(zhì)上方法區(qū)和永久代并不是等價的,但是僅僅是針對于HotSpot虛擬機而言的;也就是JAVA虛擬機規(guī)范,對于如何實現(xiàn)方法區(qū)不會做統(tǒng)一要求,現(xiàn)在來看使用永久代共容易出現(xiàn)OOM;
6)元空間的本質(zhì)和永久代類似,都是對JVM規(guī)范中方法去的實現(xiàn),不過元空間和永久代最大的區(qū)別在于元空間不在虛擬機設(shè)置的內(nèi)存中,二時使用的是本地內(nèi)存,永久代和元空間不光名字變了,況且內(nèi)部結(jié)構(gòu)也調(diào)整了,根據(jù)JAVA虛擬機規(guī)范規(guī)定,如果方法去無法滿足新的內(nèi)存分配的需求的時候,會拋出OOM異常,本地內(nèi)存是無限大的,況且靜態(tài)變量字符串常量池等等也會變化;
-XX:MetaspaceSize=100m,-XX:MaxMetaspaceSize=100m
一般在實際開發(fā)中會進行設(shè)置MetaspaceSize,不會設(shè)置MaxMetaspaceSize是默認值-1即可,這個Metaspace一開始要設(shè)置的大一些,為了避免頻繁的發(fā)生FullGC導(dǎo)致調(diào)整水平線
方法區(qū)用于存放已經(jīng)被虛擬機加載的類型信息,常量,靜態(tài)變量以及即時編譯器編譯過后的代碼緩存
1)類型信息:對于每一個加載的類型(類 class,接口 interface,枚舉Enum,注解 annoation),JVM必須在方法中存放一下類型信息:
1)這個類型的完整有效名稱(全名=包名+類名)
2)這個類型直接的父類的完整有效名字(對于interface和java.lang.object,都是沒有父類的)
3)這個類型的修飾符
4)這個類直接接口的有效列表
2)域信息:就是成員變量的屬性,JVM必須在方法區(qū)中保存類型的所有域的相關(guān)信息以及域的聲明順序,域的相關(guān)信息包括,修飾符,關(guān)鍵字等等,名稱類型,修飾符,在方法區(qū)中也是保留著這個類信息是被哪一個類加載器加載進來的,類加載器的信息也是在類的信息中是有紀(jì)錄的,同時類加載也會記錄他都加載過那些類,彼此相互記錄;
3)方法信息:操作數(shù)棧的深度,方法的權(quán)限,形參,局部變量表的深度,以及try catch包裹的代碼范圍信息;
上面的這段代碼執(zhí)行也不會出現(xiàn)空指針異常,被static和final修飾的量是在編譯過程中的準(zhǔn)備階段就被附上初值了
Class文件常量池:每個.Java源文件編譯后生成.Class文件中會保存當(dāng)前類中的字面常量以及符號信息 運行時常量池:在.Class文件被加載時,.Class文件中的常量池被加載到內(nèi)存中稱為運行時常量池,運行時常量池每個類都有一份 字符串常量池(StringTable) :字符串常量池在JVM中是StringTable類,實際是一個固定大小的HashTable(一種高效用來進行查找的數(shù)據(jù)結(jié)構(gòu)),不同JDK版本下字符串常量池的位置以及默認大小是不同的;
二)運行時常量池:
方法區(qū)中包含了字符串常量池,字節(jié)碼文件內(nèi)部包含了常量池,要想弄清楚方法區(qū),需要清楚的理解ClassFile,因為本身加載類的信息都在方法區(qū),所以要想弄清楚方法區(qū)的運行時常量池,就需要先學(xué)會ClassFile中的常量池
常量池:字面量(10,20),字符串本身,System,out等等類型信息這些都會對應(yīng)著一個符號
類型信息,方法引用,接口信息,只是存儲一份,節(jié)省空間,通過符號引用就可以直接找到對應(yīng)的常量池對應(yīng)字段的位置
class文件常量池:this也是以字面量的方式及進行存儲的
1)方法區(qū)的運行時常量池:就是字節(jié)碼文件中每一個類的接口對應(yīng)的常量池表在運行時后的一個表示形式,類型信息,方法,字段,常量字段,屬性引用,方法引用\
2)常量池就相當(dāng)于是一張表,JVM指令需要根據(jù)這張表來找到要執(zhí)行的類名,方法名,參數(shù)類型,字面量等類型,連方法名都被符號引用所代替了class文件常量池被加載到方法去以后就變成了運行時常量池;
3)運行時常量池是方法區(qū)的一部分,常量池表是Class文件的一部分,用于存放編譯器的生成的各種字面量和符號引用,這部分內(nèi)容將被類加載以后存放到方法區(qū)的運行時常量池中
4)運行時常量池在加載類和接口到虛擬機以后,就會創(chuàng)建對應(yīng)的運行時常量池
JVM為每一個已經(jīng)加載的類型,類或者是接口都維護一個常量池,池子中的數(shù)據(jù)項像數(shù)組項一樣,都是通過索引來進行訪問的,比如說#7;
5)運行時常量池中包含著多種不同的常量,包括編譯時期就已經(jīng)確定的數(shù)值字面量,也包括到運行時期解析才可以獲得的方法或者是字段引用,此時就不是常量池中的符號引用了,而是轉(zhuǎn)化成了真實地址,運行時常量池,相比于class文件常量池的另一個重要特征就是具備動態(tài)性,String.intern(),運行時常量池類似于傳統(tǒng)編程語言中的符號表,但是它所包含的數(shù)據(jù)要比符號表要更豐富一些;
6)當(dāng)創(chuàng)建類或者是接口的運行時常量池的時候,如果構(gòu)建運行時常量池的所需要的內(nèi)存空間方法去所能提供的最大值,JVM會拋出OOM異常;
三)方法區(qū)的迭代:
1)首先先明確一下,只有HotsSpot才存在永久代,BEA JRockit,IBM J9等來說,是不存在永久代的概念的,原則上如何實現(xiàn)方法區(qū)屬于虛擬機實現(xiàn)的內(nèi)部細節(jié),不受JAVA虛擬機規(guī)范約束,并不要求統(tǒng)一
2)JDK6以前,只有永久代,靜態(tài)變量就存放在永久代上面
3)JDK7存在永久代,但是已經(jīng)逐步去除永久代,字符串常量池,靜態(tài)變量仍然在堆里面
4)JDK8沒有永久代,類型信息,字段方法,常量仍然保存在本地內(nèi)存的元空間,但是字符串常量池,靜態(tài)變量仍然在堆空間里面;
1)之所以要取消永久代是因為JAVA官方收購了JRocket,之后將JRocket和HotSpot進行整合的時候,因為JROCKet沒有永久代,所以就把永久代給移除了;
2)為什么JRocket沒有永久代呢?
隨著JAVA8的到來,HotSpot VM中再也見不到永久代了,但是這并不意味著類的元數(shù)據(jù)信息也消失了,這些數(shù)據(jù)被動到了一個和堆沒有任何關(guān)系的本地內(nèi)存區(qū)域,這個區(qū)域叫做元空間,由于類的元數(shù)據(jù)信息分配在本地內(nèi)存中,元空間最大可分配空間就是系統(tǒng)可用內(nèi)存空間,這項改動是非常有必要的,因為:
2.1)為永久代設(shè)置空間大小很難確定:在某些場景下,如果說動態(tài)的加載類過多,很容易產(chǎn)生Perm區(qū)的OOM,比如說在某一個Web工程中,因為功能點比較多,那么在運行過程中,要不斷地加載很多類,但是具體加載的類的多少和大小程序員是很難進行確定的,經(jīng)常出現(xiàn)致命錯誤,空間小容易出現(xiàn)FullGC,STW時間比較長,如果發(fā)生FullGC以后沒有回收什么類,就容易出現(xiàn)OOM,分配大了浪費空間,但是元空間max=-1,更不容易發(fā)生fullGC
2.2)對永久代的調(diào)優(yōu)很困難,永久代發(fā)生FGC,JVM判斷常量池廢棄的常量和不再使用的類也很浪費時間,影響程序執(zhí)行的性能,所以說要盡量少出現(xiàn)FullGC
在JAVA7中方法區(qū)的實現(xiàn)是依靠永久代來實現(xiàn)的,主要存儲的是運行時常量池,class類信息等等,永久代是JVM運行時運行數(shù)據(jù)區(qū)的一塊內(nèi)存空間,可以通過-XX PermSize來設(shè)置永久代的大小,當(dāng)內(nèi)存不夠的時候就會觸發(fā)垃圾回收,但是JDK1.8使用元空間來替代方法區(qū)的數(shù)據(jù)存儲,元空間不屬于JVM內(nèi)存,而是本地內(nèi)存,正常情況下元空間是可以無限制的使用本地內(nèi)存的,但是還是可以通過參數(shù)來設(shè)置JVM元空間的使用內(nèi)存大小
1)在JDK1.7的永久代是有內(nèi)存限制的,是虛擬內(nèi)存,雖然可以通過參數(shù)來進行設(shè)置,但是JVM加載的class總數(shù)是很難確定的,所以很容易出現(xiàn)OOM的問題,但是元空間是存儲在本地內(nèi)存里面,內(nèi)存的上限是比較大的,很好的避免這個問題;
2)永久代的對象是通過fullGC進行垃圾回收的也就是和老年代同時實現(xiàn)垃圾回收,替換以后簡化了fullGC的過程,可以不再進行暫停的情況下去并發(fā)釋放類的數(shù)據(jù),同時也提升了GC的性能
3)Orcle公司要合并Hotspot和Jrockit的代碼,但是Jrockit沒有永久代
方法區(qū)使用的是虛擬機內(nèi)存,和本地內(nèi)存有一個映射關(guān)系,JDK8使用本地內(nèi)存,此時元空間大小只是受本地內(nèi)存的影響,是不是虛擬內(nèi)存是程序員本身設(shè)置的,通過一定的方式將虛擬內(nèi)存映射到直接內(nèi)存中,類多,方法多,不確定到底開辟多大的永久代,空間小,引起fullGC,STW時間長,但是又不能回收,最后只能造成OOM,如果開辟空間越大,也會造成浪費永久代出現(xiàn)full GC,對永久代調(diào)優(yōu)是很困難的,永久代萬一進行垃圾回收,判斷類和常量不再使用,所以說盡量少出現(xiàn)full gc
元空間默認的初始值是21M,各種加載的類信息都要存放到方法區(qū)里面,如果Web應(yīng)用系統(tǒng)加載的類信息直接大量存放在方法區(qū)達到了21M,那么此時會觸發(fā)Full GC,不光會回收堆還會回收方法區(qū),會對方法區(qū)中某一些無用的信息進行回收;
方法區(qū)容量分配大小的自動擴容機制:
1)假設(shè)一次FullGC之后方法區(qū)的垃圾回收回收了很多對象,剩余的方法區(qū)的空間大小是1M,那么此時的方法區(qū)下一次觸發(fā)FullGC的內(nèi)存大小就是回比21M小,也就是15M;
2)假設(shè)這一次FullGC方法區(qū)的垃圾回收基本沒回收對象,那么下一次觸發(fā)FullGC的達到的空間就會變得更高,會根據(jù)方法區(qū)這一次回收的大小自動做擴容
3)推薦設(shè)置此值,很容易放滿,就有可能頻繁觸發(fā)FullGC,必須設(shè)置此值,不要說讓方法區(qū)進行自動擴容,就不會讓他每一次進行FullGC,進行動態(tài)擴容,防止大量進行FullGC;
靜態(tài)變量staticObj和Class對象存放在一起,而Class對象又是存放在堆空間中的
方法區(qū)和永久代有什么區(qū)別?
方法區(qū)是JAVA虛擬機規(guī)范時候給的一個概念,包括JAVA虛擬機的運行時數(shù)據(jù)區(qū)
但是Hotspot針對于方法區(qū)的實現(xiàn)給出了不同的名稱
JDK1.7永久代=方法區(qū)實現(xiàn),但是JDK1.8元空間=方法區(qū)實現(xiàn)
方法區(qū)是定義的名稱,但是永久代和元空間都是方法區(qū)的實現(xiàn)而已
五)JDK1.8方法區(qū)有什么優(yōu)化?
1)將元空間改成了直接內(nèi)存
2)將字符串常量池移動到了堆上
在JAVA開發(fā)中最常用的兩個類型就是對象和String類型,字符串常量池比較大,最大的地方就要放在運行數(shù)據(jù)區(qū)的堆空間上
為什么把字符串常量池移動到堆上呢?
JDK7中將StringTable存放到了堆空間中,因為永久代的回收效率很低,在FullGC的時候才會被觸發(fā),但是FullGC是老年代的空間不足,永久代不足的時候才會觸發(fā),這就導(dǎo)致StringTable回收效率不高,而當(dāng)開發(fā)中會有大量的字符串需要被創(chuàng)建,回收效率低,導(dǎo)致永久代內(nèi)存不足,放到堆里面,可以及時的回收內(nèi)存;
四)方法區(qū)的垃圾回收:常量池中廢棄的常量以及不再使用的類變量
在大量使用到反射,動態(tài)代理,CGLIB等字節(jié)碼框架,動態(tài)生成JSP以及頻繁的自定義類加載器的場景,通常都是需要JAVA虛擬機具有類型卸載的能力,來保證不會對方法區(qū)有著很大的壓力
4.1)類卸載的條件:ZGC不支持類卸載,一般來說這個區(qū)域的回收效果非常復(fù)雜難以讓人滿意,但是這部分區(qū)域的回收又是比較必要的
4.2)先來說說方法區(qū)中常量池之眾所存放的兩大類常量:字面量+符號引用
字面量是比較接近于JAVA語言層次的常量概念,比如說文本字符串,被聲明成final的常量值等,但是符號引用就屬于編譯原理等方面的概念,包括以下三類常量:
1)類和接口的全限定名
2)字段的名稱和描述符
3)方法的名稱和描述符,HotSpot虛擬機對于常量池的回收策略是很明確的,只要常量池中的常量沒有被任何地方所引用,就可以被回收
方法區(qū)中的類記錄了它是由哪一個加載器進行加載的,類的加載器同時也會記錄他加載過誰,類的加載器都被卸載了,那么A也會被干掉,但是通常類的加載器是一般不會被回收的
1)嚴重性:內(nèi)存溢出>內(nèi)存泄漏: 比如說ThreadLocal沒有調(diào)用remove
2)內(nèi)存泄漏最終會導(dǎo)致內(nèi)存溢出,而內(nèi)存溢出可能是內(nèi)存泄漏導(dǎo)致的,比如說網(wǎng)絡(luò)IO未釋放資源
五)對象的實例化內(nèi)存布局和訪問定位
1)使用單例模式比如說靜態(tài)方法
2)使用反射,Class的newInstance(),只能調(diào)用空參數(shù)的構(gòu)造器,權(quán)限必須是public
3)Constructor的newInstance(XX),反射的方式可以調(diào)用空參,帶有參數(shù)的構(gòu)造器,權(quán)限沒有要求
4)使用克隆,不需要調(diào)用任何構(gòu)造器,但是必須當(dāng)前類實現(xiàn)Cloneable接口,實現(xiàn)克隆方法
5)使用反序列化:從文件中和網(wǎng)絡(luò)中來獲取到一個對象的二進制流
1)從這個字節(jié)碼中可以看到,stack是操作數(shù)棧的深度是2,局部變量表一共有兩個元素,參數(shù)是1
2)現(xiàn)在來看Code代碼,首先執(zhí)行new字節(jié)碼的操作指令,看到這里是#2,然后去找Object,首先會進行判斷運行時常量池里面是否已經(jīng)加載了Object類,如果沒有加載過,那么直接使用ClassLoader將java/lang/Object類直接加載到方法區(qū),并在堆上開辟內(nèi)存空間;
六)對象創(chuàng)建的過程:
1)是否已經(jīng)加載了此類和父類:
當(dāng)虛擬機遇到一條new的指令的時候,首先會去檢查這個指令的參數(shù)能否在Metaspace的常量池中定位到一個類的符號引用,并且檢查這個類代表的符號引用的類是否已經(jīng)加載,解析和初始化,就是來判斷類元信息是否存在,如果沒有,那么在雙親委派模式下,使用當(dāng)前的類加載器以ClassLoader+包名+類名為Key查找對應(yīng)的.class文件,如果沒有找到文件,那么就拋出classNotFoundException異常,如果找到對應(yīng)的class文件,那么直接生成對應(yīng)的Class對象;
2)為對象分配內(nèi)存:
首先進行計算對象占用空間大小,接著在堆中劃分出一塊內(nèi)存給新對象,如果實例成員變量是引用類型的變量,那么僅僅分配引用變量空間即可,即是4個字節(jié)大小
如果內(nèi)存規(guī)整,使用指針碰撞
2.1)如果內(nèi)存是規(guī)整的,那么虛擬機將采用的是指針碰撞法來為對象分配內(nèi)存,意思是將所有用過的內(nèi)存放到一邊,空閑的內(nèi)存放到一邊,中間有一個指針來作為分界點的指示器,分配內(nèi)存的時候僅僅是吧指針像空閑那邊挪動一段和對象大小相等的距離罷了,比如說垃圾收集器選擇的是Serial,ParNew這種基于壓縮算法的,虛擬機將采用這種分配方式,一般采用整理過程中的收集器的時候,使用指針碰撞;
中間有一個指針進行標(biāo)識空閑空間和非空閑空間的一個劃分區(qū)域,當(dāng)存放完成新的對象以后,指針會向右移
2.2)空閑列表:如果內(nèi)存不規(guī)整,虛擬機需要維護一個列表,使用空閑列表進行分配,如果內(nèi)存本身不是規(guī)整的,那么虛擬機使用的是空閑列表法來為對象分配內(nèi)存,意思就是虛擬機維護了一個列表,記錄那些內(nèi)存塊是可以用的,那些內(nèi)存塊是不可用的,再分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,并且更新列表上的內(nèi)容,這種分配方式稱之為空閑列表,實際上選擇哪一種分配方式是由JAVA的堆來決定的,而JAVA堆是否規(guī)整又有采用的垃圾回收算法是否帶有壓縮整理功能所決定;
3)處理并發(fā)安全問題:
再分配內(nèi)存空間的時候,另一個問題就是即使保證New對象的時候的線程安全性,創(chuàng)建對象是非常頻繁的操作,虛擬機需要解決并發(fā)問題
3.1)CAS操作:失敗重試,區(qū)域枷鎖,來保證指針更新操作的原子性
3.2)TLAB:把內(nèi)存分配的動作按照縣城劃分不同的空間進行,就是每一個線程在JAVA堆中預(yù)先分配一小塊內(nèi)存,稱之為是本地線程緩沖區(qū);
4)初始化分配的空間:0值初始化
內(nèi)存分配結(jié)束,虛擬機將分配到的內(nèi)存空間都初始化成零值,不包括對象頭,這一步保證了對象的實例字段在JAVA代碼中可以不用賦初值就可以直接使用,程序可以訪問到這些字段的數(shù)據(jù)類型所對應(yīng)的0值;
5)設(shè)置對象的對象頭:
將對象的所屬類,就是類的元數(shù)據(jù)信息,對象的hashcode和對象的GC信息,所信息等數(shù)據(jù)存儲在對象的對象頭中,這個過程具體的設(shè)置方式取決于JVM來實現(xiàn)
6)執(zhí)行init方法執(zhí)行初始化
初始化成員變量,執(zhí)行實例代碼塊,調(diào)用類的構(gòu)造方法,并把對內(nèi)對象的首地址,屬性的顯示初始化,代碼塊中初始化,構(gòu)造器中初始化
七)執(zhí)行引擎:
執(zhí)行引擎的任務(wù)就是將字節(jié)碼指令解釋/編譯為對應(yīng)平臺上的本地機器指令,一種是解釋執(zhí)行,一種是編譯執(zhí)行,執(zhí)行引擎從程序計數(shù)器中找到對應(yīng)的指令的地址,取出字節(jié)碼指令,翻譯成二進制碼讓操作系統(tǒng)執(zhí)行,來實現(xiàn)跨平臺的特性
從外觀上來看所有的JAVA虛擬機的執(zhí)行引擎輸入輸出都是一致的,輸入的是字節(jié)碼二進制流,處理過程是字節(jié)碼解釋執(zhí)行的等效過程,輸出的是執(zhí)行結(jié)果;
解釋器:當(dāng)JAVA虛擬機啟動的時候會根據(jù)預(yù)定義的規(guī)范對字節(jié)碼采用逐行解釋的方式執(zhí)行,將每一條字節(jié)碼文件中的內(nèi)容翻譯成對應(yīng)平臺的本地機器指令執(zhí)行,JIT即時編譯器就是虛擬機將源代碼直接編譯成和本地機器平臺相關(guān)的機器語言
方法區(qū)直接緩存解釋執(zhí)行的代碼緩存,通過即時編譯器可以將代碼指令轉(zhuǎn)化成機器執(zhí)行進行緩存放到方法區(qū)里面,這樣當(dāng)進行調(diào)用的時候,直接執(zhí)行機器指令,效率就會變高,JIT即時編譯只會編譯熱點代碼
機器碼:不太好排查問題
解釋器:
解釋器真正意義上所承擔(dān)的角色就是一個運行時翻譯者,將字節(jié)碼文件中的內(nèi)容翻譯成對應(yīng)的平臺的本地機器指令執(zhí)行,當(dāng)下一條字節(jié)碼指令被解釋執(zhí)行完成以后,接著再來根據(jù)PC寄存器中記錄的下一條需要被執(zhí)行的字節(jié)碼指令執(zhí)行解釋操作
機器碼直接緩存在方法區(qū),節(jié)省了解釋執(zhí)行的時間
第一種是將源代碼編譯成字節(jié)碼文件,然后再運行的時候通過解釋器將字節(jié)碼文件轉(zhuǎn)化成機器碼執(zhí)行,第二種是編譯執(zhí)行,直接將字節(jié)碼文件變成機器碼,現(xiàn)代虛擬機為了提升執(zhí)行效率會使用即時編譯技術(shù)將方法編譯成機器碼以后再來執(zhí)行;
解釋器的有顯示執(zhí)行速度快,上來就執(zhí)行解釋,JIT卻是要翻譯,響應(yīng)速度太慢
八)JIT即時編譯器:JAVA是半編譯半解釋執(zhí)行
配置參數(shù):-Xint解釋執(zhí)行響應(yīng)時間比較慢,-XComp編譯執(zhí)行,-Xmixed混合模式
JIT即時編譯器一開始編譯時期很長程序啟動速度非常慢可能導(dǎo)致程序員很長時間以后才能看到代碼執(zhí)行的邏輯,但是解釋器一開始執(zhí)行速度很快,但是之后效率就不如JIT即時編譯器了
1)當(dāng)然是否啟動JIT即時編譯器將字節(jié)碼直接編譯成對應(yīng)平臺的本地機器指令,則需要根據(jù)代碼被調(diào)用的執(zhí)行的頻率來決定,關(guān)于那些需要被編譯成本地代碼的字節(jié)碼,也被稱之為是熱點代碼,JIT即時編譯器會在運行時針對那些頻繁被調(diào)用的熱點代碼來做深度優(yōu)化,將其直接編譯成對應(yīng)平臺的本地機器指令,以此來提升JAVA程序的執(zhí)行性能
2)一個被多次調(diào)用的方法或者是一個方法體內(nèi)部循環(huán)次數(shù)較多的循環(huán)體都是可以被稱之為是熱點代碼,因此都是可以通過JIT編譯器編譯成本地機器指令,由于這種變異方式發(fā)生在方法的執(zhí)行過程中,因此也可以被稱之為是棧上替換;
3)一個方法究竟要被調(diào)用多少次,或者一個循環(huán)體究竟要執(zhí)行多少次循環(huán)才可以達到這個標(biāo)準(zhǔn),此時必須需要一個明確的閾值,JIT即時編譯器才會將這些熱點代碼編譯成本地機器指令執(zhí)行,這里面主要依靠熱點探測功能,目前虛擬機使用的是基于計數(shù)器的熱點探測
4)基于計數(shù)器的熱點探測,虛擬機竟會為每一個方法都建立兩種不同類型的計數(shù)器,分別是方法調(diào)用計數(shù)器+回邊計數(shù)器
方法調(diào)用計數(shù)器主要用于統(tǒng)計方法的調(diào)用次數(shù),回邊計數(shù)器主要用于統(tǒng)計循環(huán)體執(zhí)行的循環(huán)次數(shù)
5)方法計數(shù)器就用于統(tǒng)計方法被調(diào)用的次數(shù),他的默認閾值在Client模式下是1500詞,在Server模式下是10000詞,超過這個閾值,就會觸發(fā)JIT即時編譯,這個閾值可以通過虛擬機參數(shù)-XX:ComplieThreshold來人為設(shè)定
6)當(dāng)一個方法被調(diào)用的時候,會先檢查該方法是否存在被JIT即時編譯過后的版本,如果存在,那么優(yōu)先使用編譯過后的本地代碼來執(zhí)行,如果不存在已經(jīng)被編譯過后的版本,那么將該方法的調(diào)用計數(shù)器+1,然后方法調(diào)用計數(shù)器和回邊計數(shù)器之和是否已經(jīng)超過了方法調(diào)用計數(shù)器的閾值,如果已經(jīng)超過了該閾值,那么將會向即時編譯器提交一個該方法的代碼編譯請求
回邊計數(shù)器:它的作用是統(tǒng)計一個方法中循環(huán)體代碼執(zhí)行的次數(shù),在字節(jié)碼終于到控制流向后跳轉(zhuǎn)的指令稱為回邊,很顯然回邊計數(shù)器統(tǒng)計的目的就是為了出發(fā)JIT編譯
C1和C2編譯器有著不同的優(yōu)化策略:
不同的編譯器上面有著不同的優(yōu)化策略,C1編譯器上主要有方法內(nèi)聯(lián),去虛擬化和冗余消除
1)方法內(nèi)聯(lián):將引用的函數(shù)代碼編譯到引用點處,這樣可以減少棧幀的生成,減少參數(shù)傳遞以及跳轉(zhuǎn)的過程
2)去虛擬化:對唯一的實現(xiàn)類進行內(nèi)連
3)冗余消除:在運行期間把一些不會執(zhí)行的代碼給折疊掉
C2的優(yōu)化主要是在全局的局面,逃逸分析是優(yōu)化的基礎(chǔ)
基于逃逸分析在C2上有以下幾種優(yōu)化:
1)標(biāo)量替換:用標(biāo)量替換代替聚合對象的屬性值
2)棧上分配:對于為逃逸的對象分配對象在棧上而不是在堆
3)同步消除:清楚同步操作,通常指synchronized
分層編譯:程序解釋執(zhí)行,不開啟性能監(jiān)控可以觸發(fā)C1編譯,將字節(jié)碼翻譯成機器碼,可以進行簡單的優(yōu)化,也可以加上性能監(jiān)控,C2編譯會根據(jù)性能監(jiān)控信息進行激進優(yōu)化
不過在JAVA7版本以后一旦開發(fā)人員在程序中顯式指定-server時,默認將會開啟分層編譯策略,由C1編譯器和C2編譯器相互協(xié)作共同完成編譯任務(wù)
1)一般來說,JIT編譯出來的機器碼性能比解釋器要高
2)C2編譯器啟動市場比C1編譯器慢,系統(tǒng)穩(wěn)定執(zhí)行以后,C2編譯器執(zhí)行速度遠遠快于C1編譯器;
?解釋執(zhí)行/解釋器:將字節(jié)碼指令翻譯成字節(jié)碼一行一行執(zhí)行,解釋執(zhí)行速度非常慢
JIT即時編譯器:會直接將class文件中的字節(jié)碼指令編譯成機器碼,放到codeCache
java -XX:+PrintFlagsFinal -version
C1:只是做編譯基本上不做優(yōu)化
C2:為長期運行的應(yīng)用做性能調(diào)優(yōu)
在JDK1.7之前,要給虛擬機一個設(shè)定,JIT即時編譯器選擇到底是用C1還是C2,作為后端服務(wù)肯定選擇做性能優(yōu)化的C2,程序如果不用C2,啟動時間是5s,但是假設(shè)用了C2此時程序的啟動時間會變長,在JDK以后JIT會判斷如果程序執(zhí)行的快,會使用C1,如果程序運行的慢,那么使用C2
熱點代碼編譯:同一段代碼,被執(zhí)行過很多次
熱點探測技術(shù):如果說某一個方法只是執(zhí)行一次,那么這個代碼不需要變成熱點代碼,只需要解釋執(zhí)行,不用編譯成本地代碼緩存在JVM內(nèi)部;
一個JAVA項目要是走解釋執(zhí)行,很快就跑起來了,如果要是走JIT編譯器,可能就需要將所有class文件涉及到的代碼變成本地指令,可能啟動速度非常慢,所以分層編譯就是為了,為了解釋器的啟動速度快,運行的代碼再來使用JIT來進行頻次非常高的優(yōu)化;
1)啟動速度慢
2)占用JVM的空間
1)方法調(diào)用計數(shù)器:調(diào)用一次Map<方法名字,count>,服務(wù)器端是10000次;
2)回邊計數(shù)器:默認是10700,循環(huán)體代碼執(zhí)行的次數(shù)
1)方法內(nèi)聯(lián)的優(yōu)化方法就是把目標(biāo)方法的代碼直接復(fù)制到發(fā)起調(diào)用的方法中,避免發(fā)生真是的目標(biāo)方法調(diào)用,熱點探測技術(shù)觸發(fā),方法體大小受限制,使用方法內(nèi)聯(lián)提升性能
a)修改參數(shù):減少熱點閾值:1000次就觸發(fā)方法關(guān)聯(lián),還有方法體閾值,如果方法體閾值變大,但是還是要考慮增大緩存大小,才有空間放得下,JIT編譯后的機器碼要有充足的空間放得下本地機器代碼;
b)避免在一個方法中寫大量代碼:JVM遇到大方法只能按照解釋器解釋執(zhí)行,JIT如果要進行編譯的話還放不下到CodeCache里面,況且即時編譯時間還非常長;
2)鎖銷除:
3)逃逸分析:
方法內(nèi)部創(chuàng)建的引用沒有返回出去并且沒有定義一個成員變量賦值
JAVA虛擬機棧在操作系統(tǒng)底層是使用高速緩存或者是寄存器,但是堆一般來說是內(nèi)存條,訪問速度快,不需要進行GC垃圾回收,因為方法執(zhí)行完,就不會涉及到垃圾回收了
4)標(biāo)量替換:把對象進行拆解,放在棧上
?
柚子快報激活碼778899分享:JVM進階(2)
好文鏈接
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點和立場。
轉(zhuǎn)載請注明,如有侵權(quán),聯(lián)系刪除。