一、JVM 基本認識
1、虛擬機 與 JVM
(1)虛擬機(Virtual Machine),可以理解為一臺虛擬的計算機,其是一款軟體,用來執行一系列虛擬的計算機指令,
可以分為:系統(硬體)虛擬機、程式(軟體)虛擬機,
(2)系統(硬體)虛擬機
系統虛擬機是一個可以運行完整作業系統的一個平臺,其模擬了物理計算機的硬體,即相當于在物理計算機上 模擬出 一臺計算機(作業系統),比如:VMware,
(3)程式(軟體)虛擬機
程式虛擬機是一個可以運行某個計算機程式的一個平臺,其模擬了物理計算機某些硬體功能(比如:處理器、堆疊、暫存器等)、具備相應的指令系統(位元組碼指令),即相當于在作業系統上 模擬出 一個軟體運行平臺,比如:JVM,
(4)JVM
JVM(Java Virtual Machine),是一臺執行位元組碼指令并運行程式的虛擬機,其位元組碼并不一定由 Java 語言編譯而成,任何一個語言通過 編譯器 生成 具備 JVM 規范的位元組碼檔案時,均可以被 JVM 解釋并執行(即 JVM 是一個跨語言的平臺),
特點:自動記憶體管理、自動垃圾回收,位元組碼一次編譯,到處運行,
官方檔案地址(自行選擇合適的版本):https://docs.oracle.com/javase/specs/index.html
(5)學習 JVM 的目的
一般進行 Java 開發時,不需要關注太底層的東西,專注于業務邏輯層面,這是因為 JVM 已經對底層技術、硬體、作業系統這些方面做了相應的處理(JVM 已經幫我們完成了 硬體平臺的兼容以及記憶體資源管理 等作業),
但由于 JVM 跨平臺的特性,其會犧牲一些硬體相關的性能以達到 統一虛擬平臺 的效果,當 程式使用人數 增大、業務邏輯復雜時,程式的性能、穩定性、可靠性會受到影響,往往提升硬體的性能不能成比例的提高程式的性能,
所以有必要了解 JVM 一些底層運行原理,寫出適合 JVM 運行、優化 的代碼,從而提高程式性能(當然也可以快速定位、解決記憶體溢位等問題),
2、JVM 整體結構
(1)Java 運行程序
如下圖,Java 原始碼經過 Java 編譯器,將原始碼編譯為位元組碼,再使用 JVM 決議運行位元組碼,

(2)JVM 運行程序(圖片來源于網路)
如下圖,位元組碼檔案被類加載器匯入,加載、驗證位元組碼檔案的正確性并分配初始化記憶體,
通過執行引擎解釋執行位元組碼檔案,并與 運行時資料區 進行資料互動(當然,其中邏輯實作沒那么簡單,此處略過),

(3)JVM 架構分類
虛擬機 內部處理指令流可以分為兩種:基于堆疊的指令集架構、基于暫存器的指令集架構,
基于堆疊架構特點:
不需要硬體的支持,可移植性好(跨平臺方便)、設計與實作簡單、指令集小但指令會變多(可能會影響效率),
一般 JVM 都是基于堆疊的,比如:HotSpot 虛擬機,
基于暫存器架構特點:
依賴于硬體,可移植性差、但指令少(使用更少的指令執行更多的操作,執行效率稍高),
比如: Android 的 Dalvik 虛擬機
【舉例:(執行如下操作時)】 int a = 2; int b = 3; a += b; 【基于堆疊的指令集架構:(輸出位元組碼如下)】 0: iconst_2 常量 2 1: istore_1 常量 2 入堆疊 2: iconst_3 常量 3 3: istore_2 常量 3 入堆疊 4: iload_1 5: iload_2 6: iadd 2 + 3 相加 7: istore_1 將相加結果 5 入堆疊 【基于暫存器的指令集架構:(沒有實際操做、大致指令如下)】 mov a, 2 將 2 賦給 a add a, 3 將 a 加 3 后再將結果 賦給 a 可以看到 使用暫存器時,指令數量明顯少于堆疊,
注:
直接打開 class 位元組碼檔案會亂碼,可以通過 javap -c 位元組碼檔案 來反編譯,得到可讀的位元組碼檔案,(也可以使用 IDEA 插件 bytecode viewer 或者 jclasslib bytecode viewer 去查看位元組碼,此處不做過多介紹)
javap -c XXX.class 對代碼進行反編譯,
javap -v XXX.class 對代碼進行反編譯,并顯示額外資訊(比如:常量池等資訊)

(4)簡單了解一下 JVM 生命周期
JVM 生命周期 即 JVM 從創建、使用、銷毀的整個程序,
JVM 啟動
通過引導類加載器(bootstrap class loader)創建一個初始類(initial class)來啟動 JVM,這個初始類由虛擬機的具體實作指定(JVM 種類很多),
JVM 使用(執行)
JVM 用于運行程式,每個程式啟動運行都會存在一個 JVM 行程與之對應,
程式結束后,JVM 也就結束,
JVM 銷毀
可以分為:正常銷毀、例外銷毀,
【正常銷毀:】 程式正常結束, 【例外銷毀:】 程式執行中出現例外,且例外未被處理導致 JVM 終止, 由于作業系統例外,導致 JVM 行程結束, 呼叫 System.exit() 方法,引數為非 0 時 JVM 退出,
3、簡單了解幾個虛擬機
(1)Sun Classic VM
Sun 公司開發的第一款商用虛擬機(在JDK 1.4 時被淘汰),
內部只提供解釋器(解釋器、即時編譯器不能配合作業,二選一使用),
注:
解釋器:根據位元組碼檔案,一行一行讀取決議并執行(立即執行,回應時間短,效率較低),
即時編譯器:把整個位元組碼檔案編譯成 可執行的機器碼(需要回應時間、造成卡頓),機器碼能直接在平臺運行,將一些重復出現的代碼(熱點代碼)快取起來提高執行效率,
(2)Sun Exact VM
為了解決 Classic VM 的問題,Sun 公司提供了此虛擬機(被 HotSpot 替代),
解釋器、編譯器混合作業模式,且具備熱點探測功能,
使用 Exact Memory Management(準確式記憶體管理),可以知道記憶體中某位置的資料的型別,
(3)HotSpot 虛擬機
一家小公司開發,被 Sun 公司收購,
HotSpot 虛擬機采用 解釋器、即時編譯器 并存的架構,是 JVM 高性能代表作之一,
HotSpot 即熱點(熱點探測功能),通過 計數器 找到最具有編譯價值的代碼,觸發即時編譯(方法被頻繁呼叫)或者堆疊上替換(方法中回圈次數多),
通過解釋器、即時編譯器協同作業,在回應時間與執行性能中取得平衡,
如下圖:
Java 8 依舊采用 HotSpot 作為 JVM,

(4)BEA JRockit
專注于服務端應用,代碼由 即時編譯器 編譯執行,不包含解釋器(即不關心程式啟動速度),
是 JVM 高性能代表作之一,執行速度最快(大量行業資料測驗后得出),
BEA 被 Sun 公司收購,Sun 公司被 Oracle 收購,Oracle 以 HotSpot 為基礎,融合了 JRockit 的優秀特性(垃圾回收器、MissionControl),
(5) IBM J9
市場定位與 HotSpot 接近,廣泛應用于 IBM 各種 Java 產品,也是高性能 JVM 代表作之一,
二、類加載子系統(Class Loader SubSystem)
1、類加載子系統作用、流程
(1)作用:
類加載子系統負責從 檔案系統 或者 網路 中加載 class 檔案(class 檔案頭部有特殊標識),
將 class 檔案加載到系統記憶體中,并對資料進行 校驗、決議 以及 初始化操作,最終形成可以被虛擬機使用的 Java 型別,
注:
類加載器只負責 class 檔案的加載,由執行引擎決定是否能夠運行,
加載的類資訊 存放于名為 方法區 的記憶體空間中,方法區還會存放 運行時常量池等資訊,
(2)流程
類的生命周期指的是 類從被加載到記憶體開始、到從記憶體中移除結束,
程序如下圖所示:

而類加載程序,需要關心的就是前幾步(加載 到 初始化),需要注意的是,決議操作可能會在 初始化之后執行(比如:Java 的運行期系結),
流程圖如下:

2、加載(Loading)
(1)目的:
加載 class 二進制位元組流檔案到記憶體中,
(2)步驟:
Step1:使用類加載器 通過一個類的全限定名 去獲取此類的 二進制位元組流(獲取方式開放),
Step2:將位元組流 對應的靜態存盤結構 轉為 方法區 運行時的資料結構,
Step3:在記憶體中生成一個代表這個類的 java.lang.Class 物件,作為方法區中這個類的各種資料訪問的外部入口(HotSpot 中,該 Class 物件存放于方法區中),
(3)獲取 class 二進制位元組流方式(簡單列舉幾個)
Type1:從本地系統直接加載,
Type2:從網路中加載(比如: Applet),
Type3:從 zip 壓縮包中讀取(jar、war 包等),
Type4:運行時計算生成(動態代理技術),
Type5:從資料庫中讀取(比較少見),
Type6:從其他檔案中讀取(JSP 檔案生成對應的 Class 類),
3、連接(Linking)-- 驗證(Verification)
(1)目的:
確保 class 檔案的二進制位元組流中包含的資訊符合當前虛擬機的要求,保證資料的正確性 而不會影響虛擬機自身的安全(比如:通過某種方式修改了 class 檔案,若不去驗證位元組流是否符合格式,則可能導致虛擬機載入錯誤位元組流而崩潰),
注:
驗證階段非常重要但不一定必要,如果代碼是經過反復使用、驗證過后并沒有出現問題,可以考慮將驗證關閉(-Xverify:none),從而縮短類加載時間,
(2)驗證方式:
具體細節自行查閱相關檔案、書籍,此處來源于 “深入理解 JAVA 虛擬機 第二版 周志明 著”,
Step1:檔案格式驗證
驗證位元組流是否符合 class 檔案格式規范,并能夠被當前虛擬機處理,
比如:class 檔案要以 CAFEBABE 開頭
Step2:元資料驗證
對類的 元資料 資訊進行語意校驗,驗證當前資料是否符合 Java 語言規范,
比如:類是否存在父類、是否繼承了 final 修飾的類等,
Step3:位元組碼驗證
對類的方法體進行語意校驗,
比如:方法體中型別的轉換是否有效,
Step4:符號參考驗證,
對常量池中各符號參考進行匹配性校驗(一般發生在 決議階段),
比如:符號參考中通過字串描述的全限定名能否找到對應的類,
注:
檔案格式驗證 是 基于 二進制位元組流 進行的,通過驗證后,會將資料存入 記憶體的方法區,后續三種驗證均是對方法區資料進行操作,
4、連接(Linking)-- 準備(Preparation)
(1)目的:
為類變數分配記憶體并設定類變數的默認初始值為 零值(比如:int 為 0, boolean 為 false),
注:
此處的類變數是 static 修飾的變數,但不包含 final static 修飾的變數,
final static 修飾的即為常量,在編譯時就已經設定好了,在 準備階段(preparation)會賦值,
static 修飾的變數在 準備階段賦零值,在 初始化階段(Initialization)執行真正賦值操作,
非 static 修飾的變數為 實體變數,隨著物件分配到 堆中,并非存在于方法區中,
【舉例:】 public static int value = https://www.cnblogs.com/l-y-h/p/123; 此時 value 屬于類變數,準備階段 value = 0,初始化階段 value = https://www.cnblogs.com/l-y-h/p/123. public static final int value = https://www.cnblogs.com/l-y-h/p/123; 此時 value 屬于常量,準備階段 value = 123.
(2)零值
資料型別 零值 int 0 long 0L short (short)0 byte (byte)0 char '\u0000' float 0.0f double 0.0d boolean false reference null
5、連接(Linking)-- 決議(Resolution)
(1)目的:
將常量池中的 符號參考 轉換為 直接參考,
注:
符號參考(Symbolic References):指用一組符號(字面量)來描述所參考的目標,但參考目標并不一定加載到了記憶體中,字面量形式明確定義在 Java 虛擬機規范的 Class 檔案格式中,
直接參考(Direct References):指直接指向目標的指標 或 能間接定位到目標的句柄,參考目標一定存在于記憶體中,
(2)決議動作
決議動作主要針對 類或介面、欄位、類方法、介面方法、方法型別、方法句柄、呼叫點限定符 這 7 類符號參考(具體決議程序此處略過,自行查閱檔案、書籍),
6、初始化(Initialization)
(1)目的:
在準備階段是為 類變數賦零值,而初始化階段就是真正執行類中相關代碼操作,是執行類構造器 <clinit>() 方法的程序,
(2)值得注意的點
<clinit>() 方法是 編譯器自動收集類中所有 類變數的賦值操作、靜態陳述句塊(static{}) 等陳述句合并而成的,且其順序是由 陳述句在源檔案中出現的順序而定的,
<clinit>() 方法不同于 類的建構式(實體構造器 <init>()),若當前類具有父類,則當前類執行 <clinit>() 之前 父類的 <clinit>() 方法就已經執行完畢了,對于父介面,當前類執行時不會執行父類介面的 <clinit>() 方法,只有使用到類變數時才會去實體化(介面中不能定義 靜態陳述句塊,可以存在類變數,即常量),
若一個類中沒有類變數以及靜態陳述句塊,則不會生成 <clinit>(),
在多執行緒下,虛擬機會保證一個類的 <clinit>() 方法被加鎖、同步,即一個執行緒執行 <clinit>() 后,其余執行到此處的執行緒均會阻塞,直至當前執行緒執行完畢,其他執行緒不會再次執行 <clinit>(),
(3)初始化的方式
當類被主動使用時,會導致類的初始化,而被動使用時,不會導致類的初始化,
主動使用:
使用 new 關鍵字實體化物件時,
讀取、設定某個類、介面的靜態變數時(非 final static 修飾的常量),
呼叫某個類的靜態方法時,
初始化一個類的子類時(先觸發父類初始化),
JVM 啟動時被標明為啟動類的類(main 方法所在的類),
反射呼叫某類時,
java.lang.invoke.MethodHandle 實體(JDK 7 之后提供的動態語言支持)的決議結果REF_static、REF_putStatic、REF_invokeStatic 句柄對應的類沒有初始化,則初始化,
被動使用:
除了上面 7 種情況之外的情況都是被動使用,不會導致類的初始化,
7、類加載器
(1)目的:
前面加載程序的第一步:使用類加載器 通過一個類的全限定名 去獲取此類的 二進制位元組流,這個類加載器可以由用戶自定義實作(在 JVM 外部去實作),使程式可以自定義以何種方式去獲取需要的類(當然一般使用 JVM 提供的即可),
注:
每一個類加載器,都有一個獨立的類名稱空間,對于任何一個類,該類與加載它的類加載器共同確定它在 JVM 中的唯一性(即判斷兩個類是否相同,需要保證兩個類由同一個 JVM 且同一個類加載器加載時才有可能相等),
(2)類加載器分類
從 JVM 角度,可以分為兩種:
引導類加載器(Bootstrap ClassLoader)、其他所有類的類加載器,
注:
引導類加載器,由 C/C++ 語言撰寫,是 JVM 的一部分,其實體物件無法被獲取,
其他所有類的類加載器,由 Java 語言開發,獨立于 JVM,且派生于 java.lang.ClassLoader,
從 開發人員 角度,可以細分為四種:
引導類加載器(Bootstrap ClassLoader)、擴展類加載器(Extension ClassLoader)、應用程式類加載器(Application ClassLoader)、用戶自定義類加載器(User-Defined ClassLoader)
注:
引導類加載器,用來加載 Java 的核心類別庫(JAVA_HOME/jre/lib 或者 sun.boot.class.path 下的內容),且出于安全考慮,其只加載包名為 java、javax、sun 等開頭的類,
擴展類加載器,由 Java 語言撰寫,派生于 ClassLoader(sun.misc.Launcher$ExtClassLoader),其父類為引導類加載器(但是代碼中獲取不到),用來加載 Java 的擴展類(加載系統屬性 java.ext.dirs 或者 jre/lib/ext 下的內容),
應用程式類加載器,由 Java 語言撰寫,派生于 ClassLoader(sun.misc.Launcher$AppClassLoader),其父類為擴展類加載器,是程式中默認的類加載器(一般類均由其完成加載),負責加載環境變數(classpath) 或者系統屬性 java.class.path 指定的路徑下的內容,
用戶自定義類加載器,自定義類的加載方式,可以用于拓展加載源、修改類的加載方式,
(3)ClassLoader
ClassLoader 是一個抽象類,除了引導類加載器,其余所有類加載器均由其派生而來,
常見獲取 ClassLoader 的方式:
【獲取 ClassLoader 的方式:】 【方式一:獲取當前類的 ClassLoader(呼叫當前類的 getClassLoader() 方法))】 ClassLoader classLoader = String.class.getClassLoader(); 【方式二:獲取當前系統的 ClassLoader(即 sun.misc.Launcher$AppClassLoader)】 ClassLoader classLoader = ClassLoader.getSystemClassLoader(); 【方式三:獲取當前執行緒背景關系的 ClassLoader】 ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 【舉例:】 public class JVMDemo { public static void main(String[] args) { // 自定義類(JVMDemo),使用默認類加載器加載(系統類加載器) ClassLoader jvmDemoClassLoader = JVMDemo.class.getClassLoader(); // 獲取自定義類 的類加載器 System.out.println(jvmDemoClassLoader); // 為默認類加載器 sun.misc.Launcher$AppClassLoader@18b4aac2 // 獲取自定義類 的父類加載器(拓展類加載器) System.out.println(jvmDemoClassLoader.getParent()); // 為拓展類加載器 sun.misc.Launcher$ExtClassLoader@4554617c // 獲取拓展 類加載器 的父類加載器(引導類加載器) System.out.println(jvmDemoClassLoader.getParent().getParent()); // 為引導類加載器,獲取不到,為 null // 核心類(String),使用引導類加載器加載 ClassLoader stringClassLoader = String.class.getClassLoader(); // 獲取核心類 的類加載器 System.out.println(stringClassLoader); // 為引導類加載器,獲取不到,為 null // 獲取系統類加載器 System.out.println(jvmDemoClassLoader.getSystemClassLoader()); // 為 sun.misc.Launcher$AppClassLoader@18b4aac2 System.out.println(stringClassLoader.getSystemClassLoader()); // 為 sun.misc.Launcher$AppClassLoader@18b4aac2 // 獲取當前執行緒的 類加載器 System.out.println(Thread.currentThread().getContextClassLoader()); // 為 sun.misc.Launcher$AppClassLoader@18b4aac2 } }

常見 ClassLoader 方法:
【常見 ClassLoader 方法:】 ClassLoader getParent(); 回傳該類加載器的 父類加載器 Class<?> loadClass(String name); 加載名為 name 的類,回傳 Class 物件, Class<?> findClass(String name); 查找名為 name 的類,回傳 Class 物件, Class<?> findLoadedClass(String name); 查找名為 name 被加載過的類,回傳 Class 物件, void resolveClass(Class<?> c); 連接指定的 Java 類, 【自定義類加載器步驟:(一般格式)】 Step1:繼承 java.lang.ClassLoader,實作自定義類加載器, Step2:重寫 fingClass() 邏輯, 【自定義類加載器步驟:(簡單版)】 Step1:繼承 java.net.URLClassLoader,該類已撰寫 findClass() 方法以及獲取位元組碼流的方式,
8、雙親委派機制(Parents Delegation Model)
(1)目的:
使類加載器間具備層級結構,
防止類被重復加載,
保護程式安全,防止核心 API 被篡改,
(2)雙親委派機制原理
JVM 按需加載 class 檔案,即使用到該類時,才會去加載其 class 檔案到記憶體生成 class 物件,且采用雙親委派機制去加載,
雙親委派機制原理:
除了頂層的 引導類加載器外,其余的類加載器應該存在其 父類加載器,
如果一個類加載器 收到了 類加載 請求,其并不會立即去加載,而是把這個請求委托給 父類加載器 進行加載,若父類加載器 仍有 父類加載器,則繼續向上委托,直至到達 引導類加載器,
如果父類加載器可以完成 類加載 請求,則成功回傳,否則子類加載器才會去嘗試加載,
如下為 ClassLoader 中的雙親委派實作:
先檢查類是否被加載過,若該類沒有被加載過,則呼叫父類加載器的 loadClass() 方法去加載,
若父類加載器不存在,則默認使用 引導類加載器為 父類加載器,如果父類加載失敗后,則拋出例外,并執行子類的 findClass() 方法進行加載,
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }

(3)沙箱安全機制
沙箱即限制一個程式的運行環境,
而 JVM 中沙箱安全機制 指將 Java代碼限定在 JVM 運行范圍內,限制代碼對本地資源的訪問,從而對代碼隔離,防止核心 API 被修改,
如下圖所示:
自定義一個 java.lang.String.class,由于 JVM 的機制會使用 引導類加載器 對其加載,而 JVM 會先去加載 /jre/lib/rt.jar 下的 java.lang.String.class,但是其并沒有 main 方法,所以報錯,

再如下圖所示:
自定義一個 java.lang.StringTe.class,同樣 JVM 會使用 引導類加載器 加載,但是并沒有加載到此類,所以會報錯(SecurityException),

三、運行時資料區
1、了解下 JVM 記憶體布局
(1)記憶體布局
記憶體是非常重要的系統資源,為了保證 JVM 高效穩定的運行,JVM 記憶體布局規定了 Java 在運行程序中記憶體 申請、分配、管理 的策略,不同 JVM 對于記憶體的劃分方式以及管理機制存在部分差異,
(2)基本 JVM 記憶體布局
JVM 在執行 Java 程式程序中,會將其管理的記憶體 分為 若干個不同的資料區域,每個區域均有各自的用途(堆、方法區等),有些區域隨著 JVM 啟動、退出 而創建、銷毀,有的區域隨著 用戶執行緒的開始、結束 而創建、銷毀,
如下圖所示:
多個執行緒共享 堆、以及方法區(永久代、元空間),
每個執行緒獨有 程式計數器、虛擬機堆疊、本地方法堆疊,

2、運行時資料區各記憶體空間區別
(1)按執行緒是否共享劃分
堆、方法區(元空間 或 永久代)執行緒共享,
虛擬機堆疊、本地方法堆疊、 程式計數器 執行緒私有,
(2)按拋出例外劃分
堆、方法區 會發生 GC 以及 拋出 OOM(OutOfMemoryError),
虛擬機堆疊、本地方法堆疊 會拋出 OOM 或者 StackOverflowError,不會發生 GC,
程式計數器 不會發生 GC 以及拋出 OOM 例外,

四、運行時資料區 -- 程式計數器(Program Counter Register)
1、什么是程式計數器?
程式計數器是一塊很小的記憶體空間,用于存放 下一條位元組碼指令 所在地址(即 即將執行的指令,由執行引擎讀取下一條指令),
是執行緒私有的(每個執行緒創建時均會創建),
是 JVM 中唯一一個不會出現 OOM(OutOfMemory,記憶體溢位) 的區域,也不會存在 GC(Garbage Collection,垃圾回收),
注:
位元組碼解釋器作業時,通過改變程式計數器的值來獲取下一條需要執行的位元組碼指令(比如:分支、回圈、跳轉、例外處理、執行緒恢復等操作),

2、每個執行緒獨有程式計數器,
JVM 多執行緒通過執行緒輪流切換并分配處理器執行時間的方式實作的,在任意一個時間點,一個處理器只會處理一個執行緒的指令,而為了使執行緒切換后能回到正確的位置(執行正確的指令),每個執行緒均會有個獨立的程式計數器,各個執行緒間互不影響,通過各自的程式計數器執行正確的指令,
注:
若執行緒執行的是 Java 方法,程式計數器保存的是 即將執行的位元組碼指令的地址,
若執行緒執行的是 Native 方法,程式計數器保存的是 Undefined,
五、運行時資料區 -- 虛擬機堆疊(Virtual Machine Stacks)
1、堆疊與堆?虛擬機堆疊?
(1)堆疊與堆?
可以理解 堆疊是運行時的單位、堆時存盤時的單位,
堆疊解決的是程式運行問題,即 程式怎么執行、處理資料,
堆解決的是資料存盤問題,即 資料怎么存盤、放在何處,
(2)什么是虛擬機堆疊?
每個執行緒創建時均會創建一個虛擬機堆疊(執行緒私有),其內部保存著一個一個堆疊幀(Stack Frame),用于存盤區域變數、操作結果,參與方法呼叫和回傳,
注:
一個堆疊幀對應一個方法呼叫,即 一個堆疊幀從入堆疊 到 出堆疊 的程序,即為 一個方法從呼叫到完成的程序,
堆疊幀是一個記憶體區塊,內部維護著方法執行程序中的各種資料資訊(區域變數表、運算元堆疊、動態鏈接、方法出口、以及附加資訊),
2、虛擬機堆疊的常見例外?基本運行原理?基本內部結構?
(1)虛擬機堆疊常見例外?
JVM 規范中允許 虛擬機堆疊 的大小 是動態的 或者 是固定不變的,
如果采用固定大小的 Java 虛擬機堆疊,那每一個執行緒的虛擬機堆疊大小可以在 執行緒創建時指定,若執行緒請求分配的堆疊容量(深度)超過了虛擬機堆疊的最大容量(深度),將會導致 JVM 拋出 StackOverflowError 例外,
如果采用動態擴展容量的 虛擬機堆疊,若在嘗試拓展的程序中無法申請到足夠的記憶體(或者創建執行緒時沒有足夠的記憶體去創建對應的虛擬機堆疊),將會導致 JVM 拋出 OutOfMemoryError 例外,
如下圖:
main 方法記憶體遞回呼叫 main 方法,形成一個死回圈(導致堆疊空間耗盡),最終導致 StackOverflowError,可以通過 -Xss 引數去設定 堆疊的大小,

(2)基本運行原理
虛擬機堆疊的操作只有兩個:每個方法執行觸發入堆疊操作,方法執行結束觸發出堆疊操作,即堆疊幀的入堆疊、出堆疊操作(遵循 先進后出 FILO、后進先出 原則 LIFO),
一個執行緒運行時,在一個時間點上只會存在一個活動的堆疊幀(方法),即當前堆疊頂堆疊幀 是有效的,如果當前方法中呼叫了其他方法,則會創建新的堆疊幀并入堆疊成為 新的堆疊頂堆疊幀,當新的堆疊幀執行結束后,會將執行結果回傳給上一個堆疊幀,丟棄當前堆疊幀并將上一個堆疊幀重新作為新的堆疊頂堆疊幀,
(3)堆疊幀的內部結構分類
區域變數表(Local Variables)或者 區域變數陣列,
運算元堆疊(Operand Stack)或者 運算式堆疊,
動態鏈接(Dynamic Linking)或者 指向運行時常量池(Constant pool)的方法參考,
方法回傳地址(Return Address),
附加資訊,
3、堆疊幀結構 -- 區域變數表(Local Variables)
(1)什么是區域變數表?
一組變數值存盤空間(可以理解為 陣列),用于存盤方法引數以及定義在方法體內部的區域變數,其包括的資料型別為:基本資料型別(int、long、double 等)物件參考(reference)以及 方法回傳地址(returnAddress),
區域變數表建立在 虛擬機堆疊 上,屬于執行緒獨有資料,即不會出現執行緒安全問題,
被區域變數表直接或間接參考的物件不會被 GC(垃圾回收),
區域變數表所需容量大小是在編譯期就確定下來的,方法運行期間不會改變其大小(即編譯期就可以知道該方法需要幾個區域變數 以及 其所占用的 slot 空間),
注:
32 位以內長度型別只會占用一個 區域變數表空間(slot),比如:short、byte、boolean 等,
64 位型別會占用兩個 區域變數表空間,比如:long、double,
(2)舉例
如下圖:
靜態方法沒有 this 變數,若為 構造器方法或者 實體方法,會存在一個 this 變數,
此處 main() 方法中存在 4 個變數,其中 b 為 double 型,占用兩個 slot 空間,args 為參考型別,占用 1 個空間,也即總空間為 5,
start 表示變數開始生效的 位元組碼指令 行數,

如下圖:
slot 可以被重用,當某個區域變數作用域結束后,其后續定義的新的區域變數可以占用 過期的 slot,可用于節省資源(但可能會影響 垃圾回收),

4、堆疊幀結構 -- 運算元堆疊(Operand Stack)
(1)什么是運算元堆疊?
每一個堆疊幀中包含一個 后進先出的 運算元堆疊,在方法執行程序中,根據位元組碼指令,往堆疊中寫入資料(入堆疊, push)或者提取資料(出堆疊,pop),運算元堆疊主要用于保存計算程序中的中間結果、并作為計算程序中 變數 臨時的存盤空間,
如果被呼叫的方法(堆疊幀)存在回傳值,則將其回傳值壓入運算元堆疊中,并更新程式計數器中下一條需要執行的位元組碼指令,
注:
JVM 基于堆疊的架構,其中堆疊 指的即為 運算元堆疊,基于堆疊時 為了完成一項操作,會存在更多的入堆疊、出堆疊操作,而運算元存盤在記憶體中,頻繁入堆疊、出堆疊操作(記憶體寫、讀操作)必然會影響執行速度,HotSpot 設計者提出 堆疊頂快取技術(TOS,Top-of-Stack Cashing) 解決這個問題,將堆疊頂元素全部快取到 物理 CPU 的暫存器中,以降低記憶體的 讀、寫次數,提高執行引擎的執行效率,
5、堆疊幀結構 -- 方法回傳地址(return address)
(1)什么是方法回傳地址?
方法結束的方式有兩種:正常執行完成(Normal Method Invocation Completion)、出現例外退出(Abort Method Invocation Completion),無論哪種方式退出,均需要回到方法被呼叫的位置,
方法正常退出時,即當前堆疊幀出堆疊,并恢復上一次堆疊幀(可能涉及操作:恢復上一次堆疊幀的區域變數表以及運算元堆疊、存在回傳值時會將回傳值壓入運算元堆疊、調整 程式計數器 使其指向下一條指令),
方法例外退出時,通過例外表(保存回傳地址)查找是否有匹配的例外處理器,若沒有對應的例外處理器,則會導致方法退出(堆疊幀一般不會保存回傳地址,且一般不會產生回傳值給 上一個堆疊幀),
注:
方法正常退出時,使用哪個回傳指令由 方法回傳值 的實際資料型別決定,
ireturn 回傳值為 boolean、byte、char、short、int 時的回傳指令 lreturn 回傳值為 long 時的回傳指令 freturn 回傳值為 float 時的回傳指令 dreturn 回傳值為 double 時的回傳指令 areturn 回傳值為 參考型別 時的回傳指令 return 回傳值為 void、構造器方法等 無回傳值時的回傳指令

6、堆疊幀結構 -- 動態鏈接(Dynamic Linking)
Java 源檔案編譯成位元組碼檔案時,所有的 變數 以及 方法 都作為符號參考保存在 class 檔案的運行時常量池中,每一個堆疊幀內部都包含一個指向 運行時常量池中 該堆疊幀對應的 方法的參考(即符號參考),使用符號參考的目的 是為了使 當前方法的代碼 支持 動態鏈接(詳見下面的方法呼叫),

7、方法呼叫(方法多載、方法重寫)
Java 常用方法操作 有方法多載、方法重寫,那編譯器如何去識別 真實呼叫的方法呢?
(1)先熟悉基本概念
靜態鏈接:
類加載位元組碼檔案時,若被呼叫的目標方法 在編譯期可知且運行期不變時,此時將呼叫方法的符號參考轉為直接參考的程序叫 靜態鏈接(發生在 類加載的 連接 的 決議階段),
注:
類加載的連接的決議(Resolution)階段,會將常量池中一部分符號參考 轉為 直接參考,而決議的前提就是:方法在程式運行之前(編譯時就已確定)能夠確定下來,不會發生改變,
動態鏈接:
若被呼叫的目標方法在 編譯期無法被確定下來,即需要在程式運行時將 符號參考轉為 直接參考 的程序 叫做動態鏈接,
方法系結:
系結是一個欄位、方法或者 類 的符號參考 被替換到 直接參考的程序,僅發生一次,可以分為早期系結、晚期系結,早期系結是 方法編譯期可知且運行期不變時進行系結,也即通過靜態鏈接的方式系結,晚期系結是 方法運行期根據實際型別系結,即通過動態鏈接的方式系結,
非虛方法:
非虛方法指的是 編譯期就確定且 運行期不可變的方法,在類加載階段就會將 符號參考 決議為 直接參考,
常見型別為:靜態方法、私有方法、final 方法、實體構造器、父類方法(即不可被重寫的方法),
虛方法:
非虛方法之外的方法(即需要運行期確定的方法),
(2)方法呼叫相關虛擬機指令:
【普通指令:】 invokestatic 呼叫靜態方法 invokespecial 呼叫實體構造器 <init> 方法、私有方法、父類方法 invokevirtual 呼叫虛方法(final 方法除外) invokeinterface 呼叫介面方法(運行期確定實作此介面的物件) 注: 這四條指令固化在虛擬機內部,方法呼叫執行不可被人為干預, invokestatic、invokespecial 指令呼叫的方法為 非虛方法, invokevirtual(除 final 方法)、invokeinterface 指令呼叫的方法為 虛方法, final 修飾的方法也由 invokevirtual 指令呼叫,但其為 非虛方法, 【動態呼叫指令:】 invokedynamic 動態決議出需要呼叫的方法并執行 注: 支持人為干預, Java 為了支持 動態型別語言,在 JDK 7 中增加了 invokedynamic 指令, 但 JDK 7 中并沒有直接提供該指令,需要借助 ASM 等底層位元組碼工具實作, 直至 JDK 8 中 Lambda 運算式出現才有直接生成 invokedynamic 指令的方式, 【動態型別語言、靜態型別語言:】 二者區別在于 型別檢查 發生的時期, 動態型別語言 對型別檢查 是在運行期,即變數沒有型別資訊、變數值才有型別資訊(比如: JavaScript), 靜態型別語言 對型別檢查 是在編譯期,即變數有型別資訊(比如:Java), 比如: Java: String hello = "hello"; hello = 10; // 編譯報錯 JS: var hello = "hello"; hello = 10; // 可以運行成功
(3)方法多載
接下來再看看 方法多載 與 方法重寫,涉及到多個方法(多型),虛擬機如何去確定真實呼叫的是哪個方法呢(分派)?
如下代碼(方法多載),最終輸出結果是什么?
【代碼:】 public class JVMDemo { static abstract class Human {} static class Man extends Human {} static class Woman extends Human {} public void sayHello(Human human) { System.out.println("Human"); } public void sayHello(Man man) { System.out.println("Man"); } public void sayHello(Woman woman) { System.out.println("Woman"); } public static void main(String[] args) { Human man = new Man(); Human woman = new Woman(); JVMDemo jvmDemo = new JVMDemo(); jvmDemo.sayHello(man); jvmDemo.sayHello(woman); } } 【輸出結果:】 Human Human

對于上述代碼中:
Human man = new Man();
Human 為父類,Man 為子類,將 Human 稱為變數 man 的靜態型別(Static Type)或者 外觀型別(Apparent Type),將 Man 稱為變數的實際型別(Actual Type),
靜態型別 在編譯期是可知的,而實際型別只有在 運行期才可以確定,
在編譯期根據 靜態型別 去定位方法執行 的(分派)動作稱為 靜態分派,而靜態分派的典型代表就是 方法多載,靜態分派發生在編譯階段,其動作不需要 JVM 去執行,
在運行期根據 實際型別 去定位方法執行 的(分派)動作稱為 動態分派,而動態分派的典型代表就是方法重寫,動態分派發生在運行階段,其動作需要 JVM 去執行,
上述代碼中,man 與 woman 的靜態型別實際都是 Human,方法多載時,編譯器根據靜態型別去決定多載方法,也即在編譯期就能確定到是 sayHello(Human human) 最終執行,故輸出結果均為 Human,

(4)方法重寫
如下代碼(方法重寫),最終輸出結果是什么?
【代碼:】 public class JVMDemo2 { static class Human { public void sayHello() { System.out.println("Human"); } } static class Man extends Human { public void sayHello() { System.out.println("Man"); } } static class Woman extends Human { public void sayHello() { System.out.println("Woman"); } } public static void main(String[] args) { Human man = new Man(); Human woman = new Woman(); man.sayHello(); woman.sayHello(); } } 【輸出結果:】 Man Woman

方法重寫的程序:
Step1:找到運算元堆疊頂的 第一個元素 所指向物件的實際型別,記為 C,
Step2:如果在型別 C 中查找到與常量池中 符號參考 所代表的描述符、簡單名稱都相符的方法,則進行訪問權限校驗,如果通過校驗則回傳該方法的直接參考,結束查找,若校驗失敗,則拋出例外 java.lang.IllegalAccessError,
Step3:若在型別 C 中未查找到相關方法,則根據繼承關系從下到上 以及對 C 的父類執行 Step2 的查找與驗證程序,
Step4:如果始終沒有合適的方法,則拋出例外 java.lang.AbstractMethodError,
注:
invokevirtual 指令執行第一步就是在運行期 確定 引數的實際型別,所以盡管兩次執行的是 Human 的 sayHello() 方法,但最終執行的是 man 與 woman 的 sayHello() 方法,

8、虛方法表
平常開發中,方法重寫是非常常見的,也即 動態分派 會頻繁操作,如果每次動態分配都去 執行一遍 查找邏輯(在類的方法元資料中查找合適的目標方法),那么將有可能影響執行效率,為了提高性能, JVM 在類的方法區中 建立了一個 虛方法表(virtual method table,vtable)實作,使用虛方法表的索引來替代元資料 以提高性能,類似的,在 invokeinterface 指令執行時會用到介面方法表(interface method table,itable),
虛方法表會在 類加載的鏈接階段被創建并初始化,準備階段 給類變數 賦初始值后,JVM 會把該類的方法表也進行初始化,
虛方法表中存放著每個方法的實際入口地址,如果某個方法在子類中沒有被重寫,那么子類的虛方法表里面的地址 與 父類方法的地址一致(均指向父類的 方法入口),如果子類重寫了某方法,則子類的虛方法表的地址將 為指向子類的 方法入口,
如下圖(圖片來源于網路):
Father、Son 均沒有重寫 Object 的方法,所以虛方法表中均指向 Object,
而 Son 重寫了 Father 的兩個方法,所以 Son 的兩個方法均指向自己,沒有指向其父類 Father,

六、運行時資料區 -- 本地方法堆疊(Native Method Stack)
1、本地方法介面(Native Method Interface)
(1)什么是本地方法?
本地方法(Native Method)是非 Java 語言撰寫的方法,比如 C、C++ 語言撰寫,而 Java 可以通過呼叫 本地方法介面 去使用 本地方法,
(2)為什么使用本地方法?
可以與 Java 外面的環境進行互動,簡化邏輯,比如涉及一些底層操作時,使用 Java 較難實作,但使用 C 或者 C++ 可以很方便的實作,而本地方法采用 C 或者 C++ 很好的實作了功能,我們只需要呼叫這個本地方法介面 就可以很方便的使用其功能(不需要關心其實作邏輯),
與作業系統互動,JVM 與底層作業系統還是有區別的,若想實作與 作業系統的互動,還是需要通過本地方法實作的,
2、本地方法堆疊(Native Method Stack)
本地方法堆疊與 Java 虛擬機堆疊類似,但是 Java 虛擬機堆疊用來管理 Java 方法的呼叫,而 本地方法堆疊用來管理 本地方法的呼叫,
本地方法堆疊是執行緒私有的,在例外方面與 Java 虛擬機堆疊相同,
當執行緒呼叫 Java 方法時,JVM 會創建一個堆疊幀 并壓入 虛擬機堆疊,但是呼叫 native 方法時,JVM 直接動態連接并指向 native 方法,
本地方法堆疊可以由 JVM 自由實作,比如:在 HotSpot 中,本地方法堆疊 與 虛擬機堆疊 合二為一,
七、運行時資料區 -- 堆(Heap)
1、什么是堆?
Java 堆是 JVM 所管理記憶體中最大的一塊區域,被所有執行緒共享(存在執行緒私有的分配緩沖區 Thread Local Allocation Buffer,TLAB),
在 JVM 啟動時創建(空間大小確定,可通過 -Xms、-Xmx調節),
其目的是用于 存放 實體物件(物件實體、陣列等),
注:
-Xms 用于設定堆的初始記憶體,等價于 -XX:InitialHeapSize,默認初始記憶體 = 物理記憶體 / 64,
-Xmx 用于設定堆的最大記憶體,等價于 -XX:MaxHeapSize,默認最大記憶體 = 物理記憶體 / 4,
如果 堆 中記憶體大小超過 Xmx 所指定的最大記憶體時,將會拋出 OutOfMemoryError 例外,
一般將 -Xms 與 -Xmx 兩個引數設定成相同的值,防止 GC 垃圾回收完 堆區 物件后重新計算堆區的大小,從而提高性能,
2、堆記憶體細分
現代垃圾收集器 大部分 基于分代收集理論,可以將堆空間 細分為如下幾個區:
(1)JDK7 及 以前對 堆記憶體 劃分:
新生區(年輕代、新生代、Young Generation Space)
養老區(老年代、老年區、Old Generation Space)
永久區(Permanent Space)
(2)JDK8 及 之后對 堆記憶體 劃分:
新生區(年輕代、新生代、Young Generation Space)
養老區(老年代、老年區、Old Generation Space)
元空間(Meta Space)
一般講堆空間,講的是 新生代 與 老年代,永久區、元空間 屬于方法區的實作,
JVM 規范中指出 方法區 邏輯上屬于堆,但并沒有規定方法區具體實作方式,由 JVM 自行實作,
使用 -XX:+PrintGCDetails 可以列印 GC 詳細資訊(可以看到堆相關資訊),

使用 JDK 自帶的 jvisualvm 工具,可以分析 JVM 運行時的 JVM 引數、堆疊、CPU 等資訊,

3、年輕代、老年代
無論年輕代 還是 老年代 都是用來存盤 物件的,其不同的是 存盤物件的 生命周期,
(1)什么是 年輕代、老年代?
堆中 物件按照生命周期 可以劃分為兩類:
生命周期較短的物件,這類物件的創建、消亡很快,
生命周期較長的物件,某些極端情況下 可能與 JVM 生命周期保持一致,
年輕代一般用于存盤生命周期較短的物件,老年代一般用于存盤生命周期較長的物件,
默認 年輕代 與 老年代 的比例為 1:2,即 年輕代 占堆空間 的 1/3,可以通過 -XX:NewRatio 來設定,比如: -XX:NewRatio=4,此時年輕代 : 老年代 = 1/4,即 年輕代占堆空間 1/5(但一般不會修改),
(2)年輕代內部結構
年輕代內部又可以分為 Eden Space、Survivor0 Space、Survivor1 Space,其中 Survivor 又可以稱為 from、to,from、to 大小相同,用于保存經過垃圾回收 幸存下來的 物件,且總有一個為空,
在 HotSpot 中,默認 Eden : Survivor0 : Survivor1 = 8:1:1(但是經過自適應后,顯示出來的是 6:1:1,可以通過 -XX:SurvivorRatio=8 設定),
幾乎所有的物件 均創建在 Eden(80%,大于 Eden 記憶體的物件可直接進入 老年代),可以通過 -Xmn 設定新生代最大記憶體,

(3)為什么給堆分代?不分代就不能正常作業嗎?
分代的唯一理由是 優化 GC 性能,
堆中存盤物件的生命周期不同,且大部分生命周期非常短暫,如果不加管理(不分代)全部放在一起,則每次 GC 都需要全域掃描一次才可以知道哪些是需要被 回收的物件,每次都會掃描到很多不需要被回收的物件(生命周期長的物件),這樣會極大影響效率,
而使用分代后(年輕代、老年代),將生命周期短的物件保存在年輕代,GC 多回收此處的物件,這樣可以減少掃描資料,從而提高效率,
4、Minor GC、Major GC、Full GC
JVM 進行 GC 時,根據不同的記憶體空間 會有不同的 GC 演算法與之對應,
(1)HotSpot 根據回收區域劃分:
部分收集(Partial GC):
Minor GC 針對 年輕代 進行 GC
Major GC 針對 老年代 進行 GC
Mixed GC 針對 整個新生代以及部分老年代 進行 GC
整堆收集(Full GC):
Full GC 針對 整個堆以及方法區 進行 GC
(2)Minor GC 觸發時機:
年輕代空間(Eden)不足時,會觸發 Minor GC,而 Java 物件生命周期一般較短,所以 Minor GC 非常頻繁且回收速度也較快,Minor GC 執行會引發 STW(Stop The World),會暫停其他執行緒直至 GC 結束(可能造成 程式卡頓),
(3)Major GC 觸發時機:
老年代空間不足時,會觸發 Major GC,Major GC 速度一般比 Minor GC 慢 10 倍以上(STW 時間更長),若經過一次 Major GC 后記憶體仍不足,則會拋出 OOM 例外,
(4)Full GC 觸發時機:
呼叫 System.gc() 時,系統會建議執行 Full GC,但是不一定執行(應盡量避免此操作),
大物件(占用大量連續記憶體空間的 java 物件)直接進入老年代,但老年代沒有連續的空間存盤,此時會觸發 Full GC,
通過 Minor GC 進入老年代的平均大小 大于老年代的 可用記憶體,會觸發 Full GC,
方法區空間不足時,會觸發 Full GC,
5、記憶體分配策略(物件提升 Promotion 規則)、物件分配程序
(1)記憶體分配策略
物件在記憶體中 存在不同的生命周期,而對于不同生命周期的物件在記憶體中分配規則如下:
Rule1:
物件優先分配到年輕代,
物件優先分配到年輕代中的 Eden 區,
Rule2:
大物件直接存入老年代,
大物件(占用大量連續空間的物件)直接分配到老年代(應盡量避免出現過多的大物件),
Rule3:
長期存活物件存入老年代
在年輕代經過多次 GC 后仍存活的物件(物件年齡足夠),將其移入老年代,
Rule4:
物件動態年齡判斷
如果 Survivor 區中相同年齡的所有物件大小的總和大于 Survivor 空間的一半,則這些物件直接進入老年代(無需考慮閾值),
Rule5:
空間分配擔保
在發生 Minor GC 之前,虛擬機會檢查 老年代最大可用的連續空間是否大于新生代所有物件的總空間,如果大于,則此次 Minor GC 是安全的,如果小于,則會繼續檢查 老年代最大可用的連續空間是否大于 歷次晉升到老年代的物件的平均大小,若大于,則嘗試進行一次 Minor GC,若小于,則會進行一次 Full GC,
注:
物件存入 Eden,經過 Minor GC 后,存活的物件存入 Survivor 并將其物件年齡設為 1,每經過一次 Minor GC,物件年齡加 1,當增加到一定年齡(默認 15,不同 JVM 不同),該物件將移入 老年代,可以通過 -XX:MaxTenuringThreshold 設定年齡閾值,
(2)物件分配程序
給物件分配記憶體 是一件非常嚴謹、復雜的任務,需要考慮記憶體分配、回收、回收是否產生記憶體碎片等一系列問題,涉及到 記憶體分配演算法、記憶體回收演算法,
物件分配簡單流程如下:
Step1:
物件申請記憶體,先經過 Eden 區,若 Eden 記憶體足夠,則給物件分配記憶體,若 Eden 已滿 或者 物件超過 Eden 記憶體,則會觸發 Minor GC 進行垃圾回收,
Step2:
進行 Minor GC 回收,會將 Eden 區不再被其他物件參考的物件銷毀,并將幸存下來的物件存入 Survivor 區,此時 Eden 又可存入物件,
Step3:
當再次觸發 Minor GC 時,會將幸存下來的物件存入另一個 Survivor 中,兩個 Survivor 總有一個為空(多次 GC,幸存的物件會在 兩個 Survivor 中相互交換),
Step4:
當 Survivor 中的物件交換次數到達某一個值(物件年齡達到閾值),該物件進入老年代,
Step5:
老年代記憶體不足時,會觸發 Major GC 進行記憶體清理,若清理后仍無法保存物件,則會拋出 OutOfMemoryError 例外,
第一次 Minor GC:
物件從 Eden 區進入 Survivor 區,

第二次 Minor GC:
物件從 Eden 區、Survivor 區進入另一個 Survivor 區,

第 N 次 Minor GC:
物件從 Survivor 進入 老年代,

詳細流程如下:

6、TLAB(Thread Local Allocation Buffer)
(1)并發問題:
堆區是執行緒共享區域,任何執行緒均可以訪問到堆區中的共享資料,但是由于物件實體的創建在 JVM 中頻繁,并發環境下從堆區中劃分 記憶體空間 是執行緒不安全的,為了避免多個執行緒操作同一個地址,需要給物件加鎖或者 CAS 等機制實作執行緒安全,進而影響分配速度,
(2)為什么使用 TLAB?
TLAB 指的是Thread Local Allocation Buffer(執行緒本地分配快取),屬于執行緒私有的堆空間,TLAB 作為記憶體分配的首選(即物件分配首先經過此處),但是其空間較小,所以不是所有的物件實體都能在 TLAB 中成功分配記憶體,
默認情況下 TLAB 是開啟的(通過 -XX:UseTLAB 可以設定是否開啟 TLAB),
7、逃逸分析(Escape Analysis)-- 堆疊上分配、標量替換、同步消除
JVM 中,物件一般都存盤在 堆中,但是隨著 逃逸技術的 發展,堆疊上分配、標量替換等優化技術的產生使 物件分配在堆中不那么絕對了(堆疊上分配會將 物件直接存入 堆疊中,無需存入堆),
(1)逃逸分析(Escape Analysis):
逃逸分析 是分析物件動態作用域,當一個物件在方法中定義后,若該物件在方法外被參考(比如:作為引數傳遞到其他方法中),則稱為方法逃逸,若未被參考,則沒有發生逃逸,
其并不是直接優化代碼的手段,而是為其他優化手段提供依據的分析技術,
JDK 8 中,HotSpot 默認已開啟了逃逸分析,可以通過 -XX:DoEscapeAnalysis 手動開啟或者關閉逃逸分析,使用逃逸分析后,編譯器可以對代碼做一些優化:堆疊上分配、同步消除、標量替換,
注:
由于無法保證逃逸分析的性能消耗 與 其效果成正比(可能存在經過逃逸分析后發現沒有一個不逃逸的物件,那么這分析的程序就白白浪費了),所以不同的 JVM 可能對逃逸技術相關優化有不同的實作,比如 HotSpot 就沒有進行 堆疊上分配 這個優化,
【不會發生逃逸:(物件只存在于當前方法內部)】 public static void test(){ StringBuilder sb = new StringBuilder(); } 【會發生逃逸:(物件可能被其他方法呼叫)】 public static StringBuilder test(){ return new StringBuilder(); }
(2)堆疊上分配(Stack Allocation):
GC 回收堆記憶體不再使用的物件時,無論篩選物件還是整理物件都是對記憶體操作,會消耗時間,而使用堆疊分配后,當一個物件不會逃逸出方法時,物件可能被優化成堆疊上分配,即物件隨著堆疊幀出堆疊而銷毀(不存于堆),那對 GC 的壓力會小很多(HotSpot 默認不支持),
(3)同步消除(Synchronization Elimination):
執行緒同步是一個耗時的操作,如果一個物件不會逃逸出執行緒(無法被其他執行緒訪問),那么對這個物件的同步操作可以省略(消除),從而提高并發性能,可以通過 -XX:EliminateLocks 開啟或關閉(HotSpot 默認開啟),
【未進行同步消除:】 public static void test(){ StringBuilder sb = new StringBuilder("hello"); synchronized (sb) { System.out.println(sb); } } 【進行同步消除后:】 public static void test(){ StringBuilder sb = new StringBuilder("hello"); System.out.println(sb); }
(4)標量替換(Scalar Replacement):
經過逃逸分析,發現一個物件不能被外界訪問時,經過優化,可以將這個物件拆解成若干個成員變數來替換,可以通過 -XX:-EliminateAllocations 開啟或關閉(HotSpot 默認開啟),
注:
標量(Scalar)指的是不能再被分解的資料,比如:基本資料型別(int、long 等),
聚合量(Aggregate)指的是還可以被分解的資料,比如:自定義物件(其成員變數可以分解為其他聚合量 或者 標量),
【未進行標量替換:】 public static void test(){ People people = new People(); } class People { String name; int age; } 【進行標量替換:】 public static void test(){ String name; int age; }
8、常用 堆 相關引數設定
【命令參考:】 https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html 【常用命令:】 -XX:+PrintFlagsInitial 查看所有引數的默認初始值 -XX:+PrintFlagsFinal 查看所有引數的最終值(某些值可能被修改) -XX:+PrintGCDetails 查看 GC 詳細處理資訊 -XX:-UseTLAB 關閉 TLAB -Xms20m 設定初始堆記憶體空間,等價于 -XX:InitialHeapSize=20m -Xmx20m 設定最大堆記憶體空間,等價于 -XX:MaxHeapSize=20m -Xmn10m 設定年輕代大小 -XX:NewRatio=4 配置年輕代與老年代的占比(默認 1:2) -XX:SurvivorRatio=4 配置年輕代中 Eden 與 Survivor 的占比(默認 8:1:1), -XX:MaxTenuringThreshold=10 設定年輕代 GC 處理物件最大年齡 -XX:-DoEscapeAnalysis 關閉逃逸分析 -XX:-EliminateLocks 關閉同步消除 -XX:-EliminateAllocations 關閉標量替換
八、運行時資料區 -- 方法區(Method Area)
1、什么是方法區?
方法區隨 JVM 啟動、關閉而創建、銷毀,屬于 各執行緒 共享的記憶體區域,大小可以固定或拓展,
JVM 規范中指出 方法區邏輯上屬于 堆的一部分,但是不要求具體的實作方式,
比如:
HotSpot 中方法區可以看成一個獨立于 堆 的空間,

2、設定方法區記憶體大小
方法區大小可以根據應用進行動態調整,
【JDK 7 及以前:】 -XX:PermSize 設定永久代初始分配記憶體,默認為 20.75M -XX:MaxPerSize 設定永久代最大分配記憶體,默認 82M(64 位機器)或者 64M(32 位機器) 注: 當 JVM 加載的類資訊 超過 MaxPerSize 時,會拋出 OOM:PermGenSpace 【JDK 8 及之后:】 -XX:MetaspaceSize 設定元空間初始分配記憶體,默認為 20.75M -XX:MaxMetaspaceSize 設定元空間最大分配記憶體,默認為本地記憶體(系統可用記憶體) 注: 當 JVM 耗盡系統可用記憶體后,會拋出 OOM:MetaspaceSize 當超過初始記憶體后,會觸發 Full GC,為了避免頻繁 GC,MetaspaceSize 值應該設定為一個相對較高的值,
3、方法區內部結構
【經典版內部結構:】 虛擬機加載的 類資訊(型別、屬性、方法) 靜態變數 運行時常量池 即時編譯器編譯后的代碼快取(JIT 代碼快取) 注: JDK 8 后 靜態變數以及運行時常量池 存于堆中, 【類資訊 -- 型別】 包含了加載的 類 的型別資訊(全類名(包名.類名)、修飾符、繼承的父類、實作的介面等資訊), 【類資訊 -- 屬性(域 Field)】 包含了加載的 類 的變數資訊(變數名稱、變數型別、變數修飾符等), 注: 靜態變數(static 修飾的變數)隨類加載而加載,被類的所有實體共享(即使沒有類實體也可訪問), 全域常量(static final 修飾的變數)在編譯時就被分配了, 【類資訊 -- 方法】 包含了加載的 類 的方法資訊(方法名稱、方法回傳型別、方法引數與型別、方法修飾符、運算元堆疊、區域變數表、例外表等), 【常量池 與 運行時常量池】 常量池中存放 Java 原始碼編譯期生成的資料, 包含 各種字面量、型別、屬性、方法等 的符號參考(可以理解成一個常量表,虛擬機指令通過 符號參考 在常量表中找到需要執行的類、方法、屬性等), 運行時常量池 為常量池中被 類加載后 存放到方法區中的資料,包含編譯期就確定的常量 以及 運行期決議后才能獲得的方法、欄位參考,
4、HotSpot 方法區演變
JDK 7 及之前,方法區稱為 永久代(Permanent Generation,屬于虛擬機記憶體),
JDK 8 及之后,方法區稱為 元空間(MetaSpace,屬于本地記憶體),
注:
不同虛擬機 實作方法區的方式不同,永久代 僅針對于 HotSpot,JRockit 以及 J9 不存在永久代概念,
HotSpot 方法區演進細節:
JDK 1.6 及之前,使用永久代實作方法區,靜態變數 存放于永久代上,
JDK 1.7,使用永久代實作方法區,字串常量池、靜態變數 存放于堆,
JDK 1.8 及之后,使用元空間實作方法區,類資訊、屬性、方法、常量等存放于本地記憶體,但字串常量池、靜態變數 仍存放于 堆中,

5、為何使用 元空間 替代 永久代?
為永久代設定空間大小 是不好確定的,如果動態加載類過多,則容易導致 OOM,永久代使用的是虛擬機記憶體,而元空間使用的是 本地記憶體(與系統可用記憶體有關),
永久代的調優 也是挺困難的,
6、字串常量池 為什么移到堆中?
字串常量池 存放于 永久代時,由于 永久代 垃圾回收(Full GC)效率很低,而開發程序中容易創建大量字串,若一直存放于 永久代(記憶體空間小),則永久代記憶體不足導致 OOM,而存于堆中,可以即時回收記憶體,
注:
不同 JVM 對方法區垃圾回收的實作可能不同(有的甚至都不去實作方法區垃圾回收),
方法區垃圾回收主要回收:常量池中廢棄的常量、不再使用的類,
回收廢棄常量:
常量未被參考即可被回收,
回收不使用的類:
首先得判斷該類的所有實體是否 已經被回收(即 堆中不存在該類以及其子類的實體物件),
其次得判斷該類的類加載器是否 已經被回收(一般都不會回收),
最后得判斷該類是否 沒有在任意地方參考,
滿足上述三個條件,則該類允許被回收,
通過 -XX:+TraceClassLoading、-XX:+TraceClassUnloading 可以看到類加載、卸載資訊,
九、執行引擎(Execution Engine)
1、基本概念了解一下
(1)機器碼:
使用二進制編碼方式表示指令,即機器碼,比如: 1001、0001 等,CPU 可以直接讀取并運行,但是機器碼不易記憶,且容易出錯,不同硬體的機器碼可能不同,
(2)匯編指令:
匯編指令指的是 將機器碼中特定的 0、1 組合的序列,簡化成 對應的指令,比如:mov,inc 等,統一指令在不同的硬體中可能對應不同的機器碼,可讀性比機器碼 稍好,
(3)匯編指令集:
不同的平臺,支持不同的指令,每個平臺所支持的指令,即指令集,比如: x86 指令集,
(4)高級語言:
為了使編程更輕松、可讀,出現了高級語言,計算機執行高級語言撰寫的程式時,需要先把程式解釋、編譯成機器指令(高級語言 -》匯編指令 -》機器碼),然后執行,
(5)位元組碼:
是一種中間狀態的二進制檔案,比機器碼抽象,需直譯器轉譯后才能成為機器碼,
源代碼 編譯成 位元組碼,位元組碼再通過 指令平臺的 JVM 轉譯為可執行的 指令,
2、執行引擎
(1)執行引擎概述
執行引擎是 JVM 核心之一,其將 位元組碼指令 解釋/編譯 為對應平臺的本地機器指令并執行,
執行代碼時常分為兩種:解釋執行(解釋器)、編譯執行(即時編譯器),
注:
解釋器(Interpreter):JVM 啟動時根據預定義的規范對 位元組碼 采用逐行解釋的方式執行(逐行翻譯 位元組碼檔案 為對應的 本地機器指令執行),直接解釋并執行,
即使編譯器(Just in time compiler):JVM 直接將 位元組碼 編譯成 對應的 本地機器指令(一般用于編譯 熱點代碼,再次訪問熱點代碼時 直接回傳 機器指令,從而提高代碼執行效率),需要消耗程式運行時間 進行編譯操作,
(2)HotSpot 采用 解釋器、即時編譯器 并存架構
早期 Java 由解釋器進行 解釋運行,為了提高熱點代碼的執行效率,引入即時編譯器,將熱點代碼直接編譯成 機器指令,提高執行效率,
當程式啟動、執行時,解釋器首先發揮作用,省去編譯時間,立即執行,隨著程式運行時間增長,即時編譯器開始發揮作用,將代碼編譯成 本地機器指令后,提高代碼的執行效率,
注:
熱點代碼 指的是 某個方法、代碼塊 執行頻率高,將其標記為 熱點代碼,
JVM 規范中并未規定 JVM 必須使用即時編譯器,但是即時編譯器的 性能是 衡量一款 JVM 是否優秀的標準,比如 J9 中只存在 即時編譯器(沒有解釋器),
(3)HotSpot 設定程式執行方式
默認 HotSpot 采用解釋器、即時編譯器 并存的架構,但可以根據實際情況執行 JVM 運行時是完全采用解釋器執行,還是完全采用即時編譯器執行,
【命令:】 -Xint 完全采用解釋器模式運行, -Xcomp 完全采用即時編譯器模式運行, -Xmixed 采用解釋器 + 即時編譯器混合模式運行, 注: 可以通過 java -version 查看當前模式,

(4)HotSpot 中 即時編譯器分類
HotSpot 中內嵌兩個 JIT 編譯器,分別為 Client Compiler(C1)、Server Compiler(C2),
C1 編譯器會對位元組碼進行簡單、可靠的優化,耗時較短,編譯速度較快,
C2 編譯器會對位元組碼進行耗時較長的優化、激進優化,但優化后的代碼執行效率更高,
分層編譯(Tiered Compilation)策略:
為了使程式在啟動回應速度以及運行效率上達到平衡狀態,在 JDK7 的 Server 模式下,采用 分層編譯策略 作為默認編譯策略,
分層編譯根據編譯器編譯、優化規模與耗時劃分出不同的編譯層次,
其中:
第 0 層,程式解釋執行,僅使用解釋器,不開啟性能監控,可觸發第 1 層編譯,
第 1 層,C1 編譯,將位元組碼編譯為本地代碼,進行簡答、可靠優化(可以加上性能監控),
第 2 層,C2 編譯,將位元組碼編譯為本地代碼,啟動耗時較長的優化(可以根據性能監控資訊進行一些不可靠的激進優化),
注:
分層編譯開啟后,C1、C2 可能會同時作業,一些代碼可能被多次編譯,
C1 可以提高編譯速度,C2 可以提高執行效率,
C1 優化策略:
方法行內:將參考的函式代碼編譯到參考點處,減少堆疊幀生成、引數傳遞以及跳轉程序,
去虛擬化:對唯一的實作類進行行內,
冗余消除:將運行期間一些不會執行的代碼消除掉,
C2 優化策略(基于逃逸分析):
標量替換:用標量值替換聚合物件的屬性值,
堆疊上分配:對于未逃逸的物件 將其分配在堆疊 而非堆,
同步消除:消除未逃逸的物件的同步操作,
3、解釋器、即時編譯器、熱點探測
(1)解釋器
由于不同平臺底層的 機器碼 不同,而為了實作 Java 程式跨平臺的特性,不能直接將原始碼編譯成 本地機器指令(可以直接編譯成機器指令,不同平臺對應不同的 機器指令,可移植性差),所以出現位元組碼檔案,不同 JVM 逐行解釋位元組碼檔案 來執行程式,
解釋器 逐行讀取 位元組碼檔案并解釋成 相應的機器指令,當一條位元組碼解釋完成后,從 PC 計數器中 獲取下一條 被執行的位元組碼 指令進行解釋執行操作,
解釋器分類:
位元組碼解釋器:純軟體代碼模擬位元組碼執行,效率低下,
模板解釋器:將每一條位元組碼和一個模板函式關聯,模板函式直接產生位元組碼執行的機器碼,從而提高解釋器性能,
(2)編譯期相關概念
前端編譯器:
將 .java 檔案轉為 .class 檔案的程序,比如:javac,
即使編譯器(JIT 編譯器、Just In Time Compiler):
將 .class 檔案轉為 機器碼 的程序,比如:HotSpot 的 C1、C2,
靜態提前編譯器(AOT 編譯器、Ahead Of Time Compiler):
直接將 .java 檔案轉為 機器碼 的程序,
(3)即時編譯器
即時編譯器 將熱點代碼 直接編譯成 機器指令,從而提高執行性能,通過熱點探測 判斷 某個方法、代碼塊 是否為熱點代碼,
熱點代碼常指:
多次被呼叫的方法,將整個方法 作為編譯物件(標準 JIT 編譯方式),
方法中多次被呼叫的回圈體,將整個方法 作為編譯物件(堆疊上替換,發生在方法執行程序中,即方法堆疊幀還存在于 堆疊中,但是方法被替換了),
熱點探測分類:
基于采樣的熱點檢測(Sample Based Hot Spot Detection):使用該方法的虛擬機會周期性的檢查各個執行緒的堆疊頂,若某個方法經常出現在堆疊頂達到閾值,則為熱點方法,
基于計數器的熱點探測(Counter Based Hot Spot Detection):使用該方法的虛擬機會為每個方法建立計數器,統計方法執行次數,若執行次數達到某個閾值,則為熱點方法,
(4)HotSpot 采用基于熱點計數器的熱點探測
HotSpot 采用基于熱點計數器的熱點探測,其內部維護了兩個計數器,
計數器:
方法呼叫計數器(Invocation Counter),用于統計方法的執行次數,
回邊計數器(Back Edge Counter),用于統計回圈體執行的次數,
方法呼叫計數器:
用于統計方法的執行次數,默認閾值在 client 模式下為 1500 次,在 Server 模式下為 10000 次,超過閾值觸發 JIT 編譯,這個閾值可以通過 -XX:CompileThreshold 來設定,一般 HotSpot 會根據自身版本以及機器硬體性能自動選擇運行模式,可以使用 -client 或者 -server 手動指定模式,
當一個方法被呼叫時,首先會檢查該方法是否被 JIT 編譯過,如果存在則使用編譯后的代碼執行,如果不存在,則將該方法呼叫計數器值加 1,然后判斷方法呼叫計數器 以及 回邊計數器之和 是否超過方法呼叫的閾值,若超過閾值則觸發 JIT 編譯(只是提交請求,執行還是解釋執行,當編譯完成后,下一次呼叫才會是已編譯的版本),
注:
方法呼叫計數器記錄的并非方法被呼叫的絕對次數,而是一個相對的執行頻率(某段時間內方法被呼叫的次數),當超過一定時間且方法呼叫次數 并未達到閾值,則次數減少一半,此程序稱為 方法計數器熱度的衰減,此時間稱為 方法統計的半衰周期,熱度衰減是執行垃圾回收順便執行的,可以使用 -XX:-UseCounterDecay 關閉熱度衰減(此時方法呼叫計數器記錄的為方法呼叫的絕對次數,只要系統運行時間夠長,大部分方法均會執行 JIT 編譯),

回邊計數器:
用于統計回圈體執行次數,回邊指的是 位元組碼中 控制流向后跳轉的指令,建立回邊計數器目的是觸發 OSR(On Stack Replacement,堆疊上替換),執行 JIT 編譯,
當一個方法被呼叫時,首先會檢查該方法是否被 JIT 編譯過,如果存在則使用編譯后的代碼執行,如果不存在,則將回邊計數器值加 1,然后判斷方法呼叫計數器 以及 回邊計數器之和 是否超過回邊呼叫的閾值,若超過閾值則觸發 JIT 編譯(提交 OSR 編譯請求,并降低回邊計數器的值,并解釋執行,等待編譯完成),
注:
回邊計數器沒有熱度衰減的程序,其統計的是回圈體執行的絕對次數,

未完待續,,,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/79264.html
標籤:Java
上一篇:回傳指標的函式
下一篇:JDK1.8 新特性(全)
