1. 程式人生 > 其它 >深入瞭解JVM虛擬機器總結

深入瞭解JVM虛擬機器總結

1、介紹一下Java記憶體區域(執行時資料區)

(1) 程式計數器

程式計數器是一塊較小的空間,在虛擬機器的概念模型中,位元組碼直譯器工作時就是通過這個計數器的值來獲取下一條執行位元組碼的指令。程式計數器繫結的是執行緒,即每條執行緒只會有一個獨立的程式計數器。如果執行的是Java方法,這個計數器記錄的則是正在執行虛擬機器位元組碼指令的地址。如果方法是native修飾的則程式計數器為空且此記憶體區域也是唯一一個在Java虛擬機器中沒有規定任何OutMemoryError情況的區域。

(2) Java虛擬機器棧

與程式計數器一樣,都是執行緒私有的,且它的生命週期和執行緒相同。虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法在執行同時都會建立一個棧幀用來儲存區域性變量表、運算元棧、動態連結、方法出口等資訊。每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。

區域性變量表存放了編譯期的各種基本資料型別(boolean,char,byte,int,short,long,float,double)、物件引用(reference型別、它不同於物件本身,可能是一個指向物件起始地址的引用指標,也可能是一個指向代表物件的控制代碼或其他與此物件相關的位置)和returnAddress型別(指向了一條位元組碼指令的地址)。

在Java虛擬機器規範中,對這個區域規定了兩種異常狀況:如果執行緒請求的棧深度大於虛擬機器所允許的深度則丟擲StackOverflowError異常,如果虛擬機器棧可以動態擴充套件,擴充套件時無法申請到足夠的記憶體將會丟擲OutOfMemoryError異常。

(3) 本地方法棧

和上述一樣都是執行緒私有的,與虛擬機器棧所發揮的作用都是非常相似,它們之間最大的區別無非就是虛擬機器棧執行的是Java方法服務,而本地方法棧執行的是native方法服務。本地方法棧同時也會丟擲OutOfMemoryError異常和StackOverflowError異常。

(4) Java堆

執行緒共享的資料區域,同時也是Java虛擬機器管理的記憶體最大的區域。在虛擬機器啟動時建立,此記憶體區域唯一目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體。

Java堆也是GC堆(垃圾收集器)管理的主要區域,堆中細分可以分為新生代和老年代,再細分可以分為Eden控制元件、Form Survivor空間、ToSurvivor空間。

當Java堆無法擴充套件時,丟擲OutOfMemoryError異常。

(5) 方法區

所有執行緒共享,方法區又稱為永生代或者持久區,用於儲存已經被虛擬機器載入的類資訊(即載入類時需要載入的資訊,包括版本、field、方法、介面等資訊)、final常量、靜態變數、編譯器即時編譯的程式碼等。

當方法區無法滿足記憶體分配需求時,丟擲OutOfMemoryError異常。

(6) 執行時常量池

執行時常量池是方法區的一部分,Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項就是常量池用於存放編譯期生成的各種字面量和符號引用,這部分記憶體在類載入進方法區時在常量池中存放。

既然是執行時的常量池是方法區的一部分,自然而然也受到方法區的限制,當常量池無法再申請到記憶體時就會丟擲OutOfMemoryError異常。

(7) 直接記憶體

並不是虛擬機器執行時資料區的一部分,也不是Java虛擬機器規範中定義的記憶體區域。

JDK1.4加入了NIO,引入一種基於通道與緩衝區的I/O方式,它可以使用Native函式庫直接分配堆外記憶體,然後通過一個儲存在Java堆中的DirectByteBuffer物件作為這塊記憶體的引用進行操作。因為避免了在Java堆和Native堆中來回複製資料,提高了效能。

當各個記憶體區域總和大於實體記憶體限制,丟擲OutOfMemoryError異常。

2、物件的訪問定位的兩種方式

建立物件的目的是為了使用物件,我們的Java程式需要通過Java棧是哪個的reference資料來操作堆上的具體物件。由於reference型別在Java虛擬機器規範中只規定了一個指向物件的引用,並沒有定義這個引用通過何種方式去定位,訪問堆中的物件具體位置,所以物件訪問方式也是取決於虛擬機器實現而定,目前主流的訪問方式是使用控制代碼和直接指標兩種。

1、通過控制代碼方式來訪問

如果使用的是控制代碼訪問,Java堆就會劃分一塊記憶體來作為控制代碼池,reference中儲存的就是物件的控制代碼地址,而控制代碼包含了物件例項資料與型別資料各種的具體地址資訊。

2.直接指標訪問方式

如果使用的是指標直接訪問,那麼Java堆物件的佈局就必須考慮如何放置訪問型別資料的相關資訊,而reference中儲存的直接就是物件地址。

這兩種物件的訪問方式各有特點,使用控制代碼最大好處就是reference中儲存的是穩定的控制代碼地址,在物件移動的時只會改變例項資料的指標,而reference本身不需要修改。

直接使用指標訪問方式最大好處就是速度更快,因為其節省了一次指標定位的時間開銷,由於物件的訪問在Java中非常頻繁,所有這種方式就HotSpot虛擬機器而言採用的就是直接指標訪問的方式。

3、如何判斷物件是否死亡(兩種方法)

(1)引用計數演算法

引用計數演算法就是給物件中新增一個引用計數器,每當有一個地方引用它時,計數器則加一,如果引用失效則計數器減一。任何時刻引用計數器為0時物件就是不可能再被使用了。

(2)可達性分析演算法

在主流的商用程式語言中。都是使用可達性分析來判斷物件是否存活,這個演算法的基本思想就是通過一系列的“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋走過的路稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連,則證明此物件不可用。

如圖所示 Object5、Object6、Object7雖然互有關聯,但是GC Roots是不可達的,所以判定是可回收的物件。

在Java語言中,可作為GC Roots的物件包括下面幾種

1:虛擬機器棧(棧幀中的本地變量表)中引用的物件。

2:方法區中類的靜態屬性引用物件。

3:方法區中常量引用的物件。

4:本地方法棧中引用的物件。

4、簡單的介紹一下強引用、軟引用、弱引用、虛引用(虛引用與軟引用和弱引用的區別、使用軟引用能帶來的好處)。

JDK1.2後,Java對引用概率進行了擴充,將引用分為了強引用、軟引用、弱引用和虛引用。下面對這4個引用進行簡單的講解。

強引用:指程式程式碼中普遍存在的,類似new一個物件即Object obj = new Object()這類引用,只要強引用還存在,垃圾收集器永遠不會回收掉這被引用的物件。

軟引用:用來描述一些有用但並非必須的物件,對於軟引用關聯著的物件,在系統將要發生記憶體溢位異常之前,將會把這些物件列為回收範圍進行第二次回收。如果這次回收還沒有足夠記憶體,才會丟擲記憶體溢位的異常。在JDK1.2後,提供了SoftReference類來實現軟引用。

弱引用:用來描述非必需物件,強度比軟引用更弱,被弱引用的關聯物件只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論記憶體是否足夠,都會回收弱引用關聯的物件。JDK1.2 使用WeakReference類來實現弱引用。

虛引用:最弱的一種引用關係,每次垃圾回收都會被回收,虛引用的get方法用於獲取到的資料為null,虛引用主要是用來檢測物件是否已經被記憶體中刪除。JDK1.2使用 PhantomReference類來實現虛引用。

軟引用的好處?

軟引用的好處可以用來實現快取記憶體區域,例如某一次操作需要載入大量圖片,如果每次都從硬碟讀取會嚴重影響效能,如果你全部放記憶體中又會導致記憶體洩露,這時候就需要用來軟引用來來實現快取記憶體,快取記憶體的特點如果命中則能夠加快響應,如果未命中還能重新獲取原始資料。對於某更新頻率低,但查詢很慢的資料可以將其放入軟引用類中,當GC發現將要記憶體溢位時就釋放這軟引用中的資料來提供給其他物件使用。

  1. /**
  2. * @ClassName SoftReferenceTest
  3. * @Description 軟引用好處例項
  4. * @Author huangwb
  5. * @Date 2019-02-18
  6. * @Version 1.0
  7. **/
  8. public class SoftReferenceTest {
  9. private static Map<String, SoftReference<Object>> cacheMap = new HashMap<>();
  10. public static Object getStudent(String stuId){
  11. SoftReference<Object> softReference = cacheMap.get(stuId);
  12. //判斷快取中是否存在
  13. Object cacheStudent = softReference==null?null:softReference.get();
  14. if(cacheStudent==null){
  15. cacheStudent = getMysqlQuery(stuId);
  16. cacheMap.put(stuId,new SoftReference<>(cacheStudent));
  17. }
  18. return cacheStudent;
  19. }
  20. //從資料庫中查詢
  21. public static Object getStudentById(String stuId){
  22. //模擬查詢資料過程
  23. Object object = new Object();
  24. return object;
  25. }
  26. }

5、垃圾收集器有哪些演算法,各自的特點。

垃圾收集器的演算法有標記-清除演算法、複製演算法、標記整理演算法、分代收集演算法。下面給大家詳細介紹這四種演算法的優點缺點。

(1)標記-清除演算法

最基礎的收集演算法,如果它的名字一樣,演算法分為“標記”和“清除”兩個階段:首先標記處所有需要回收的物件,標記完成後統一回收所有被標記的物件。之所以是最基礎的收集演算法,因為後續的演算法都是基於這種演算法的不斷改進的。

缺點:效率問題,標記清除兩個過程效率都不高。空間問題,標記清除後產生了大量不連續的記憶體碎片,空間碎片太多導致物件進來時無法給其分配足夠的連續記憶體空間,不得不再次觸發一次垃圾收集操作。

(2)複製演算法

它將可用記憶體按容量分為兩塊等大的記憶體區域,每次只使用其中一塊記憶體區域,當這一塊記憶體區域使用完畢之後,就將還存活的物件複製到另外一塊區域中去,然後把已經使用過的記憶體空間再清理一次。這樣就使得每次回收都是一半記憶體進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要移動堆頂的指標,按照順序分配記憶體即可。

優點:實現簡單,執行高效。

缺點:代價太大,將原有的記憶體縮小一半。

(3)標記-整理演算法

複製演算法對物件存活率較高時就需要進行較多的複製操作,效率會很低。

根據老年代的特點,標記-整理演算法就誕生了,不是直接對可回收物件進行清理,而是讓所有存貨物件都向一段移動,然後直接清理掉端邊界以外的記憶體。嗯

(4)分代收集演算法

先介紹一下什麼是年輕代、老年代heapspace分為年輕代和年老代,年輕代的垃圾回收叫MonorGC 年老代的垃圾回收叫FullGC。

年老代和年輕代比例會2:1年輕代中又有Eden空間 8/10、To Survivor空間 1/10、FromSurvior空間 1/10的空間比例。

HotSpot JVM把年輕代分為了三部分:1個Eden區和2個Survivor區(分別叫from和to)。預設比例為8:1,為啥預設會是這個比例,接下來我們會聊到。一般情況下,新建立的物件都會被分配到Eden區(一些大物件特殊處理),這些物件經過第一次Minor GC後,如果仍然存活,將會被移到Survivor區。物件在Survivor區中每熬過一次Minor GC(young GC),年齡就會增加1歲,當它的年齡增加到一定程度(15歲)時,就會被移動到年老代中。

當前的商業虛擬機器的垃圾收集器都採用“分代收集演算法”,這種演算法將Java堆分為新生代和老年代,這樣就能夠根據各個年代的特點採用最適當的收集演算法。在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量物件存活於是可以採用複製演算法,只需要付出少量存活物件的複製成本就可以完成收集。而老年代因為物件存活率高、沒有額外的空間對它分配擔保,就必須採用“標記-清理”或者“標記-整理”演算法來進行回收。

6、HotSpot為什麼要分為年輕代和老年代?

由於80%以上的物件都是“朝生夕死”,如果不按照分代進行隔離虛擬機器就只能掃描整個Java堆去拿取物件、清楚物件這樣會非常影響垃圾回收的效率。如果分代的話,我們把新建立的物件放到一地方,當GC的時候我們只需要把這一塊區域給回收,這樣會節省很多的記憶體空間。

年輕代中就可以使用複製演算法,因為複製演算法的特點就是複製存活的物件,而年輕代中的物件大部分都是朝生夕死的,所以能夠提升GC的回收效率執行更加高效。而年老代的特點都是一些生命週期比較長的物件,我們則可以採用標記清楚的垃圾回收演算法更好的提高效率。

7、常見的垃圾回收期有哪些?

一共7種垃圾收集器,根據分代的不同選擇不同的垃圾收集器

(1)Serial收集器

Serial收集器是最基本的、發展歷史最悠久的垃圾收集器。Serial垃圾收集器的特點是單執行緒收集器,它的單執行緒不是隻會使用一個CPU或一條收集執行緒去完成工作。這裡的單執行緒收集器指的是在它進行垃圾回收的過程中需要停止所有的工作執行緒,直到它收集完成。例如你工作一小時停頓5分鐘一樣,這樣想可能沒問題但是在程式中這5分鐘會非常影響使用者體驗。

下面這張圖介紹了Serial收集器的垃圾回收演算法的實現。

2)ParNew收集器

ParNew收集器是Serial收集器的多執行緒版本,除了多條現場進行垃圾收集之外其餘各種特點和Serial類似,其中最大的亮點就是能夠和跨時代的CMS收集器配合工作,後面我們會講解到CMS收集器是什麼,這款收集器簡單介紹就是一款真正意義上的併發收集器,能夠讓垃圾回收執行緒和使用者執行緒工作同時進行。

ParNew和Serial的效能區別:ParNew和Serial在單執行緒的環境下,ParNew的效能弱於Serial,但ParNew的特點就是多執行緒垃圾收集器,隨著CPU的數量增加它對於GC時的系統資源有效利用很有好處的。

(3)Parallel Scavenge收集器

Parrallel Scavenge收集器是一個新生代的多執行緒並行收集器,它也是一個採用複製演算法的收集器。可能各位覺得和ParNew收集器一樣。但它最大的特點是可以控制吞吐量,什麼是吞吐量呢就是CPU用於執行使用者程式碼時間與CPU總消耗時間的比值,即吞吐量=執行使用者程式碼時間/(執行使用者程式碼+垃圾收集時間),如果此時虛擬機器共運行了100分鐘,其中的垃圾收集花掉了一分鐘,則此時的吞吐量就是99%,停頓時間越短在需要和使用者互動的程式,良好的響應速度就能提升使用者體驗,而高吞吐量就可以高效率的利用CPU時間,儘快完成程式的運算任務,主要適用於後臺運算而不需要太多互動的任務。大家可以去搜一搜Parallel Scavenge收集器 會有更加深刻的理解。

(4)Serial Old收集器

Serial Old收集器就是Serial收集器的老年代版本,它同樣是一個單執行緒收集器,適用“標記-整理”演算法。這個收集器的主要意義也是在於給Client模式下虛擬機器使用。如果再Server模式下,它有兩大用途:一種是和JDK1.5以及之前的版本中與Parallel Scavenge收集器搭配使用,另一種使用者就是作為CMS收集器的備選預案。

(6)CMS收集器

終於講到這個跨時代的收集器了,這一種以最短回收停頓時間為目標的收集器。目前很大一部分的Java應用幾種在網際網路或者B/S系統服務端上,這類應用由其重視伺服器的響應速度,希望系統停頓時間最短,以給使用者帶來較好的體驗。

CMS收集器是一種基於“標記-清除”演算法實現的,比前幾種來說實現更加複雜,整個過程分為4步:

1:初始標記

2:併發標記

3:重新標記

4:併發清除

初始標記和重新下標記這兩個步驟仍然會造成使用者執行緒的挺短,初始標記僅僅只是標記一下GC Roots能直接關聯的物件,速度非常快。而重新標記則是為了修正併發標記期間因使用者程式繼續執行而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記要短。

整個過程中耗時最長的是併發標記和併發清除過程的收集器執行緒但都可以和使用者執行緒一起工作,總體來說CMS收集器的記憶體回收過程是與使用者執行緒一起併發執行的。

CMS的優點:併發收集、低停頓。

CMS的缺點:

1:CMS收集器對CPU資源非常的敏感。其實面向併發設計的程式都對CPU資源比較敏感,在併發階段雖然不會導致使用者執行緒的挺短,但是會佔用了一部分執行緒從而導致應用程式變慢,總吞吐量降低。CMS預設啟動的回收執行緒數是(CPU數量+3)/4,也就是在CPU為4個以上時,併發回收時垃圾收集執行緒不少於25%的CPU資源,並且隨著CPU數量的增加而下降,但是當CPU不足4個時(例如兩個),CMS對使用者程式影響就變得很大了,如果本來CPU的負載就比較大,還分出一半運算能力去執行收集器執行緒,則會導致使用者程式執行速度忽然降低50%,其實也讓人無法接受。

2:CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。由於CMS併發清理階段使用者執行緒還在執行,伴隨程式執行自然就會有新的垃圾不斷產生,這一部分垃圾在標記過程之後,CMS無法再當次手機中處理掉它們,只好等待下一次GC時再清理掉。這一份垃圾就稱為“浮動垃圾”。

3:這個缺點就是由於CMS是一款基於“標記-清楚”演算法實現的收集器,則必然會在垃圾收集之後造成大量空間碎片產生。空間碎片過多時,將會給大對蝦分配帶來很大麻煩,往往會出現老年代還有很大空間,卻無法找到足夠大的連續空間來儲存大物件,不得不觸發一次Full GC操作。

(7)G1垃圾收集器

G1收集器是當今收集器技術發展最前沿的成果

9、Minor GC和Full GC有什麼不同?

年輕代GC(Minor GC):指符合年輕代大多數物件“朝夕生死”的特性,所以Minor GC使用非常頻繁,回收速度也比較快。

老年代GC(Full GC/Major GC):FullGC是清理整個堆空間的包括年輕代、老年代和元空間(JDK1.8之前定義為持久區或永久代)FullGC一般消耗的時間遠比MinorGC ,因此我們必須降低FullGC發生的頻率。

Minor GC的觸發機制:當年輕代滿時就會觸發Minor GC 這裡的年輕代滿指的是Eden空間 不是Survivor空間,因為To Survivor和From Survivor總是保持一端有資料另一端無資料的情況是用來執行復制演算法的。

Full GC的觸發機制:(1)呼叫System.gc時,系統建議執行Full Gc的操作 (2)老年代空間不足時(3)方法區空間不足時(4)通過Minor GC後進入老年代的物件大於老年代的空間時(5)由Eden區、Form Survivor區向ToSurvivor區複製時,物件大於ToSurvivor空間時,則把該物件放入老年代,且老年代可用連續空間小於該物件大小時。

10、JVM調優的常見命令列工具有哪些?

11、簡單介紹一下Class類檔案結構(常量池主要存放的是那兩大常量?Class檔案的繼承關係是如何確定的?欄位表、方法表、屬性表主要包含那些資訊?)

12、簡單說說類載入過程,裡面執行了哪些操作?!

類載入過程中最重要的就是載入、驗證、準備、初始化和解除安裝的順序是確定的,類的載入過程必須按照這種順序按部就班的進行,而解析卻不一定了,為了支援Java語言的晚期繫結或者動態繫結,通常會在一個階段執行過程中啟用另外一個階段。

載入:是類載入的一個過程通常會通過一個類的全限定名來獲取定義的二進位制位元組流。將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時的資料結構。在記憶體中生成一個代表這個類的的Class物件,作為方法區這個類的各個資料的訪問入口。

驗證:是連線階段的第一步,主要是確保Class檔案的位元組流包含的資訊符合當前虛擬機器的要求,並且不會危害到虛擬機器的自身安全。

準備:正是為類變數分配記憶體並設定初始化值的階段,這些變數所使用的記憶體都在方法區中進行分配。說明:這時候進行記憶體分配的僅僅只是類變數(static修飾的變數)不是例項變數,例項變數會在物件例項化時隨著物件一起被分配在Java堆中。另外,這裡說的初始化只是將類變數設定為資料型別的零值

public static int value = 666;

那變數value在準備階段初始化的值為0而不是666,只有當程式編譯完成之後才開始執行賦值的操作。

解析:解析階段是指虛擬機器將常量池中的符號引用替換為直接引用的過程。

初始化:初始化階段是類載入最後一個階段,前面的類載入階段之後,除了在載入階段可以自定義類載入器以外,其它操作都由JVM主導。到了初始階段,才開始真正執行類中定義的Java程式程式碼。