1. 程式人生 > >JAVA 虛擬機器(一)內容總結

JAVA 虛擬機器(一)內容總結

說明:此文章非本人原創,是分享作者“知其然,後知其所以然”,原部落格地址:https://www.cnblogs.com/gl-developer/p/6502600.html

JVM記憶體模型以及分割槽

JVM記憶體分為:

1.方法區:執行緒共享的區域,儲存已經被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料

2.堆:執行緒共享的區域,儲存物件例項,以及給陣列分配的記憶體區域也在這裡。

3.虛擬機器棧:執行緒隔離的區域,每個執行緒都有自己的虛擬機器棧,生命週期和執行緒相同。虛擬機器棧描述方法執行的記憶體模型,以站棧幀為單位,每個棧幀儲存和方法執行有關的區域性變量表、運算元棧、動態連結、方法返回地址等資訊。

4.程式計數器:執行緒隔離的區域,每個執行緒都有自己的程式計數器,儲存程式當前執行的位元組碼的行號。

5.本地方法棧:執行緒隔離,和虛擬機器棧類似,是虛擬機器呼叫Native方法時使用的。

 

堆的分割槽,以及各個分割槽的特點:

Java堆是垃圾收集器管理的主要區域,按照分代收集演算法的劃分,堆記憶體空間可以繼續細分為年輕代,老年代。年輕代又可以劃分為較大的Eden區,兩個同等大小的From Survivor,To Survivor區。預設的Eden區和Survivor區的大小比例為8:1:1,這個比例可以調節。在為新建立的物件分配記憶體的時候先將物件分配到Eden區和From Survivor區,在立即回收時,會將Eden區和Survivor區還存活的物件複製到To Survivor區中,如果To Survivor區的大小不能容納存活的物件,會把存活的物件分配到老年區。總體來說,新建立的小物件會放在年輕代,年輕代的物件大多在下一次垃圾回收時被回收,老年代儲存大的物件和存活時間長的物件。

 

物件的建立方法,物件的記憶體佈局,物件的訪問定位

物件的建立:

1.普通物件的建立過程:虛擬機器遇到一條new指令時,首先檢查這個指令的引數(類的型別)是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類時候已經被載入、解析、初始化過,如果沒有要執行類載入過程。

2.陣列物件的建立:虛擬機器遇到一條newarray位元組碼指令會在記憶體中直接分配一塊區域。

3.Class物件的建立:在虛擬機器載入類的時候,通過類的全限定名獲取此類的二進位制位元組流,再通過檔案驗證後把位元組流代表的靜態結構轉化為方法區的執行時資料結構,並且在記憶體中生成一個代表這個類的Class物件,存在方法區中,作為這個類的各種資料的訪問入口。

 

物件的記憶體佈局:

物件在記憶體中的佈局分為三塊區域:物件頭、例項資料、對齊填充

物件頭:儲存物件自身的執行時資料,包括雜湊嗎,GC分代年齡,鎖狀態標識,執行緒持有的鎖,偏向執行緒ID,偏向時間戳等;物件頭的另外一部分是型別指標,即物件指向它在方法區中的類元資料的指標,虛擬機器通過這個指標來確定該物件是哪個類的例項。如果物件是一個數組,物件頭還有一塊用語記錄陣列長度的資料。

例項資料:物件真正儲存的有效資訊,是在類中定義的各種型別的欄位內容。

對齊填充:虛擬機器要求物件的大小必須是8位元組的整數倍,對齊填充起佔位符的作用,保證物件大小為8位元組的整數倍。

 

物件的訪問定位:Java程式通過棧上的引用資料操作堆中的具體物件,物件訪問方式有兩種:控制代碼訪問,直接指標訪問

控制代碼訪問:Java堆劃分出一塊區域用作控制代碼池,引用中儲存物件的控制代碼地址,控制代碼中才實際包含著物件例項資料和物件型別資料各自的具體地址資訊。

直接指標訪問:棧中的引用直接指向物件在堆中的地址,物件在頭資料中指向方法區中其類元資料的地址。

使用控制代碼的好處是引用中儲存的是穩定的控制代碼地址,在物件被移動(垃圾回收導致物件的移動)時只會改變局並重的例項資料指標。使用直接指標訪問的好處是速度更快。

 

垃圾回收的判定方法:引用計數法,引用鏈法

引用計數法:給物件新增一個引用計數器,有物件引用計數器加1,引用失效計數器減1,計數器為0表示物件不再被使用,可以被回收。

引用鏈法(可達性分析):通過GC Roots作為起點,當一個物件到到GC Roots沒有任何引用鏈相連時,證明物件時不可用的。

可作為GC Roots的物件是虛擬機器棧中引用的物件、本地方法棧中引用的物件、方法區中類靜態屬性引用的物件,方法區中常量引用的物件(執行上下文和全域性性引用)

 

Java的四種引用型別及特點:

1.強引用:程式中普遍存在的,類似“String s=”hello wold””這類的引用,強引用的物件不會被回收。

2.軟引用:有用但是非必須的物件在系統將要發生記憶體溢位之前會對軟引用的物件進行垃圾回收,SoftReference類實現軟引用。

3.弱引用:非必須的物件,被弱引用關聯的物件只能存活到下一次垃圾收集發生之前。

4.虛引用:最弱的引用關係,不能通過虛引用取得物件的例項,為物件設定虛引用的唯一目的就是在這個物件被收集器回收時收到一個系統通知。

四種引用強度依次減弱,強軟弱虛。

 

GC的三種收集演算法的原理和特點,用途,優化思路

三種垃圾收集演算法:複製演算法,標記-清除演算法、標記-整理演算法

標記-清除演算法:首先標記出所有需要回收的物件,標記完成後統一回收所有被標記的物件。缺點:標記和清除兩個過程效率都不高;標記清楚後會產生空間碎片,空間碎片導致分配較大物件時可能提前出發垃圾回收。

複製演算法:將可用記憶體分為兩個區域,每次只使用其中一塊,當使用的那一塊記憶體用完時,將還存活的物件複製到另外一塊記憶體中,然後把已使用過的記憶體空間一次清理掉。優點:解決的空間碎片問題,實現簡單。缺點:將記憶體縮小為兩塊,記憶體使用率不高。複製操作頻繁效率變低。

標記-整理演算法:可回收物件標記後,讓所有存活的物件向一端移動,然後清理掉邊界以外的記憶體。優點:不會產生空間碎片,比複製演算法提高了記憶體空間利用率。

複製演算法用在年輕代的垃圾回收中,標記整理和標記清除演算法用在老年代垃圾回收的收集器中。

 

GC收集器有哪些?CMS和G1收集器的特點

GC收集器按照回收區域不同,新生代有Serial,Parnew,Paralell Scanvage,老年代有Serial Old,CMS,Parallel old,還有新生代老年代通用的G1

Serial 和Serial old是早期jdk中釋出的垃圾收集器,特點是都為單執行緒,新生代採用複製演算法,老年代採用標記整理演算法,兩個垃圾收集器在工作的時候必須要停掉所有的使用者執行緒,直到收集完成後才能回覆使用者執行緒,由於是單執行緒工作方式,沒有執行緒互動的開銷所以能夠活的最高的單執行緒收集效率,使用在client模式下的虛擬機器。

ParNew收集器是Serial收集器的多執行緒版本,是年輕代的垃圾收集器,可以和Serial old以及CMS老年代收集器搭配使用。Parnew在單CPU環境中的效能沒有Serial好,因為單CPU環境下的多執行緒按照時間順序序列執行,還要承擔執行緒間互動的額外開銷,不過在多cpu環境下,Parnew的效能就會好很多,是執行在server模式下的虛擬機器首選的新生代收集器。

在jdk1.4時新推出的垃圾收集器是Parallel Scanvage 和對應的Parallel Old,新生代基於複製演算法,老年代基於標記整理演算法.Parallel Scanvage也是並行性的多執行緒收集器,它和Parnew 的區別在於兩者的關注點不同。Parnew關注於減少垃圾回收時使用者執行緒停頓的時間,而Parllel Scanvage 關注點事獲得最大的吞吐量,也就是CPU執行使用者程式碼與CPU總消耗時間的比值。停頓時間短適合於和使用者有互動的程式,吞吐量高則可以高效的利用CPU時間,儘快完成運算任務,主要是和在後臺運算不需要太多的互動任務。

Jdk1.5時推出了能夠和使用者執行緒併發執行的CMS收集器,CMS是老年代垃圾收集器。CMS是一種以獲取最短回收停頓時間為目標的收集器,基於標記清除演算法來實現。它的工作過程先後分為初始標記、併發標記、重新標記、併發清除四個步驟,其中初始標記和重新標記是需要停頓使用者執行緒的,併發標記和併發清理過程是可以和使用者執行緒併發執行的,在整體垃圾收集時間裡,初始標記和重新標記所佔的時間很少,重新標記階段又是可以多個垃圾回收執行緒並行執行的,所以整體使用者執行緒停頓的時間很短。CMS的缺點:對CPU資源敏感,CMS預設啟動的垃圾回收執行緒數為(CPU數量+3)/4,在併發階段由於佔用使用者執行緒導致應用變慢,cpu不足4個時候對使用者程式影響很大;CMS無法處理在併發清理階段新產生的垃圾,只有等下一次垃圾回收標記後才能清除;CMS基於標記清除演算法會產生空間碎片,CMS的解決方式是在進行Full GC時開啟記憶體整理,這一過程無法併發,延長了使用者執行緒的停頓時間。

G1收集器是在jdk1.7時推出的用語新生代和來年代的垃圾收集器,面向server模式。G1把記憶體區域劃分成多個大小相同的獨立區域,G1跟蹤每個區域裡面垃圾堆積的價值大小,在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的區域,這種收集策略可以在有限時間內獲取儘可能高的收集效率。G1垃圾回收過程:初始標記(單執行緒,停頓)、併發標記(單執行緒,併發)、最終標記(多執行緒,並行,停頓)、篩選回收(多執行緒,並行,停頓)。

 

Minor GC和Full GC分別發生在什麼時候?

當建立物件分配的記憶體空間不足時會啟動一次Minor GC,收集新生代的Eden區和From Survivor區,把還存活的物件分配到To Survivor區,如果To Survivor區的空間不足以容納存活的物件,會把存活的物件分配到老年代,如果老年代也沒有足夠的空間會啟動一次Full GC

 

類載入過程:載入、驗證、準備、解析、初始化

虛擬機器的類載入機制就是把描述類的資料從Class檔案(或者其他途徑)載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別。

載入:1.通過一個類的全限定名獲取定義此類的二進位制位元組流2、將這個位元組流所戴曉的靜態結構轉化為方法區的執行時資料結構3、在記憶體中生成一個代表這個類的Class物件,作為方法區這個類的各種資料的訪問入口。

驗證:1、檔案格式驗證,保證輸入的位元組流在格式上符合Class檔案的格式規範,保證輸入的位元組流能正確的解析,只有通過這個驗證,位元組流才會儲存在方法區之內2、元資料驗證,對類的元資料進行語義校驗,保證類描述的資訊符合Java語言規範。比如驗證類的是否實現了父類或者介面中的方法等3、位元組碼驗證,通過資料流和控制流的分析,確保類的方法符合邏輯,不會在執行時對虛擬機器產生危害4、符號引用校驗,發生在解析階段,確保解析階段將符號引用轉化為直接飲用的正常執行。

準備:正式為類變數(static)分配記憶體,並設定類變數初始值(資料型別的零值),這些變數所使用的記憶體在方法區中分配。

解析:虛擬機器將常量池內的符號引用轉化為直接飲用,解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制代碼和呼叫點限定符7類符號引用進行。

初始化:初始化階段才真正執行類中定義的Java程式碼,初始化階段是執行類構造器<clinit>方法的過程。<clinit>方法(類構造器)是由編譯器自動收集類中的靜態變數和靜態程式碼塊合併產生的。子類和父類的初始化過程優先順序為:父類類構造器->子類類構造器->父類物件建構函式->子類物件建構函式。類中靜態類變數和靜態程式碼塊是按照在類中定義的順序執行的。

 

什麼時候進行類的初始化?

JVM規定了有且僅有5中情況——對類進行主動引用,必須立即執行類的初始化。

1)、遇到new,putstatic,getstatic,invokespecial四條位元組碼指令的時候,如果沒有進行類的初始化要立即初始化。這四條位元組碼指令對應的程式設計中的環境為:使用new關鍵字例項化物件,讀取或設定類的靜態變數,呼叫類的靜態方法。

2)、使用java.lang.reflect包對類進行反射的時候,如果沒有初始化要立即初始化。

3)、初始化一個類的時候,如果其父類沒有進行初始化要先出發父類的初始化

4)、虛擬機器啟動的時候,main方法所在的主類會被虛擬機器先初始化

5)、使用動態語言在lava.lang.invoke.MethodHandle例項最後的解析結果是REF_getdtatic,REF_putStatic,REF_invokeStatic的方法控制代碼,這個控制代碼對應的類沒有被初始化需要先觸發其初始化。

 

雙親委派模型:

類載入器的雙親委派模型是指從頂層到底層分別是啟動類載入器、擴充套件類載入器、應用程式類載入器、自定義類載入器。類載入器之間的父子關係不是通過繼承來實現,而是通過組合來實現。雙親委派模型的工作過程是:如果一個類載入器收到了類載入的請求,首先把這個請求委派給父類載入器去完成,所有的載入請求最終都應該傳送到頂層的啟動類載入器中,只有當父類反饋自己無法完成類載入請求的時候,自載入器才會嘗試自己去載入。

使用雙親委派模型的好處:java類隨著他的載入器一起具備了帶有優先順序的層次結構,最基礎的類由頂層的類載入器載入,這樣保證在程式中使用該類的地方使用的都是這同一個類。