全面解读java虚拟机
JVM是虛擬機(jī),也是一種規(guī)范,他遵循著馮·諾依曼體系結(jié)構(gòu)的設(shè)計(jì)原理。馮·諾依曼體系結(jié)構(gòu)中,指出計(jì)算機(jī)處理的數(shù)據(jù)和指令都是二進(jìn)制數(shù),采用存儲(chǔ)程序方式不加區(qū)分的存儲(chǔ)在同一個(gè)存儲(chǔ)器里,并且順序執(zhí)行,指令由操作碼和地址碼組成,操作碼決定了操作類型和所操作的數(shù)的數(shù)字類型,地址碼則指出地址碼和操作數(shù)。從dos到window8,從unix到ubuntu和CentOS,還有MAC OS等等,不同的操作系統(tǒng)指令集以及數(shù)據(jù)結(jié)構(gòu)都有著差異,而JVM通過在操作系統(tǒng)上建立虛擬機(jī),自己定義出來的一套統(tǒng)一的數(shù)據(jù)結(jié)構(gòu)和操作指令,把同一套語言翻譯給各大主流的操作系統(tǒng),實(shí)現(xiàn)了跨平臺(tái)運(yùn)行,可以說JVM是java的核心,是java可以一次編譯到處運(yùn)行的本質(zhì)所在。
我研究學(xué)習(xí)了JVM的組成和運(yùn)行原理,JVM的統(tǒng)一數(shù)據(jù)格式規(guī)范、字節(jié)碼文件結(jié)構(gòu),JVM關(guān)于內(nèi)存的管理。
一、JVM的組成和運(yùn)行原理 。
JVM的畢竟是個(gè)虛擬機(jī),是一種規(guī)范,雖說符合馮諾依曼的計(jì)算機(jī)設(shè)計(jì)理念,但是他并不是實(shí)體計(jì)算機(jī),所以他的組成也不是什么存儲(chǔ)器,控制器,運(yùn)算器,輸入輸出設(shè)備。在我看來,JVM運(yùn)行在真實(shí)的操作系統(tǒng)中表現(xiàn)的更像應(yīng)用或者說是進(jìn)程,他的組成可以理解為JVM這個(gè)進(jìn)程有哪些功能模塊,而這些功能模塊的運(yùn)作可以看做是JVM的運(yùn)行原理。JVM有多種實(shí)現(xiàn),例如Oracle的JVM,HP的JVM和IBM的JVM等,而在本文中研究學(xué)習(xí)的則是使用最廣泛的Oracle的HotSpot JVM。
1.JVM在JDK中的位置。
JDK是java開發(fā)的必備工具箱,JDK其中有一部分是JRE,JRE是JAVA運(yùn)行環(huán)境,JVM則是JRE最核心的部分。我從oracle.com截取了一張關(guān)于JDK Standard Edtion的組成圖,
從最底層的位置可以看出來JVM有多重要,而實(shí)際項(xiàng)目中JAVA應(yīng)用的性能優(yōu)化,OOM等異常的處理最終都得從JVM這兒來解決。HotSpot是Oracle關(guān)于JVM的商標(biāo),區(qū)別于IBM,HP等廠商開發(fā)的JVM。Java HotSpot Client VM和Java HotSpot Server VM是JDK關(guān)于JVM的兩種不同的實(shí)現(xiàn),前者可以減少啟動(dòng)時(shí)間和內(nèi)存占用,而后者則提供更加優(yōu)秀的程序運(yùn)行速度(參考自:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/index.html ,該文檔有關(guān)于各個(gè)版本的JVM的介紹)。在命令行,通過java -version可以查看關(guān)于當(dāng)前機(jī)器JVM的信息,下面是我在Win8系統(tǒng)上執(zhí)行命令的截圖,
?
可以看出我裝的是build 20.13-b02版本,HotSpot 類型Server模式的JVM。?
2.JVM的組成
JVM由4大部分組成:ClassLoader,Runtime Data Area,Execution Engine,Native Interface。
我從CSDN找了一張描述JVM大致結(jié)構(gòu)的圖:
?
2.1.?ClassLoader 是負(fù)責(zé)加載class文件,class文件在文件開頭有特定的文件標(biāo)示,并且ClassLoader只負(fù)責(zé)class文件的加載,至于它是否可以運(yùn)行,則由Execution Engine決定。
2.2.Native Interface 是負(fù)責(zé)調(diào)用本地接口的。他的作用是調(diào)用不同語言的接口給JAVA用,他會(huì)在Native Method Stack中記錄對(duì)應(yīng)的本地方法,然后調(diào)用該方法時(shí)就通過Execution Engine加載對(duì)應(yīng)的本地lib。原本多于用一些專業(yè)領(lǐng)域,如JAVA驅(qū)動(dòng),地圖制作引擎等,現(xiàn)在關(guān)于這種本地方法接口的調(diào)用已經(jīng)被類似于Socket通信,WebService等方式取代。
2.3.Execution Engine 是執(zhí)行引擎,也叫Interpreter。Class文件被加載后,會(huì)把指令和數(shù)據(jù)信息放入內(nèi)存中,Execution Engine則負(fù)責(zé)把這些命令解釋給操作系統(tǒng)。
2.4.Runtime Data Area 則是存放數(shù)據(jù)的,分為五部分:Stack,Heap,Method Area,PC Register,Native Method Stack。幾乎所有的關(guān)于java內(nèi)存方面的問題,都是集中在這塊。下圖是javapapers.com上關(guān)于Run-time Data Areas的描述:
可以看出它把Method Area化為了Heap的一部分,javapapers.com中認(rèn)為Method Area是Heap的邏輯區(qū)域,但這取決于JVM的實(shí)現(xiàn)者,而HotSpot JVM中把Method Area劃分為非堆內(nèi)存,顯然是不包含在Heap中的。下圖是javacodegeeks.com中,2014年9月刊出的一片博文中關(guān)于Runtime Data Area的劃分,其中指出,NonHeap包含PermGen和Code Cache,PermGen包含Method Area,而且PermGen在JAVA SE 8中已經(jīng)不再用了。查閱資料(https://abhirockzz.wordpress.com/2014/03/18/java-se-8-is-knocking-are-you-there/)得知,java8中PermGen已經(jīng)從JVM中移除并被MetaSpace取代,java8中也不會(huì)見到OOM:PermGen Space的異常。目前Runtime Data Area可以用下圖描述它的組成:
2.4.1.?Stack 是java棧內(nèi)存,它等價(jià)于C語言中的棧,棧的內(nèi)存地址是不連續(xù)的,每個(gè)線程都擁有自己的棧。 棧里面存儲(chǔ)著的是StackFrame,在《JVM Specification》中文版中被譯作java虛擬機(jī)框架,也叫做棧幀。StackFrame包含三類信息:局部變量,執(zhí)行環(huán)境,操作數(shù)棧。局部變量用來存儲(chǔ)一個(gè)類的方法中所用到的局部變量。執(zhí)行環(huán)境用于保存解析器對(duì)于java字節(jié)碼進(jìn)行解釋過程中需要的信息,包括:上次調(diào)用的方法、局部變量指針和操作數(shù)棧的棧頂和棧底指針。操作數(shù)棧用于存儲(chǔ)運(yùn)算所需要的操作數(shù)和結(jié)果。StackFrame在方法被調(diào)用時(shí)創(chuàng)建,在某個(gè)線程中,某個(gè)時(shí)間點(diǎn)上,只有一個(gè)框架是活躍的,該框架被稱為Current Frame,而框架中的方法被稱為Current Method,其中定義的類為Current Class。局部變量和操作數(shù)棧上的操作總是引用當(dāng)前框架。當(dāng)Stack Frame中方法被執(zhí)行完之后,或者調(diào)用別的StackFrame中的方法時(shí),則當(dāng)前棧變?yōu)榱硗庖粋€(gè)StackFrame。Stack的大小是由兩種類型,固定和動(dòng)態(tài)的,動(dòng)態(tài)類型的??梢园凑站€程的需要分配。 下面兩張圖是關(guān)于棧之間關(guān)系以及棧和非堆內(nèi)存的關(guān)系基本描述(來自 http://www.programering.com/a/MzM3QzNwATA.html ):
?
?
?
2.4.2. Heap 是用來存放對(duì)象信息的,和Stack不同,Stack代表著一種運(yùn)行時(shí)的狀態(tài)。換句話說,棧是運(yùn)行時(shí)單位,解決程序該如何執(zhí)行的問題,而堆是存儲(chǔ)的單位,解決數(shù)據(jù)存儲(chǔ)的問題。Heap是伴隨著JVM的啟動(dòng)而創(chuàng)建,負(fù)責(zé)存儲(chǔ)所有對(duì)象實(shí)例和數(shù)組的。堆的存儲(chǔ)空間和棧一樣是不需要連續(xù)的,它分為Young Generation和Old Generation(也叫Tenured Generation)兩大部分。Young Generation分為Eden和Survivor,Survivor又分為From Space和 ToSpace。
和Heap經(jīng)常一起提及的概念是PermanentSpace,它是用來加載類對(duì)象的專門的內(nèi)存區(qū),是非堆內(nèi)存,和Heap一起組成JAVA內(nèi)存,它包含MethodArea區(qū)(在沒有Code Cache的HotSpotJVM實(shí)現(xiàn)里,則MethodArea就相當(dāng)于GenerationSpace)。 在JVM初始化的時(shí)候,我們可以通過參數(shù)來分別指定,PermanentSpace的大小、堆的大小、以及Young Generation和Old Generation的比值、Eden區(qū)和From Space的比值,從而來細(xì)粒度的適應(yīng)不同JAVA應(yīng)用的內(nèi)存需求。
2.4.3. PC Register 是程序計(jì)數(shù)寄存器,每個(gè)JAVA線程都有一個(gè)單獨(dú)的PC Register,他是一個(gè)指針,由Execution Engine讀取下一條指令。如果該線程正在執(zhí)行java方法,則PC Register存儲(chǔ)的是 正在被執(zhí)行的指令的地址,如果是本地方法,PC Register的值沒有定義。PC寄存器非常小,只占用一個(gè)字寬,可以持有一個(gè)returnAdress或者特定平臺(tái)的一個(gè)指針。
2.4.4. Method Area 在HotSpot JVM的實(shí)現(xiàn)中屬于非堆區(qū),非堆區(qū)包括兩部分:Permanet Generation和Code Cache,而Method Area屬于Permanert Generation的一部分。Permanent Generation用來存儲(chǔ)類信息,比如說:class definitions,structures,methods, field, method (data and code) 和 constants。Code Cache用來存儲(chǔ)Compiled Code,即編譯好的本地代碼,在HotSpot JVM中通過JIT(Just In Time) Compiler生成,JIT是即時(shí)編譯器,他是為了提高指令的執(zhí)行效率,把字節(jié)碼文件編譯成本地機(jī)器代碼,如下圖:
?
引用一個(gè)經(jīng)典的案例來理解Stack,Heap和Method Area的劃分,就是Sring a="xx";Stirng b="xx",問是否a==b? 首先==符號(hào)是用來判斷兩個(gè)對(duì)象的引用地址是否相同,而在上面的題目中,a和b按理來說申請(qǐng)的是Stack中不同的地址,但是他們指向Method Area中Runtime Constant Pool的同一個(gè)地址,按照網(wǎng)上的解釋,在a賦值為“xx”時(shí),會(huì)在Runtime Contant Pool中生成一個(gè)String Constant,當(dāng)b也賦值為“xx”時(shí),那么會(huì)在常量池中查看是否存在值為“xx”的常量,存在的話,則把b的指針也指向“xx”的地址,而不是新生成一個(gè)String Constant。我查閱了網(wǎng)絡(luò)上大家關(guān)于String Constant的存儲(chǔ)的說說法,存在略微差別的是,它存儲(chǔ)在哪里,有人說Heap中會(huì)分配出一個(gè)常量池,用來存儲(chǔ)常量,所有線程共享它。而有人說常量池是Method Area的一部分,而Method Area屬于非堆內(nèi)存,那怎么能說常量池存在于堆中?
我認(rèn)為,其實(shí)兩種理解都沒錯(cuò)。Method Area的確從邏輯上講可以是Heap的一部分,在某些JVM實(shí)現(xiàn)里從堆上開辟一塊存儲(chǔ)空間來記錄常量是符合JVM常量池設(shè)計(jì)目的的,所以前一種說法沒問題。對(duì)于后一種說法,HotSpot JVM的實(shí)現(xiàn)中的確是把方法區(qū)劃分為了非堆內(nèi)存,意思就是它不在堆上。我在HotSpot JVM做了個(gè)簡(jiǎn)單的實(shí)驗(yàn),定義多個(gè)常量之后,程序拋出OOM:PermGen Space異常,印證了JVM實(shí)現(xiàn)中常量池是在Permanent Space中的說法。但是,我的JDK版本是1.6的。查閱資料,JDK1.7中InternedStrings已經(jīng)不再存儲(chǔ)在PermanentSpace中,而是放到了Heap中;JDK8中PermanentSpace已經(jīng)被完全移除,InternedStrings也被放到了MetaSpace中(如果出現(xiàn)內(nèi)存溢出,會(huì)報(bào)OOM:MetaSpace,這里有個(gè)關(guān)于兩者性能對(duì)比的文章: 。 所以,仁者見仁,智者見智,一個(gè)饅頭足以引發(fā)血案,就算是同一個(gè)商家的JVM,畢竟JDK版本在更新,或許正如StackOverFlow上大神們所說,對(duì)于理解JVM Runtime Data Area這一部分的劃分邏輯,還是去看對(duì)應(yīng)版本的JDK源碼比較靠譜,或者是參考不同的版本JVM Specification。
2.4.5.?Native Method Stack 是供本地方法(非java)使用的棧。每個(gè)線程持有一個(gè)Native Method Stack。
3.JVM的運(yùn)行原理簡(jiǎn)介
Java 程序被javac工具編譯為.class字節(jié)碼文件之后,我們執(zhí)行java命令,該class文件便被JVM的Class Loader加載,可以看出JVM的啟動(dòng)是通過JAVA Path下的java.exe或者java進(jìn)行的。JVM的初始化、運(yùn)行到結(jié)束大概包括這么幾步:
調(diào)用操作系統(tǒng)API判斷系統(tǒng)的CPU架構(gòu),根據(jù)對(duì)應(yīng)CPU類型尋找位于JRE目錄下的/lib/jvm.cfg文件,然后通過該配置文件找到對(duì)應(yīng)的jvm.dll文件(如果我們參數(shù)中有-server或者-client, 則加載對(duì)應(yīng)參數(shù)所指定的jvm.dll,啟動(dòng)指定類型的JVM),初始化jvm.dll并且掛接到JNIENV結(jié)構(gòu)的實(shí)例上,之后就可以通過JNIENV實(shí)例裝載并且處理class文件了。class文件是字節(jié)碼文件,它按照J(rèn)VM的規(guī)范,定義了變量,方法等的詳細(xì)信息,JVM管理并且分配對(duì)應(yīng)的內(nèi)存來執(zhí)行程序,同時(shí)管理垃圾回收。直到程序結(jié)束,一種情況是JVM的所有非守護(hù)線程停止,一種情況是程序調(diào)用System.exit(),JVM的生命周期也結(jié)束。
關(guān)于JVM如何管理分配內(nèi)存,我通過class文件和垃圾回收兩部分進(jìn)行了學(xué)習(xí)。
二、JVM的內(nèi)存管理和垃圾回收
JVM中的內(nèi)存管理主要是指JVM對(duì)于Heap的管理,這是因?yàn)镾tack,PC Register和Native Method Stack都是和線程一樣的生命周期,在線程結(jié)束時(shí)自然可以被再次使用。雖然說,Stack的管理不是重點(diǎn),但是也不是完全不講究的。
1.棧的管理
JVM允許棧的大小是固定的或者是動(dòng)態(tài)變化的。在Oracle的關(guān)于參數(shù)設(shè)置的官方文檔中有關(guān)于Stack的設(shè)置(http://docs.oracle.com/cd/E13150_01/jrockit_jvm/jrockit/jrdocs/refman/optionX.html#wp1024112),是通過-Xss來設(shè)置其大小。關(guān)于Stack的默認(rèn)大小對(duì)于不同機(jī)器有不同的大小,并且不同廠商或者版本號(hào)的jvm的實(shí)現(xiàn)其大小也不同,如下表是HotSpot的默認(rèn)大小:
| Windows IA32 | 64 KB |
| Linux IA32 | 128 KB |
| Windows x86_64 | 128 KB |
| Linux x86_64 | 256 KB |
| Windows IA64 | 320 KB |
| Linux IA64 | 1024 KB (1 MB) |
| Solaris Sparc | 512 KB |
我們一般通過減少常量,參數(shù)的個(gè)數(shù)來減少棧的增長(zhǎng),在程序設(shè)計(jì)時(shí),我們把一些常量定義到一個(gè)對(duì)象中,然后來引用他們可以體現(xiàn)這一點(diǎn)。另外,少用遞歸調(diào)用也可以減少棧的占用。
棧是不需要垃圾回收的,盡管說垃圾回收是java內(nèi)存管理的一個(gè)很熱的話題,棧中的對(duì)象如果用垃圾回收的觀點(diǎn)來看,他永遠(yuǎn)是live狀態(tài),是可以reachable的,所以也不需要回收,他占有的空間隨著Thread的結(jié)束而釋放。(參考自:http://stackoverflow.com/questions/20030120/java-default-stack-size)
關(guān)于棧一般會(huì)發(fā)生以下兩種異常:
1.當(dāng)線程中的計(jì)算所需要的棧超過所允許大小時(shí),會(huì)拋出StackOverflowError。
2.當(dāng)Java棧試圖擴(kuò)展時(shí),沒有足夠的存儲(chǔ)器來實(shí)現(xiàn)擴(kuò)展,JVM會(huì)報(bào)OutOfMemoryError。
我針對(duì)棧進(jìn)行了實(shí)驗(yàn),由于遞歸的調(diào)用可以致使棧的引用增加,導(dǎo)致溢出,所以設(shè)計(jì)代碼如下:
我的機(jī)器是x86_64系統(tǒng),所以Stack的默認(rèn)大小是128KB,上述程序在運(yùn)行時(shí)會(huì)報(bào)錯(cuò):
而當(dāng)我在eclipse中調(diào)整了-Xss參數(shù)到3M之后,該異常消失
?
另外棧上有一點(diǎn)得注意的是,對(duì)于本地代碼調(diào)用,可能會(huì)在棧中申請(qǐng)內(nèi)存,比如C調(diào)用malloc(),而這種情況下,GC是管不著的,需要我們?cè)诔绦蛑?#xff0c;手動(dòng)管理?xiàng)?nèi)存,使用free()方法釋放內(nèi)存。
2.堆的管理
堆的管理要比棧管理復(fù)雜的多,我通過堆的各部分的作用、設(shè)置,以及各部分可能發(fā)生的異常,以及如何避免各部分異常進(jìn)行了學(xué)習(xí)。
?
上圖是 Heap和PermanentSapce的組合圖,其中?Eden區(qū)里面存著是新生的對(duì)象,From Space和To Space中存放著是每次垃圾回收后存活下來的對(duì)象 ,所以每次垃圾回收后,Eden區(qū)會(huì)被清空。 存活下來的對(duì)象先是放到From Space,當(dāng)From Space滿了之后移動(dòng)到To Space。當(dāng)To Space滿了之后移動(dòng)到Old Space。Survivor的兩個(gè)區(qū)是對(duì)稱的,沒先后關(guān)系,所以同一個(gè)區(qū)中可能同時(shí)存在從Eden復(fù)制過來 對(duì)象,和從前一個(gè)Survivor復(fù)制過來的對(duì)象,而復(fù)制到年老區(qū)的只有從第一個(gè)Survivor復(fù)制過來的對(duì)象。而且,Survivor區(qū)總有一個(gè)是空的。同時(shí),根據(jù)程序需要,Survivor區(qū)是可以配置為多個(gè)的(多于兩個(gè)),這樣可以增加對(duì)象在年輕代中的存在時(shí)間,減少被放到年老代的可能。
Old Space中則存放生命周期比較長(zhǎng)的對(duì)象,而且有些比較大的新生對(duì)象也放在Old Space中。
堆的大小通過-Xms和-Xmx來指定最小值和最大值,通過-Xmn來指定Young Generation的大小(一些老版本也用-XX:NewSize指定), 即上圖中的Eden加FromSpace和ToSpace的總大小。然后通過-XX:NewRatio來指定Eden區(qū)的大小,在Xms和Xmx相等的情況下,該參數(shù)不需要設(shè)置。通過-XX:SurvivorRatio來設(shè)置Eden和一個(gè)Survivor區(qū)的比值。(參考自博文:)
堆異常分為兩種,一種是Out of Memory(OOM),一種是Memory Leak(ML)。Memory Leak最終將導(dǎo)致OOM。實(shí)際應(yīng)用中表現(xiàn)為:從Console看,內(nèi)存監(jiān)控曲線一直在頂部,程序響應(yīng)慢,從線程看,大部分的線程在進(jìn)行GC,占用比較多的CPU,最終程序異常終止,報(bào)OOM。OOM發(fā)生的時(shí)間不定,有短的一個(gè)小時(shí),有長(zhǎng)的10天一個(gè)月的。關(guān)于異常的處理,確定OOM/ML異常后,一定要注意保護(hù)現(xiàn)場(chǎng),可以dump heap,如果沒有現(xiàn)場(chǎng)則開啟GCFlag收集垃圾回收日志,然后進(jìn)行分析,確定問題所在。如果問題不是ML的話,一般通過增加Heap,增加物理內(nèi)存來解決問題,是的話,就修改程序邏輯。
3.垃圾回收
JVM中會(huì)在以下情況觸發(fā)回收:對(duì)象沒有被引用,作用域發(fā)生未捕捉異常,程序正常執(zhí)行完畢,程序執(zhí)行了System.exit(),程序發(fā)生意外終止。
JVM中標(biāo)記垃圾使用的算法是一種根搜索算法。簡(jiǎn)單的說,就是從一個(gè)叫GC Roots的對(duì)象開始(GC ROOT節(jié)點(diǎn)主要在全局性的引用(例如常量或靜態(tài)屬性)與執(zhí)行上下文(例如棧幀中的本地變量表)中),向下搜索,如果一個(gè)對(duì)象不能達(dá)到GC Roots對(duì)象的時(shí)候,說明它可以被回收了。這種算法比一種叫做引用計(jì)數(shù)法的垃圾標(biāo)記算法要好,因?yàn)樗苊饬水?dāng)兩個(gè)對(duì)象啊互相引用時(shí)無法被回收的現(xiàn)象。
注意:1.?如果在節(jié)點(diǎn)搜索中從ROOT不能到達(dá)這個(gè)對(duì)象,并不是一定會(huì)被回收,因?yàn)镴VM給了這些對(duì)象第二次機(jī)會(huì),這些對(duì)象會(huì)被第一次標(biāo)記(“緩刑”)并且會(huì)進(jìn)行一次篩選,篩選條件就是此對(duì)象是否有必要執(zhí)行finalize()方法,(當(dāng)對(duì)象覆蓋finalized方法或者已經(jīng)被執(zhí)行過一次,都視為沒必要執(zhí)行finalize),通過篩選的對(duì)象放入F-Queue隊(duì)列,低優(yōu)先級(jí)的finalize線程會(huì)執(zhí)行這個(gè)方法,這里的“執(zhí)行”是說虛擬機(jī)會(huì)觸發(fā)這個(gè)方法,但并不承諾會(huì)等到這個(gè)方法執(zhí)行完成,因?yàn)閒inalize方法中可能有死循環(huán),如果在這次執(zhí)行中,能將自己拯救(將自身(this)與引用鏈上的任何一個(gè)對(duì)象關(guān)聯(lián)即可(比如吧自己的this賦值給某個(gè)類變量或者成員變量)),那么JVM在進(jìn)行第二次標(biāo)記的時(shí)候就會(huì)將他移除即將回收的集合。
2. Elden沒有足夠的內(nèi)存時(shí)會(huì)MInor GC,可以通過 -XX:PreteureSizeThreshold參數(shù)設(shè)置當(dāng)對(duì)象 >=這個(gè)值時(shí),會(huì)直接放入老年代。
JVM中對(duì)于被標(biāo)記為垃圾的對(duì)象進(jìn)行回收時(shí)又分為了一下3種算法:
1.標(biāo)記清除算法,該算法是從根集合掃描整個(gè)空間,標(biāo)記存活的對(duì)象,然后在掃描整個(gè)空間對(duì)沒有被標(biāo)記的對(duì)象進(jìn)行回收,這種算法在存活對(duì)象較多時(shí)比較高效,但會(huì)產(chǎn)生內(nèi)存碎片。
2.復(fù)制算法,該算法是從根集合掃描,并將存活的對(duì)象復(fù)制到新的空間,這種算法在存活對(duì)象少時(shí)比較高效。(適合新生代每次生存的對(duì)象很少)
3.標(biāo)記整理算法,標(biāo)記整理算法和標(biāo)記清除算法一樣都會(huì)掃描并標(biāo)記存活對(duì)象,在回收未標(biāo)記對(duì)象的同時(shí)會(huì)整理被標(biāo)記的對(duì)象,解決了內(nèi)存碎片的問題,(適合老年代:沒有過多內(nèi)存)。
4.分代收集。
?
JVM中,不同的 內(nèi)存區(qū)域作用和性質(zhì)不一樣,使用的垃圾回收算法也不一樣,所以JVM中又定義了幾種不同的垃圾回收器(圖中連線代表兩個(gè)回收器可以同時(shí)使用):
?
?
1.Serial GC。從名字上看,串行GC意味著是一種單線程的,所以它要求收集的時(shí)候所有的線程暫停。這對(duì)于高性能的應(yīng)用是不合理的,所以串行GC一般用于Client模式的JVM中。
2.ParNew GC。是在SerialGC的基礎(chǔ)上,增加了多線程機(jī)制。但是如果機(jī)器是單CPU的,這種收集器是比SerialGC效率低的。
3.Parrallel Scavenge GC。這種收集器又叫吞吐量?jī)?yōu)先收集器,而吞吐量=程序運(yùn)行時(shí)間/(JVM執(zhí)行回收的時(shí)間+程序運(yùn)行時(shí)間),假設(shè)程序運(yùn)行了100分鐘,JVM的垃圾回收占用1分鐘,那么吞吐量就是99%。Parallel Scavenge GC由于可以提供比較不錯(cuò)的吞吐量,所以被作為了server模式JVM的默認(rèn)配置。
4.ParallelOld是老生代并行收集器的一種,使用了標(biāo)記整理算法,是JDK1.6中引進(jìn)的,在之前老生代只能使用串行回收收集器。
5.Serial Old是老生代client模式下的默認(rèn)收集器,單線程執(zhí)行,同時(shí)也作為CMS收集器失敗后的備用收集器。
6.CMS又稱響應(yīng)時(shí)間優(yōu)先回收器,使用標(biāo)記清除算法。他的回收線程數(shù)為(CPU核心數(shù)+3)/4,所以當(dāng)CPU核心數(shù)為2時(shí)比較高效些。CMS分為4個(gè)過程:初始標(biāo)記、并發(fā)標(biāo)記、重新標(biāo)記、并發(fā)清除。
7.GarbageFirst(G1)。比較特殊的是G1回收器既可以回收Young Generation,也可以回收Tenured Generation。它是在JDK6的某個(gè)版本中才引入的,性能比較高,同時(shí)注意了吞吐量和響應(yīng)時(shí)間。
對(duì)于垃圾收集器的組合使用可以通過下表中的參數(shù)指定:
?
默認(rèn)的GC種類可以通過jvm.cfg或者通過jmap dump出heap來查看,一般我們通過jstat -gcutil [pid] 1000可以查看每秒gc的大體情況,或者可以在啟動(dòng)參數(shù)中加入:-verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:./gc.log來記錄GC日志。
GC中有一種情況叫做Full GC,以下幾種情況會(huì)觸發(fā)Full GC:
1.Tenured Space空間不足以創(chuàng)建打的對(duì)象或者數(shù)組,會(huì)執(zhí)行FullGC,并且當(dāng)FullGC之后空間如果還不夠,那么會(huì)OOM:java heap space。
2.Permanet Generation的大小不足,存放了太多的類信息,在非CMS情況下回觸發(fā)FullGC。如果之后空間還不夠,會(huì)OOM:PermGen space。
3.CMS GC時(shí)出現(xiàn)promotion failed和concurrent mode failure時(shí),也會(huì)觸發(fā)FullGC。promotion failed是在進(jìn)行Minor GC時(shí),survivor space放不下、對(duì)象只能放入舊生代,而此時(shí)舊生代也放不下造成的;concurrent mode failure是在執(zhí)行CMS GC的過程中同時(shí)有對(duì)象要放入舊生代,而此時(shí)舊生代空間不足造成的。
4.判斷MinorGC后,要晉升到TenuredSpace的對(duì)象大小大于TenuredSpace的大小,也會(huì)觸發(fā)FullGC。
可以看出,當(dāng)FullGC頻繁發(fā)生時(shí),一定是內(nèi)存出問題了。
三、JVM的數(shù)據(jù)格式規(guī)范和Class文件
1.數(shù)據(jù)類型規(guī)范
依據(jù)馮諾依曼的計(jì)算機(jī)理論,計(jì)算機(jī)最后處理的都是二進(jìn)制的數(shù),而JVM是怎么把java文件最后轉(zhuǎn)化成了各個(gè)平臺(tái)都可以識(shí)別的二進(jìn)制呢?JVM自己定義了一個(gè)抽象的存儲(chǔ)數(shù)據(jù)單位,叫做Word。一個(gè)字足夠大以持有byte、char、short、int、float、reference或者returnAdress的一個(gè)值,兩個(gè)字則足夠持有更大的類型long、double。它通常是主機(jī)平臺(tái)一個(gè)指針的大小,如32位的平臺(tái)上,字是32位。
同時(shí)JVM中定義了它所支持的基本數(shù)據(jù)類型,包括兩部分:數(shù)值類型和returnAddress類型。數(shù)值類型分為整形和浮點(diǎn)型。
整形:
| byte | 值是8位的有符號(hào)二進(jìn)制補(bǔ)碼整數(shù) |
| short | 值是16位的有符號(hào)二進(jìn)制補(bǔ)碼整數(shù)? |
| int | 值是32位的有符號(hào)二進(jìn)制補(bǔ)碼整數(shù)? |
| long | 值是64位的有符號(hào)二進(jìn)制補(bǔ)碼整數(shù)? |
| char | 值是表示Unicode字符的16位無符號(hào)整數(shù) ,注意Java中 是Unicode字符,占兩個(gè)字節(jié) ?,ASCii占8位但是他沒有中文字符 |
浮點(diǎn):
| float | 值是32位IEEE754浮點(diǎn)數(shù) |
| double | 值是64位IEEE754浮點(diǎn)數(shù)? |
?
?
returnAddress類型的值是Java虛擬機(jī)指令的操作碼的指針。
對(duì)比java的基本數(shù)據(jù)類型,jvm的規(guī)范中沒有boolean類型。這是因?yàn)閖vm中對(duì)boolean的操作是通過int類型來進(jìn)行處理的,而boolean數(shù)組則是通過byte數(shù)組來進(jìn)行處理。
至于String,我們知道它存儲(chǔ)在常量池中,但他不是基本數(shù)據(jù)類型,之所以可以存在常量池中,是因?yàn)檫@是JVM的一種規(guī)定。如果查看String源碼,我們就會(huì)發(fā)現(xiàn),String其實(shí)就是一個(gè)基于基本數(shù)據(jù)類型char的數(shù)組。如圖:
?
?
2.字節(jié)碼文件
通過字節(jié)碼文件的格式我們可以看出jvm是如何規(guī)范數(shù)據(jù)類型的。下面是ClassFile的結(jié)構(gòu):
關(guān)于各個(gè)字段的定義(參考自JVM Specification 和 博文:http://www.cnblogs.com/zhuYears/archive/2012/02/07/2340347.html),
magic:
魔數(shù),魔數(shù)的唯一作用是確定這個(gè)文件是否為一個(gè)能被虛擬機(jī)所接受的Class文件。魔數(shù)值固定為0xCAFEBABE,不會(huì)改變。
minor_version、major_version:
分別為Class文件的副版本和主版本。它們共同構(gòu)成了Class文件的格式版本號(hào)。不同版本的虛擬機(jī)實(shí)現(xiàn)支持的Class文件版本號(hào)也相應(yīng)不同,高版本號(hào)的虛擬機(jī)可以支持低版本的Class文件,反之則不成立。
constant_pool_count:
常量池計(jì)數(shù)器,constant_pool_count的值等于constant_pool表中的成員數(shù)加1。
constant_pool[]:
常量池,constant_pool是一種表結(jié)構(gòu),它包含Class文件結(jié)構(gòu)及其子結(jié)構(gòu)中引用的所有字符串常量、類或接口名、字段名和其它常量。常量池不同于其他,索引從1開始到constant_pool_count -1。
access_flags:
訪問標(biāo)志,access_flags是一種掩碼標(biāo)志,用于表示某個(gè)類或者接口的訪問權(quán)限及基礎(chǔ)屬性。access_flags的取值范圍和相應(yīng)含義見下表:
?
this_class:
類索引,this_class的值必須是對(duì)constant_pool表中項(xiàng)目的一個(gè)有效索引值。constant_pool表在這個(gè)索引處的項(xiàng)必須為CONSTANT_Class_info類型常量,表示這個(gè)Class文件所定義的類或接口。
super_class:
父類索引,對(duì)于類來說,super_class的值必須為0或者是對(duì)constant_pool表中項(xiàng)目的一個(gè)有效索引值。如果它的值不為0,那constant_pool表在這個(gè)索引處的項(xiàng)必須為CONSTANT_Class_info類型常量,表示這個(gè)Class文件所定義的類的直接父類。當(dāng)然,如果某個(gè)類super_class的值是0,那么它必定是java.lang.Object類,因?yàn)橹挥兴菦]有父類的。
interfaces_count:
接口計(jì)數(shù)器,interfaces_count的值表示當(dāng)前類或接口的直接父接口數(shù)量。
interfaces[]:
接口表,interfaces[]數(shù)組中的每個(gè)成員的值必須是一個(gè)對(duì)constant_pool表中項(xiàng)目的一個(gè)有效索引值,它的長(zhǎng)度為interfaces_count。每個(gè)成員interfaces[i] 必須為CONSTANT_Class_info類型常量。
fields_count:
字段計(jì)數(shù)器,fields_count的值表示當(dāng)前Class文件fields[]數(shù)組的成員個(gè)數(shù)。
fields[]:
字段表,fields[]數(shù)組中的每個(gè)成員都必須是一個(gè)fields_info結(jié)構(gòu)的數(shù)據(jù)項(xiàng),用于表示當(dāng)前類或接口中某個(gè)字段的完整描述。
methods_count:
方法計(jì)數(shù)器,methods_count的值表示當(dāng)前Class文件methods[]數(shù)組的成員個(gè)數(shù)。
methods[]:
方法表,methods[]數(shù)組中的每個(gè)成員都必須是一個(gè)method_info結(jié)構(gòu)的數(shù)據(jù)項(xiàng),用于表示當(dāng)前類或接口中某個(gè)方法的完整描述。
attributes_count:
屬性計(jì)數(shù)器,attributes_count的值表示當(dāng)前Class文件attributes表的成員個(gè)數(shù)。
attributes[]:
屬性表,attributes表的每個(gè)項(xiàng)的值必須是attribute_info結(jié)構(gòu)。
四、一個(gè)java類的實(shí)例分析
為了了解JVM的數(shù)據(jù)類型規(guī)范和內(nèi)存分配的大體情況,我新建了MemeryTest.java:
編譯為MemeryTest.class后,通過WinHex查看該文件,對(duì)應(yīng)字節(jié)碼文件各個(gè)部分不同的定義,我了解了下面16進(jìn)制數(shù)值的具體含義,盡管不清楚ClassLoader的具體實(shí)現(xiàn)邏輯,但是可以想象這樣一個(gè)嚴(yán)謹(jǐn)格式的文件給JVM對(duì)于內(nèi)存管理和執(zhí)行程序提供了多大的幫助。
運(yùn)行程序后,我在windows資源管理器中找到對(duì)應(yīng)的進(jìn)程ID.
并且在控制臺(tái)通過jmap -heap 10016查看堆內(nèi)存的使用情況:
?
輸出結(jié)果中表示當(dāng)前java進(jìn)程啟動(dòng)的JVM是通過4個(gè)線程進(jìn)行Parallel GC,堆的最小FreeRatio是40%,堆的最大FreeRatio是70%,堆的大小是4090M,新對(duì)象占用1.5M,Young Generation可以擴(kuò)展到最大是1363M, Tenured Generation的大小是254.5M,以及NewRadio和SurvivorRadio中,下面更是具體給出了目前Young Generation中1.5M的劃分情況,Eden占用1.0M,使用了5.4%,Space占了0.5M,使用了93%,To Space占了0.5M,使用了0%。
下面我們通過jmap dump把heap的內(nèi)容打印打文件中:
使用Eclipse的MAT插件打開對(duì)應(yīng)的文件:
選擇第一項(xiàng)內(nèi)存泄露分析報(bào)告打開test.bin文件,展示出來的是MAT關(guān)于內(nèi)存可能泄露的分析。
從結(jié)果來看,有3個(gè)地方可能存在內(nèi)存泄露,他們占據(jù)了Heap的22.10%,13.78%,14.69%,如果內(nèi)存泄露,這里一般會(huì)有一個(gè)比值非常高的對(duì)象。打開第一個(gè)Probem Suspect,結(jié)果如下:
?
ShallowHeap是對(duì)象本身占用的堆大小,不包含引用,RetainedHeap是對(duì)象所持有的Shallowheap的大小,包括自己ShallowHeap和可以引用的對(duì)象的ShallowHeap。垃圾回收的時(shí)候,如果一個(gè)對(duì)象不再引用后被回收,那么他的RetainedHeap是能回收的內(nèi)存總和。通過上圖可以看出程序中并沒有什么內(nèi)存泄露,可以放心了。如果還有什么不太確定的對(duì)象,則可以通過多個(gè)時(shí)間點(diǎn)的HeapDumpFile來研究某個(gè)對(duì)象的變化情況。
五、小結(jié)
以上便是我最近幾天對(duì)JVM相關(guān)資料的整理,主要圍繞他的基本組成和運(yùn)行原理等,內(nèi)存管理,節(jié)本數(shù)據(jù)類型和字節(jié)碼文件。JVM是一個(gè)非常優(yōu)秀的JAVA程序,也是個(gè)不錯(cuò)的規(guī)范,這次整理學(xué)習(xí)讓我對(duì)他有了更加清晰的認(rèn)知,對(duì)Java語言的理解也更加加深。
這里補(bǔ)充一點(diǎn) :java的重載與多態(tài)其實(shí)是與虛擬機(jī)相關(guān)的,重載是靜態(tài)分派(編譯時(shí)決定運(yùn)行哪個(gè)方法),多態(tài)是動(dòng)態(tài)分派(運(yùn)行時(shí)決定運(yùn)行哪個(gè)方法)。
下面給出重載代碼:
?
?public class JVM {
static abstract class A{
}
static class B extends A{
}
static class C extends A{
}//去掉此方法 會(huì)編譯出錯(cuò)
public void say(A a){
System.out.println("a");
}
public void say(B b){
System.out.println("b");
}
public void say(C c){
System.out.println("c");
}
public static void main(String args[]){
JVM jvm = new JVM();
A b = new B();
A c = new C();
jvm.say(b);
jvm.say(c);
}
輸出:
?
?
a a當(dāng)上面修改為:
?
?
jvm.say((B)b);時(shí) 則輸出結(jié)果為b,調(diào)用哪個(gè)方法是在編譯時(shí)就確定的。
對(duì)于基本類型的重載級(jí)別:
?
char->int->long->float->double char和byte short是同一級(jí)別,類型轉(zhuǎn)換不安全。
比如char的重載 會(huì)先看1.char的參數(shù) 2.int 3.long 4.float 5.double 6.包裝器類型(character) 7.Serializable或者Comparable接口,他倆優(yōu)先級(jí)一樣,當(dāng)同時(shí)存在時(shí)會(huì)提示類型模糊,拒絕編譯。8.Object 9. char.. 可變參數(shù)類型
總結(jié)
以上是生活随笔為你收集整理的全面解读java虚拟机的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java类加载-ClassLoader双
- 下一篇: TCP三次握手及关闭时的2MSL分析