為什么要有JVM?
JVM就是JAVA運行虛擬機,那么虛擬機又分為系統(tǒng)虛擬機和程序虛擬機,而JVM是屬于程序虛擬機,所以不要看到是虛擬機就誤認為JVM是系統(tǒng)虛擬機。
JVM是幫助Java程序開發(fā)者在開發(fā)過程中無需考慮無用的資源需要進行回收,避免內(nèi)存溢出等問題且實現(xiàn)在不同平臺上運行Java程序。
如: 開餐館,你每天要把店鋪的垃圾拉到垃圾廠去,如果你不拉或忘記拉,越積越多垃圾會堆滿你的店鋪,甚至還會堆到別的店鋪去,不止你自己的店鋪無法營業(yè),別人的店鋪也無法營業(yè),隨著時間的累積,整條街的店鋪都給垃圾堆滿,都無法營業(yè)。
在看到這種情況房東就有意見了,房東說:”你們每個月都給我多交一些錢,我解決垃圾這個問題。(JVM運行也要資源)”
房東會在街道上擺放上大的垃圾桶,定時的檢查,如果垃圾桶滿了,先拉到一個集中的地方,等這個區(qū)域的垃圾慢慢的差不多滿了,就把這些垃圾拉到垃圾廠進行處理。
當(dāng)這個時候我覺得房租太貴了,我搬去別的地方,那么又是一個新的房東,這個房東對垃圾處理這個問題就不一樣了,他可能要求你必須要買我的袋子裝著的,我才會去處理這個垃圾。
相關(guān)的管理者看到這個情況,馬上說:“接下來,垃圾的這個問題,由我們安排的人來統(tǒng)一處理,以后不管你搬去哪里,只要你到我們的官網(wǎng)上填一份表格就行了。(運行環(huán)境)”
如果沒有JVM,可以腦補一下。
JVM是什么?
在弄清楚JVM是什么之前,先弄清楚JDK、JRE是什么?
JDK就是開發(fā)的工具包,包括了JRE。
JRE是Java運行環(huán)境,包括了JVM和Java核心類庫
JVM就是Java程序運行平臺,擁有自己的指令集,抽象操作系統(tǒng)和CUP結(jié)構(gòu)、內(nèi)存結(jié)構(gòu),在運行時操作不同的內(nèi)存區(qū)域。
Java程序編譯后的文件是*.class文件,*.class文件是按照Java標(biāo)準(zhǔn)編譯的文件,JVM是實現(xiàn)了Java制定的標(biāo)準(zhǔn),因此JVM是可以運行Java程序的,而JVM是一個虛擬出來的機器,通過自定義的執(zhí)行引擎、接口等實現(xiàn)方式與實際機器各種交互,使得Java程序在運行過程與實際機器無耦合,從而實現(xiàn)跨平臺。
如: 以上的例子,相關(guān)管理者只管理各自的區(qū)域垃圾問題,各自的管理者都使用不同的顏色,導(dǎo)致每次只要有分店在別的地點開張,那么這個分店就要換垃圾袋顏色,否則管理者不承認這個垃圾是他管的。
生產(chǎn)垃圾袋的廠家覺得這樣也不好,回收回來的袋子還要做分類,不利于他回收袋子,于是和各個店家商量都只用一種顏色的袋子,廠家也只生產(chǎn)這個顏色的袋子,不管管理者,廠家愿意這樣做,店家也愿意用。
于是生產(chǎn)廠家就只生產(chǎn)一種顏色的袋子,一種袋子到處通用(Compile Once,Run Everywhere.)。
作用
給Java程序提供一個獨立的運行環(huán)境
特點
無需依賴于任何系統(tǒng)、平臺之上。
優(yōu)點
跨平臺
可擴展性強
……
按照無需依賴任何系統(tǒng),獨立運行環(huán)境大家可以試下這個思路去分析,這里就不寫那么多了。
注:每個人的學(xué)習(xí)方式不一樣,以上只是提供一個思路而已。
缺點
JVM是一個程序虛擬機,但始終還是要運行在操作系統(tǒng)之上的,初始化的時候需要與操作系統(tǒng)建立各種交互,導(dǎo)致啟動時間長,與操作系統(tǒng)交互導(dǎo)致資源的消耗……
相當(dāng)于一個蘋果放在一個盤子上,盤子放了一些水,而蘋果在放進去的時候,盤子的水會高漲,如果超過盤子就會溢出,所以有一個盤子的要求,要么就對放入的蘋果大小,質(zhì)量進行控制,以達到要求,另外蘋果的靈活性也無法自由的變化形狀,所以放入占用了一定的位置導(dǎo)致能放的東西越來越少,盤子放入更多的東西的時候會越來越擠……
至于其他的缺點,大家可以試下這個思路去做分析。
注:每個人的學(xué)習(xí)方式不一樣,以上只是提供一個思路而已。
主流JVM
名稱 |
研發(fā)者 |
特點 |
Hotspot |
Longview Technologies開發(fā),然后被sun收購 |
性能出色,復(fù)雜度有點高 |
JRockit |
BEA公司,被oracle收購 |
任務(wù)控制能力出色,合并到Hotspot |
J9 |
IBM研發(fā) |
IBM內(nèi)部使用,往往需要和IBM套件共同使用 |
Harmony |
IBM和Intel研發(fā)的,捐給Apache作為孵化項目 |
Apache退出了JCP之后,慢慢的就沒有什么商用了 |
注:接下來講的是Hotspot虛擬機,但虛擬機基本差不多,但JRockit是沒有解釋器的,這些區(qū)別自己去了解。
探索JVM內(nèi)部
編譯器
為什么要有編譯器?
那么我們先來假設(shè)下沒有編譯器的情況吧
如果沒有編譯器,我們現(xiàn)在編寫一個程序,這個程序是在windows上編寫的,開發(fā)人員的本地測試也是在windows進行測試的,但環(huán)境部署上去的機器是Liunx時,這個時候兩個操作系統(tǒng)的機制以及執(zhí)行的字節(jié)碼可能不一樣,如( / )在Liunx和windows的表示都是不一樣的,所以這種差別是使得開發(fā)人員很痛苦,難道每一次部署的系統(tǒng)環(huán)境不一樣時或開發(fā)的系統(tǒng)環(huán)境不一樣的時候就要寫不同的代碼嗎?
而編譯器的作為就是將開發(fā)人員寫的代碼編譯成為一份是由JVM專門識別的一個字節(jié)碼,直接由JVM進行運行,不在與操作系統(tǒng)有關(guān)。
是什么?
將開發(fā)人員寫的*.java的源代碼編譯成字節(jié)碼(*.class),這種字節(jié)碼也可以叫做JVM的機器語言
執(zhí)行過程
符號表:就是由符號地址和符號的信息所組成的表格,符號表其實就是記錄編譯的時候讀取的信息。
詞法分析:源代碼的字符流,轉(zhuǎn)換為標(biāo)記的集合(字段標(biāo)記,方法的標(biāo)記等),并檢查詞法是否是正確的。
語法分析:是將詞法分析后的這個標(biāo)記的集合轉(zhuǎn)換為一個樹狀結(jié)構(gòu)的表現(xiàn)形式,并檢查語法等是否是正確的。
注解處理:就是處理語法分析之后的這個樹狀結(jié)構(gòu)的內(nèi)容,注解處理時是可以對內(nèi)容進行增刪改查的,如果對這個語法分析后的樹狀結(jié)構(gòu)數(shù)據(jù)進行更改了,那么編譯器將回到解析和填充符號表的過程中重新處理。比如:標(biāo)識這個值是a和b的變量相加得來的,大家去看看*.class文件的內(nèi)容就知道了
語義分析:對語法分析之后的這個樹狀結(jié)構(gòu)進行讀取,并且對其上下文的聯(lián)系是否合理進行上下文的分析,類型是否匹配、方法是否有返回值、將判斷泛型等編譯成簡單的語法結(jié)構(gòu)等……
字節(jié)碼生成:將各個步驟的所產(chǎn)生的信息及存儲在符號表內(nèi)的信息進行轉(zhuǎn)換為字節(jié)碼,寫出為*.class文件。
如:現(xiàn)在商家想要得到垃圾袋,要去管理者那申請,先填寫申請單,管理者要制作這些申請單,管理者會先記下大概要填寫的幾個模塊(詞法分析),在各個模塊中將要填寫的內(nèi)容寫上去,在形成一個樹狀化的展示形式申請單(基本信息– 名字)(語法分析),對一些要填寫的地方進行注解(注解處理),這個時候申請單就做好了,給到各個商家,商家填寫完成后,要檢查商家填寫的內(nèi)容是否有錯誤(語義分析),最后沒有問題了,那么根據(jù)申請單信息進行審核,審核通過了則給袋子給商家。
*.class文件內(nèi)容:
結(jié)構(gòu)信息:文件版本號、各個部分數(shù)量、大小等信息。
元數(shù)據(jù):常量的信息、繼承的類、實現(xiàn)的接口、聲明的信息、常量池等信息。
方法信息:語句和表達式對應(yīng)信息,字節(jié)碼、異常處理表、求值棧和局部變量的大小,求值棧的類型記錄等信息
類裝載系統(tǒng)
為什么要有類裝載機制?
類加載系統(tǒng)-圖一
類加載系統(tǒng)-圖二
?
大家看下類加載系統(tǒng)-圖一和類加載系統(tǒng)-圖二,作者建立了一個類,這個類是和Java自身提供的java.lang.String是一樣的包名和類名。
可以想象一下如果這個類成功執(zhí)行了,那么接下來如果有別的類在引用的時候,應(yīng)該先引用作者寫的這個,還是引用Java自身的java.lang.String類?
如:以上的例子大家都知道,垃圾袋的顏色已經(jīng)統(tǒng)一成一模一樣了,有一天A商家在門口放了兩袋垃圾袋,一袋是裝著打碎的瓷盤碎片,一袋是裝著廚房的垃圾,都綁著。
馬上就要下班了,員工準(zhǔn)備拿垃圾袋去扔,由于垃圾袋綁著,這位員工趕著去約會,直接往垃圾袋隨時一抓,好了,結(jié)果大家猜到了,于是在第二天員工聰明了向商家要求,由于垃圾袋顏色一樣,要求垃圾袋貼上小紙條,標(biāo)明哪個垃圾袋是裝什么的,這樣做的之后,提高了安全性,而且又能夠保證在做垃圾分類的時候可以保證各個垃圾袋放的東西是按照垃圾分類的要求放的。
是什么?
類裝載系統(tǒng)是由數(shù)個加載器組成的,負責(zé)將class文件信息加載進內(nèi)存,存放在數(shù)據(jù)區(qū)-方法區(qū)內(nèi)。
執(zhí)行過程
?
加載:通過完全限定名查找到這個類的字節(jié)碼文件,將其靜態(tài)的存儲結(jié)構(gòu)轉(zhuǎn)化虛擬機的方法區(qū)運行時數(shù)據(jù)結(jié)構(gòu),并生成一個代表這個類的對象,這個就是為什么可以進行反射操作。
類加載過程:
?
為什么要這樣加載?
當(dāng)類在加載的時候,如類加載系統(tǒng)-圖一,定義一個java.lang.String是一樣的包名和類名,而類加載的時候是通過限定名去查找這個類字節(jié)碼文件的,那樣就出現(xiàn)了相同的內(nèi)容,那么則出現(xiàn)了沖突,破壞了Java內(nèi)部的完整性以及一致性。
在類加載系統(tǒng)-圖二中我們可以看到,類加載器是有父類的,所以在↑查找的時候,是查找類是否已經(jīng)在啟動等過程中,已啟動的加載器加載了,如果查找到的所有加載器都沒有加載,那么則向下查找,哪個加載器是可以加載到這個類的,而這個也叫做雙親委派機制,而Tomcat、Jboss都依照Java規(guī)范有著實現(xiàn)了加載器。
雙親委派機制:可以理解為不止是坑爹的,且還是坑到爺一代的,在接到一個類加載的請求的時候,會先問他爹加載了沒有,他爹會問他爺加載了沒有,他爺也沒有加載,那么就會給回去,最后就只好自己加載了,也就是只有父類無法完成的任務(wù)才自己完成。
驗證:文件格式驗證、元數(shù)據(jù)驗證、字節(jié)碼驗證、符號引用驗證,目的在于確保字節(jié)碼文件內(nèi)的信息符合虛擬機的要求并不會破壞到虛擬機的內(nèi)容(虛擬機的一致性,完整性)。
準(zhǔn)備:為類變量(靜態(tài)變量)分配對應(yīng)的內(nèi)存,并設(shè)置這些類變量的值,這些內(nèi)存是分配方法區(qū)的內(nèi)存,但設(shè)置的這些類變量的值,具體要看是否有final修飾符,如果沒有那么則無論值是多少都是為0,如果修飾符有final,那么設(shè)置的這個值則就是類變量的值。
解析:將符號引用轉(zhuǎn)化為直接引用,符號引用就是通過對應(yīng)的符號找到目標(biāo)對象,符號可以是字面量,符號引用和虛擬機內(nèi)存的布局是無關(guān)的,因符號引用的對象可以不加載到內(nèi)存里,直接引用就是存在于內(nèi)存中的,是有指針可以指向到的。
虛擬機沒有規(guī)定解析的時間,只需要在anew arry、check cast、get field、instance of、invoke interface、invoke special、invoke static、invoke virtual、multi anew array、new、put field和put static這13個用于操作符號引用指令執(zhí)行之前,對符號引用進行解析。
所以虛擬機會判斷是在類被加載器加載的時候?qū)Ψ栆眠M行解析還是等符號引用在要被使用前去解析,可以通過看上面的13個指令去得到答案。
解析的東西主要是類、接口、字段、類方法、接口方法這五類進行解析。
無非就是不管你這個類是接口還是實現(xiàn)類,還是什么,只要你是個類,那么就解析你里面的所有內(nèi)容。
初始化:類初始化的時候就是觸發(fā)到了new、getstatic、putstatic或invokestatic這4條指令的時候,也就是通常在開發(fā)過程中new對象的時候,讀取或設(shè)置一個的靜態(tài)字段,以及調(diào)用靜態(tài)方法,被final修飾與已被編譯器把結(jié)果放入常量池的靜態(tài)字段除外。
初始化一個類的時候其父類還沒初始化,也會觸發(fā)其父類初始化。
但JVM最先初始化的是,main()方法這個類。
如:現(xiàn)在衛(wèi)生局要檢查衛(wèi)生了,衛(wèi)生局通過信息文檔的省、市、區(qū)、詳細地址這個信息知道了接下來要檢查衛(wèi)生的店在哪里,檢查的結(jié)果是要寫在對應(yīng)的檢查結(jié)果文檔上的,所以要將這個信息文檔上的信息(地址、營業(yè)執(zhí)照等)轉(zhuǎn)化為檢查結(jié)果文檔的格式,并生成一個這個要檢查的店的專門一個檢查結(jié)果的基本文檔,接下來要先在系統(tǒng)上記錄什么時候去檢查,驗證這些信息有沒有輸入錯誤,接下來要根據(jù)這家店的情況,準(zhǔn)備下檢查的固定事項(final),以及一些可能臨時的事項(類變量),檢查人員要準(zhǔn)備出發(fā)了,要先把檢查結(jié)果文檔下載下來,打印成文件出來便于做記錄,接下來檢查人員到了店面進行檢查,檢查完了之后將記錄下來的內(nèi)容結(jié)果上傳到對應(yīng)的系統(tǒng)上。
解釋器
為什么要有?
我們都知道JVM是為了實現(xiàn)跨平臺,寫一次到處跑的實現(xiàn)理念,但不同的機器可能因為生產(chǎn)廠家或操作系統(tǒng)等原因有著不一樣的標(biāo)準(zhǔn),那么其機器底層執(zhí)行的指令等可能各有區(qū)別。
是什么?
將要執(zhí)行的字節(jié)碼轉(zhuǎn)換為機器碼,而這個解釋是一句一句的解釋,這個也說明了Java是解釋性語言。
即時編譯器(JIT)
為什么要有?
剛剛我們說到了解釋器,其實JIT和解釋器做的事情是一樣的,但如果每次都要進行一句句的解釋,那么效率太低。
是什么?
JIT和解釋器做的事情是一樣的,都是為了將要執(zhí)行字節(jié)碼轉(zhuǎn)換為機器碼,而不一樣的是,JIT類似在編程的過程中將經(jīng)常使用的數(shù)據(jù)放到緩存中,所以JIT會把經(jīng)常使用的字節(jié)碼,如:循環(huán)等高頻率使用方法,它是以方法為單位一次性將整個方法的字節(jié)碼編譯為機器碼。
而對于一個方法是否是經(jīng)常使用,會通過探測熱點的方式。
既然是探測熱點的方式,這里提一個最基本的思路,用一個計數(shù)器,但達到相應(yīng)的閾值的時候就判定是熱點代碼,但是維護比較麻煩,接下來我們會提到內(nèi)存哪一塊是線程獨有,哪一塊是線程共享,這里就會存在問題,技術(shù)有優(yōu)點也有缺點。
執(zhí)行引擎
為什么要有?
當(dāng)編譯器轉(zhuǎn)換為機器碼了之后總要有東西去告知底層操作系統(tǒng)或某個操作者,接下來要做什么。
是什么?
執(zhí)行引擎主要還是告知底層操作要做的事情,只是一個概念上的詞,上面說到的編譯器也可以理解為是有一個編譯執(zhí)行引擎,所以這個只是概念上的東西。
本地接口
為什么要有?
這其中是有歷史原因吧,因為Java在問世的時候,C語言的程序是主流的,那么多程序是使用C語言,那么Java必然不可避免的要與C語言的程序進行交互,且Java是無法對操作系統(tǒng)底層進行訪問和操作的,但是可以通過本地接口調(diào)用其他語言的實現(xiàn)實現(xiàn)對底層進行訪問的操作。
是什么?
就是為了融合不同的變成語言的程序為Java語言的程序所用,所以在在內(nèi)存中開辟了一塊專門的處理標(biāo)記是native的代碼,。
目前這種方法的使用越來越少了,除非是直接和硬件交互的,因為現(xiàn)在基本通過Socket等通信方式實現(xiàn)程序直接的交互。
?
垃圾回收系統(tǒng)
為什么要有?
程序的運行的過程中,有一些是只運行一次或數(shù)次之后就不再運行了,而隨著運行的時間增長,在系統(tǒng)中堆積的越來越多,最終超過系統(tǒng)的極限程序就停止了運行了。
站在用戶的使用角度來看,用一下就不能用,或進行一些數(shù)據(jù)運行的時候就忽然不能用了,作者相信這個是沒有用戶是可以容忍的。
是什么?
將在一段時間不再使用,或在系統(tǒng)內(nèi)部不再活躍的時候,則在系統(tǒng)中釋放掉這部分的信息。
運行時數(shù)據(jù)區(qū)
指令區(qū):是線程獨有的
虛擬機棧:也可以叫棧內(nèi)存,是在線程創(chuàng)建的時候創(chuàng)建,也就說明了,一個線程是有一個獨有的棧,虛擬機棧的生命周期是隨著線程的結(jié)束而釋放內(nèi)存,對于棧而言不存在垃圾回收的問題,只要線程結(jié)束,那么生命周期和線程是一致的,是棧會存儲基本類型變量、實例方法、引用類型變量,都是在棧內(nèi)存中分配,就是線程執(zhí)行獨有的方法的時候,會將方法區(qū)的對應(yīng)的對象,類信息copy需要的部分信息到棧內(nèi)存中,執(zhí)行每一個方法的時候可以理解為是一個棧幀,具體看個人怎么去理解JVM棧內(nèi)存,棧是遵循一個LIFO的一個原則,而棧一般是由三個部分組成,局部變量表,棧數(shù)據(jù)區(qū),操作數(shù)棧。
局部變量表:存儲報錯的行數(shù)和方法使用到的局部變量等
操作數(shù)棧:保存計算過程中的結(jié)果,且作為計算過程中的變量臨時存儲空間。
棧數(shù)據(jù)區(qū):除了局部變量和操作數(shù)棧,棧還需要一些數(shù)據(jù)來支持常量池的解析,這里的棧數(shù)據(jù)區(qū)就是保存常量池的指針、方法返回地址等,另外發(fā)送異常和處理異常代碼等,所以棧數(shù)據(jù)區(qū)還有一個異常處理表。
棧執(zhí)行過程:當(dāng)一個線程在執(zhí)行某個方法的時候,就是在棧內(nèi)運行的,而棧是遵循LIFO的原則,那么就可以解釋當(dāng)A方法調(diào)用B方法的時候,只有B方法執(zhí)行完了,那么A方法才會繼續(xù)向下執(zhí)行,那么在調(diào)用的時候是先調(diào)用A方法,那么A方法是先進棧,而B是后進棧的,B方法執(zhí)行完了,彈出棧,繼續(xù)A方法,A方法執(zhí)行完了,那么則出棧。
?
本地方法棧:用于本地方法調(diào)用,允許java調(diào)用本地方法,具體可以看本地接口上述,本地接口如同一個大的存儲每一個對象,而本地方法棧就是存儲這個對象要執(zhí)行的方法信息。
程序計數(shù)器/PC寄存器:指向方法區(qū)中的方法字節(jié)碼(用于存儲指向下一條指令的地址,也就是要指向的指令代碼),由執(zhí)行引擎讀取下一條指令,是一個非常小的內(nèi)存空間,如果執(zhí)行的方法是本地方法,寄存器值為undefined,如果不是那么寄存器會存放當(dāng)前環(huán)境指針、程序計數(shù)器、操作棧指針、計算的變量指針等信息。
數(shù)據(jù)區(qū):是所有線程共享的
方法區(qū):保存類的元結(jié)構(gòu)信息、運行時常量池、靜態(tài)變量、常量、字節(jié)碼、在類/實例/接口初始化用到的特殊方法等,方法區(qū)是可以調(diào)節(jié)大小的,且方法區(qū)是可以會進行垃圾回收的,所以可以理解方法區(qū)是一塊邏輯區(qū)域。
堆:存儲Java對象和數(shù)組等,但這個對象在堆中的首地址會在棧存儲,堆內(nèi)存的大小是可以調(diào)節(jié)的。
堆可以說是Java一塊比較特殊的區(qū)域,因所有的Java對象實例都存儲在這個地方,那么Java實例多了,則需要回收,那么也不可能每一次使用完這個實例之后就把這個實例在內(nèi)存馬上銷毀,如果過多幾秒時候又使用到了呢?
所以針對這種情況,Java堆的設(shè)計就有點特殊了。
堆 = 新生代 + 老年代 + 永久代(特殊)
新生代 = Eden + sv0+ sv1
新生代:主要存儲一個新的對象,通過不同的垃圾回收策略進行計算什么時候進入老年代,什么時候進行回收,具體看垃圾回收機制。
TLAB(Thread-local allocation buffer)區(qū):是一個線程獨有的區(qū)域,
是為了加速內(nèi)存分配而存在的,也就是線程獨享的緩沖區(qū),避免多線程問題,沒有鎖的問題,也就沒有了鎖對資源的開銷,提高對象分配使用,但這個區(qū)域是只存儲小對象的,無法進入TLAB區(qū)的對象會直接進入堆,而TLAB區(qū)對對象是否進入的條件是按照對象的大小是否小于整個空間的64/1,這是一個默認的比例值,這個比例值是可以調(diào)整的。
老年代:存儲在新生代達到一定閾值(默認15),則進入老年代。
永久代:這是一個非常特殊的區(qū)域,這個是屬于Hotspot這個虛擬機比較獨有的,不同的虛擬機實現(xiàn)不一樣,但如J9、JRockit是沒有永久代的概念,永久代只是一個概念上的意義,永久代也會發(fā)生垃圾回收,但條件比較苛刻,稍后會在JDK6、7、8的區(qū)別中講到,而永久代在JDK8時就給移除了,改為元空間。
直接內(nèi)存/堆外內(nèi)存:不屬于JVM運行時數(shù)據(jù)區(qū)的,應(yīng)用于NIO中,直接內(nèi)存是跳過了Java堆,來提升內(nèi)存的訪問速度,其實就是使用通道和緩沖區(qū)的方式進行IO交互的方式,在操作的過程中如果沒有設(shè)置這一塊的堆空間大小,會引起OOM,可以通過-XX:MaxDirectMemonrySize進行設(shè)置,如果不配置默認為最大的堆空間大小。
注:直接內(nèi)存也會觸發(fā)GC的。
思考:Java為什么要這樣設(shè)計JVM?為什么還要區(qū)分線程共享數(shù)據(jù),和線程獨有數(shù)據(jù)?
作者的思考思路,集中式有集中式的好,分布式有分布式的好,具體看其語言定位與發(fā)展。
?
Java運行過程
?
垃圾回收機制
為什么要進行垃圾回收?垃圾是什么?已經(jīng)在上面講了。
如何判定對象是可回收的?
?
如同這段代碼,在方法內(nèi)執(zhí)行完了之后,這個test1對象在內(nèi)存中則是為null了,因方法結(jié)束了,也沒有其他引用了,在進行對象的引用查找時,則查找不到任何的引用,所以為null,那么則判定這個對象是不可達的,可以進行回收。
垃圾回收級別:作者將其分為三個級別,初級回收(minor GC),二級回收(major GC),完全回收(Full GC)。
初級回收(minor GC):當(dāng)有新的對象要進入新生代的Eden區(qū)時, Eden區(qū)的空間不足以存放這個對象,則發(fā)生初級回收,而活躍的對象會先存放到新生代的sv區(qū)域,并記錄年齡+1,當(dāng)達到閾值(默認15)時就進入老年代。
二級回收(major GC):新生代對象達到閾值或新生代的eden區(qū)無法裝入大對象時也會進入老年代,但老年代的空間不足以存放這個對象,則會二級回收。
完全回收(Full GC):就是當(dāng)老年代的空間不足以存放新對象時或永久代的內(nèi)存不足以存放內(nèi)容時等。
那么完全回收和二級回收的區(qū)別在哪里?
JVM是有自動調(diào)節(jié)功能的,會根據(jù)程序在運行中進行調(diào)節(jié)的,所以何時觸發(fā)完全回收,那么具體要看JVM的策略,但如果進行了完全回收之后還是出現(xiàn)空間不足以存放,那么則會出現(xiàn)OOM。
算法
主要說幾個主流的垃圾回收算法的思想。
引用計數(shù)算法:計數(shù)器計算引用的次數(shù),達到閾值就進入老年代,次數(shù)為0,則進行回收,對資源消耗嚴重,每次引用都要進行計算,但精確。
標(biāo)記清除算法:分為標(biāo)記和清除階段,對標(biāo)記的對象進行清除,清除后導(dǎo)致內(nèi)存空間不連續(xù),因而產(chǎn)生空間碎片。
對象何時標(biāo)記清除?
就是一個樹狀結(jié)構(gòu),根節(jié)點向下查找是否可以查找到這個對象,查找不到的對象,則標(biāo)記清除。
復(fù)制算法:將內(nèi)存區(qū)域分為兩塊,假設(shè)現(xiàn)在在使用A區(qū)域,這時要進行垃圾回收,把A區(qū)域正在使用的對象復(fù)制到B區(qū)域去,清除A區(qū)域所有對象,反復(fù)的如此進行,完成垃圾收集。
復(fù)制算法:將內(nèi)存區(qū)域分為兩塊,假設(shè)現(xiàn)在在使用A區(qū)域,這時要進行垃圾回收,把A區(qū)域正在使用的對象復(fù)制到B區(qū)域去,清除A區(qū)域所有對象,反復(fù)的如此進行,完成垃圾收集,主要用于新生代。
標(biāo)記壓縮算法:將存活的對象進行壓縮,放到一個區(qū)域后,在進行垃圾回收,就是結(jié)合了標(biāo)記清除算法和復(fù)制算法,主要用于老年代。
為什么復(fù)制算法和標(biāo)記壓縮算法主要的應(yīng)用地方不一致?
新生代GC頻繁,老年代對象大多數(shù)都是穩(wěn)定的狀態(tài),對象多、耗時長。
分代算法:按照對應(yīng)的策略將內(nèi)存分為N塊區(qū)域,根據(jù)策略的規(guī)定將不同的對象放入不同的區(qū)域,控制回收的空間,而不是每次都針對整個空間進行回收,減少GC停頓時間。
如:有個城市是這樣規(guī)劃的女孩子做針線活比較厲害,則把女孩子放到針線區(qū),男孩子力氣比較大則放到搬運區(qū),小孩子喜歡玩,放到游樂區(qū),游樂區(qū)的人慢慢多了,那么就只對游樂區(qū)進行人行疏導(dǎo),而不需要整個城市都需要進行疏導(dǎo),對游樂區(qū)進行疏導(dǎo)的時候也不會影響到別的區(qū)的運行。
分區(qū)算法:將內(nèi)存分為N塊獨立空間,每次只控制回收多少空間,而不是每次都針對整個空間進行回收,減少GC停頓時間。
分代算法和分區(qū)算法區(qū)別:分代就是根據(jù)對象的特點進行劃分,分區(qū)就是不管你是什么對象,控制每次回收多少個空間。
注:GC停頓就是把在進行垃圾回收的時候,會掛起正在運行的線程,使得其不在產(chǎn)生新的垃圾,回收完了之后才重新運行這些線程。
回收器
注:使用參數(shù)和設(shè)置線程數(shù)這些,讀者請自己去找文檔。
串行收集器:單線程進行垃圾回收,適用于并行能力不強的計算機(CPU),可以在新生代和老年代中使用,根據(jù)作用于不同的堆空間分為新生代串行回收器和老年代串行回收器。
Serial回收器:采用復(fù)制算法,在進行垃圾回收的時候其他線程會給掛起,直到垃圾回收完成(俗稱:STW,全世界停止),開啟后年輕代和老年代都采用這個回收器。
并行收集器:在串行的基礎(chǔ)改為多線程并行進行垃圾回收,適用于并行能力強的計算機。
ParNew回收器:適用于新生代的垃圾回收器,只是進行簡單的串行多線程化,回收策略和算法和串行是一樣的。
ParallelGC回收器:適用于新生代,采用了復(fù)制算法的收集器,在進行垃圾回收的時候會進入STW,直到垃圾回收完成,ParallelGC是非常關(guān)注系統(tǒng)吞吐量。
ParallelOldGC回收器:適用于老年代, ParallelGC回收器一樣,但采用標(biāo)記壓縮算法實現(xiàn)
CMS回收器:應(yīng)用于老年代的多線程回收器,采用標(biāo)記清除算法,主要關(guān)注系統(tǒng)停頓時間,是目前主流的回收器,CMS的整個回收過程分為,初始標(biāo)記、并發(fā)標(biāo)記、重新標(biāo)記、并發(fā)清除四個步驟,在初始標(biāo)記和重新標(biāo)記時會進入STW,在并發(fā)標(biāo)記和并發(fā)清除的過程中是不會進入STW的,而是應(yīng)用程序可以不停的工作,但CMS在回收的過程中要保證內(nèi)存有足夠資源,CMS回收時機是達到閾值后,觸發(fā)回收,老年代默認閾值是68%。
如果在CMS回收過程中,內(nèi)存不足,那么則觸發(fā)老年代的串行回收器,且CMS無法處理浮動垃圾(第一次告訴GC不使用,標(biāo)記吧,留待第二次GC回收,第二次GC回收時告訴GC,我現(xiàn)在又要用了,但GC還是回收了。)
注:可以通過參數(shù)設(shè)置CMS回收多少次進行碎片整理和壓縮。
G1回收器:采用了分區(qū)算法,獨特的回收策略的多線程回收器,區(qū)分新生代和老年代依然有Eden、Sv0、Sv1區(qū),不要求整個Eden區(qū)或新生代,老年代空間是連續(xù)的,G1的出現(xiàn)主要為了替代CMS,CMS采用標(biāo)記清除導(dǎo)致出現(xiàn)空間碎片,對CPU資源的要求等,G1回收器是可以應(yīng)用到新生代和老年代,但還是無法解決浮動垃圾等問題。
JVM與多線程
注:這里不談?wù)撨^多的多線程的內(nèi)容,未來作者會單獨對多線程進行撰寫。
JVM與多線程-圖一
?
JVM與多線程-圖二
?
多線程為什么需要鎖?
在上面的時候就解釋了,JVM的數(shù)據(jù)區(qū)內(nèi)的方法區(qū)和堆是線程共享的,在JVM與多線程-圖一說明了方法區(qū)與堆是共享的,JVM與多線程-圖二則說明了線程的棧是獨有的,方法是在棧中運行的,當(dāng)兩個線程互相搶占CPU資源,會導(dǎo)致執(zhí)行順序不可控,促使執(zhí)行結(jié)果是不可控的。
多線程鎖:大體上線程并發(fā)常見的鎖有,自旋鎖、偏向鎖、輕量鎖、重量鎖。
自旋鎖:當(dāng)前線程不會進入阻塞等待鎖的狀態(tài),而是會通過循環(huán)的方式嘗試獲取到鎖。
偏向鎖:某個線程一直在執(zhí)行某一段代碼的時候,獲取到鎖一次,之后就默認是自動獲取到鎖了,是一種提高性能的方式。
輕量鎖:當(dāng)前線程還是處于偏向鎖的狀態(tài),當(dāng)有別的線程在訪問時則會升級為輕量鎖,其他線程可以通過自旋鎖進行獲取。
重量鎖:A線程在處于輕量級鎖時,B線程通過自旋的方式去嘗試獲取到鎖,當(dāng)達到自旋的閾值時還沒有獲取到鎖,B線程則會進入阻塞狀態(tài),A線程的鎖就變?yōu)橹亓挎i。
JVM調(diào)試參數(shù)
Java8:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
以下展示的是Java7常用的:
行為參數(shù)
指令 |
描述 |
-XX:-AllowUserSignalHandlers |
如果應(yīng)用程序安裝了信號處理程序,請不要抱怨。(只適用于Solaris和linux)。 |
-XX:AltStackSize=16384 |
備用信號棧大小(以Kbytes表示)。(僅與Solaris相關(guān),從5.0刪除)。 |
-XX:-DisableExplicitGC |
在默認情況下,調(diào)用System.gc()是啟用的(-XX:- disableitgc)。使用-XX:+ disableitgc來禁用對System.gc()的調(diào)用。注意,JVM仍然在必要時執(zhí)行垃圾收集。 |
-XX:+FailOverToOldVerifier |
當(dāng)新的類型檢查失敗時,故障轉(zhuǎn)移到舊的驗證器。(介紹6)。 |
-XX:+HandlePromotionFailure |
最年輕的一代收集不需要保證所有的活物體都能得到充分的推廣。(在1.4.2更新中引入)[5.0和更早:false。] |
-XX:+MaxFDLimit |
將文件描述符的數(shù)量增加到最大值。(Solaris。) |
-XX:PreBlockSpin=10 |
自旋計數(shù)變量使用-XX:+ usesping。在輸入操作系統(tǒng)線程同步代碼之前,控制最大的自旋迭代。(1.4.2中介紹)。 |
-XX:-RelaxAccessControlCheck |
在驗證器中放松訪問控制檢查。(介紹6)。 |
-XX:+ScavengeBeforeFullGC |
在完整的GC之前進行年輕一代GC。(介紹1.4.1)。 |
-XX:+UseAltSigs |
使用替代信號代替SIGUSR1和SIGUSR2,用于VM內(nèi)部信號。(在1.3.1更新中引入,1.4.1。與Solaris。) |
-XX:+UseBoundThreads |
將用戶級線程綁定到內(nèi)核線程。(與Solaris。) |
-XX:-UseConcMarkSweepGC |
為老一代人使用并發(fā)的標(biāo)記-清除集合。1.4.1(介紹) |
-XX:+UseGCOverheadLimit |
使用一個策略,在拋出OutOfMemory錯誤之前,限制在GC中使用的VM時間的比例。(介紹6)。 |
-XX:+UseLwpsynchronization |
使用基于lwp的而不是基于線程的同步。(介紹1.4.0。與Solaris。) |
-XX:-UseParallelGC |
使用并行垃圾收集來清除垃圾。1.4.1(介紹) |
-XX:-UseParallelOldGC |
為完整的集合使用并行垃圾收集。啟用此選項將自動設(shè)置-XX:+UseParallelGC。(在5.0更新中引入) |
-XX:-UseSerialGC |
使用串行垃圾收集。(5.0中引入的)。 |
-XX:-UseSpinning |
在進入操作系統(tǒng)線程同步代碼之前,允許在Java監(jiān)視器上進行簡單的旋轉(zhuǎn)。(只適用于1.4.2和5.0)[1.4.2,多處理器Windows平臺:true] |
-XX:+UseTLAB |
使用線程本地對象分配(在1.4.0中引入,在此之前被稱為UseTLE)[1.4.2和更早的,x86或與-客戶端:false] |
-XX:+UseSplitVerifier |
使用具有StackMapTable屬性的新類型檢查器。(5.0中引入的。)(5.0:假) |
-XX:+UseThreadPriorities |
使用本機線程優(yōu)先級。 |
-XX:+UseVMInterruptibleIO |
在OS_INTRPT中,線程中斷之前或與EINTR之間的I/O操作結(jié)果。(介紹了6。與Solaris。) |
G1垃圾回收器參數(shù)
指令 |
描述 |
-XX:+UseG1GC |
使用垃圾優(yōu)先(G1)收集器。 |
-XX:MaxGCPauseMillis=n |
設(shè)置最大GC暫停時間的目標(biāo)。這是一個軟目標(biāo),JVM將盡最大努力實現(xiàn)它。 |
-XX:InitiatingHeapOccupancyPercent=n |
啟動一個并發(fā)GC循環(huán)的(整個)堆占用率。它是由GCs使用的,它基于整個堆的占用而觸發(fā)一個并發(fā)的GC循環(huán),而不僅僅是一代(例如G1)。0的值表示“持續(xù)GC循環(huán)”。默認值是45。 |
-XX:NewRatio=n |
新舊一代的比例。默認值是2。 |
-XX:SurvivorRatio=n |
伊甸園/幸存者空間大小的比率。默認值是8。 |
-XX:MaxTenuringThreshold=n |
保持閾值的最大值。默認值是15。 |
-XX:ParallelGCThreads=n |
設(shè)置在垃圾收集器的并行階段中使用的線程數(shù)。默認值隨JVM運行的平臺而異。 |
-XX:ConcGCThreads=n |
并發(fā)垃圾收集器將使用的線程數(shù)。默認值隨JVM運行的平臺而異。 |
-XX:G1ReservePercent=n |
設(shè)置保留為假上限的堆數(shù)量,以減少升級失敗的可能性。默認值是10。 |
-XX:G1HeapRegionSize=n |
在G1中,Java堆被細分為一致大小的區(qū)域。這設(shè)置了每個子分區(qū)的大小。該參數(shù)的默認值是根據(jù)堆大小確定的。最小值為1Mb,最大值為32Mb。 |
性能參數(shù)
指令 |
描述 |
-XX:+AggressiveOpts |
打開在即將發(fā)布的版本中默認為默認的點性能編譯器優(yōu)化。(在5.0更新中引入) |
-XX:CompileThreshold=10000 |
編譯前的方法調(diào)用/分支數(shù)量[-客戶端:1,500] |
-XX:LargePageSizeInBytes=4m |
設(shè)置用于Java堆的大頁面大小。(引入1.4.0更新1)[amd64: 2m] |
-XX:MaxHeapFreeRatio=70 |
在GC之后最大百分比的堆釋放,以避免收縮。 |
-XX:MaxNewSize=size |
新生成的最大大小(以字節(jié)為單位)。自1.4以來,MaxNewSize被計算為NewRatio的函數(shù)。(1.3.1 Sparc:32 m;1.3.1 x86:2.5。] |
-XX:MaxPermSize=64m |
永久世代的規(guī)模。[5.0和更新:64位虛擬機的比例增加了30%;1.4 amd64:96;1.3.1客戶:32 m。) |
-XX:MinHeapFreeRatio=40 |
在GC后,堆的最小百分比以避免擴展。 |
-XX:NewRatio=2 |
新舊一代的比例。[Sparc客戶:8;x86 - server:8;x86客戶:12。]-客戶端:4 (1.3)8 (1.3.1+),x86: 12] |
-XX:NewSize=2m |
新生成的默認大小(以字節(jié)為單位)[5.0和更新:64位虛擬機的比例增加了30%;x86:1米;x86, 5.0及以上:640k] |
-XX:ReservedCodeCacheSize=32m |
保留代碼緩存大小(以字節(jié)為單位)——最大的代碼緩存大小。[Solaris 64位,amd64和-server x86: 2048m;在1.5.0_06和更早的版本中,Solaris 64位和amd64: 1024m。 |
-XX:SurvivorRatio=8 |
eden/幸存者空間尺寸的比例[Solaris amd64: 6;Sparc在1.3.1:25;其他Solaris平臺在5.0和更早:32] |
-XX:TargetSurvivorRatio=50 |
清除后使用的幸存者空間的期望百分比。 |
-XX:ThreadStackSize=512 |
線程堆棧大小(以Kbytes表示)。(0表示使用默認棧大小)[Sparc: 512;Solaris x86: 320(在5.0和更早之前是256);Sparc 64位:1024;Linux amd64: 1024(5.0或更早時為0);所有其他0。) |
-XX:+UseBiasedLocking |
使偏向鎖。有關(guān)更多細節(jié),請參見此調(diào)優(yōu)示例。(在5.0更新中引入)[5.0:false] |
-XX:+UseFastAccessorMethods |
使用得到<原始>字段的優(yōu)化版本。 |
-XX:-UseISM |
使用的共享內(nèi)存。不接受非solaris平臺。)有關(guān)細節(jié),請參見親密共享內(nèi)存。 |
-XX:+UseLargePages |
使用大頁面內(nèi)存。(在5.0更新中引入)有關(guān)詳細信息,請參見Java對大內(nèi)存頁的支持。 |
-XX:+UseMPSS |
使用多個頁面大小來支持堆的w/4mb頁面。不要用“主義”來代替“主義”的需要。(在1.4.0版本中引入,與Solaris 9和更新版本相關(guān))[1.4.1和更早:false] |
-XX:+UseStringCache |
啟用通常分配的字符串的緩存。 |
-XX:AllocatePrefetchLines=1 |
使用JIT編譯代碼中生成的預(yù)取指令,在最后一個對象分配之后加載的緩存行數(shù)。如果最后一個分配的對象是一個實例,如果它是一個數(shù)組,默認值是1。 |
-XX:AllocatePrefetchStyle=1 |
為預(yù)取指令生成的代碼樣式。 0 -無預(yù)取指令產(chǎn)生*d*, 1 -每次分配后執(zhí)行預(yù)取指令, 2 -在執(zhí)行預(yù)取指令時,使用TLAB分配水印指針到gate。 |
-XX:+UseCompressedStrings |
對可以表示為純ASCII的字符串使用一個字節(jié)[]。(引入Java 6更新21性能版本) |
-XX:+OptimizeStringConcat |
盡可能優(yōu)化字符串連接操作。(Java 6更新20) |
日志參數(shù)
指令 |
描述 |
-XX:-CITime |
打印時間花在JIT編譯器上。(介紹1.4.0)。 |
-XX:ErrorFile=./hs_err_pid<pid>.log |
如果發(fā)生錯誤,將錯誤數(shù)據(jù)保存到該文件。(介紹6)。 |
-XX:-ExtendedDTraceProbes |
啟用performance-impacting dtrace探測。(介紹了6。與Solaris。) |
-XX:HeapDumpPath=./java_pid<pid>.hprof |
用于堆轉(zhuǎn)儲的目錄或文件名路徑。可控的。(1.4.2更新12,5.0更新7) |
-XX:-HeapDumpOnOutOfMemoryError |
當(dāng)java.lang時將堆轉(zhuǎn)儲到文件中。拋出OutOfMemoryError。可控的。(1.4.2更新12,5.0更新7) |
-XX:OnError="<cmd args>;<cmd args>" |
在致命錯誤上運行用戶定義的命令。(在1.4.2更新中介紹) |
-XX:OnOutOfMemoryError="<cmd args>; |
當(dāng)?shù)谝淮螔伋鯫utOfMemoryError時,運行用戶定義的命令。(介紹1.4.2更新12,6) |
-XX:-PrintClassHistogram |
在Ctrl-Break上打印類實例的直方圖。可控的。(1.4.2中介紹)。jmap -histocommand提供了等價的功能。 |
-XX:-PrintConcurrentLocks |
打印java.util。在Ctrl-Break線程轉(zhuǎn)儲中并發(fā)鎖。可控的。(介紹6)。jstack -lcommand提供了等價的功能 |
-XX:-PrintCommandLineFlags |
在命令行上出現(xiàn)的打印標(biāo)志。(5.0中引入的)。 |
-XX:-PrintCompilation |
在編譯方法時打印消息。 |
-XX:-PrintGC |
在垃圾收集中打印消息。可控的。 |
-XX:-PrintGCDetails |
在垃圾收集中打印更多的細節(jié)。可控的。(介紹1.4.0)。 |
-XX:-PrintGCTimeStamps |
在垃圾收集中打印時間戳。管理(介紹1.4.0)。 |
-XX:-PrintTenuringDistribution |
打印任期年齡信息。 |
-XX:-PrintAdaptiveSizePolicy |
允許打印關(guān)于自適應(yīng)生成規(guī)模的信息。 |
-XX:-TraceClassLoading |
跟蹤加載的類。 |
-XX:-TraceClassLoadingPreorder |
跟蹤所有已加載的類(未加載)。(1.4.2中介紹)。 |
-XX:-TraceClassResolution |
跟蹤常量池的決議。(1.4.2中介紹)。 |
-XX:-TraceClassUnloading |
跟蹤卸貨的類。 |
-XX:-TraceLoaderConstraints |
加載器約束的跟蹤記錄。(介紹6)。 |
-XX:+PerfDataSaveToFile |
在退出時保存jvmstat二進制數(shù)據(jù)。 |
-XX:ParallelGCThreads=n |
在年輕和舊的并行垃圾收集器中設(shè)置垃圾收集線程的數(shù)量。默認值隨JVM運行的平臺而異。 |
-XX:+UseCompressedOops |
允許使用壓縮指針(對象引用表示為32位的偏移量,而不是64位指針)以優(yōu)化64位性能,Java堆大小小于32gb。 |
-XX:+AlwaysPreTouch |
在JVM初始化期間預(yù)觸摸Java堆。因此,堆的每一頁都是在初始化過程中,而不是在應(yīng)用程序執(zhí)行期間遞增的。 |
-XX:AllocatePrefetchDistance=n |
設(shè)置對象分配的預(yù)取距離。在這個距離(以字節(jié)為單位),在最后一個分配對象的地址之外,以新對象的值寫入內(nèi)存。每個Java線程都有自己的分配點。默認值隨JVM運行的平臺而異。 |
-XX:InlineSmallCode=n |
僅當(dāng)生成的本機代碼大小小于這個時,內(nèi)聯(lián)一個以前編譯的方法。默認值隨JVM運行的平臺而異。 |
-XX:MaxInlineSize=35 |
一個方法的最大字節(jié)碼大小。 |
-XX:FreqInlineSize=n |
最大字節(jié)碼大小的經(jīng)常執(zhí)行的方法被內(nèi)聯(lián)。默認值隨JVM運行的平臺而異。 |
-XX:LoopUnrollLimit=n |
使用服務(wù)器編譯器中間表示節(jié)點的展開循環(huán)體的計數(shù)小于該值。服務(wù)器編譯器使用的限制是這個值的函數(shù),而不是實際值。默認值隨JVM運行的平臺而異。 |
-XX:InitialTenuringThreshold=7 |
設(shè)置在并行的年輕收集器中用于自適應(yīng)GC分級的初始閾值。招貼閾值是指一個物體在被提升到舊的或終身的一代之前,在年輕的集合中存活的次數(shù)。 |
-XX:MaxTenuringThreshold=n |
設(shè)置在自適應(yīng)GC分級中使用的最大閾值。當(dāng)前最大的值是15。并行收集器的默認值為15,CMS的默認值為4。 |
-Xloggc:<filename> |
日志GC詳細輸出到指定的文件。詳細輸出由正常的詳細GC標(biāo)志控制。 |
-XX:-UseGCLogFileRotation |
啟用GC日志旋轉(zhuǎn),需要-Xloggc。 |
-XX:NumberOfGClogFiles=1 |
設(shè)置旋轉(zhuǎn)日志時要使用的文件數(shù)量,必須是>= 1。旋轉(zhuǎn)的日志文件將使用以下命名方案,<filename>。0,<文件名>。1,…,<文件名> .n-1。 |
-XX:GCLogFileSize=8K |
日志文件的大小將會被旋轉(zhuǎn),必須是>= 8K。 |
JDK6、7、8的JVM區(qū)別
1.6
?
1.7
?
1.8
?
可以看到1.6到1.7可以說變化并不大,但到了1.8時,大家可以發(fā)現(xiàn)非常大了,出現(xiàn)了元空間的區(qū)域,并這個區(qū)域是在本地內(nèi)存中的,且這個區(qū)域是存儲類的元數(shù)據(jù)信息的,類的常量、方法等。
看起來元空間似乎和之前的方法區(qū)/永久代沒有什么區(qū)別,元空間是使用本地內(nèi)存的,受制于本地內(nèi)存大小,在沒有通過(MaxMetaspaceSize)VM參數(shù)設(shè)置時,會根據(jù)程序的運行時間動態(tài)調(diào)控大小。
那么也就不會再出現(xiàn)OOM的情況了,但元空間只是為了解決OOM的問題嗎?
為什么要有元空間?
永久代主要是用于存儲類的信息,但很難確定類的大小,所以在指定的時候就有點困難,容易造成OOM,另外一個原因就是Hotspot和JRockit的合并,JRockit是沒有永久代的。
為什么Hotspot要和JRockit合并?
必然合并肯定是要實現(xiàn)互補的,JRockit的任務(wù)控制、垃圾收集算法、監(jiān)控等能力都是比較優(yōu)秀的,而Hotspot在性能優(yōu)勢也就使得其比較復(fù)雜,所以結(jié)合雙方等各個優(yōu)點進行合并,形成更強大的JVM。
另外Hotspot和JRockit都是Oracle旗下的。
元空間帶來的影響?
有部分的數(shù)據(jù)移到堆,所以在1.8的時候會發(fā)現(xiàn)堆的空間會增加的比以往快,由于是使用本地內(nèi)存,如果吞吐量大的時候,會帶來大量的交換區(qū)交換。
元空間是否有垃圾回收?
當(dāng)然也會有垃圾回收,不可能說應(yīng)用程序不用這個類了,這個類失效了,還要一直保留著這些信息,這個是絕對不合理的。
元空間垃圾回收觸發(fā)時機?
上面我們提到,元空間是存儲類的元數(shù)據(jù)信息的,類加載器加載類的信息到元空間中,當(dāng)這個類不在有引用時,這個類的信息就會給回收了。
調(diào)試JVM
為什么這里寫的是調(diào)試,而不是優(yōu)化。
免得誤人子弟,優(yōu)化這個問題,是要根據(jù)不同的應(yīng)用程序進行優(yōu)化,而調(diào)試也是一個比較大的話題吧,具體的調(diào)試的參數(shù)還是要根據(jù)應(yīng)用程序而定。
這里分享下作者的思路:程序的定位(吞吐量等),程序的運行,位置定位。
- 1. 系統(tǒng)內(nèi)存泄漏時會先定位是程序的哪里的代碼導(dǎo)致的內(nèi)存泄漏,如果是NIO導(dǎo)致的內(nèi)存泄漏,則可能是堆外內(nèi)存泄漏,如果遞歸循環(huán)則有可能導(dǎo)致的是棧,先定位到位置,之后在進行參數(shù)的調(diào)試,一步一步的確認位置。
- 2. 程序在運行的過程中,不斷的越來越慢,而應(yīng)用程序的吞吐量是比較大(這個時候我們還是要先定位到位置,如果是產(chǎn)生大量的對象,而這些對象的使用次數(shù)也不多,當(dāng)有相當(dāng)一部分在很多時候達到了進入老年代的條件,從而進入了老年代,但進入老年代后呈現(xiàn)就不在使用這個對象,我們都知道老年代的對象比較穩(wěn)定,回收的不多,那么處理的時間長,所以對老年代的回收時間會比較久),那么可以通過調(diào)整進入老年代的條件,盡量使得對象在新生代時就給回收了,并減少GC次數(shù)。
注:JDK7版本后(包含),部分JVM參數(shù)已經(jīng)是擁有自動化調(diào)整的能力,如TLAB區(qū)域,除非是對系統(tǒng)等各個方面熟悉,否則建議不要亂調(diào)參數(shù)。
可以寫一個遞歸方法造成內(nèi)存泄漏在程序啟動時配置導(dǎo)出dump文件參數(shù),可以使用Eclipse的MAT插件查看dump文件,或jvisualvm等工具查看。
為什么要學(xué)JVM?
學(xué)習(xí)了JVM后,我們來看個問題,為什么學(xué)JVM?
以下只是作者的個人觀點:
工作:JVM是作為一名Java程序員所必備了解的過程,但隨著工作的年限的增長,可能接觸到的項目越來越多,而項目本身業(yè)務(wù)的復(fù)雜性可能會出現(xiàn)一次性加載的東西太多,導(dǎo)致內(nèi)存出現(xiàn)泄漏,而我們當(dāng)我們沒有去了解JVM的時候,會認為是硬件問題,或許上百度查一下知道是內(nèi)存泄漏,要把堆調(diào)大,但如果是堆外內(nèi)存泄漏呢?那么當(dāng)應(yīng)用程序吞吐量大的時候,是否可以通過調(diào)整進入老年代的條件而利用好內(nèi)存空間呢?
面試:現(xiàn)在很多公司在面試的時候都會問關(guān)于JVM的內(nèi)容。