上週我剛把和小姐姐關於JVM的愉快探討過程整理成文字發出來,就慘遭蛋哥的diss。java
對了,還沒看過上篇文章的小可愛請先移步這裏: 那天我和小姐姐扯了半天的JVMweb
蛋哥:關於JVM小姐姐理解的挺不錯的,爲何你不整理完整!面試
我:由於文章字數有限,濃縮的都是精華嘛~算法
蛋哥:懶就懶嘛,還把懶說的那麼清新脫俗~(並扔給我一個白眼)微信
我:嘻嘻~那我再補充補充...數據結構
蛋哥沒再搭理我,扔給我一個文件,就開始悶頭寫代碼。編輯器
過了一下子,微信上彈出蛋哥發的兩行消息:看了你的文章以後我大體圍繞如下幾點進行了簡單的補充:佈局
在上篇文章中咱們知道,方法區主要存儲類的相關信息,且被全部線程共享,採用永久代的方式實現了方法區。post
不過方法區和永久代又有着本質的區別。方法區是JVM的規範,而永久代則是JVM規範的一種實現,而且只有HotSpot纔有永久代,而對於其餘類型的虛擬機,如 JRockit(Oracle)、J9(IBM) 並無永久代一說。url
本文咱們主要以HotSpot爲例展開探討,咱們先看一下jdk1.6及之前的JVM運行區域,以下圖:
但在jdk1.8之後,永久代被移除,改成了元空間。具體演變過程是醬紫的:
可是爲何在jdk1.8之後永久代被移除,改成了元空間呢?這樣作有什麼好處呢?
在討論這個問題以前咱們要先明白元空間
這個概念:
元空間:簡單來講就是存儲類的元數據信息的一個空間區域,元空間並不在虛擬機中,而是使用本地內存。
那什麼又是元數據
呢?
元數據,關於數據的數據或者叫作用來描述數據的數據或者叫作信息的信息。
聽起來是否是很抽象???
這麼說吧,咱們能夠把元數據簡單的理解成,最小的數據單位。元數據能夠爲數聽說明其元素或屬性,好比名稱、大小、數據類型、等,或其結構像長度、字段、數據列之類,或其相關數據,位於何處、如何聯繫、擁有者等等。
明白了元空間
和元數據
這兩個概念以後,那麼咱們就來講說他的好處。
本質上來說,元空間和永久代相似,都是對JVM規範中方法區的實現。咱們知道,類及方法的信息等比較難肯定其大小,所以對於永久代的大小指定比較困難,過小容易出現永久代溢出,太大則容易致使老年代溢出;而元空間並不在虛擬機中,而是使用本地內存。所以,默認狀況下,元空間的大小不受JVM控制,也不會再進行GC了,它僅受本地內存的限制,所以不會出現OOM異常。
Java代碼的整個運行過程能夠分爲編譯階段和加載階段。
咱們先來看下在編譯階段Java源代碼經歷了些什麼:
Java源代碼經過詞法解析、語法解析、語義解析等一系列執行過程,生成字節碼。
很抽象有木有?啥是詞法解析、啥是語法解析、啥是語義解析???別急,下面咱們一一解釋:
詞法解析:即經過空格分隔出單詞、操做符、控制符等信息,將其造成token信息流,傳遞給語法解析器。
語法解析:把詞法解析得到的token信息流按照Java語法規則組裝成語法樹。
語義解析:檢查關鍵字的使用是否合理、類型是否匹配、做用域是否正確等。
明白了這些,下面咱們一塊兒來看看加載階段都作了什麼:
加載階段類加載器對字節碼通過Load階段、Link階段、Init階段等一系列動做,將其加載到JVM,才能夠執行。執行模式有三種:解釋執行,JIT編譯執行,JIT編譯與解釋混合執行。
啥啥啥???啥Load階段?啥Link階段?啥Init階段?可不能夠具體一點???
具體來講是醬紫的:
Load階段讀取類文件,產生二進制流,並轉化爲特定的數據結構,初步經過cafe babe魔法數來校驗是否爲Java類文件或文件是否受損、常量池、文件長度、是否有父類等等,而後建立對應的java.lang.Class實例。
Link階段包括驗證、準備、解析三個步驟。驗證是更詳細的校驗,好比final是否合規、類型是否正確、靜態變量是否合理等;準備階段是爲靜態變量分配內存,並設定默認值;解析類和方法確保類與類之間相互引用的正確性,完成內存結構佈局。
Init階段執行類構造器clinit方法,若是賦值運算是經過其餘類的靜態方法完成的,那麼會立刻解析另一個類,在虛擬機中執行完畢後經過返回值進行賦值。
它的整個過程以下圖:
運行時棧幀結構是醬紫的:
虛擬機棧的內部結構是棧幀,每一個方法在執行的時候都會建立一個棧幀,用於存儲局部變量表,操做數棧,動態連接,方法返回地址等信息;
局部變量表用來完成方法參數以及局部變量列表的傳遞過程,若是是實例方法,那麼局部變量表中的每0位索引的Slot默認是用於傳遞方法所屬對象實例的引用;
在方法執行的過程當中,會有各類字節碼指向操做數棧中寫入和提取值;
某方法經過動態連接在常量池中查詢方法的引用來調用另外一個方法,進而完成方法調用;
方法返回地址即在方法退出以前,都須要返回到方法被調用的位置,程序才能繼續執行;
某方法在調用另外一個方法的過程,便是一個棧幀在虛擬機中的入棧到出棧的過程。
最後,虛擬機中的方法入棧的順序和方法的調用順序是一致的,先入棧的方法後出棧,後入棧的方法先出棧。
目前內存分配方法主要有指針碰撞法和空閒列表法。
指針碰撞法:假設堆中內存是絕對規整的,全部用過的內存放一邊,未使用過的放一邊,中間有一個指針做爲臨界點,若是新建立了一個對象則是把指針往未分配的內存挪動與對象內存大小相同距離,這個稱爲指針碰撞。以下圖所示:
空閒列表法:其基於標記清除算法,內存劃分紅網格區,內存分配不規整,即已使用的和未使用的內存隨機分佈。JVM 會維護一個記錄表,用於記錄那些內存可用於分配,當須要給對象分配內存區域時,尋找一塊足夠大的內存空間分配給對象,並更新記錄表,這種分配內存的方法叫作空閒列表法。以下圖所示:
咱們建立對象天然是爲了後續使用該對象,Java程序會經過棧上的reference數據來操做堆上的具體對象。
但是在《Java虛擬機規範》裏面,只規定了reference類型是一個指向對象的引用,並無定義這個引用應該經過什麼方式去定位、訪問到堆中對象的具體位置,因此對象訪問方式也是由虛擬機實現而定的。
不過,目前主流的訪問方式有句柄和直接指針兩種方式。
指針: 指向對象,表明一個對象在內存中的起始地址。
句柄: 能夠理解爲指向指針的指針,維護着對象的指針。句柄不直接指向對象,而是指向對象的指針(句柄不發生變化,指向固定內存地址),再由對象的指針指向對象的真實內存地址。
瞭解了指針和句柄的概念,咱們就來看看他們具體是怎麼對對象的訪問進行定位的。
Java堆中劃分出一塊內存來做爲句柄池,引用中存儲對象的句柄地址,而句柄中包含了對象實例數據與對象類型數據各自的具體地址信息,具體構造以下圖所示:
其優勢是,引用中存儲的是穩定的句柄地址,在對象被移動。好比:垃圾收集時移動對象是很是廣泛的行爲時,只會改變句柄中的實例數據指針,而引用自己不須要修改。
若是使用直接指針訪問,引用中存儲的直接就是對象地址,那麼Java堆對象內部的佈局中就必須考慮如何放置訪問類型數據的相關信息。
速度更快,節省了一次指針定位的時間開銷。因爲對象的訪問在Java中很是頻繁,所以這類開銷聚沙成塔後也是很是可觀的執行成本。HotSpot中採用的就是這種方式。
很是感謝小可愛們能看到這裏,JVM內容複雜繁多,小碼仔不可能在僅僅兩篇文章中分析的面面俱到。最近這兩篇文章整理的是咱們面試中最爲常見的一些問題,問題基本上比較常規,但也正是面試中的高頻問題。
越是常規,咱們越更要領悟、理解、掌握。可是這些相對常規的知識點你說你還不知道,那麼我還能說什麼~自裁吧。哦不,是關注我吧~
若是本篇文章有任何錯誤,請批評指教,不勝感激 !
若是你喜歡本文,那就點個贊吧~
歡迎關注個人微信公衆號【小碼仔】,咱們一塊兒探討代碼與人生。
文章參考: