天天看點

JVM原理JVM原理

JVM原理

一、運作時資料區域

  • 線程私有:程式計數器、java虛拟機棧、本地方法棧
  • 線程共有:方法區(運作時常量池)、堆、直接記憶體

1、程式計數器

作用:可以看成是目前線程所執行的位元組碼的行号訓示器(即表征程式運作到何處)。如果執行java方法,則記錄正在執行的虛拟位元組碼指令的位址;如果執行本地(native)方法,則為空(undefined)

特點:

  • 線程私有
  • 不會存在記憶體溢出

2、java虛拟機棧

2.1、定義

虛拟機棧:每一個線程運作時所需的記憶體空間。一個線程中,會包含多個java方法,那麼每執行一個java方法,都會建立一個棧幀

棧幀:每個java方法運作時需要的記憶體空間。用于存儲局部變量表、操作數棧等資訊。

局部變量表:存放編譯時期可知的各種基本資料類型,引用類型等。表的大小在編譯時期就已經确定,并且運作時期不可改變。

2.2、棧的大小

棧的大小:可以通過**-Xss**這個虛拟機參數來指定每一個線程的java虛拟機棧的大小。

**棧記憶體溢出:**抛出StackOverFlowError異常

  • 棧幀過多導緻棧記憶體溢出(遞歸方法中常常出現)
  • 棧幀過大導緻棧記憶體溢出(不常見)

異常:該區域可能引起以下兩種異常:

  • 當線程請求的棧深度超過最大值,會引發StackOverFlowError異常
  • 棧進行動态擴充時,如果無法申請到足夠的記憶體,會抛出OutofMemoryError異常
2.3、特點:
  • 每個棧由多個棧幀組成
  • 每個線程隻能有一個活動棧幀,對應目前正在執行的java方法(棧頂部的棧幀是活動棧幀)
  • 線程私有

3、本地方法棧

為本地方法提供記憶體空間。

本地方法:一般指用其他語言(c/c++/彙編)編寫的,并且被編譯為基于本機硬體和作業系統的程式,這些方法需要特别處理。

4、堆

  • 是java虛拟機中最大的記憶體區域,存放對象執行個體,是垃圾收集的主要區域。
  • 通過new關鍵字建立對象都會使用堆記憶體
4.1、特點
  • 線程共享的,堆記憶體中的對象都需要考慮線程安全問題
  • 有垃圾回收機制
4.2、堆的大小

大小:堆不需要連續的記憶體,并且可以動态擴充其大小,擴充失敗會抛出OutofMemoryError異常。可以通過-Xms和-Xmx這兩個虛拟機參數來指定一個程式的堆記憶體大小,-Xms設定初始值、-Xmx設定替代值。

堆記憶體溢出:抛出OutofMemoryError異常

4.3、堆記憶體診斷
  • jps工具:檢視目前系統中有哪些java程序(查詢程序id)
  • jmap工具:jmap -head 程序id 檢視堆記憶體占用情況
  • jconsole工具:圖形界面形式,多功能檢測工具,可以連續監測

5、方法區

5.1、定義

用于存放已被加載的類資訊、常量、靜态常量等資訊。不需要連續的記憶體,可以進行動态擴充,擴充失敗會抛出OutofMemoryError異常。

對這塊區域進行垃圾回收的主要目标是常量池的回收和類的解除安裝,但是一般比較難實作。

5.2、實作方式

方法區是一個概念,其有具體的實作方式:

  • 在JKD1.8之前,HotSpot方法區的實作是永久代,采用的是堆記憶體的一部分作為方法區
  • 在JDK1.8之後,HotSpot将永久代移除,方法區的實作是元空間,采用的是本地記憶體(作業系統記憶體)的一部分作為方法區
5.3、記憶體溢出

抛出OutofMemoryError異常,通常是因為加載的類太多

  • JDK1.8之前,永久代記憶體溢出
  • JDK1.8以後,元空間記憶體溢出
5.4、運作時常量池

運作時常量池是方法區的一部分,主要為了存放程式中的常量值。

除了在編譯期間生成常量,還運作動态生成常量,使用String類的intern()

public class JvmTest {
	public static void main(String[] args) {
		String s1="abc";  //放入運作時常量池的StringTable中
		String s2="abc";   
		String s3=new String("abc");   //new出來的對象都在堆記憶體中,隻是這個對象的值是“abc”
		System.out.println(s1==s3);   //false
		System.out.println(s1==s3.intern());   //true,将堆記憶體中的"abc"字元串轉換成常量放入StringTable中
	}
}
           
public class JvmTest {
	public static void main(String[] args) {
		String s=new String("a")+new String("b");  //["a","b"]  
		String s2=s.intern();   //["a","b","ab"]
        String s3="a"+"b";  //直接在串池中尋找字元串"ab"
		System.out.println(s2==s3);  //true
	}
}
           

執行第一句代碼時,在串池中放入了常量"a"、“b”。此時s相當于s=new String(“ab”),這個new出來的對象放在堆記憶體中,其值是”ab“。

執行第二句代碼時,會嘗試将"ab"字元串放入串池,如果有則不放入,如果沒有則放入串池,并把串池的對象傳回

6、直接記憶體

  • 是作業系統的記憶體,不屬于java虛拟機的記憶體。
  • 在JDK1.4中新釋出了NIO類,使用Native函數庫直接配置設定堆外記憶體,避免了在堆内和堆外來回拷貝資料的過程,顯著提高性能。
  • 不受JVM記憶體回收管理

注意

  • 如何在堆中給對象配置設定記憶體???

    兩種配置設定方式:指針碰撞和空閑清單

  • 對象的通路定位???

    兩種通路方式:句柄通路和指針直接通路

    句柄通路:java堆中會劃分一塊記憶體作為句柄池,引用變量存儲的是句柄位址,而句柄中包含了兩個位址,一個是對象執行個體資料,一個是對象類型資料(存放于方法區中)

JVM原理JVM原理

**直接指針通路:**引用變量存儲的直接是對象位址,堆中不會分句柄池,對象位址中包含了對象類型資料的位址

二、垃圾回收

1、判斷一個對象是否可回收

1.1引用計數算法

在對象中添加一個引用計數器,當有地方引用這個對象的時候,引用計數器的值就加1,當引用失效的時候(變量記為null),計數器的值就減1。但Java虛拟機中沒有使用這種算法,這是由于如果堆内的對象之間互相引用,就始終不會發生計數器-1,那麼就不會回收。

1.2可達性分析算法:

通過一系列稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜尋,搜尋走過的路徑稱為“引用鍊”,當一個對象到GC Roots沒有任何的引用鍊相連時,證明此對象不可用。

可作為GC Roots的對象:

  • 虛拟機棧中局部變量表引用的對象
  • 本地方法棧中引用的對象
  • 方法區的類屬性所引用的對象
  • 方法區中常量所引用的對象

引用類型

無論是通過引用計數算法判斷對象的引用數量,還是通過可達性分析算法判斷對象是否可達,判斷對象是否可被回收都與引用有關。

  1. 強引用

    使用new關鍵字來建立一個強引用。被強引用關聯的對象不會被回收。

  2. 軟引用

    使用SoftReference類來建立軟引用。被軟引用的對象隻有在記憶體不足的情況下才會被回收

    Object obj=new Object();  
    SoftReference<Object> sf=new SoftReference<Object>(obj);
    obj=null;  //使得第一行建立的new Object()對象隻被sf這個軟引用對象關聯
               
  3. 弱引用

    使用WeakReference類來建立弱引用。被弱引用的對象一定會被回收,它值存活到下一次垃圾回收發生之前

    Object obj=new Object();  
    WeakReference<Object> wf=new WeakReference<Object>(obj);
    obj=null;  
               
  4. 虛引用

    使用PhantomReference來建立虛引用。

    虛引用又被稱為幽靈引用或幻影引用,無法通過一個虛引用得到一個對象,其設定的唯一目的是能在這個對象被回收時收到一個系統通知。

    Object obj=new Object();  
    PhantomReference<Object> pf=new PhantomReference<Object>(obj);
    obj=null;  
               

2、垃圾回收算法

2.1 标記-清除算法
JVM原理JVM原理

先标記出要回收的對象(一般使用可達性分析算法),再去清除,但會有效率問題和空間問題。标記的空間被清除後,會造成我的記憶體中出現越來越多的不連續空間,當要配置設定一個大對象的時候,再次進行尋址的要花費很多時間,可能會再一次觸發垃圾回收。

2.2 标記-整理算法
JVM原理JVM原理

與标記-清除算法類似,隻是清除對象後,還要将所有存活的對象都向一端移動,并更新引用其對象的指針。

  • 優點:不會産生記憶體碎片
  • 缺點:需要移動大量對象,處理效率比較低
2.3 複制算法
JVM原理JVM原理

将記憶體劃分為大小不同的2塊,每次隻使用其中一塊,當這一塊記憶體用完了就将仍存活的對象複制到另一塊上面,然後把使用過的那一塊全部清除。

缺點:隻用了記憶體的一半

目前常用的虛拟機都采用這種複制算法來回收新生代,主要将新生代分為一塊較大的Eden和2塊較小的Survivor空間,每次隻使用Eden和一塊Survivor,當回收時,将Eden和Survivor中還活着的對象一次性複制到另一塊Survivor上,最後清理掉Eden和剛才使用過的Survivor空間。HotSpot虛拟機預設,的Eden和Survivor的大小比例是8:1,這樣每次新聲嗲中可用的記憶體為整個新生代容量的90%(80%Eden+10%Survivor),隻有10%的記憶體會“浪費”。

JVM原理JVM原理
2.4 分代收集算法

将記憶體分為新生代與老年代,不同的代使用不同的收集算法

  • 新聲代使用:複制算法
  • 老年代使用:标記-清除算法或标記-整理算法

3、垃圾回收器

根據要回收的記憶體區域的不同,可以使用不同的垃圾收集器。

3.1、串行回收器
  • 單線程。隻有一個線程(一個垃圾回收器)進行垃圾回收,在回收的期間,所有的使用者線程都被阻塞,。
  • 适合堆記憶體較小的情況下使用,适合個人電腦
  • Serial收集器

開啟方式:-XX:+UseSerialGC=Serial(新生代)+SerialOld(老年代)

新生代使用複制收集算法,老年代使用标記-整理算法。

JVM原理JVM原理
3.2、吞吐量優先回收器
  • 多線程。多個線程(可了解為有多個垃圾回收器)進行垃圾回收,在回收期間,所有的使用者線程被阻塞。
  • 适合堆記憶體較大,多核CPU
  • 讓機關時間内STW的時間最短(在機關時間内,讓垃圾回收時間所占比例越小)
  • Parallel收集器,并行執行

開啟方式:-XX:+UseParallelGC ~ -XX:+UseParallelOldGC

JVM原理JVM原理
3.3、響應時間優先回收器
  • 多線程。多線程進行垃圾回收,在回收期間,一定階段内,使用者線程繼續執行,不被阻塞
  • 适合堆記憶體較大,多核CPU
  • 盡可能讓STW的單次時間最短(讓一次垃圾回收的時間越小)
  • CMS收集器,并發執行,适用于老年代,采用标記-清除收集算法

開啟方式:-XX:+UseConcMarkSweepGC (老年代)~ -XX:+UseParNewGC(新生代)、

如果CMS并發失敗,會退化到SerialOld回收器

JVM原理JVM原理
  • 初始标記

    标記老年代中所有的GC Roots對象和年輕代中活着的對象引用到的老年代的對象,時間短;

  • 并發标記

    從“初始标記”階段标記的對象開始找出所有存活的對象;

  • 重新标記

    用來處理前一個階段因為引用關系改變導緻沒有标記到的存活對象,時間短;

  • 并發清理

    清除那些沒有标記的對象并且回收空間。

缺點:占用大量的cpu資源、無法處理浮點垃圾、出現ConcurrentMarkFailure、空間碎片。

3.4、G1回收器

JDK9廢棄了CMS回收器,預設使用Garbage First回收器。

特點:

  • G1可以直接對新生代和老年代一起回收,其他回收器的範圍都是整個新生代或老年代
  • G1将堆劃分為多個大小不同的獨立區域(Region),這些Region就分為了Eden、Survivor、Old區域
JVM原理JVM原理

使用場景:

  • 同時注重吞吐量和低延遲,預設的暫停目标是200ms
  • 超大堆記憶體,将堆分為多個大小相等的Region
  • 整體上采用标記-整理算法,兩個區域之間采用複制算法

相關參數:

  • -XX:+UseG1GC 開啟方式(JKD9預設開啟)
  • -XX:G1HeapRegionSize=Size 設定堆中Region的大小
  • -XX:MaxGCPauseMillis=time 設定暫停時間(垃圾回收的時間)

回收過程:循環進行

  • Young Collection新生代回收:采用标記-整理算法,在該過程中會進行GC Root的初始标記
  • Young Collection+CM新生代回收與并發标記:老年代占用堆空間比例達到一定門檻值,進行并發标記(不會STW)。預設的門檻值是45%
  • Mixed Collection混合回收:對E、S、O區域進行一次全面的垃圾回收(Full GC)

三、記憶體配置設定與回收政策

  • Minor GC:回收新生代。因為新生代對象存活時間很短,是以Minor GC會持續執行,執行的速度一般也會比較快。
  • Full GC:回收老年代和新生代,老年代對象其存活時間長,是以Full GC很少執行,執行速度會比Minor GC慢很多。

1、記憶體配置設定原則

  • 優先配置設定到Eden。當Eden不夠時,發起Minor GC
  • 大對象直接配置設定到老年代。-XX:PretenureSizeThreshold 大于該值的對象直接配置設定在老年代
  • 長期存活的對象直接配置設定到老年代。為對象定義年齡計數器,對象在Eden出生并經過Minor GC依然存活,将移動到Survivor中,年齡就增加1歲,增加到一定年齡則移動到老年代中。-XX:MaxTenuringThreshold用作定義年齡的門檻值。
  • 動态對象年齡判斷。虛拟機并非永遠要求對象的年齡必須達到MaxTenuringThreshold才能晉升老年代,如果在Survivor中相同年齡所有對象大小的總和大于Survivor空間的一半,則年齡大于或等于該年齡的對象可以直接進入老年代,無需等到MaxTenuringThreshold中要求的年齡。
  • 空間配置設定擔保。

    (1)在發生Minor GC之前,虛拟機會先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間,如果條件成立的話,那麼Minor GC确認是安全的,可以執行。

    (2)如果不成立的話,虛拟機會檢視HandlePromotionFailure的值是否允許擔保失敗,如果允許那麼就會繼續檢查老年代最大可用的連續空間是否大于曆次晉升到老年代對象的平均大小,如果大于,将嘗試着進行一次Minor GC ;如果小于,或者HandlePromotionFailure的值表示冒險,那麼就要進行一次Full GC。

2、Full GC觸發條件

  • 調用System.gc():隻是建議虛拟機執行Full GC,但虛拟機不一定真正去執行
  • 老年代空間不足:常見的是大對象直接進入老年代、長期存活的對象進入老年代等,為了避免:

​ (1)盡量不要建立過大的對象

​ (2)通過-Xmn虛拟機參數将新生代記憶體調大,讓對象試圖在新生代被回收掉,不進入老年代。

​ (3)通過-XX:MaxTenuringThreshold 調大對象進入老年代的年齡,讓對象在新生代多幸存。

  • 空間配置設定擔保失敗:使用複制算法的Minor GC需要老年代的記憶體空間作擔保,如果擔保失敗會執行一次Full GC
  • JDK1.7之前的永久代空間不足
  • 并發模式故障:執行CMS GC的過程中同時有對象要加入老年代,而此時老年代空間不足(可能是GC過程中浮動垃圾過多導緻暫時性的空間不足),便會報并發模式失敗錯誤,并觸發Full GC。

四、類加載機制

類是在運作期間第一次使用時動态加載的,而不是一次性加載所有類。因為如果一次性加載,那麼會占用很多的記憶體

1、類的生命周期

類從class檔案---->進入java虛拟機------->最終解除安裝的整個過程,分為7個階段:加載、驗證、準備、解析、初始化、使用、解除安裝

2、類的加載過程

包括類的生命周期中的5個部分:

  • 加載:查找類的二進制檔案(class檔案)

​ |-方法區:存放類資訊

​ |-堆:存放class檔案對應的類執行個體

  • 驗證:確定Class的位元組流中包含的資訊符合目前虛拟機的要求,并且不會危害虛拟機自身的安全
  • 準備:為類的靜态變量進行初始化,配置設定空間并賦予初始值(使用方法區記憶體)
    public static int value=123;  //準備階段value賦予的初始值為0
    public static final int value=123;   //如果類變量是常量,即用final修飾,則準備階段value賦予的初始值為123
               
  • 解析:将常量池的符号引用替換為直接引用
  • 初始化:JVM對類進行初始化,對類的靜态變量賦予正确的值

3、類初始化時機

  • 遇到new、getstatic、putstatic、invokestatic四條指令代碼時,如果沒有進行類初始化,則必須先觸發其初始化。

    ​ |–常見情況:使用new關鍵字建立類執行個體、讀取或設定一個類的類靜态變量值、調用一個類的類方法

  • 使用java.lang.reflect包方法對類進行反射調用時
  • 當初始化一個類時,發現其父類還沒有被初始化,則需要先觸發父類初始化
  • 當虛拟機啟動時,使用者需要指定一個要執行的主類(含main方法的類),虛拟機會初始化這個主類
  • 當使用JDK 1.7的動态語言支援時,如果一個java.lang.invoke.MethodHandle執行個體最後的解析結果為REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且此方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化;

4、類加載器

從java虛拟機角度分類:

  • 啟動類加載器:使用C++實作,是虛拟機本身的一部分
  • 所有其他類的加載器:使用java實作,獨立于虛拟機,繼承自抽象類java.lang.ClassLoader

開發角度分類:

  • 啟動類加載器:BootStrapClassLoader(C語音寫的)

    ​ |-(JDK/JRE/LIB java.*)所有類的類加載器

  • 擴充類加載器:ExtClassLoader

    ​ |-(JDK/JRE/LIB javax.*)所有類的類加載器

  • 應用程式類記載器:AppClassLoader(如果使用者沒有自定義加載器,那麼使用者定義的類預設使用該加載器)

    ​ |-(使用者自己定義的類)類加載器

  • 使用者自定義類加載器

    ​ |-檔案流、網絡、資料庫

***雙親委派模型:***如果一個類收到了類加載請求,它首先不會自己去嘗試加載這個類,而是把請求委托給父類加載器去完成,依次向上,是以,所有的類加載請求最終都應該被傳遞到頂層的啟動類加載器中。隻有當父類加載器在它的搜尋範圍内沒有找到所需的類時,即無法完成加載時,子類加載器才會嘗試自己去加載這個類(先依次向上,然後在向下)、

優點:

  • 防止重複加載一個類,保證資料安全
  • 防止java核心API庫被随意篡改

打破雙親委派機制:使用者自定義類加載器

參考文章:

https://blog.csdn.net/TJtulong/article/details/89598598

[https://cyc2018.github.io/CS-Notes/#/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA](https://cyc2018.github.io/CS-Notes/#/notes/Java 虛拟機)

繼續閱讀