1. 程式人生 > >Java基礎之詳解jvm

Java基礎之詳解jvm

一、JVM記憶體結構

這裡寫圖片描述

1、方法區(Method Area)

別名Non-Heap(非堆)、永久代(Permanent Generation)、持久代(PermGen),各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、以及編譯器編譯後的程式碼等資料(jdk1.7的HotSpot中,已經把原本放在方法區中的靜態變數、字串常量池等移到堆記憶體中)。
:在Java 8裡已被廢除了,被元空間取代;

2、Java堆(Java Heap)

Java堆也是執行緒共享的一塊記憶體區域,此記憶體區域的唯一目的就是存放物件例項(物件和陣列),可以想象一個系統會產生很多例項,因此java堆的空間也是Java虛擬機器所管理的記憶體中最大的一塊。
Java堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“GC堆”。如果從記憶體回收的角度看,由於現在收集器基本都是採用的分代收集演算法,所以Java堆中還可以細分為:新生代和老年代;再細緻一點的有Eden空間、From Survivor空間、To Survivor空間等,Eden : from : to = 8 : 1 : 1 ( 可以通過引數 –XX:SurvivorRatio 來設定 )。

3、Java棧(Java Stacks)

虛擬機器會為每個執行緒分配一個虛擬機器棧,每個虛擬機器棧中都有若干個棧幀,每個棧幀中儲存了局部變量表、運算元棧、動態連結、返回地址等。一個棧幀就對應 Java 程式碼中的一個方法,當執行緒執行到一個方法時,就代表這個方法對應的棧幀已經進入虛擬機器棧並且處於棧頂的位置,每一個 Java 方法從被呼叫到執行結束,就對應了一個棧幀從入棧到出棧的過程。
如果java棧空間不足了,程式會丟擲StackOverflowError異常,遞迴深度過深,會執行大量的方法,方法越多java棧的佔用空間越大,常常會引起StackOverflowError異常。

4、本地方法棧(Native Method Stacks)

本地方法棧角色和java棧類似,也是執行緒私有的,只不過它是用來執行本地方法的,本地方法棧存放的方法呼叫本地方法介面,最終呼叫本地方法庫,實現與作業系統、硬體互動的目的。

5、程式計數器(Program Counter Register)

程式計數器是一塊較小的記憶體空間,他也是執行緒私有的,它的作用可以看做是當前執行緒所執行的位元組碼的行號指示器,控制程式指令的執行順序。位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。
如果執行緒正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;如果正在執行的是Natvie方法,這個計數器值則為空(Undefined)。
此記憶體區域是唯一一個在Java虛擬機器規範中沒有

規定任何OutOfMemoryError情況的區域。

6、元空間

上面說到,jdk1.8 中,已經不存在永久代(方法區),替代它的一塊空間叫做 “ 元空間 ”,和永久代類似,都是 JVM 規範對方法區的實現,但是元空間並不在虛擬機器中,而是使用本地記憶體,元空間的大小僅受本地記憶體限制,但可以通過 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 來指定元空間的大小。

二、類載入

這裡寫圖片描述
類裝載流程可以概括為:載入–>連線–>初始化–>使用–>解除安裝。
我們主要介紹這一過程的主角:類載入器ClassLoader,它是一個抽象類,負責整個類裝載流程中的“載入”階段。ClassLoader的具體例項負責把java位元組碼讀取到JVM當中,ClassLoader還可以自定義以滿足不同位元組碼流的載入方式,比如從網路載入、從檔案載入。

1、類載入器

這裡寫圖片描述
注意:這裡父類載入器並不是通過繼承關係來實現的,而是採用組合實現的。
類載入器中啟動類載入器屬於 JVM 的一部分,其他類載入器都用 java 實現,並且最終都繼承自 java.lang.ClassLoader。
(1)、啟動類載入器
Bootstrap ClassLoader,是由 C/C++ 編譯而來的,看不到原始碼,所以在 java.lang.ClassLoader 原始碼中看到的 Bootstrap ClassLoader 的定義是 native 的 private native Class findBootstrapClass(String name);負責載入存放在JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被-Xbootclasspath引數指定的路徑中的,並且能被虛擬機器識別的類庫(如rt.jar,所有的java.開頭的類均被Bootstrap ClassLoader載入)。啟動類載入器是無法被Java程式直接引用的。具體載入哪些類可以通過 System.getProperty(“sun.boot.class.path”) 來檢視。
(2)、擴充套件類載入器
Extension ClassLoader,負責載入JDK\jre\lib\ext目錄中,或者由java.ext.dirs系統變數指定的路徑中的所有類庫(如javax.開頭的類),開發者可以直接使用擴充套件類載入器。可以用通過 System.getProperty(“java.ext.dirs”) 來檢視具體都載入哪些類。
(3)、應用程式類載入器
Application ClassLoader,負責載入使用者類路徑(ClassPath)所指定的類,開發者可以直接使用該類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。
(4)、自定義載入器
應用程式都是由以上三種類載入器互相配合進行載入的,如果有必要,我們還可以加入自定義的類載入器。因為JVM自帶的ClassLoader只是懂得從本地檔案系統載入標準的java class檔案,因此可以通過編寫自己的ClassLoader,實現從特定的場所取得java class(例如資料庫中和網路中)等功能。

2、類載入原則

全盤負責,當一個類載入器負責載入某個Class時,該Class所依賴的和引用的其他Class也將由該類載入器負責載入,除非顯示使用另外一個類載入器來載入
父類委託,先讓父類載入器試圖載入該類,只有在父類載入器無法載入該類時才嘗試從自己的類路徑中載入該類。
快取機制,快取機制將會保證所有載入過的Class都會被快取,當程式中需要使用某個Class時,類載入器先從快取區尋找該Class,只有快取區不存在,系統才會讀取該類對應的二進位制資料,並將其轉換成Class物件,存入快取區。這就是為什麼修改了Class後,必須重啟JVM,程式的修改才會生效

3、類載入的三種方式

(1)、命令列啟動應用時候由JVM初始化載入
(2)、通過Class.forName()方法動態載入
(3)、通過ClassLoader.loadClass()方法動態載入

三、垃圾回收演算法

垃圾收集 Garbage Collection 通常被稱為“GC”,它誕生於1960年 MIT 的 Lisp 語言。有了C語言的前車之鑑,Java語言在設計之初就避免讓程式設計師進行GC, jvm 中,程式計數器、Java棧、本地方法棧都是隨執行緒而生隨執行緒而滅,棧幀隨著方法的進入和退出做入棧和出棧操作,實現了自動的記憶體清理,因此,我們的記憶體垃圾回收主要集中於 java 堆和方法區中.

1、物件存活判斷

(1)引用計數:每個物件有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數為0時可以回收。此方法簡單,但是無法解決物件相互迴圈引用的問題。
(2)可達性分析(Reachability Analysis):從GC Roots開始向下搜尋,搜尋所走過的路徑稱為引用鏈。當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的。不可達物件。
在Java語言中,GC Roots包括:

  • 虛擬機器棧中引用的物件。
  • 方法區中類靜態屬性實體引用的物件。
  • 方法區中常量引用的物件。
  • 本地方法棧中JNI引用的物件。

2、選擇垃圾收集的時機

當程式執行時,各種資料、物件、執行緒、記憶體等都時刻在發生變化,當下達垃圾收集命令後就立刻進行收集嗎?肯定不是。這裡來了解兩個概念:安全點(safepoint)和安全區(safe region)。
安全點
從執行緒角度看,安全點可以理解為是在程式碼執行過程中的一些特殊位置,當執行緒執行到安全點的時候,說明虛擬機器當前的狀態是安全的,如果有需要,可以在這裡暫停使用者執行緒。當垃圾收集時,如果需要暫停當前的使用者執行緒,但使用者執行緒當時沒在安全點上,則應該等待這些執行緒執行到安全點再暫停。
舉個例子,媽媽在掃地,兒子在吃瓜子(瓜子皮會扔到地上),媽媽掃到兒子跟前時,兒子說:“媽媽等一下,讓我吃完這塊再掃。”兒子吃完這個瓜子把瓜子皮扔到地上後就是一個安全點,媽媽可以繼續掃地(垃圾收集器可以繼續收集垃圾)。理論上,直譯器的每條位元組碼的邊界上都可以放一個安全點,實際上,安全點基本上以“是否具有讓程式長時間執行的特徵”為標準進行選定。
安全區
安全點是相對於執行中的執行緒來說的,對於如sleep或blocked等狀態的執行緒,收集器不會等待這些執行緒被分配CPU時間,這時候只要執行緒處於安全區中,就可以算是安全的。安全區就是在一段程式碼片段中,引用關係不會發生變化,可以看作是被擴充套件、拉長了的安全點。
還以上面的例子說明,媽媽在掃地,兒子在吃瓜子(瓜子皮會扔到地上),媽媽掃到兒子跟前時,兒子說:“媽媽你繼續掃地吧,我還得吃10分鐘呢!”兒子吃瓜子的這段時間就是安全區,媽媽可以繼續掃地(垃圾收集器可以繼續收集垃圾)。

3、垃圾收集演算法

(1)標記 -清除演算法
“標記-清除”(Mark-Sweep)演算法,如它的名字一樣,演算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的物件,在標記完成後統一回收掉所有被標記的物件。它是最基礎的收集演算法,後續的收集演算法都是基於這種思路並對其缺點進行改進而得到的。
它的主要缺點有兩個:一個是效率問題,因為涉及大量的記憶體遍歷工作,所以執行效能較低,這也會導致“stop the world”(程式停頓)時間較長,java程式吞吐量降低;
另外一個是空間問題,標記清除之後會產生大量不連續的記憶體碎片,這會導致當程式在以後的執行過程中需要分配較大物件時無法找到足夠的連續記憶體,就不得不提前觸發另一次垃圾收集動作。

(2)標記-壓縮演算法
標記過程仍然與“標記-清除”演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體,解決了標記-清除演算法的空間問題,但是效率問題沒有解決。Erqie在壓縮過程中一些物件記憶體地址會發生改變。

(3)複製演算法
“複製”(Copying)演算法,它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。在垃圾回收時,將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。
複製演算法相對標記壓縮演算法來說更簡潔高效,但它的缺點也顯而易見,它不適合用於存活物件多的情況,因為那樣需要複製的物件很多,複製效能較差,所以複製演算法往往用於記憶體空間中新生代的垃圾回收,因為新生代中存活物件較少,複製成本較低。它另外一個缺點是記憶體空間佔用成本高,因為它基於兩份記憶體空間做物件複製,在非垃圾回收的週期內只用到了一份記憶體空間,記憶體利用率較低。整體來說:解決了標記-清除演算法的效率問題,但是空間問題沒有解決

(4)分代收集演算法
GC分代的基本假設:絕大部分物件的生命週期都非常短暫,存活時間短。
“分代收集”(Generational Collection)演算法,把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集演算法。在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量存活,那就選用複製演算法,只需要付出少量存活物件的複製成本就可以完成收集。而老年代中因為物件存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或“標記-整理”演算法來進行回收。

四、垃圾回收器

如果說收集演算法是記憶體回收的方法論,垃圾收集器就是記憶體回收的具體實現,不同的演算法各有各的優缺點,在JVM中並不是單純的使用某一種演算法進行垃圾回收,而是將不同的垃圾回收演算法包裝在不同的垃圾回收器當中。
1、序列(Serial)收集器
序列收集器是最古老,最穩定以及效率高的收集器,可能會產生較長的停頓,只使用一個執行緒去回收。新生代、老年代使用序列回收;新生代複製演算法、老年代標記-壓縮;垃圾收集的過程中會Stop The World(服務暫停)
引數控制:-XX:+UseSerialGC 序列收集器
適用場景:Client 模式(桌面應用);單核伺服器。
這裡寫圖片描述
3、並行回收器
PS:關於垃圾收集器的並行和併發
並行(Parallel):指多條垃圾收集執行緒並行工作,但此時使用者執行緒仍處於等待狀態;
併發(Concurrent):指使用者執行緒與垃圾收集執行緒同時執行,使用者執行緒在繼續執行而垃圾收集程式執行在另外一個CPU上;

並行回收器是使用多執行緒並行回收,不過針對新生代和老年代,是否都使用並行,有不同的回收器選擇:
(1)ParNew收集器
ParNew收集器其實就是序列收集器的多執行緒版本。新生代並行,老年代還是序列;新生代依然使用複製演算法、老年代也依然使用標記-壓縮演算法
引數控制:
-XX:+UseParNewGC ParNew收集器
-XX:ParallelGCThreads 限制執行緒數量
適用場景:多核伺服器;與 CMS 收集器搭配使用。當使用 -XX:+UserConcMarkSweepGC 來選擇 CMS 作為老年代收集器時,新生代收集器預設就是 ParNew
這裡寫圖片描述
(2)Parallel收集器
依然是並行回收器,但這種回收器有兩種配置,一種是Parallel Scavenge類似於ParNEW:新生代使用並行回收、老年代使用序列回收。ParNew 的目標是儘可能縮短垃圾收集時使用者執行緒的停頓時間, Parallel Scavenge的目標是達到一個可控制的吞吐量。可以認為在相同的條件下它比ParNew更優。可以使用 -XX:+UseParallelGC 來選擇 Parallel Scavenge 作為新生代收集器,jdk7、jdk8 預設使用 Parallel Scavenge 作為新生代收集器。
適用場景:Client 模式(桌面應用);單核伺服器;與 Parallel Scavenge 收集器搭配;作為 CMS 收集器的後備預案。
這裡寫圖片描述
吞吐量: CPU 執行使用者執行緒的的時間與 CPU 執行總時間的比值【吞吐量 = 執行使用者代程式碼時間/(執行使用者程式碼時間+垃圾收集時間)】,比如虛擬機器一共運行了 100 分鐘,其中垃圾收集花費了 1 分鐘,那吞吐量就是 99% 。比如下面兩個場景,垃圾收集器每 90 秒收集一次,每次停頓 10 秒,和垃圾收集器每 50 秒收集一次,每次停頓時間 7秒,雖然後者每次停頓時間變短了,但是總體吞吐量變低了(0.90>0.88),CPU 總體利用率變低了。

Parallel回收器另外一種配置Parallel Old則不同於ParNew,對於新生代和老年代均適應並行回收,這個收集器是在JDK 1.6中才開始提供
這裡寫圖片描述
引數控制: -XX:+UseParallelOldGC
可以通過 -XX:MaxGCPauseMillis 來設定收集器儘可能在多長時間內完成記憶體回收,可以通過 -XX:GCTimeRatio 來精確控制吞吐量。
適用場景:注重吞吐量,高效利用 CPU,需要高效運算且不需要太多互動。
4、CMS收集器
以犧牲吞吐量為代價來獲得最短回收停頓時間的垃圾回收器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用都集中在網際網路站或B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給使用者帶來較好的體驗。需要注意的是CMS回收器是一種針對老年代的回收器,不對新生代產生作用。
這裡寫圖片描述
從名字(包含“Mark Sweep”)上就可以看出CMS收集器是基於“標記-清除”演算法實現的,它的運作過程相對於前面幾種收集器來說要更復雜一些,整個過程分為4個步驟,包括:
① 初始標記(CMS initial mark):標記一下 GC Roots 能直接關聯到的物件,速度較快。
② 併發標記(CMS concurrent mark):進行 GC Roots Tracing,標記出全部的垃圾物件,耗時較長。
③ 重新標記(CMS remark):修正併發標記階段引使用者程式繼續執行而導致變化的物件的標記記錄,耗時較短。
④ 併發清除(CMS concurrent sweep):用標記-清除演算法清除垃圾物件,耗時較長。
其中初始標記、重新標記這兩個步驟仍然需要“Stop The World”。初始標記僅僅只是標記一下GC Roots能直接關聯到的物件,速度很快,併發標記階段就是進行GC Roots Tracing的過程,而重新標記階段則是為了修正併發標記期間,因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短。
由於整個過程中耗時最長的併發標記和併發清除過程中,收集器執行緒都可以與使用者執行緒一起工作,所以總體上來說,CMS收集器的記憶體回收過程是與使用者執行緒一起併發地執行。老年代收集器(新生代使用ParNew)
優點: 併發收集、低停頓
缺點: CMS對 CPU 資源敏感(佔用部分執行緒及CPU資源,影響總吞吐量):它的回收並不徹底。這也導致了CMS回收的頻率相較其他回收器要高,頻繁的回收將影響應用程式的吞吐量,預設分配的垃圾收集執行緒數為(CPU 數+3)/4,隨著 CPU 數量下降,佔用 CPU 資源越多,吞吐量越小;
無法處理浮動垃圾:在併發清理階段,由於使用者執行緒還在執行,還會不斷產生新的垃圾,CMS 收集器無法在當次收集中清除這部分垃圾。同時由於在垃圾收集階段使用者執行緒也在併發執行,CMS 收集器不能像其他收集器那樣等老年代被填滿時再進行收集,需要預留一部分空間提供使用者執行緒執行使用。當 CMS 執行時,預留的記憶體空間無法滿足使用者執行緒的需要,就會出現 “ Concurrent Mode Failure ” 的錯誤,這時將會啟動後備預案,臨時用 Serial Old 來重新進行老年代的垃圾收集。
因為 CMS 是基於標記-清除演算法,所以垃圾回收後會產生空間碎片,可以通過 -XX:UserCMSCompactAtFullCollection 開啟碎片整理(預設開啟),在 CMS 進行 Full GC 之前,會進行記憶體碎片的整理。還可以用 -XX:CMSFullGCsBeforeCompaction 設定執行多少次不壓縮(不進行碎片整理)的 Full GC 之後,跟著來一次帶壓縮(碎片整理)的 Full GC。

適用場景:重視伺服器響應速度,要求系統停頓時間最短。
引數控制:
-XX:+UseConcMarkSweepGC 使用CMS收集器
-XX:+ UseCMSCompactAtFullCollection Full GC後,進行一次碎片整理;整理過程是獨佔的,會引起停頓時間變長
-XX:+CMSFullGCsBeforeCompaction 設定進行幾次Full GC後,進行一次碎片整理
-XX:ParallelCMSThreads 設定CMS的執行緒數量(一般情況約等於可用CPU數量)

4、G1收集器
G1 收集器是 jdk1.7 才正式引用的商用收集器,現在已經成為 jdk9 預設的收集器,HotSpot開發團隊賦予它的使命是未來可以替換掉JDK1.5中釋出的CMS收集器。與CMS收集器相比G1收集器有以下特點:
1.空間整合,G1收集器採用標記整理演算法,不會產生記憶體空間碎片。分配大物件時不會因為無法找到連續空間而提前觸發下一次GC。
2.可預測停頓,這是G1的另一大優勢,降低停頓時間是G1和CMS的共同關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為N毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時Java(RTSJ)的垃圾收集器的特徵了。
上面提到的垃圾收集器,收集的範圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的記憶體佈局與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔閡了,它們都是一部分(可以不連續)Region的集合。
這裡寫圖片描述
和CMS的過程比較類似:
① 初始標記:標記出 GC Roots 直接關聯的物件,這個階段速度較快,需要停止使用者執行緒,單執行緒執行。
② 併發標記:從 GC Root 開始對堆中的物件進行可達新分析,找出存活物件,這個階段耗時較長,但可以和使用者執行緒併發執行。
③ 最終標記:修正在併發標記階段引使用者程式執行而產生變動的標記記錄。
④ 篩選回收:篩選回收階段會對各個 Region 的回收價值和成本進行排序,根據使用者所期望的 GC 停頓時間來指定回收計劃(用最少的時間來回收包含垃圾最多的區域,這就是 Garbage First 的由來——第一時間清理垃圾最多的區塊),這裡為了提高回收效率,並沒有採用和使用者執行緒併發執行的方式,而是停頓使用者執行緒。
適用場景:要求儘可能可控 GC 停頓時間;記憶體佔用較大的應用。可以用 -XX:+UseG1GC 使用 G1 收集器,jdk9 預設使用 G1 收集器。
特點

  • 並行與併發(充分利用多核多CPU縮短STW時間)
  • 分代收集(獨立管理整個Java堆,但針對不同年齡的物件採取不同的策略)
  • 空間整合(區域性看是基於複製演算法,從整體來看是基於標記-整理演算法,都不會產生記憶體碎片)
  • 可預測的停頓(可以明確指定在一個長度為M毫秒的時間片內垃圾收集不會超過N毫秒)
  • 將堆分為大小相等的獨立區域,避免全區域的垃圾收集;新生代和老年代不再物理隔離,只是部分Region的集合;
  • G1跟蹤各個Region垃圾堆積的價值大小,在後臺維護一個優先列表,根據允許的收集時間優先回收價值最大的Region;
  • Region之間的物件引用以及其他收集器中的新生代與老年代之間的物件引用,採用Remembered Set來避免全堆掃描;

六、常見面試題
1、雙親委派模型內容及意義
這裡寫圖片描述
雙親委派模型的工作流程是:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把請求委託給父載入器去完成,依次向上,因此,所有的類載入請求最終都應該被傳遞到頂層的啟動類載入器中,只有當父載入器在它的搜尋範圍中沒有找到所需的類時,即無法完成該載入,子載入器才會嘗試自己去載入該類。
雙親委派機制:
1、當AppClassLoader載入一個class時,它首先不會自己去嘗試載入這個類,而是把類載入請求委派給父類載入器ExtClassLoader去完成。
2、當ExtClassLoader載入一個class時,它首先也不會自己去嘗試載入這個類,而是把類載入請求委派給BootStrapClassLoader“`去完成。
3、如果BootStrapClassLoader載入失敗(例如在$JAVA_HOME/jre/lib裡未查詢到該class),會使用ExtClassLoader來嘗試載入;
4、若ExtClassLoader也載入失敗,則會使用AppClassLoader來載入,如果AppClassLoader也載入失敗,則會報出異常ClassNotFoundException。
雙親委派模型意義:
系統類防止記憶體中出現多份同樣的位元組碼
保證Java程式安全穩定執行

1、總結
Jvm知識點很多,一篇文章很難包含所有,我以重點概括為中心思想,進行整理,意在讓讀者有整體認識,以後有機會再行展開