您好,登錄后才能下訂單哦!
本篇文章給大家分享的是有關怎樣解析JVM虛擬機,小編覺得挺實用的,因此分享給大家學習,希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。
首先我們需要了解什么是虛擬機,為什么虛擬機可以實現夸平臺,虛擬機在計算機中扮演一個什么樣的角色。
(從下向上看)
看上圖的操作系統與虛擬機層,可以看到,JVM是在操作系統之上的。他幫我們解決了操作系統差異性操作問題,所以可以幫我們實現夸操作系統。
接著向上看,來到虛擬機可解析執行文件這里,虛擬機就是根據這個.class的規范來實現夸平臺的。
在向上到語言層,不同的語言可以有自己的語法、實現方式,但最終都要編譯為一個滿足.class規范的文件,來讓虛擬機執行。
所以理論上,任何語言想使用JVM虛擬機實現夸平臺的操作,都可以根據規范生成.class文件,就可以使用JVM,并實現“一次編譯,多次運行”。
字節碼規范(.class)
內存管理
第一點已經在上邊說過,不在重復。
第二點內存管理也是我們接下來主要講的內容。在沒有JVM的時代,在C/C++時期,寫代碼中除了寫正常的業務代碼之外,有很大一部分代碼是內存分配與銷毀相關的代碼。稍有不慎就會造成內存泄露。而使用虛擬機之后關于內存的分配、銷毀操作就都由虛擬機來管理了。
相對的肯定會造成虛擬機占用更多內存,在性能上與C/C++對比會較差,但隨著虛擬機的慢慢成熟性能差距正在縮小。
Jvm虛擬機主要分為五大模塊:類裝載子系統、運行時數據區、執行引擎、本地方法接口和垃圾收集模塊。
類的加載過程包含以下7步:
加載 -->校驗-->準備-->解析-->初始化-->使用-->卸載
其中連接校驗、準備-解析可以統稱為連接。
1. 通過Class的全限定名獲取Class的二進制字節流 2. 將Class的二進制內容加載到虛擬機的方法區 3. 在內存中生成一個java.lang.Class對象表示這個Class
獲取Class的二進制字節流這個步驟有多種方式:
1. 從zip中讀取,如:從jar、war、ear等格式的文件中讀取Class文件內容 2. 從網絡中獲取,如:Applet 3. 動態生成,如:動態代理、ASM框架等都是基于此方式 4. 由其他文件生成,典型的是從jsp文件生成相應的Class
有兩種類型的類加載器
虛擬機自帶的類加載器
該類加載器沒有父加載器,他負責加載虛擬機的核心類庫。 如:java.lang.*等。 根類加載器從系統屬性sun.boot.class.path所指定的目錄中加載類庫。 根類加載器的實現依賴于底層操作系統,屬于虛擬機的實現的一部分,他并沒有繼承java.lang.ClassLoader類。 如:java.lang.Object就是由根類加載器加載的。
它的父類加載器為根類加載器。 他從java.ext.dirs系統屬性所指定的目錄中加載類庫,或者從JDK的安裝目錄的jre\lib\ext子目錄(擴展目錄)下加載類庫 如果把用戶創建的JAR文件放在這個目錄下,也會自動有擴展類加載器加載。 擴展類加載器是純java類,是java.lang.ClassLoader類的子類。
也稱為應用加載器,他的父類加載器為擴展類加載器。 他從環境變量classpath或者系統屬性java.class.path所指定的目錄中加載類。 他是用戶自定義的類加載器的默認父加載器。 系統類加載器是純java類,是java.lang.ClassLoader子類。
App ClassLoader(系統<應用>類加載器)
Extension ClassLoader(擴展類加載器)
BootStrap ClassLoader(根加載器)
用戶自定義的類加載器
其一定是java.lang.ClassLoader抽象類(這個類本身就是提供給自定義加載器繼承的)的子類
用戶可以定制的加載方式
注意: 《類加載器的子父關系》非《子父類繼承關系》,而是一種數據結構,可以比做一個鏈表形式或樹型結構。
代碼:
public class SystemClassLoader { public static void main(String[] args) { ClassLoader classLoader = ClassLoader.getSystemClassLoader(); System.out.println(classLoader); while (classLoader != null){ classLoader = classLoader.getParent(); System.out.println(classLoader); } } } 輸出: sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@7a7b0070 null
獲得類加載器的方法
方式 | 說明 |
---|---|
clazz.getClassLoader(); | 獲得當前類的ClassLoader,clazz為類的類對象,而不是普通對象 |
Thread.currentThread().getContextClassLoader(); | 獲得當先線程上下文的ClassLoader |
ClassLoader.getSystemClassLoader(); | 獲得系統的ClassLoader |
DriverManager.getCallerClssLoader(); | 獲得調用者的ClassLoader |
/** * 獲取字符串的類加載器 * 返回為null表示使用的BootStrap ClassLoader */ public static void getStringClassLoader(){ Class clazz; try { clazz = Class.forName("java.lang.String"); System.out.println("java.lang.String: " + clazz.getClassLoader()); } catch (ClassNotFoundException e) { e.printStackTrace(); } } 輸出: java.lang.String: null 表示使用BootStrap ClassLoader加載
除了根加載器,每個加載器被委托加載任務時,都是第一時間選擇讓其父加載器來執行加載操作,最終總是讓根類加載器來嘗試加載,如果加載失敗,則再依次返回加載,只要這個過程有一個加載器加載成功,那么就會執行完成(這是Oracle公司Hotpot虛擬機默認執行的類加載機制,并且大部分虛擬機都是如此執行的),整個過程如下圖所示:
自定義類加載器:
public class FreeClassLoader extends ClassLoader { private File classPathFile; public FreeClassLoader(){ String classPath = FreeClassLoader.class.getResource("").getPath(); this.classPathFile = new File(classPath); } @Override protected Class<?> findClass(String name){ if(classPathFile == null) { return null; } File classFile = new File(classPathFile,name.replaceAll("\\.","/") + ".class"); if(!classFile.exists()){ return null; } String className = FreeClassLoader.class.getPackage().getName() + "." + name; Class<?> clazz = null; try(FileInputStream in = new FileInputStream(classFile); ByteArrayOutputStream out = new ByteArrayOutputStream()){ byte [] buff = new byte[1024]; int len; while ((len = in.read(buff)) != -1){ out.write(buff,0,len); } clazz = defineClass(className,out.toByteArray(),0,out.size()); }catch (Exception e){ e.printStackTrace(); } return clazz; } /** * 測試加載 * @param args */ public static void main(String[] args) { FreeClassLoader classLoader = new FreeClassLoader(); Class<?> clazz = classLoader.findClass("SystemClassLoader"); try { Constructor constructor = clazz.getConstructor(); Object obj = constructor.newInstance(); System.out.println("當前:" + obj.getClass().getClassLoader()); ClassLoader classLoader1 = obj.getClass().getClassLoader(); while (classLoader1 != null){ classLoader1 = classLoader1.getParent(); System.out.println("父:" + classLoader1); } SystemClassLoader.getClassLoader("com.freecloud.javabasics.classload.SystemClassLoader"); } catch (Exception e) { e.printStackTrace(); } } } 輸出: 當前:com.freecloud.javabasics.classload.FreeClassLoader@e6ea0c6 父:sun.misc.Launcher$AppClassLoader@18b4aac2 父:sun.misc.Launcher$ExtClassLoader@1c6b6478 父:null com.freecloud.javabasics.classload.SystemClassLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
驗證一個Class的二進制內容是否合法
1. 文件格式驗證,確保文件格式符合Class文件格式的規范。 如:驗證魔數、版本號等。 2. 元數據驗證,確保Class的語義描述符合Java的Class規范。 如:該Class是否有父類、是否錯誤繼承了final類、是否一個合法的抽象類等。 3. 字節碼驗證,通過分析數據流和控制流,確保程序語義符合邏輯。 如:驗證類型轉換是合法的。 4. 符號引用驗證,發生于符號引用轉換為直接引用的時候(轉換發生在解析階段)。 如:驗證引用的類、成員變量、方法的是否可以被訪問(IllegalAccessError),當前類是否存在相應的方法、成員等(NoSuchMethodError、NoSuchFieldError)。
使用記事本或文本工具打開任意.class文件就會看到如下字節碼內容:
左邊方框內容表示魔數: cafe babe(作用是確定這個文件是否為一個能被虛擬機接收的Class文件) 右邊方框表示版本號 :0000 0034 (16進制轉為10進制為52表示JDK1.8)
在準備階段,虛擬機會在方法區中為Class分配內存,并設置static成員變量的初始值為默認值。
注意這里僅僅會為static變量分配內存(static變量在方法區中),并且初始化static變量的值為其所屬類型的默認值。 如:int類型初始化為0,引用類型初始化為null。 即使聲明了這樣一個static變量: public static int a = 123; 在準備階段后,a在內存中的值仍然是0, 賦值123這個操作會在中初始化階段執行,因此在初始化階段產生了對應的Class對象之后a的值才是123 。
public class Test{ private static int a =1; public static long b; public static String str; static{ b = 2; str = "hello world" } } 為int類型的靜態變量 a 分配4個字節(32位)的內存空間,并賦值為默認值0; 為long類的靜態變量 b 分配8個字節(64位)的內存空間,并默認賦值為0; 為String類型的靜態變量 str 默認賦值為null。
解析階段,虛擬機會將常量池中的符號引用替換為直接引用,解析主要針對的是類、接口、方法、成員變量等符號引用。在轉換成直接引用后,會觸發校驗階段的符號引用驗證,驗證轉換之后的直接引用是否能找到對應的類、方法、成員變量等。這里也可見類加載的各個階段在實際過程中,可能是交錯執行。
public class DynamicLink { static class Super{ public void test(){ System.out.println("super"); } } static class Sub1 extends Super{ @Override public void test(){ System.out.println("Sub1"); } } static class Sub2 extends Super { @Override public void test() { System.out.println("Sub2"); } } public static void main(String[] args) { Super super1 = new Sub1(); Super super2 = new Sub2(); super1.test(); super2.test(); } }
在解析階段,虛擬機會把類的二進制數據中的符號引用替換為直接引用。
初始化階段即開始在內存中構造一個Class對象來表示該類,即執行類構造器<clinit>()的過程。需要注意下,<clinit>()不等同于創建類實例的構造方法<init>()
1. <clinit>()方法中執行的是對static變量進行賦值的操作,以及static語句塊中的操作。 2. 虛擬機會確保先執行父類的<clinit>()方法。 3. 如果一個類中沒有static的語句塊,也沒有對static變量的賦值操作,那么虛擬機不會為這個類生成<clinit>()方法。 4. 虛擬機會保證<clinit>()方法的執行過程是線程安全的。
Java程序對類的使用方式可以分為兩種
主動使用
被動使用
主動使用類的七中方式,即類的初始化時機:
1. 創建類的實例; 2. 訪問某個類或接口的靜態變量(無重寫的變量繼承,變量其屬于父類,而不屬于子類),或者對該靜態變量賦值(靜態的read/write操作); 3. 調用類的靜態方法; 4. 反射(如:Class.forName("com.test.Test")); 5. 初始化一個類的子類(Chlidren 繼承了Parent類,如果僅僅初始化一個Children類,那么Parent類也是被主動使用了); 6. Java虛擬機啟動時被標明為啟動類的類(換句話說就是包含main方法的那個類,而且本身main方法就是static的); 7. JDK1.7開始提供的動態語言的支持:java.lang.invoke.MethodHandle實例的解析結果REF_getStatic,REF_public,REF_invokeStatic句柄對應的類沒有初始化,則初始化;
除了上述所講七種情況,其他使用Java類的方式都被看作是對類的被動使用,都不會導致類的初始化,比如:調用ClassLoader類的loadClass()方法加載一個類,并不是對類的主動使用,不會導致類的初始化。
注意: 初始化單單是上述類加載、連接、初始化過程中的第三步,被動使用并不會規定前面兩個步驟被使用與否 也就是說即使被動使用只是不會引起類的初始化,但是完全可以進行類的加載以及連接。 例如:調用ClassLoader類的loadClass方法加載一個類,這并不是對類的主動使用,不會導致類的初始化。 需要銘記于心的一點: 只有當程序訪問的靜態變量或靜態變量確實在當前類或當前接口中定義時,才可以認為是對類或接口的主動使用,通過子類調用繼承過來的靜態變量算作父類的主動使用。
JVM中的Class只有滿足以下三個條件,才能被被卸載(unload)
1. 該類所有的實例都已經被GC,也就是JVM中不存在該Class的任何實例。 2. 加載該類的ClassLoader已經被GC。 3. 該類的java.lang.Class 對象沒有在任何地方被引用。 如:不能在任何地方通過反射訪問該類的方法。
運行時數據區主要分兩大塊: 線程共享:方法區(常量池、類信息、靜態常量等)、堆(存儲實例對象) 線程獨占:程序計數器、虛擬機棧、本地方法棧
程序計數器是一塊較小的內存空間,它的作用可以看作是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型里字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。
特點: 1. 如果線程正在執行的是Java 方法,則這個計數器記錄的是正在執行的虛擬機字節碼指令地址 2. 如果正在執行的是Native 方法,則這個技術器值為空(Undefined) 3. 此內存區域是唯一一個在Java虛擬機規范中沒有規定任何OutOfMemoryError情況的區域
public class ProgramCounterJavap { public static void main(String[] args) { int a = 1; int b = 10; int c = 100; System.out.println( a + b * c); } }
使用javap反匯編工具可看到如下圖:
圖中紅框位置就是字節碼指令的偏移地址,當執行到main(java.lang.String[])時在當前線程中會創建相應的程序計數器,在計數器中存放執行地址(紅框中內容)。
這也說明程序在運行過程中計數器改變的只是值,而不是隨著程序的運行需要更大的空間,也就不會發生溢出情況。
一個方法表示一個棧,遵循先進后出的方式。每個棧中又分局部變量表、操作數棧、動態鏈表、返回地址等等。
虛擬機棧是線程隔離的,即每個線程都有自己獨立的虛擬機棧。
局部變量:存儲方法參數和方法內部定義的局部變量名 操作數棧:棧針指令集(表達式棧) 動態鏈接:保存指向運行時常量池中該指針所屬方法的引用 。作用是運行期將符號引用轉化為直接引用 返回地址:保留退出方法時,上層方法執行狀態信息
虛擬機棧的StackOverflowError
單個線程請求的棧深度大于虛擬機允許的深度,則會拋出StackOverflowError(棧溢出錯誤)
JVM會為每個線程的虛擬機棧分配一定的內存大小(-Xss參數),因此虛擬機棧能夠容納的棧幀數量是有限的,若棧幀不斷進棧而不出棧,最終會導致當前線程虛擬機棧的內存空間耗盡,典型如一個無結束條件的遞歸函數調用,代碼見下:
/** * 虛擬機棧的StackOverflowError * JVM參數:-Xss160k * @Author: maomao * @Date: 2019-11-12 09:48 */ public class JVMStackSOF { private int count = 0; /** * 通過遞歸調用造成StackOverFlowError */ public void stackLeak() { count++; stackLeak(); } public static void main(String[] args) { JVMStackSOF oom = new JVMStackSOF(); try { oom.stackLeak(); }catch (Throwable e){ System.out.println("stack count : " + oom.count); e.printStackTrace(); } } }
設置單個線程的虛擬機棧內存大小為160K,執行main方法后,拋出了StackOverflow異常
stack count : 771 java.lang.StackOverflowError at com.freecloud.javabasics.jvm.JVMStackSOF.stackLeak(JVMStackSOF.java:18) at com.freecloud.javabasics.jvm.JVMStackSOF.stackLeak(JVMStackSOF.java:19) at com.freecloud.javabasics.jvm.JVMStackSOF.stackLeak(JVMStackSOF.java:19) at com.freecloud.javabasics.jvm.JVMStackSOF.stackLeak(JVMStackSOF.java:19) at com.freecloud.javabasics.jvm.JVMStackSOF.stackLeak(JVMStackSOF.java:19)
虛擬機棧的OutOfMemoryError
不同于StackOverflowError,OutOfMemoryError指的是當整個虛擬機棧內存耗盡,并且無法再申請到新的內存時拋出的異常。
JVM未提供設置整個虛擬機棧占用內存的配置參數。虛擬機棧的最大內存大致上等于“JVM進程能占用的最大內存(依賴于具體操作系統) - 最大堆內存 - 最大方法區內存 - 程序計數器內存(可以忽略不計) - JVM進程本身消耗內存”。當虛擬機棧能夠使用的最大內存被耗盡后,便會拋出OutOfMemoryError,可以通過不斷開啟新的線程來模擬這種異常,代碼如下:
/** * java棧溢出OutOfMemoryError * JVM參數:-Xms20M -Xmx20M -Xmn10M -Xss2m -verbose:gc -XX:+PrintGCDetails * @Author: maomao * @Date: 2019-11-12 10:10 */ public class JVMStackOOM { private void dontStop() { try { Thread.sleep(24 * 60 * 60 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } } /** * 通過不斷的創建新的線程使Stack內存耗盡 */ public void stackLeakByThread(){ while (true){ Thread thread = new Thread(() -> dontStop()); thread.start(); } } public static void main(String[] args) { JVMStackOOM oom = new JVMStackOOM(); oom.stackLeakByThread(); } }
方法區,主要存放已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。
常亮池中的值是在類加載階段時,通過靜態方法塊加載到內存中
對于絕大多數應用來說,這塊區域是 JVM 所管理的內存中最大的一塊。線程共享,主要是存放對象實例和數組。內部會劃分出多個線程私有的分配緩沖區(Thread Local Allocation Buffer, TLAB)。可以位于物理上不連續的空間,但是邏輯上要連續。也是我們在開發過程中主要使用的地方。
Heap的數據是二叉樹實現,每個分配的地址會存儲內存地址、與對象長度。
在jdk 1.8之前的版本heap分新生代、老年帶、永久代,但在1.8之后永久代修改為元空間,本質與永久代類似,都是對JVM規范中方法區的實現。元空間不在虛擬機中,而是在本地內存中。
我們使用下面一個生活中的例子來說明:
首先我們把整個內存處理過程比作一個倉庫管理,用戶會有不同的東西要在我們倉庫做存取。
倉庫中的貨物比作我們內存中的實例,用戶會不確定時間來我們這做存取操作,現在讓我們來管理這個倉庫,我們如何做到效率最大化。
用戶會有不同大小的貨物要寄存,我們不做特殊處理,就是誰先來了按照固定的順序存放。如下圖
但過了一段時間之后,用戶會不定期拿走自己的貨物
這時在我們倉庫中就會產生大小不同的空位,如果這時還有用戶來存入貨物時,就會發現我們需要拿著貨物在倉庫中找到合適的空位放進去(像俄羅斯方塊),但用戶的貨物不一定會正好放到對應的空位中,就會產生不同大小的空位,而且不好找。
如果在有貨物取走之后我們就整理一次的話,又會非常累也耗時。
這時我們就會發現,如果我們不對倉庫做有效的劃分管理的話,我們的使用效率非常低。
我們將倉庫邏輯的劃分為:
最常用: 用戶所有的貨物都先進入到這里,如果用戶只是臨時存放,可以快速從這里取走。除非貨物大小超過倉庫剩余空間(或我們認定的大貨物)。
臨時緩沖1、2: 臨時緩沖存放,存放小于一定天數的貨物暫時放到這里,當超出天數還未取走再放到后臺倉庫中。
后臺倉庫: 存放大貨物與長期無人取的貨物
上圖劃分了倆大區域,左邊比較小的是常用區域,用戶在存入貨物時最先放到這里,對于臨時存取的貨物可以非常快的處理。 右邊比較大的區域做為后臺倉庫,存放長時間無人取的或者常用區無法放下的大貨物。
通過這樣的劃分我們就可以把存取快的小貨物在一個較小的區域中處理,而不需要到大倉庫中去找,可以極大的提升倉庫效率。
JVM的垃圾回收算法是對內存空間管理的一種實現算法,是在逐漸演進中的內存管理算法。
標記-清除算法,就像他的名字一樣,分為“標記”和“清除”兩個階段。首先遍歷所有內存,將存活對象進行標記。清除階段遍歷堆中所有沒被標記的對象進行全部清除。在整個過程中會造成整個程序的stop the world。
缺點:
造成stop the world(暫停整個程序)
產生內存碎片
效率低
為什么要stop the world?
舉個簡單的例子,假設我們的程序與GC線程是一起運行的,試想這樣一個場景。
假設我們剛標記完的A對象(非存活對象),此時在程序當中又new了一個新的對象B,且A對象可以到達B對象。 但由于此時A對象在標記階段已被標記為非存活對象,B對象錯過了標記階段。因此到清除階段時,新對象會將B對象清除掉。如此一來GC線程會導致程序無法正常工作。 我們剛new了一個對象,經過一次GC,變為了null,會嚴重影響程序運行。
產生內存碎片
內存被清理完之后就會產生像下圖3中(像俄羅斯方框游戲一樣),空閑的位置不連續,如果需要為新的對象分配內存空間時,無法創建連續較大的空間,甚至在創建時還需要搜索整個內存空間哪有空余空間可以分配。
效率低
也就是上邊兩個缺點的集合,會造成程序stop the world影響程序執行,產生內存碎片勢必在分配時會需要更多的時間去找合適的位置來分配。
為解決標記清除算法的缺點,提升效率,“復制”收集算法出現了。它將可用的內存空間按容量劃分為大小相等的兩塊,每次只使用其中一塊。當這一塊內存用完了,就將還存活的對象復制到另外一快上,然后把已使用過的內存空間一次清理掉。
這樣使每次都是對其中一塊進行內存回收,內存分配也不用考慮內存碎片等復雜情況,只要移動指針按順序分配內存就可以了,實現簡單運行高效。
缺點:
在存活對象較多時,復制操作次數多,效率低。
內存縮小了一半
針對以上兩種算法的問題,又出現了“標記-整理”算法,看名字與“標記-清除”算法相似,不同的地方就是在“整理”階段。
在《深入理解Java虛擬機》中對“整理”階段的說明是:"讓所有存活對象都向一端移動,然后直接清理掉端邊界以外的內存"
沒有找到具體某一個使用的方案,我分別畫了3張圖來表示我的理解:
標記-移動-清除
類似冒泡排序,把存活對象像最左側移動
疑問:
如果確定邊界?記錄最后一個存活對象移動的位置,后邊的全部清除?
為什么不是遇到可回收對象先回收再移動,這樣可以減少移動可回收對象的操作(除非回收需要的性能比移動還高)
標記-移動-清除 2
劃分移動區域,將存活對象暫時放到該區域,然后一次清理使用過的內存,最后再將存活對象一次移動
疑問:
如何分配邏輯足夠存活對象的連續內存空間?
如果空間不足怎么辦?
標記-清除-整理
以上我對標記-整理算法理解,如有不對的地方還請指正。
參考資料:
https://liujiacai.net/blog/2018/07/08/mark-sweep/
https://www.azul.com/files/Understanding_Java_Garbage_Collection_v41.pdf
分代收集不是一種新的算法,是針對對象的存活周期的不同將內存劃分為幾塊。當前商業虛擬機的垃圾收集都采用“分代收集”。
GC分代的基本假設:絕大部分對象的生命周期都非常短暫,存活時間短。
把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。
新生代 每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。
老年代 因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或“標記-整理”算法來進行回收。
垃圾收集器,就是針對垃圾回收算法的具體實現。
下圖是對收集器的推薦組合關系圖,有連線的說明可以搭配使用。沒有最好的收集器,也沒有萬能的收集器,只有最合適的收集器。
Serial
特點:
- 單線程、簡單高效(與其他收集器的單線程相比),對于限定單個CPU的環境來說,Serial收集器由于沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。 - 收集器進行垃圾回收時,必須暫停其他所有的工作線程,直到它結束(Stop The World)。
應用場景:
適用于Client模式下的虛擬機
ParNew
ParNew收集器其實就是Serial收集器的多線程版本。
除了使用多線程外其余行為均和Serial收集器一模一樣(參數控制、收集算法、Stop The World、對象分配規則、回收策略等)
特點:
- 多線程、ParNew收集器默認開啟的收集線程數與CPU的數量相同,在CPU非常多的環境中,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。 - 與Serial收集器一樣存在Stop The World問題
應用場景:
ParNew收集器是許多運行在Server模式下的虛擬機中首選的新生代收集器,因為它是除了Serial收集器外,唯一一個能與CMS收集器配合工作的。
Parallel Scavenge
與吞吐量關系密切,故也稱為吞吐量優先收集器。 除了使用多線程外其余行為均和Serial收集器一模一樣(參數控制、收集算法、Stop The World、對象分配規則、回收策略等)
特點:
屬于新生代收集器也是采用復制算法的收集器,又是并行的多線程收集器(與ParNew收集器類似)。
該收集器的目標是達到一個可控制的吞吐量。還有一個值得關注的點是:GC自適應調節策略(與ParNew收集器最重要的一個區別)
GC自適應調節策略:
Parallel Scavenge收集器可設置-XX:+UseAdptiveSizePolicy參數。 當開關打開時不需要手動指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRation)、晉升老年代的對象年齡(-XX:PretenureSizeThreshold)等。 虛擬機會根據系統的運行狀況收集性能監控信息,動態設置這些參數以提供最優的停頓時間和最高的吞吐量,這種調節方式稱為GC的自適應調節策略。 Parallel Scavenge收集器使用兩個參數控制吞吐量: XX:MaxGCPauseMillis 控制最大的垃圾收集停頓時間 XX:GCRatio 直接設置吞吐量的大小。
Serial Old
Serial Old是Serial收集器的老年代版本。
特點:同樣是單線程收集器,采用標記-整理算法。
應用場景:主要也是使用在Client模式下的虛擬機中。也可在Server模式下使用。
Server模式下主要的兩大用途
1.在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用。 2.作為CMS收集器的后備方案,在并發收集Concurent Mode Failure時使用。
CMS
一種以獲取最短回收停頓時間為目標的收集器。
特點:基于標記-清除算法實現。并發收集、低停頓。
應用場景:
適用于注重服務的響應速度,希望系統停頓時間最短,給用戶帶來更好的體驗等場景下。如web程序、b/s服務。
CMS收集器的運行過程分為下列4步:
初始標記:標記GC Roots能直接到的對象。速度很快但是仍存在Stop The World問題。 并發標記:進行GC Roots Tracing 的過程,找出存活對象且用戶線程可并發執行。 重新標記:為了修正并發標記期間因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄。仍然存在Stop The World問題。 并發清除:對標記的對象進行清除回收。
CMS收集器的內存回收過程是與用戶線程一起并發執行的。
CMS收集器的缺點:
對CPU資源非常敏感。
無法處理浮動垃圾,可能出現Concurrent Model Failure失敗而導致另一次Full GC的產生。
因為采用標記-清除算法所以會存在空間碎片的問題,導致大對象無法分配空間,不得不提前觸發一次Full GC。
G1
一款面向服務端應用的垃圾收集器。不再是將整個內存區域按代整體劃分,他根據,將每一個內存單元獨立為Region區,每個Region還是按代劃分。 如下圖:
特點:
- 并行與并發:G1能充分利用多CPU、多核環境下的硬件優勢,使用多個CPU來縮短Stop-The-World停頓時間。 部分收集器原本需要停頓Java線程來執行GC動作,G1收集器仍然可以通過并發的方式讓Java程序繼續運行。 - 分代收集:G1能夠獨自管理整個Java堆,并且采用不同的方式去處理新創建的對象和已經存活了一段時間、熬過多次GC的舊對象以獲取更好的收集效果。 - 空間整合:G1運作期間不會產生空間碎片,收集后能提供規整的可用內存。 - 可預測的停頓:G1除了追求低停頓外,還能建立可預測的停頓時間模型。能讓使用者明確指定在一個長度為M毫秒的時間段內,消耗在垃圾收集上的時間不得超過N毫秒。
G1為什么能建立可預測的停頓時間模型?
因為它有計劃的避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region里面的垃圾堆積的大小,在后臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region。這樣就保證了在有限的時間內可以獲取盡可能高的收集效率。
G1與其他收集器的區別:
其他收集器的工作范圍是整個新生代或者老年代、G1收集器的工作范圍是整個Java堆。在使用G1收集器時,它將整個Java堆劃分為多個大小相等的獨立區域(Region)。雖然也保留了新生代、老年代的概念,但新生代和老年代不再是相互隔離的,他們都是一部分Region(不需要連續)的集合。
G1收集器存在的問題:
Region不可能是孤立的,分配在Region中的對象可以與Java堆中的任意對象發生引用關系。在采用可達性分析算法來判斷對象是否存活時,得掃描整個Java堆才能保證準確性。其他收集器也存在這種問題(G1更加突出而已)。會導致Minor GC效率下降。
G1收集器是如何解決上述問題的?
采用Remembered Set來避免整堆掃描。G1中每個Region都有一個與之對應的Remembered Set,虛擬機發現程序在對Reference類型進行寫操作時,會產生一個Write Barrier暫時中斷寫操作,檢查Reference引用對象是否處于多個Region中(即檢查老年代中是否引用了新生代中的對象),如果是,便通過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remembered Set中。當進行內存回收時,在GC根節點的枚舉范圍中加入Remembered Set即可保證不對全堆進行掃描也不會有遺漏。
如果不計算維護 Remembered Set 的操作,G1收集器大致可分為如下步驟:
- 初始標記:僅標記GC Roots能直接到的對象,并且修改TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序并發運行時,能在正確可用的Region中創建新對象。(需要線程停頓,但耗時很短。) - 并發標記:從GC Roots開始對堆中對象進行可達性分析,找出存活對象。(耗時較長,但可與用戶程序并發執行) - 最終標記:為了修正在并發標記期間因用戶程序執行而導致標記產生變化的那一部分標記記錄。且對象的變化記錄在線程Remembered Set Logs里面,把Remembered Set Logs里面的數據合并到Remembered Set中。(需要線程停頓,但可并行執行。) - 篩選回收:對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計劃。(可并發執行)
上邊詳細說了垃圾收集相關的內容,那有很重要的一點沒有說,就是如何確定某個對象是垃圾對象,可被回收呢? 有下邊兩種方式,虛擬機中使用的是可達性分析算法。
引用計數法
給對象添加一個引用計數器,每當有一個地方引用他的時候,計數器的數值就+1,當引用失效時,計數器就-1。
任何時候計數器的數值都為0的對象時不可能再被使用的。
可達性分析算法 (java使用)
以GC Roots的對象作為起始點,從這些起始點開始向下搜索,搜索所搜過的路徑稱為引用鏈Reference Chain,當一個對象到GC Roots沒有任何引用鏈相連接時,則證明此對象時不可用的。
什么是GC Roots?
在虛擬機中可作為GC Roots的對象有以下幾種:
虛擬機棧中引用的對象
方法區中類靜態屬性引用的對象
方法區常量引用的對象
本地方法棧引用的對象
匯編指令是指可被虛擬機識別指令,我們平時看到的.class字節碼文件中就存放著我們某個類的匯編指令,通過了解匯編指令,可以幫助我們更深入了解虛擬機的工作機制與內存分配方式。
javap是jdk自帶的反解析工具。它的作用就是根據class字節碼文件,反解析出當前類對應的code區(匯編指令)、本地變量表、異常表和代碼行偏移量映射表、常量池等等信息。
當然這些信息中,有些信息(如本地變量表、指令和代碼行偏移量映射表、常量池中方法的參數名稱等等)需要在使用javac編譯成class文件時,指定參數才能輸出,比如,你直接javac xx.java,就不會在生成對應的局部變量表等信息,如果你使用javac -g xx.java就可以生成所有相關信息了。
javap的用法格式: javap <options> <classes>
用法與參數: -help --help -? 輸出此用法消息 -version 版本信息,其實是當前javap所在jdk的版本信息,不是class在哪個jdk下生成的。 -v -verbose 輸出附加信息(包括行號、本地變量表,反匯編等詳細信息) -l 輸出行號和本地變量表 -public 僅顯示公共類和成員 -protected 顯示受保護的/公共類和成員 -package 顯示程序包/受保護的/公共類 和成員 (默認) -p -private 顯示所有類和成員 -c 對代碼進行反匯編 -s 輸出內部類型簽名 -sysinfo 顯示正在處理的類的系統信息 (路徑, 大小, 日期, MD5 散列) -constants 顯示靜態最終常量 -classpath <path> 指定查找用戶類文件的位置 -bootclasspath <path> 覆蓋引導類文件的位置
一般常用的是-v -l -c三個選項。
下面通過一個簡單例子說明一下匯編指令,具體說明會以注釋形式說明。
具體指令作用與意思可參考該地址:
https://my.oschina.net/u/1019754/blog/3116798
package com.freecloud.javabasics.javap; /** * @Author: maomao * @Date: 2019-11-01 09:57 */ public class StringJavap { /** * String與StringBuilder */ public void StringAndStringBuilder(){ String s1 = "111" + "222"; StringBuilder s2 = new StringBuilder("111").append("222"); System.out.println(s1); System.out.println(s2); } public void StringStatic(){ String s1 = "333"; String s2 = "444"; String s3 = s1 + s2; String s4 = s1 + "555"; } private static final String STATIC_STRING = "staticString"; public void StringStatic2(){ String s1 = "111"; String s2 = STATIC_STRING + 111; } }
匯編指令
//文件地址 Classfile /Users/workspace/free-cloud-test/free-javaBasics/javap/target/classes/com/freecloud/javabasics/javap/StringJavap.class //最后修改日期與文件大小 Last modified 2019-11-5; size 1432 bytes MD5 checksum 1c6892dd51b214a205eae9612124535d Compiled from "StringJavap.java" //類信息 public class com.freecloud.javabasics.javap.StringJavap minor version: 0 //編譯版本號(jdk1.8) major version: 52 flags: ACC_PUBLIC, ACC_SUPER //常量池 Constant pool: #1 = Methodref #18.#45 // java/lang/Object."<init>":()V #2 = String #46 // 111222 #3 = Class #47 // java/lang/StringBuilder #4 = String #48 // 111 #5 = Methodref #3.#49 // java/lang/StringBuilder."<init>":(Ljava/lang/String;)V #6 = String #50 // 222 #7 = Methodref #3.#51 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #8 = Fieldref #52.#53 // java/lang/System.out:Ljava/io/PrintStream; #9 = Methodref #54.#55 // java/io/PrintStream.println:(Ljava/lang/String;)V #10 = Methodref #54.#56 // java/io/PrintStream.println:(Ljava/lang/Object;)V #11 = String #57 // 333 #12 = String #58 // 444 #13 = Methodref #3.#45 // java/lang/StringBuilder."<init>":()V #14 = Methodref #3.#59 // java/lang/StringBuilder.toString:()Ljava/lang/String; #15 = String #60 // 555 #16 = Class #61 // com/freecloud/javabasics/javap/StringJavap #17 = String #62 // staticString111 #18 = Class #63 // java/lang/Object #19 = Utf8 STATIC_STRING #20 = Utf8 Ljava/lang/String; #21 = Utf8 ConstantValue #22 = String #64 // staticString #23 = Utf8 <init> #24 = Utf8 ()V #25 = Utf8 Code #26 = Utf8 LineNumberTable #27 = Utf8 LocalVariableTable #28 = Utf8 this #29 = Utf8 Lcom/freecloud/javabasics/javap/StringJavap; #30 = Utf8 main #31 = Utf8 ([Ljava/lang/String;)V #32 = Utf8 args #33 = Utf8 [Ljava/lang/String; #34 = Utf8 MethodParameters #35 = Utf8 StringAndStringBuilder #36 = Utf8 s1 #37 = Utf8 s2 #38 = Utf8 Ljava/lang/StringBuilder; #39 = Utf8 StringStatic #40 = Utf8 s3 #41 = Utf8 s4 #42 = Utf8 StringStatic2 #43 = Utf8 SourceFile #44 = Utf8 StringJavap.java #45 = NameAndType #23:#24 // "<init>":()V #46 = Utf8 111222 #47 = Utf8 java/lang/StringBuilder #48 = Utf8 111 #49 = NameAndType #23:#65 // "<init>":(Ljava/lang/String;)V #50 = Utf8 222 #51 = NameAndType #66:#67 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #52 = Class #68 // java/lang/System #53 = NameAndType #69:#70 // out:Ljava/io/PrintStream; #54 = Class #71 // java/io/PrintStream #55 = NameAndType #72:#65 // println:(Ljava/lang/String;)V #56 = NameAndType #72:#73 // println:(Ljava/lang/Object;)V #57 = Utf8 333 #58 = Utf8 444 #59 = NameAndType #74:#75 // toString:()Ljava/lang/String; #60 = Utf8 555 #61 = Utf8 com/freecloud/javabasics/javap/StringJavap #62 = Utf8 staticString111 #63 = Utf8 java/lang/Object #64 = Utf8 staticString #65 = Utf8 (Ljava/lang/String;)V #66 = Utf8 append #67 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #68 = Utf8 java/lang/System #69 = Utf8 out #70 = Utf8 Ljava/io/PrintStream; #71 = Utf8 java/io/PrintStream #72 = Utf8 println #73 = Utf8 (Ljava/lang/Object;)V #74 = Utf8 toString #75 = Utf8 ()Ljava/lang/String; { //默認構造方法 public com.freecloud.javabasics.javap.StringJavap(); //輸入參數(該處表示無參) descriptor: ()V flags: ACC_PUBLIC //指令代碼《也是執行代碼,重點關注》 Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return //指令與代碼中的行號關系 LineNumberTable: line 7: 0 //本地變量表 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/freecloud/javabasics/javap/StringJavap; // 對應StringAndStringBuilder方法 public void StringAndStringBuilder(); descriptor: ()V //描述方法關鍵字 flags: ACC_PUBLIC Code: //stack() locals(本地變量數/方法內使用的變量數) args_size(入參數,所有方法都有一個this所以參數至少為1) stack=3, locals=3, args_size=1 //通過#2可在常量池中找到111222字符串,表示在編譯時就把原本的"111" + "222"合并為一個常量 0: ldc #2 // String 111222 2: astore_1 3: new #3 // class java/lang/StringBuilder 6: dup 7: ldc #4 // String 111 9: invokespecial #5 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V 12: ldc #6 // String 222 14: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 17: astore_2 18: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream; 21: aload_1 22: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 25: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream; 28: aload_2 29: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V //返回指針,無論方法是否有返回值,都會有該指令,作用是出棧 32: return LineNumberTable: line 19: 0 line 20: 3 line 22: 18 line 23: 25 line 24: 32 LocalVariableTable: Start Length Slot Name Signature 0 33 0 this Lcom/freecloud/javabasics/javap/StringJavap; 3 30 1 s1 Ljava/lang/String; 18 15 2 s2 Ljava/lang/StringBuilder; public void StringStatic(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=5, args_size=1 0: ldc #11 // String 333 2: astore_1 3: ldc #12 // String 444 5: astore_2 6: new #3 // class java/lang/StringBuilder 9: dup 10: invokespecial #13 // Method java/lang/StringBuilder."<init>":()V 13: aload_1 14: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 17: aload_2 18: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 21: invokevirtual #14 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 24: astore_3 25: new #3 // class java/lang/StringBuilder 28: dup 29: invokespecial #13 // Method java/lang/StringBuilder."<init>":()V 32: aload_1 33: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 36: ldc #15 // String 555 38: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 41: invokevirtual #14 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 44: astore 4 46: return LineNumberTable: line 27: 0 line 28: 3 line 29: 6 line 30: 25 line 31: 46 LocalVariableTable: Start Length Slot Name Signature 0 47 0 this Lcom/freecloud/javabasics/javap/StringJavap; 3 44 1 s1 Ljava/lang/String; 6 41 2 s2 Ljava/lang/String; 25 22 3 s3 Ljava/lang/String; 46 1 4 s4 Ljava/lang/String; public void StringStatic2(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=3, args_size=1 0: ldc #4 // String 111 2: astore_1 3: ldc #17 // String staticString111 5: astore_2 6: return LineNumberTable: line 35: 0 line 36: 3 line 37: 6 LocalVariableTable: Start Length Slot Name Signature 0 7 0 this Lcom/freecloud/javabasics/javap/StringJavap; 3 4 1 s1 Ljava/lang/String; 6 1 2 s2 Ljava/lang/String; } SourceFile: "StringJavap.java"
可以在指令集中明確看到我們上邊講解的內存運行時數據區的一些影子。
比如常量池、本地變量表、虛擬機棧(每個方法可以理解為一個棧,具體方法內就是Code區)、返回地址(return)
以上就是怎樣解析JVM虛擬機,小編相信有部分知識點可能是我們日常工作會見到或用到的。希望你能通過這篇文章學到更多知識。更多詳情敬請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。