再看 JVM(1)

那些年翻來覆去折騰 JVM

這不是我第一次學習 JVM 的知識了,從開始學習 java 語法開始,老師就告訴咱們堆啊、棧啊的,那會真是不理解啊,狗捉耗子多管閒事,知道怎麼寫代碼不就好了嘛~java

後來逐漸的知道了,java 內存分配的意思,不瞭解關於 java 內存的部分,你都不知道你的變量何時就不是你想要的那個值了android

所以專門去看了 java 的內存分配,爲了性能優化又去看了GC 垃圾回收,這其中反覆看了好幾回web

此次應該是我第5次看 JVM 的內容,也是第2次寫 JVM 的博客,上次那篇已經做廢了。之前即使學習 內存分配GC 垃圾回收 那也是單獨的看,從沒有站在 JVM 整體設計的角度一塊兒看思考,此次站在 JVM 整體設計的角度,我發現了更多的知識點,好比:類的加載機制,也發展其實諸如這些其實都是緊密相互關聯的,只要咱們能理解 JVM 設計的初衷,理解這些其實也沒有多大難度了,單個內容理解起來有的真的挺費勁的面試

學習資料

web 上的博文基本都是說 JVM 單個知識點的,隨着 java 版本的變遷,其中有太多的錯誤,讓咱們理解起來既費勁,也搞不明白數據庫

JVM 的書卻是有基本不錯的,可是閱讀門檻比較高,強讀不少都理解不了bootstrap

這裏我推薦B站尚硅谷的JVM視頻,講的很是好,不光有理論,還有嚴謹的推導過程,使用轉用工具一步步驗證,而不是胡說白咧、胡講,不少內容也是引用自學習JVM的經典書籍:《深刻理解 JVM 虛擬機》小程序

這一個視頻+這本書,學習 JVM 的不二法寶,你們不用再找其餘資料了,你想知道的,你想不到的這裏都有,尤爲是視頻,即使小白都能看的懂,感謝尚硅谷。更難能難得的是該視頻裏有不少分析工具和思路,這點是很是值得學習的東西,甚至比JVM自己更值得學習後端

另外在學習過程當中,有一些連帶的點很重要,面試文的不少的,可是不太適合放在本文的,本文也不能寫的太長了,就另開了JVM面試的文章,由於關聯性很大,但願你們都去看看,能對JVM的理解更上一個臺階數組

須要自定義類加載器的請看這裏的內容:瀏覽器

簡單說下 java 發展歷程


java 最重要的3個虛擬機:hotspot,JRockit,J9 IBM的

10年 Oracle 收購了 REA 以後,致力於融合 hotspot 和 JRockit 這2個知名的虛擬機,可是2者之間差別太大,hotspot 能夠借鑑的比較少,這個成果在 JDK 8 中得以體現,JDK 8的虛擬機雖然還叫 hotspot,但這個 hotspot 是大量借鑑 JRockit 技術以後的成果了,不可同日而語

JDK 11時,革命性的垃圾回收器 ZGC 出來了,目前 ZGC 仍是實驗性的,可是實驗來看性能遠超 G1,雖然 G1 垃圾回收器仍是主流,可是將來必定會被 ZGC 替代

JDK 11 開始,Oracle 每3個版本發佈一個長期穩定支持版本,其餘版本都支持半年,而且更新內容有限,只有大版本纔有大的變化。可是也是從11開始,Oracle 每次都發布2個版本,一個免費 OpenJDK,一個商業收費 OracleJDK

因此 JDK8 是目前使用最多的版本,也是目前咱們學習的基準,另外阿里巴巴有本身的虛擬機 Taobao JVM,這裏不得不讚一個,阿里真是國內互聯網的基石啊

JVM 咱們學習什麼呢

學習 JVM,咱們固然要學習 JVM 的3大組成部分了:類加載器運行時數據區執行引擎

可是咱們以前都是每一個點學每一個點的,從沒有站在 JVM 整體的角度上串起來看,這就是此次我要展現給你們的,從整體上看,從整體上理解,其實每一個點都是相互關聯的

1. JVM 和硬件緊密相聯,站在全局的角度去理解 JVM

任何代碼都是跑在硬件上的,咱們以前學習 JVM 的內容都是學習的 API,從沒有考慮硬件上的內容,其實咱們如果把 JVM 和硬件上的關聯搞清楚,不少晦澀難懂的知識點迎刃而解。好比多線程,難倒不是由於內存的緣由而設計的嗎,難倒 java 的多線程不是 JVM 決定、管理的嘛,歸根結底,多線程就是內存、字節碼、指令的運做

java 相比 c 多了什麼,多的就是 JVM,就是今天咱們研究的東西。java 裏咱們只要關係邏輯代碼怎麼寫就好了,內存分配不用咱們管,內存回收不用咱們管,和操做系統的交互不用咱們管。經典的 Thread 就是 JVM 代咱們去和操做系統內核交互

C++ 須要咱們本身分配內存,本身回收,你 C++ 要是技術很好,內存可使用的很是高效,也不會出現涌餘。但要是你技術不高的話,內存可能會很是混亂,從語言發展的角度說自動管理也是大的趨勢

有人說:JVM 已是一個獨立的虛擬的計算機了,一臺新的機器只要安轉了 java 運行環境,立馬就行跑 java 代碼,可是 C 行嗎... 在C 裏面咱們要本身操做內存,要本身和操做系統交互

這就是爲何 JVM 在如今愈來愈受歡迎的原理,封裝了底層操做,讓咱們專心於邏輯,這點也是高級語法發展的趨勢,就算不是 JVM,也會有本身的 VM,讓代碼愈來愈簡單

不光如此,JVM 不只僅是對開發者屏蔽了硬件和操做系統層面的操做,JVM 更是有本身的指令系統:字節碼,就是這麼個東西,咱們知道 CPU 硬件實際執行的是 010101 這樣的二進制指令代碼。而 JVM 有本身的指令代碼,就是編譯完成的 .class 裏面的內容

這裏是一個反編譯出來的方法,你們看看用本身碼是怎麼寫的

public void speak();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=3, args_size=1
         0: bipush        10
         2: istore_1
         3: bipush        20
         5: istore_2
         6: return
      LineNumberTable:
        line 12: 0
        line 13: 3
        line 15: 6
複製代碼

bipush 10istore_1 這些你們認識嗎,看着是否是和彙編多少優勢像啊,這就是 JVM 本身設計的,專屬於本身的指令集。因此說 JVM 更像是一個虛擬的計算機,只要有一個硬件設備,上面安裝有一個內核,JVM 就能順利的運行,甚至不須要完整的操做系統支持

什麼是虛擬機:就是一套用來執行特定虛擬指令的軟件。好比要在 mac 上跑 wins 就須要一個虛擬機,要不 mac 怎麼認識 X86 指令呢...

2. JVM 已經超脫 java 了

若是說 java 是跨平臺的語言:

那麼 JVM 就是跨語言的平臺:

愈來愈多的語言選擇運行在 JVM 環境上,無論這個語言怎麼寫的,主要該語言的編譯器把代碼最終編譯成 .class 標準字節碼文件,那麼就都能在 JVM 上運行。像上圖中的,這些永遠均可以在 JVM 上運行

JVM 已經變成一個生態了,這不能不讓咱們去思考,我以爲你們看到這裏都思考一下是有好處的,感慨下,這就是一種趨勢

整體來講JVM就一句話:從軟件層面屏蔽不一樣操做系統在底層硬件個指令上的區別,也包括頂層的高級語言

3. 理解學習 JVM 的好處

這是 java 程序的結構,JVM 提供最底層運行支持,使用 java 提供的 API 開發了不少框架,咱們使用這些框架開發出最終的服務,app等

JVM 是最終承載咱們代碼的地方,你的服務運行的好很差,卡不卡不都看 JVM 的反饋嘛。單從性能優化的角度看,咱們都得對最底層的知識體系有足夠了解

懂得 JVM 內存結構,工做機制,是設計高擴展性應用和優化性能的基礎,阻礙程序運行的永遠是咱們對硬件使用的效率,對硬件使用效率的高低決定了咱們程序執行的效率

下面這些問題我想你們都會遇到吧:

  • 線上系統忽然卡死,OOM
  • 內存抖動
  • 線上 GC 問題,無從下手
  • 新項目上線,JVM 參數配置一臉懵逼
  • 面試 JVM 直接被拍暈在地上,JVM 如何調優,如何解決 GC,OOM

JVM 玩不轉別想吧上面這些搞順溜了...

就算是否是後臺服務的,你搞 android 或者其餘就沒有 內存抖動 的問題啦,不可能的,只要你語言用的 java 或者跑在 JVM 上,這 JVM 都是你逃不過去的

瞭解 JVM

知道了這些點以後,有助於咱們理解後面 JVM 的內容

1. 再次理解什麼是JVM

這是摘抄過來的一句話,不用再解釋了,你們仔細揣摩

虛擬機的概念是相對於物理機而言的,這兩種機器都有執行代碼的能力。
物理機的執行引擎是直接創建在硬件處理器、物理寄存器、指令集和操做系統層面的
而虛擬機的執行引擎是本身實現的,所以能夠自定義指令集和執行引擎的結構體系
並且能夠執行那些不能被硬件直接支持的指令

在不一樣的「虛擬機」實現裏面,執行引擎在執行JAVA代碼的時候有兩種方式:
1. 解析實行(經過解釋器執行)
2. 和編譯執行(經過即時編譯器編譯成本地代碼執行)
複製代碼

2. Java進程之間以及跟JVM關係

java程序是跑在JVM上的,嚴格來說,是跑在JVM實例上的,一個JVM實例其實就是JVM跑起來的進程,兩者合起來稱之爲一個JAVA進程

各個JVM實例之間是相互隔離的

  • 一個進程能夠擁有多個線程
  • 一個程序能夠有多個進程(屢次執行,也能夠沒有進程,不執行)
  • 一臺機器上能夠有多個JVM實例(也能夠沒有JVM實例)
  • 進程是指一段正在執行的程序
  • 線程是程序執行的最小單位
  • 經過屢次執行一個程序能夠有多個進程,經過調用一個進程能夠有多個程序

程序運行時,會首先創建一個JVM實例----------因此說,JVM實例是多個的,每一個運行的程序對應一個JVM實例。每一個java程序都運行在一個單獨的JVM實例上,(new建立實例,存放在堆空間),因此說一個java程序的多個線程,共享堆內存

總的來講,操做系統的執行單元是進程,每個JVM實例就是一個進程,而在該實例上運行的主程序是一個主線程(能夠當作一個輕量級的進程),該程序下還存在不少線程

還有一個 JVM 實例對應一個 Runtime 對象,咱們能夠從該 Runtime 對象中獲取一些參數,好比堆內存的初始值和最大值

3. java 也是起源自小程序

有意思的是,java 最先是爲了在 IE3 瀏覽器中執行 java applets,原來早先 java 也是小程序出身,可是誰讓後來 java 火了呢...

4. Taobao JVM

阿里很NB,本身基於 OpenJDK 深度定製了本身的 alibabaJDK,而且定製了本身的 Taobao JVM,很厲害的

其特色:

  1. 提出了 GCIH 技術,把生命週期較長的對象放到堆外了,提升了 GC 效率,下降了 GC 頻率
  2. GCIH 中的對象能夠在多個 JVM 實例中相互共享
  3. 使用 crc32 指令下降 JNI 開銷
  4. 針對大數據場景的 ZenGC

缺點是高度依賴 Intel cpu,目前在天貓,淘寶上應用,全面替代 Oracle 官方 JVM

5. JVM 和線程

線程是一個程序裏的運行單元,JVM 容許一個應用有多個線程並行執行

在 Hotspot 虛擬機中,每一個線程都與操做系統的本地線程直接映射。當一個 java 線程準備好執行以後,一個操做系統的本地線程也會同時被建立。java 線程終止後,本地線程也會被回收

操做修通負責把全部線程安排哦調度到任何一個可用的 CPU 上去執行,一旦本地線程初始化完成,就會調用 java 線程中的 run()

一個 JVM 實例裏有不少後臺線程:

  • 虛擬機線程: 這種線程的操做是須要JVM達到安全點纔會出現,這種線程的執行類型包括"stop-the-wrold"的垃圾收集,線程棧回收,線程掛起,偏向鎖撤銷
  • 週期任務線程: 這種線程是時間週期事件的體現,好比中斷
  • GC線程
  • 編譯線程: 把字節碼編譯成本地代碼
  • 信號調度線程: 這種線程接收信號併發送給JVM

JVM 命令和運行參數調整

IDE 配置 JVM 參數

只須要在 VM options 裏面寫設置便可,好比:

-XX:MetaspaceSize=100m
複製代碼

MetaspaceSize 是方法區的大小,這樣寫就行,想改哪一個就用對應的英文單詞好了

JVM 參數簡寫問題

後面你們會看到諸如:-Xms 這樣的JVM參數,一看就知道是簡寫,其實 -Xms = -XX:InitialHeapSize,你們知道就行,對照着就知道了,別2個都碰到了不知道啥意思

jps 命令

能夠查看進程信息

  • jps: 打印全部進程
➜  ~ jps
71187 Jps
70867 GradleDaemon
70814
複製代碼
  • jps -l: 輸出完整package整路徑,android 進程也能打印出來,可是僅限於本身安裝的 app
➜  ~ jps -l
70867 org.gradle.launcher.daemon.bootstrap.GradleDaemon
71193 sun.tools.jps.Jps
70814
複製代碼

jinfo 命令

能夠打印出想看的JVM參數信息,想看那個參數後面跟英文單詞和進程ID就行啦

// 打印信息,74290 是進程ID,能夠用上面 jps -l 命令查看
➜  ~ jinfo -flag MetaspaceSize 74290
// JVM 配置
-XX:MetaspaceSize=21807104
複製代碼

PrintGCDetails 打印堆棧信息

-XX:+PrintGCDetails VM options 配置項,能夠在日誌裏面把堆棧信息打印出來,挺有用的

// 堆內存
Heap

 // 年輕代
 PSYoungGen      total 38400K, used 4663K [0x0000000795580000, 0x0000000798000000, 0x00000007c0000000)
  eden space 33280K, 14% used [0x0000000795580000,0x0000000795a0dc88,0x0000000797600000)
  from space 5120K, 0% used [0x0000000797b00000,0x0000000797b00000,0x0000000798000000)
  to   space 5120K, 0% used [0x0000000797600000,0x0000000797600000,0x0000000797b00000)
  
 // 老年代 
 ParOldGen       total 87552K, used 61440K [0x0000000740000000, 0x0000000745580000, 0x0000000795580000)
  object space 87552K, 70% used [0x0000000740000000,0x0000000743c00010,0x0000000745580000)
  
 // 元空間 
 Metaspace       used 3387K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 376K, capacity 388K, committed 512K, reserved 1048576K
複製代碼

方法區參數

基本就4個參數:

  • MetaspaceSize - 方法區大小
  • MaxMetaspaceSize - 方法區最大值,這個數代碼裏是無限,但實際上不能超過物理內存最大值
  • MinMetaspaceFreeRatio - 在GC以後,最小的Metaspace剩餘空間容量的百分比
  • MaxMetaspaceFreeRatio ...
  • -XX:MetaspaceSize=100m - VM options 設置寫法

MaxMetaspaceSize 通常咱們不動,這個默認是無窮大數,咱們通常都會把 MetaspaceSize 調大一點,避免由於 MetaspaceSize 太小形成的 FullGC

堆內存JVM參數

  • -XX:UseTLAB - TLAB 線程專屬空間大小
  • Xmx - 堆內存最大值,默認=物理內存的 1/4
  • Xms500m - VM options 這麼寫
  • -XX:NewRatio=2 - 新生代老年代比例,2的意思是新生代是1佔總數的1/3,老年代代是2佔總數的2/3,通常咱們不改這個參數,由於新生代小了,意味這GC回收頻率就要高了
  • -XX:SurvivorRatio=8 - Eden、S0、S1 的比例,8的意思 Eden 是8佔總數的8/10,S0是1佔總數的1/10,S1是1佔總數的1/10
  • Xmn - 新生代最大值,通常不動,通常都用比例,這個寫了比例就不算數了
  • -XX:+UseAdaptiveSizePolicy - 自適應內存分配策略,-號是取消設置,+號是採用設置,這個其實不起做用的...
  • jinfo -flag NewRatio 進程ID - 打印新生代老年代比例
  • jinfo -flag SurvivorRatio 進程ID - 打印新生代內比例

棧內存JVM參數

  • -Xss - 棧內存值,只有這個一個參數,能夠理解爲最大值
  • -Xss900k - VM options 設置寫法

JVM 生命週期

JVM 也是有生命週期的,上文說到咱們能夠把進程當作一個JVM實例

JVM 生命週期:

  • 啓動: JVM啓動時會按照其配置要求,申請一塊內存,並根據JVM規範和實現將內存劃分爲幾個區域。而後建立出引導類加載器(Bootstrap Classloader)實例,引導類加載器是使用C++語言實現的,負責加載JVM虛擬機運行時所需的基本系統級別的類,如java.lang.String,java.lang.Object等等。而後加載咱們寫有main方法入口的那個類,執行 main 函數
  • 運行: 就是執行 main 函數,何時 main 函數結束了,JVM 也就完結了
  • 退出: 退出護着異常退出,用戶線程徹底退出了,jvm示例結束生命週期
// 通常 java 裏面咱們退出進程就是這2個方法
// System.exit 其實也是調的 Runtime,咱們跟進去看看

System.exit(0);
Runtime.getRuntime().exit(0);

------------------------------------------------------------------

// System.exit(0)
public static void exit(int status) {
   Runtime.getRuntime().exit(status);
}

// Runtime.getRuntime().exit(0)
public void exit(int status) {
    // Make sure we don't try this several times
    synchronized(this) {
        if (!shuttingDown) {
            shuttingDown = true;
            ........
            // Get out of here finally...
            nativeExit(status);
            }
        }
    }

// nativeExit 最終是一個本地方法
private static native void nativeExit(int code);

複製代碼

可能你們對 Runtime 不熟悉,Runtime 是什麼呢,就是整個運行時數據區

紅框裏框起來的就是 Runtime

JVM 總體結構

這裏咱們以 JDK 爲準,以 Hotspot 虛擬機爲主

圖片來源於:魯班學院-子牙老師

JVM 的3大組成部分:類加載器運行時數據區執行引擎

下文我會按照 JVM 最佳學習順序來逐個介紹:類加載器->方法區->內存結構->GC->執行引擎

在上圖裏你們能夠看的很清楚了,這是我能找到的 JVM 最準確、全面的一張結構圖了,你們之後以這個爲準吧

運行時數據區 這個一貫是你們理解的重點,這裏有一點其實不少人搞不清楚

線程獨有的區域包括:虛擬機棧、本地方法棧、程序計數器 這3個,這3個區域是線程間不可見的,只有本身所在線程能夠訪問

更詳細一點的是這張圖:

有句話這麼說的:棧管運行,堆管存儲,因此堆內存很大,天然要放在物流內存中,也就是內存條裏

類加載器 這個很重要的,好多黑科技都有使用到類加載器手動new對象,咱們要對這塊有清晰的瞭解才行,雖然 android 有本身的 DexClassLoader,可是也是以 java 的類加載器位基礎的,學了不吃虧

執行引擎 包括3部分:解釋器,JIT 即時編譯器,GC 垃圾回收器3部分組成

java 默認是逐行解釋的,運行時,運行到那行字節碼了,解釋器就去執行該行本身碼,字節碼怎麼執行呢,很簡單,沒一個字節碼指令對應一個 C++ 的方法,JVM 總體都是用 C++ 寫的,因此最終字節碼都是轉換成 C++ 代碼去執行

從 OpenJDK cpp 裏能夠找到執行器的源碼,java_executor.cpp 就是

很清楚吧,switch、case,每一個字節碼指令都對應一個或者多個 C 的方法

IT 即時編譯器 是 ART 虛擬機新加入的特性,也是目前 VM 發展的趨勢,不少 VM 也加入了 JIT 這個特性,JIT 乾的事就是記錄並判斷熱點代碼,正常流程解釋器解釋本身碼要吧相關參數帶入到相應的C方法路面去執行,會C方法進一步翻譯成彙編語言才能給CPU硬件去執行

JIT 就是把熱點代碼提早編譯成彙編代碼,能夠直接運行,比解釋器省了一部操做,這樣能夠提升CPU執行效率

JIT 對於熱點代碼編譯成機器碼以後是緩存在方法區的

從程序共享角度來看內存劃分

  • 堆內存: java heap space,滿了會拋出OOM異常
  • 元空間: Metaspace ,滿了同樣會拋出OOM異常
  • 棧空間: Satck,滿了同樣也會拋出OOM異常

類加載系統

無論咱們在 IDE 裏面代碼寫的如何飛起,編譯以後也僅僅是冷冷的一個class文件,躺在硬盤裏。可是電腦最終是要運行的,靠的是內存。類加載乾的就是讀取硬盤裏面的class文件,轉換成能夠在內存中,提供給計算機運行、執行程序的類信息(DNA元數據模板)

類信息保存在方法區裏面,方法區在本地內存,可是在JVM的堆內存也會跟着生成一個class對象,這個就是咱們反射用到的 class 對象,詳細後面會說

整體來講class文件從硬盤加載到內存並能夠運行,要經歷3個大的步奏:

  • 加載
    • 引導類加載器
    • 擴展類加載器
    • 系統類加載器
  • 連接
    • 驗證驗證
    • 準備
    • 解析
  • 初始化

類加載器系統和方法區緊密相聯,畢竟類加載出來的東西是放在方法區的,可是這其中仍是又不少講頭的。字節碼、常量池、運行時常量池、符號引用轉直接引用我都放在後面方法區那部分了,你們像瞭解請轉到後面那裏

1. 加載

經過類的全限定名獲取二進制字節流,將二進制字節流轉換成方法區中的運行時數據結構對象,並在內存中生成對應的Java.lang.class對象

加載類的方式其實不少,你們必定要清楚,黑科技都是藉助這個的:

  • 從本地系統直接加載
  • 網絡獲取,場景:web Applet
  • 從 zip、jar、war 等壓縮包中讀取
  • 運行時動態生成,好比:動態代理技術
  • 由其餘文件生成,好比:JSP 應用
  • 從數據庫中獲取 class 文件
  • 從加密文件中獲取

你們必定要清除啊,android 的黑科技那個沒用帶這個呢...

2. 連接

連接裏面3個小的步奏:驗證、準備、解析

  • 校驗: 檢查導入類或接口的二進制數據的正確性:(文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證)
  • 準備: 給類的靜態變量分配並初始化存儲空間,既然都分配屬性的內存空間了,那麼確定就有對象了,因此加載那一步在方法區建立出運行時數據結構對象確定是沒錯的,要不到這裏解釋不了。我曾經對什麼時候生成方法區數據對象產生懷疑
  • 解析: 將常量池中的符號引用轉成直接引用

3. 初始化

類加載時的初始化方法可不是默認的構造方法啊,構造方法是位對象服務的,類的初始化方法是位類服務的

JVM編譯器在編譯時會收集類靜態變量賦值操做和靜態代碼塊的操做,把這2者合併成一個方法:clinit(),注意這個方法是C++的,對咱們不可見。若類沒有靜態屬性,也沒有靜態代碼塊那就就沒有這個 clinit() 啦

clinit() 方法中代碼執行的順序是按照代碼書寫順序來的,誰寫在前面,誰就在前面的

好比:

public class Max {
    static {
        age = 100;
    }
    public static int age = 10; 
}
複製代碼

這個 age 最後賦值結果是10,誰讓static聲明在後呢,那age確定就是10了,具體的分析請看:JVM 面試題【中級】,這裏寫的很清楚,答案都在字節碼裏

另外 clinit() 只會執行一次,也就是在類首次被加載的時候執行,因此JVM對於clinit()方法是加鎖的。這個鎖是誰呢,就是該類方法區類元信息在JVM堆內存中的映射class對象,class也是一個對象,也有本身的對象鎖,這裏用的就是這個鎖,該鎖最多見的應用就是經典的雙判斷單例寫法了

另外還要注意,靜態代碼塊裏面不要寫死循環,要不其餘線程在同時加載該類型時會一直阻塞在 clinit 的

好比:

public class Dog {
    static {
        while (true) {

        }
    }
}

public class Max {

    public static void main(String[] args) {

       Runnable r = new Runnable() {
           @Override
           public void run() {
               Dog dog = new Dog();
           }
       };

       Thread t1 = new Thread(r);
       Thread t2 = new Thread(r);

       t1.start();
       t2.start();

    }
}
複製代碼

對於 t1,t2 來講,無論誰先加載 Dog 類,都會讓對方一直阻塞在 Dog 的初始化函數這裏沒發往下執行,因此 static{} 靜態代碼塊裏咱們不要寫耗時操做

clinit() 函數在執行時會優先執行父類的 clinit() 函數

4. 初始化方法執行時機

java 類是咱們何時用,何時才加載,這一點你們最好內心有數,有時候有些 BUG 就是由於類雖然加載了可是初始化方法沒有自行,你認爲在那個時刻類確定會初始化,但實際上她沒有

類的使用能夠分主動使用、被動使用,主動使用時會初始化該類,被動使用時不會初始化該類。類加載的3部中,初始化方法不是必須順着加載、連接這2部執行完後執行的,而是能夠自由決定何時用

類主動使用的狀況:

  • 經過new關鍵字、反射、clone、反序列化機制實例化對象
  • 調用類的靜態方法時
  • 使用類的靜態字段或對其賦值時
  • 經過反射調用類的方法時
  • 初始化該類的子類時(初始化子類前其父類必須已經被初始化)
  • JVM啓動時被標記爲啓動類的類(簡單理解爲具備main方法的類)

5. 類加載器介紹

上面說過類加載能夠分3分個大的步奏,在代碼上全靠系統提供的類加載來完成,不光涉及java部分,更是涉及C++部分

ClassLoader 是全部類加載的抽象基類,最終返回堆內存種的class對象

public abstract class ClassLoader{
    
    private final ClassLoader parent;
    
    protected Class<?> loadClass(String name, boolean resolve){
        ......
    }
}
複製代碼

每一個 ClassLoader 都有本身的本身的上一級 ClassLoader,也就是父 ClassLoader,這裏在雙親委派機制時會說

實際上系統類加載器有3種,分別加載不一樣範圍的類:

  • BootStrapClassLoader: 引導類加載器,用C++語言寫的,它是在Java虛擬機啓動後初始化的,能夠理解爲JVM的一部分。加載java系統核心類庫,加載JDK目錄下:/jre/librt.jar、resources.jar、charsets.jar的類,爲了安全起見,BootStrapClassLoader 只加載包名以:java、javax、sun開頭的了類。BootStrapClassLoader 徹底由JVM本身控制,咱們不只控制不了,甚至都不可見,在JAVA層面即使拿到 BootStrapClassLoader 的實例,對咱們來講也是一個null。BootStrapClassLoader 沒有父加載器,BootStrapClassLoader是C++實現的,不可能再走一個java的父類了
  • EtxClassLoader: 擴展類加載器,是java級別的了,是咱們能夠獲取的到的了。注意其父加載器是引導類加載器,加載JDK:/jre/lib/ext中的類(擴展目錄),
  • AppClassLoader: 系統類加載器,父加載器是擴展類加載器,加載全部非java核心類庫,簡單的說就是否是java官方寫的代碼,好比咱們本身,第三方開源類庫都是由她加載。

引導類加載器加載目錄:

// 系統類加載器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

// 擴展類加載器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@27716f4

// 引導類加載器,java.lang.string 已是java核心類庫範圍了
ClassLoader bootStrapClassLoader = String.class.getClassLoader();
System.out.println(bootStrapClassLoader);//null
複製代碼

獲取類加載器的幾種方式:

// 經過class獲取類加載
ClassLoader classLoader1 = new Dog().getClass().getClassLoader();
ClassLoader classLoader2 = Class.forName("com.bbb.xxx").getClassLoader();

// 經過線程上下文獲取類加載,拿到的是系統類加載器
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

// 直接獲取系統類加載器的單例
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
複製代碼

ExtClassLoader 和 AppClassLoader 都是在 Launcher 中初始化的,並將 AppClassLoader 設置爲線程上下文類加載器。 ExtClassLoader 和 AppClassLoader 都繼承自 URLClassLoader ,而最終的父類則爲 ClassLoader

Launcher public Launcher() {
ExtClassLoader localExtClassLoader;
try {
// 擴展類加載器
localExtClassLoader = ExtClassLoader.getExtClassLoader();
} catch (IOException localIOException1) {
throw new InternalError("Could not create extension class loader", localIOException1);
}
try {
// 應用類加載器
this.loader = AppClassLoader.getAppClassLoader(localExtClassLoader);
} catch (IOException localIOException2) {
throw new InternalError("Could not create application class loader", localIOException2);
}
// 設置AppClassLoader爲線程上下文類加載器
Thread.currentThread().setContextClassLoader(this.loader);
// ...

static class ExtClassLoader extends java.net.URLClassLoader static class AppClassLoader extends java.net.URLClassLoader } 複製代碼

6. 自定義類加載器

固然類加載器也能夠自定義的

通常這幾種狀況下會考慮自定義類加載器:

  • 防止源碼泄露 - 對字節碼加密,類加載時解密,預防反編譯篡改
  • 擴展加載源 - 插件化,熱修復
  • 修改類的加載方法
  • 隔離加載類- 中間件,中間件和應用模塊是隔離的,把類加載到不一樣環境當中,相互之間不衝突,防止不一樣依賴之間包名類名相同的類的衝突

咱們能夠選擇繼承 ClassLoader 類,重寫 findClass() 方法返回目標 class 對象,實際上須要咱們本身實現IO流,從磁盤加載class文件到內存生成對應的 bute[] 字節數組,以前咱們要是對字節碼加密了,那麼這個過程咱們能夠進行解密操做,而後使用使用 ClassLoader 自帶的 defineClass() 方法把字節數組轉換成 class 對象並返回

class MyClassload extends ClassLoader {

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {

            byte[] bytes = getCustomeClass(name);

            Class<?> aClass = defineClass(name, bytes, 0, bytes.length);

            return aClass;
        }

        public byte[] getCustomeClass(String name) {
            return null;
        }

    }
複製代碼

還有更多我就不詳細寫了,有須要的請自行 google

7. 雙親委派機制

咱們先回國頭來再看一遍 ClassLoader 的設計:

public abstract class ClassLoader{
    
    private final ClassLoader parent;
    
    protected Class<?> loadClass(String name, boolean resolve){
        ......
    }
}
複製代碼

ClassLoader 對象設計有 parent 父加載器,你們看着像不像鏈表。鏈表的next指向下一個,ClassLoader parent 這裏上一層級

類加載加載機制中默認不會直接由本身加載,會先用本身的父加載器 parent 去加載,父加載器加載不到再本身加載

JVM 3級類加載器,每一級都有本身能加載類的範圍,類加載器一級一級提交給父加載器去加載,每一級類加載在碰到本身能加載的類時,沒加載過的會去加載,加載過的會返回已經加載的class對象給下一級

看看 ClassLoader.loadClass() 方法代碼:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                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.
                    c = findClass(name);
                }
            }
            return c;
    }
複製代碼

這個就叫作:雙親委派機制,爲啥叫雙親,由於系統類加載器上面就2級類加載器

java 核心類庫有訪問權限限制,類加載器在發現容許加載範圍以外的類加載的加載請求以後,會直接報錯的。這個判斷通常都是用包名來判斷的,好比你本身搞了一個 String 類,包名仍是 java.lang,那引導類加載器在處理這個加載請求時會直接報錯,這種報錯機制就叫作沙箱安全機制

沙箱安全機制有個典型例子:360沙箱隔離,好比U盤程序只在360認爲隔離出來的沙箱內運行,以保護沙箱外的系統不受可能的病毒污染

雙親委派機制的目的是爲了保證安全,防止核心 API 被篡改

方法區

1. 基本介紹

方法區這個名稱是 JVM 規範對管理 class 類信息的內存區域的稱謂,你們能夠當作是一個接口,聲明出方法來了,可是具體怎麼實現還得看具體的實現類不是

JDK1.6 以前 hotspot 虛擬機關於方法區的實現叫永久帶,和堆內存同樣都在JVM實例進程內,OOM的問題比較嚴重,GC也會頻繁掃描這個區域,性能比較低

JDK1.8 hotspot 虛擬機換了個方法區的實現叫元空間,把類信息從JVM實例內存區域移出來,放到本地內存 native memory 中去了,這樣 OOM 風險小多了,不再怕加載的類太多爆 OOM 了,GC 掃描的頻率也下降了

JVM 各部分之間聯繫很緊密,方法區承載類加載器加載、解析到內存中的字節碼文件,記錄類的元信息,包括:

  • 類的信息: 類名,報名,訪問限制符,父類,實現的接口,註解
  • 字段信息: 也叫域信息,是類中全部的成員變量
  • 方法信息: 方法的名字,參數,訪問限制符,還包括方法自己須要執行的字節碼
  • 類加載器的引用: 方法區的類信息中會記錄加載該類的類加載器,一樣類加載器也會記錄本身加載了哪些類
  • class 引用: 這裏是指堆內存的 class 對象引用
  • 常量池: 其實都是字符串和編譯時就能肯定的數據,好比 final int 的值,編譯的時候就能肯定時多少,由於 final 的 int 是沒有機會變化的,不要和運行時常量池混了,這裏的常量池實際上是爲了減小字節碼文件體積,儘可能複用可能會重複的字符串,以後解析時會把這些字符串即符號引用轉換成對應的對象引用,好比父類啊,屬性類型啊,這些都會再解析時把對應的類加載出來,這樣字符串就變成了類引用了
  • JIT 即時編譯器編譯事後的代碼緩存

或者這張圖,classFile 就是編譯以後的class文件,它的數據結構就是這樣的,單詞不難,你們一看就知道咋回事了,就像一個Bean數據對象同樣,記錄一個類裏面的都有啥,難點很差理解的是 constant_pool,這個下面會仔細說一下的

這是代中文對照的圖:

堆、棧、元空間的相互關係:

2. 從字節碼入手

其實咱們從反編譯下字節碼就知道怎麼回事了,字節碼文件會加載到方法區,也就是數據儲存結構有些變化,可是東西仍是字節碼裏面的東西

public class Max {

    public static int staticIntValue = 100;

    static {
        staticIntValue = 300;
    }

    public final int finalIntValue = 3;
    public int intValue = 1;

    public static void main(String[] args) {
        try {
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void speak() {
        int a = 10;
        int b = 20;
        int c = a + b;
    }
}
複製代碼

反編譯下:java -v -p Max.class > Max.txt,加 -p 是由於私有屬性不加這個不先是,最後 > Max.txt 是把反編譯出來的字節碼寫入到txt文件中,這樣方便看

Classfile /Users/zbzbgo/Desktop/Max.class
  
  // 字節碼參數
  Last modified 2020-6-20; size 746 bytes
  MD5 checksum 5c6bccb4965bf8e6408c8e3ef8bca862
  Compiled from "max.java"
  
// 包名+類名  
public class com.bloodcrown.bw.Max
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER // 訪問限定符
 
// 常量池,裏面其實都是字符串,須要解析時加載 
Constant pool:
   #1 = Methodref          #11.#30        // java/lang/Object."<init>":()V
   #2 = Fieldref           #10.#31        // com/bloodcrown/bw/Max.finalIntValue:I
   #3 = Fieldref           #10.#32        // com/bloodcrown/bw/Max.intValue:I
   #4 = Long               100000l
   #6 = Methodref          #33.#34        // java/lang/Thread.sleep:(J)V
   #7 = Class              #35            // java/lang/InterruptedException
   #8 = Methodref          #7.#36         // java/lang/InterruptedException.printStackTrace:()V
   #9 = Fieldref           #10.#37        // com/bloodcrown/bw/Max.staticIntValue:I
  #10 = Class              #38            // com/bloodcrown/bw/Max
  #11 = Class              #39            // java/lang/Object
  #12 = Utf8               staticIntValue
  #13 = Utf8               I
  #14 = Utf8               finalIntValue
  #15 = Utf8               ConstantValue
  #16 = Integer            3
  #17 = Utf8               intValue
  #18 = Utf8               <init>
  #19 = Utf8               ()V
  #20 = Utf8               Code
  #21 = Utf8               LineNumberTable
  #22 = Utf8               main
  #23 = Utf8               ([Ljava/lang/String;)V
  #24 = Utf8               StackMapTable
  #25 = Class              #35            // java/lang/InterruptedException
  #26 = Utf8               speak
  #27 = Utf8               <clinit>
  #28 = Utf8               SourceFile
  #29 = Utf8               max.java
  #30 = NameAndType        #18:#19        // "<init>":()V
  #31 = NameAndType        #14:#13        // finalIntValue:I
  #32 = NameAndType        #17:#13        // intValue:I
  #33 = Class              #40            // java/lang/Thread
  #34 = NameAndType        #41:#42        // sleep:(J)V
  #35 = Utf8               java/lang/InterruptedException
  #36 = NameAndType        #43:#19        // printStackTrace:()V
  #37 = NameAndType        #12:#13        // staticIntValue:I
  #38 = Utf8               com/bloodcrown/bw/Max
  #39 = Utf8               java/lang/Object
  #40 = Utf8               java/lang/Thread
  #41 = Utf8               sleep
  #42 = Utf8               (J)V
  #43 = Utf8               printStackTrace
{

  // 成員變量信息
  public static int staticIntValue;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC

  public final int finalIntValue;
    descriptor: I
    flags: ACC_PUBLIC, ACC_FINAL
    ConstantValue: int 3

  public int intValue;
    descriptor: I
    flags: ACC_PUBLIC

  // 默認的構造方法
  public com.bloodcrown.bw.Max();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_3
         6: putfield      #2                  // Field finalIntValue:I
         9: aload_0
        10: iconst_1
        11: putfield      #3                  // Field intValue:I
        14: return
      LineNumberTable:
        line 8: 0
        line 17: 4
        line 19: 9

  // 方法信息
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: ldc2_w        #4                  // long 100000l
         3: invokestatic  #6                  // Method java/lang/Thread.sleep:(J)V
         6: goto          14
         9: astore_1
        10: aload_1
        11: invokevirtual #8                  // Method java/lang/InterruptedException.printStackTrace:()V
        14: return
      Exception table:
         from    to  target type
             0     6     9   Class java/lang/InterruptedException
      LineNumberTable:
        line 23: 0
        line 26: 6
        line 24: 9
        line 25: 10
        line 27: 14
      StackMapTable: number_of_entries = 2
        frame_type = 73 /* same_locals_1_stack_item */
          stack = [ class java/lang/InterruptedException ]
        frame_type = 4 /* same */

  public void speak();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10
         2: istore_1
         3: bipush        20
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: istore_3
        10: return
      LineNumberTable:
        line 30: 0
        line 31: 3
        line 32: 6
        line 33: 10

  // 類的初始化方法 clinit(C++) 方法,編譯時自動生成的。注意不是默認的構造函數,靜態代碼塊和靜態屬性賦值,寫代碼時誰寫在前面,誰的賦值就在前面,注意有前後順序
  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: bipush        100
         2: putstatic     #9                  // Field staticIntValue:I
         5: sipush        300
         8: putstatic     #9                  // Field staticIntValue:I
        11: return
      LineNumberTable:
        line 10: 0
        line 13: 5
        line 14: 11
}
SourceFile: "max.java"
複製代碼

你們看個意思,方法區儲存的類信息其實和字節碼差不了對少

3. 方法區的儲存結構

圖裏的字符串常量池不方法區裏,JVM 規範雖然是這樣的,可是具體的虛擬機實現都會有變化的,具體看實際

  • 存儲位置:
    方法區不在用戶進程的內存中,而是在本地內存 native memory 中,這樣的好處是加載過多的類也不會形成堆內存的OOM了
  • 方法區數據結構:
    操做系統中,能夠有多個 JVM 實例,看着每一個JVM都有本身的方法區。但實際上在 native memory 內存中,方法區只有一塊。每一個類加載器在方法區均可以申請一塊本身的空間,類加載器相互之間不能訪問,每一個類加載器本身的空間內,給每個類信息都分配一塊空間,就像 Map<Classload,Map<String,Class>> 這樣的數據結構同樣。系統類加載器是 static 的,每一個進程的系統類加載器是都是不一樣的對象,對應的方法區空間也不同,因此他們之間加載的類信息是不能共享的,比如A進程加載Dog的1.3版本,B進程加載Dog的1.0版本,這並不影響進程A和B之間的獨立運行
  • classload 和方法區class相互記錄:
    方法區裏的class類信息對象會記錄本身是哪一個類加載器加載的,類加載器同樣會記錄本身加載過哪些類信息

4. 方法區的 OOM

方法區默認大小是20.75M,android 上是20.79M,最大值是一個無限大的數,可是其實是物理內存的上限,超過這個上限同樣也會 OOM

  • jinfo -flag MetaspaceSize 74290 命令能夠查看方法區大小
  • -XX:MetaspaceSize=100m 在 VM options 設置方法區大小,方法區的最大值通常不動,咱們調節的都是方法區一上來的默認大小

方法區咱們能夠設置固定大小,也能夠設定動態調整,默認是動態調整的,一旦方法區滿了就會觸發 Full GC,GC 會去回收方法區中不被用到的 class 類信息,何時 class 類信息不被用到呢。就是加載 class 類信息的 classload 銷燬了,那麼這個這個 classload 加載的全部的 class 類信息都無用了,能夠被回收了

Full GC 要是發現仍是不能方法區內存需求,就會擴大方法區的內存大小,可是一次確定不會增長不少,估計就是幾M 的事。這裏就有個問題了,要是咱們一上來設置的太小,咱們加載的類又不少,那會方法區就會頻繁的觸發 Full GC,這是一個能夠優化的點

5. 理解什麼是常量池

常量池這東西咱們應該清楚的,即使網上的資料,看那些文字描述基本看不懂,可是這不是咱們不去理解的理由,方法區和類加載機制是緊密聯繫的,因此方法區的一切咱們都應該知道

常量池這塊挺複雜的:

  • classfile 裏面的叫常量池
  • 方法區裏面的叫運行時常量池

他倆之間的關係:

  • 字節碼文件 classfile 被 classload 加載到內存以後,字節碼文件中的常量池就變成運行時常量池了

必定要搞清楚他倆是什麼,我一開始看這裏的時候頭疼啊,一會常量池,一會運行時常量池,我都懷疑網上的文章是否是寫錯了,去看《深刻理解java虛擬機》這本書又寫的不連貫,寫的莫名其妙,看着描述的文字不少,但就是沒說明白這是啥

其實他倆的關係就是一句話:文件裏的字節碼常量池加載到內存以後就是運行時常量池了。學習他倆其實把字節碼的常量池搞明白就好了,剩下那個天然就懂了

先看看常量池的字節碼吧,用的是前面反編譯出來的字節碼

Constant pool:
   #1 = Methodref          #11.#30        // java/lang/Object."<init>":()V
   #2 = Fieldref           #10.#31        // com/bloodcrown/bw/Max.finalIntValue:I
   #3 = Fieldref           #10.#32        // com/bloodcrown/bw/Max.intValue:I
   #4 = Long               100000l
   #6 = Methodref          #33.#34        // java/lang/Thread.sleep:(J)V
   #7 = Class              #35            // java/lang/InterruptedException
   #8 = Methodref          #7.#36         // java/lang/InterruptedException.printStackTrace:()V
   #9 = Fieldref           #10.#37        // com/bloodcrown/bw/Max.staticIntValue:I
  #10 = Class              #38            // com/bloodcrown/bw/Max
  #11 = Class              #39            // java/lang/Object
  #12 = Utf8               staticIntValue
  #13 = Utf8               I
  #14 = Utf8               finalIntValue
  #15 = Utf8               ConstantValue
  #16 = Integer            3
  #17 = Utf8               intValue
  #18 = Utf8               <init>
  #19 = Utf8               ()V
  #20 = Utf8               Code
  #21 = Utf8               LineNumberTable
  #22 = Utf8               main
  #23 = Utf8               ([Ljava/lang/String;)V
  #24 = Utf8               StackMapTable
  #25 = Class              #35            // java/lang/InterruptedException
  #26 = Utf8               speak
  #27 = Utf8               <clinit>
  #28 = Utf8               SourceFile
  #29 = Utf8               max.java
  #30 = NameAndType        #18:#19        // "<init>":()V
  #31 = NameAndType        #14:#13        // finalIntValue:I
  #32 = NameAndType        #17:#13        // intValue:I
  #33 = Class              #40            // java/lang/Thread
  #34 = NameAndType        #41:#42        // sleep:(J)V
  #35 = Utf8               java/lang/InterruptedException
  #36 = NameAndType        #43:#19        // printStackTrace:()V
  #37 = NameAndType        #12:#13        // staticIntValue:I
  #38 = Utf8               com/bloodcrown/bw/Max
  #39 = Utf8               java/lang/Object
  #40 = Utf8               java/lang/Thread
  #41 = Utf8               sleep
  #42 = Utf8               (J)V
  #43 = Utf8               printStackTrace
複製代碼

你們看看這常量池的字節碼感受像啥,像不像list列表,有int索引,數據一行一行很規則

沒錯,常量池本質就是一張表,虛擬機根據字節碼指令來這張表找到和這個類關聯的類、方法名、參數類型、字面量等信息

你們注意啊,常量池裏面存的都是字符串,爲啥?class文件不是字符串能是什麼,就算加載到內存裏對應的仍是字符串,只有類加載器根據這麼字符串信息把這個類加載出來,這些字符串纔有意思

因此像:java/lang/Object、java/lang/Thread、com/bloodcrown/bw/Max.intValue這些,咱們知道其實他們是什麼,是類的類型,接口,方法,包名等等,常量池中的這些字符串就叫作符號引用

可是單單字符串咱們是無法用的,必需要類加載器把這些字符串描述的類都正式在內存中加載出來纔有意義。這個過程在類加載機制中的解析環節,會把常量池中這些字符串轉換成加載事後的class類型信息在方法區中的地址,這個地址叫作:直接引用

從常量池->運行時常量池=從符號引用->直接引用,說白了就是把字節碼中描述信息的字符串都正式加載成出來,生成對應的類、接口、註解等等可使用的信息

總結一下,常量池的內容包括:

  • 數值量: 好比 int=10 中的 10
  • 字符串
  • 類引用
  • 字段引用
  • 方法引用

那爲何要涉及一個常量池出來呢,既然都是字符串,咱們寫在用的地方不就行了嘛~距網上的解釋,JVM 官方是考慮到有的字符串會重複被使用,爲了儘量減小class文件體積。另外一個考慮是,每一個類裏面其實都涉及其餘類,若是不用字符串代替class自己涉及到的其餘的類型信息,那麼就要把這些涉及到的類型信息都寫在同一個class文件裏,那麼這回形成災難性的後果,class文件大到難以接收,文件結構也會變得不可預期,大量的class文件中都會有重複信息,甚至涉及到不一樣類型的版本,這樣就無法搞了

6. JDK1.8 方法區變化

前文說過,方法區是 JVM 規範的稱爲,只是一種建議規範,而且尚未作強制限制。具體設計成什麼樣,還得看看方法區的具體實現,永久帶和元空間就是方法區的具體實現,區別很大

永久帶這東西只有 hotspot 虛擬機再 JDK1.6 以前纔有,其餘虛擬機像 JRockit、J9 人家壓根就不用,而是用本身的實現:元空間

永久代:設計在JVM內存中,和堆內存連續的一塊內存中,儲存類元信息、字符串常理池、靜態數據,由於有JVM虛擬機單個實例的內存限制,永久帶會較多概率觸發 FullGC,而且垃圾回收的效率、性能還低,類加載的多還會出現 OOM,尤爲是後臺程序加載的模塊多了

元空間:設計在本地內存 native memory,沒有了JVM虛擬機內存限制,OOM 基本就杜絕了,FullGC 觸發的概率較低。類元信息隨着方法區中的遷移,改在本地內存中保存,字符串常量池和靜態數據則保存在堆內存中

JDK 1.6 以前方法區採用永久帶,JDK1.8 開始,方法區換用元空間,JDK1.7 在其中起過分

7. 方法區的GC

方法區不是沒有GC的,只是規範沒強制有,具體看方法區實現的心情了,固然元空間確定是有的

你們須要知道方法區不足會引發 GC,而這個 GC 是 FullGC,性能消耗很大。方法區GC回收的其實就是運行時常量池裏的東西

類元信息的回收條件很是苛刻,必須同時知足下面全部條件:

  • 該類 類型的全部實例都被回收了
  • 加載該類的類加載器已經被回收了
  • 該類對應的在堆內存中的class映射對象,沒有被任何地方引用

蛋疼不,第三條有點說到的地方,咱們反射時但是大量會用到class的,因此反射可能會形成類元信息的內存泄露

正是由於方法區回收的條件衆多且必須一一知足又和堆內存息息相關,因此纔會觸發最重量家的 FullGC,把堆內存總體過一遍。回收的內容又沒有堆內存那樣多,可能有的人以爲這點內存其實不必回收,可是之前Sun公司由於方法區沒有GC回收問題而引發過很多重量級bug,因此方法區的回收是一件必須的事情,可是又是一件費力不討好,還性能消耗大的事,因此在後端開發時,方法區初始值通常都儘可能設置的大一些,爲了就是減小方法區GC

大量使用反射、動態代理、CGLib等字節碼框架,動態生成JSP,及其 OSGI 這類頻繁自定義類加載器的場景中,一般都是須要 JVM 具有類型卸載的能力,以保證不會對方法區形成多大的壓力

8. 補充

這是從別人那裏看過來的,想了想應該是正確的

一個稱之爲類數據共享(CDS)的特性自HotspotJVM 5.0開始被引進。在安裝JVM期間,安裝器加載一系列的Java核心類(如rt.jar)到一個通過映射過的內存區進行共享存檔。CDS減小了加載這些類的時間從而提高了JVM的啓動速度,同時容許這些類在不一樣的JVM實例之間共享。這大大減小了內存碎片

對象在堆內存中的儲存結構

1. 儲存結構

說完類加載器和方法區我就能夠來看對象是怎麼在堆內存存儲的了

就是這個樣子:

對象在堆內存中的存儲結構:

  • 對象頭
    • Mark Word:也稱運行時元數據,一個64位的數字,使用位運算分段保存對象的一些信息,包括:哈希值(對象內存的首地址),GC分代年齡,鎖狀態,偏向線程ID,偏向時間搓
    • Kclass Word 類型指針:指向該類在方法區對應的類元數據地址(KClass對象)
    • 數組長度:若是這是數組對象的話,惠濟路數組的長度
  • 對象實體
    • 注意會先儲存父類的屬性,內存佔用相同的屬性會放在一塊兒
  • 對齊方式
    • 64位系統JVM默認對8字節對齊,簡單說就是必須被8整除,不能被8整除,這裏會添加一些大小以實現被8整除

其中詳細的指針看下圖:

2. 空對象佔內存多少

這是一道常問的面試題,咱們只考慮通常對象,對象實體是空的,Mark Word 佔64位8個字節

類型指針默認是8個字節的,可是在開啓指針壓縮時會變成4個字節,JDK8 是默認開啓的

參考對齊方式,因此一個空對象默認佔用內存大小是12個字節,算上對齊方法的化是16個字節

3. java 中的 oop-klass 模型

學習JVM的話,oop-klass 模型永遠是一個繞不過去話題。咱們都知道 HotSpot VM 幾乎能夠說是純 C++ 語言編寫的 Java 虛擬機,那麼 Java 的對象模型和 C++ 的對象模型之間究竟有什麼關係呢?這個問題簡單回答就是 oop-kclass 二分對象模型

具體來講:HotSpot 虛擬機採用 Klass-OOP 模型來存儲 java 對象,OOP(Ordinary Object Pointer)指的是普通對象指針,是 java 部分的。而 Klass 用來描述對象實例的具體類型,是 C++ 的。oop 在堆內存中,就是對象頭,Kclass 是方法區類元數據的數據模型

至於爲啥要搞這一個東西呢,我從網上找來的,這個應該是解釋的最正確的了吧

事實上HotSpot底層究竟怎麼表示一個Java對象這個問題歸根結底就是C++怎麼表述一個Java對象。有一個樸素的實現方案就是將每個Java對象都影射爲一個對等的C++對象,然而這麼作確實是太樸素了,它有一個嚴重的弊端就是若是這樣作的話那麼就不得不爲每個Java對象都保存一份VTable(虛函數表),由於C++的多態實現就是爲每個對象都保留一份VTable。這是很浪費空間的,因此HotSpot設計者採用了oop-class二分模型來表述一個Java對象。其中這裏的oop表示Ordianry Object Pointer(普通對象指針,注意可不是object-oriented programming),它用來表示對象的實例信息,看起來像個指針其實是藏在指針裏的對象。而 klass 則包含 元數據和方法信息,用來描述 Java 類

那麼爲什麼要設計這樣一個一分爲二的對象模型呢?這是由於HotSopt JVM的設計者不想讓每一個對象中都含有一個vtable(虛函數表),因此就把對象模型拆成klass和oop,其中oop中不含有任何虛函數,而klass就含有虛函數表,能夠進行method dispatch。這個模型實際上是參照的 Strongtalk VM 底層的對象模型

總結來講:就是對象數據在 java 和 C++ 2種語言直接聯繫(我的理解)

還記的對象的對象頭碼,Mark Word + Kclass Word,oop-klass 模型的 oop 就是這個對象頭。對象的建立這一步中就專門有生成分配設置對象頭這一步

oop 的具體類型有:instanceOopDesc(實例對象)/arrayOopDesc(數組對象),oop 是 方法區 C++ 類元信息在 java 數據,模型中的映射

instanceOopDesc 繼承自 oopDesc,看看 oopDesc 的源碼:

class oopDesc {
  friend class VMStructs;
 private:
  volatile markOop  _mark;
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;

  // Fast access to barrier set. Must be initialized.
  static BarrierSet* _bs;
...
}
複製代碼

_mark 是對象頭裏的 Mark World,_klass 和 _compressed_klass 都是方法區 Kclass 對象的引用地址,_compressed_klass 是開啓指針壓縮以後的內存地址

instanceKlass 是類元信息的具體數據模型,具體不解釋了

class InstanceKlass: public Klass {
  ...
  enum ClassState {
    allocated,                          // allocated (but not yet linked)
    loaded,                             // loaded and inserted in class hierarchy (but not linked yet)
    linked,                             // successfully linked/verified (but not initialized yet)
    being_initialized,                  // currently running class initializer
    fully_initialized,                  // initialized (successfull final state)
    initialization_error                // error happened during initialization
  };
  ...
 } 
複製代碼

Java 虛擬機是如何經過棧幀中的對象引用找到對應的對象實例的,看圖:

通常介紹 oop-Kclass 的到這裏就結束了,可是這裏還有一個重要角色,也是不少人搞不明白的,就是 CLASS 對象,咱們知道每一個類在堆內存中都有一個對應的class對象,尤爲是反射的時候

// 這個class就是咱們要說的東西
Class<Dog> dogClass = Dog.class;
複製代碼

具體來講仍是由於 C++ 和 java 語言的差別,Kclass 對象在方法區內,而方法區又不在堆內存而是在 Native Memory 本地內存中,Native Memory 不容許咱們直接訪問,因此爲了便於銜接JVM內存結構,因此搞了一個句柄出來,對 JVM堆內存中的 class 對象就是 Kclass 對象的句柄,class 並能夠訪問 Kclass ,加之反射咱們須要幫助JVM解析類的結構,因此就有了堆內存裏的class對象

class對象是何時生成的呢,是和 Kclass 對象一塊兒生成的,類加載機制在加載的時候在方法區生成一個 Kclass 對象,那麼就會在類加載器所屬的 JVM 堆內存中同步生成一個 class 對象

Class 自己也是一個 java 類型,其中基本都是 Native 方法

Class{
    public native Field getDeclaredField(String name) throws NoSuchFieldException;

    private native Field[] getPublicDeclaredFields();
}
複製代碼

值得注意的是,class 沒有直接指向 Kclass,而是 Kclass 內部指向了 class,咱們需找 class 的路線是:oop(對象頭)->kclass(方法區)->class(堆內存)

棧內存

棧內存簡介

網上有句話:棧是運行時結構,堆是儲存結構。棧是管程序如何執行,怎麼處理數據的。這句話基本道盡了 java 棧內存的做用,棧就是管運行的

棧內存同程序計數器,本地方法棧一塊兒是線程私有的,有一個線程 new 出來,就會開闢一塊棧內存出來

棧內存採用數據結構,其內部保存的是一個一個棧幀,一個棧幀對應一個java方法。棧是一種快速有效的分配存儲方式,訪問速度僅次於程序計數器,棧內存有OOM,可是沒有GC的需求,空間不夠了直接OOM

棧內存的做用就是主管程序的運行,保存方法的局部變量,部分結果,並參與方法的調用和返回,生命週期和線程一致

棧內存調試

JVM 容許棧內存的大小是固定的或者是動態調整的,這個看你具體設置的JVM 參數了

JDK8 時 棧默認大小1M,最小160K,小於 160K直接報錯

還有一個參數,棧深度,就是方法調用鏈的深度,用遞歸來舉例 10086,就是遞歸10086此調用本身這個方法了

棧內存不夠用了也是會 OOM 的:

  • 棧內存是的固定話,會拋 stackOverFlowError
  • 棧內存是動態的話,會拋 outOfMemoryError

JVM 參數:

  • Xss: 棧內存值
  • 實際這麼寫:-Xss256k

記住棧內存只有這麼一個 JVM 參數,當最大值用就行啦

默認狀況下咱們搞一個 OOM 看看:

public class Max {

    public static int index = 0;

    public static void main(String[] args) {
        index++;
        System.out.println(index);
        main(args);
    }

}
複製代碼

能夠看到拋出的哪一個異常,這對於JVM調優是很是重要的,另外能夠看到棧深度大概是1000左右,默認1M的棧大小,這麼一算的話一個棧幀大概要佔200個字節左右,因此寫遞歸時必定要注意,稍不注意棧內存就溢出了,別堆內存沒事,棧到先頂不住了...

打印棧內存大小: XX:+PrintFlagsFinal -version | grep ThreadStack

➜  ~ java -XX:+PrintFlagsFinal -version | grep ThreadStack
     intx CompilerThreadStackSize                   = 0                                   {pd product}
     intx ThreadStackSize                           = 1024                                {pd product}
     intx VMThreadStackSize                         = 1024                                {pd product}
複製代碼

特別須要注意的是,GC 垃圾回收機制不會涉及到棧內存,棧內存相對於堆內存是不可見的,咱們常說的 GC 只會對堆內存起做用,甚至方法區都有本身專門的垃圾回收器

什麼是棧幀

棧內存是 stack 棧這種先入後出的數據結構,每個棧幀表明的是一個方法,方法執行時所須要,所產生的數據都包含在棧幀中,棧幀就能夠當作方法執行在內存中的樣子

其實這幾句話我都不想寫的,能看我這篇文章的,棧和堆基本都知道

棧幀結構

一個棧幀表明一個方法,方法在運行過程當中會有傳入的參數,本身建立的屬性,執行結果等等的東西,因此棧幀的結構比較複雜,由於要分門別類儲存這些東西:

  • 局部變量表
  • 操做數棧
  • 動態連接 運行時常量池中方法的引用
  • 返回地址
  • 附加信息

先說 動態連接 這個東西,方法的字節碼存在哪?固然是方法區裏面啦,一個類的方法確定會有不少地方都用到啊,不能每個地方都保存一份方法運行的字節碼啊,那內存可就摟不住啦,因此棧幀裏面必須有一個屬性要能在方法運行之時把方法的字節碼拿到交給執行引擎去執行,動態連接乾的就是指向方法區Kclass對象運行時常量池中該方法的引用

動態連接這裏有個點,靜態連接和動態連接,涉及黑科技的東西你們要知道:

  • 靜態連接: 當一個字節碼被裝載進JVM內部時,若是被調用的目標方法在編譯期可知,且運行期保持不變時,在這種狀況下將調用方法的符號引用轉換位直接引用,這個成爲靜態連接。就是一旦編譯就不變了
  • 動態連接: 若是被調用的目標方法在編譯期沒法肯定下來,只能在程序運行時將調用方法的符號引用轉換位直接引用,因爲這種引用轉換過程具有動態性,所以被成爲動態連接。好比一個方法接受一個接口對象類型的參數,對於方法來講,編譯時怎麼知道會具體傳什麼類型的實現類進來,這個就是動態綁定

返回地址,附加信息這2個沒必要說,相比你們都能猜的出,重點是局部變量表和操做數棧

局部變量表 其實也好理解,她就是一個數組,保存方法接收到的參數和在方法運行時生成的對象的引用,對象自己仍是保存在堆內存裏面的,在方法運行的時刻會拷貝賦值一份到線程所屬的工做內存中,棧幀所屬的棧內存也在工做內存中。局部變量表的基本單位是 slot,一個slot佔4個字節,8個字節的基本數據類型佔2個slot,引用類型的指針佔2個slot。局部變量表是棧幀中佔據內存空間最大的部分,一個對象引用就要佔8個字節出去

操做數棧 在方法運行過程當中,根據字節碼指令,往棧中寫入活提取數據,push 入棧、pop 出棧,保存方法運行過程當中產生的中間數據和臨時數據,是徹底爲了方法執行服務的,其保存的值沒有最終的實際意義,是位字節碼指令執行服務的。好比讓 Dog a、Dog b 交換對象實例的操做,中間產生的tem這個臨時變量的引用,就會存儲在操做數棧裏

操做數棧的字節碼分析按理說淂寫一遍的,要不就是不到位,可是這裏我就真的不想寫了,B站上講JVM的都會把這塊說的倒背如流,留給你們當個思考題吧,不能什麼都靠別人不是,本身動手豐衣足食...

棧在內存的哪一個位置

這個問題絕對會難倒絕大部分人的,由於我就是 ︿( ̄︶ ̄)︿

棧內存中值得咱們深刻思考的是棧和工做內存的關係以及執行狀況,JMM 不瞭解的請看:JMM和底層實現原理

JMM 是什麼:MM定義了Java 虛擬機(JVM)在計算機內存(RAM)中的工做方式。JMM規定每一個線程都有本身的工做內存,工做內存是什麼:工做內存是寄存器和高速緩存的抽象。說棧就是工做內存絕對是錯誤的,棧雖然默認是1M的,很小,但每一個進程容許建立的現場數最多能夠達5000個,棧要是不設置小一點,內存裝的下嘛~,棧是運行在線程所屬工做內存的,可是會隨着硬件吃緊緩存到內存中

棧表明的是方法的執行,須要快速高效,而內存相對於CPU內部的寄存器和緩存來講要慢上百倍,這是不能接收的,因此每一個線程纔會在cpu緩存上有本身的一塊空間也就是工做內存。可是cpu緩存很小,桌面CPU AMD R3600X L3才32M,一個線程就要佔1M走,那線程多了cpu也摟不住啊。移動cpu 高通家的曉龍865 3級緩存在4M,明顯不夠用

因此線程在獲取cpu時間片時,其工做內存必定會在cpu L3中運行,一旦失去cpu時間片仍是會保存在cpu緩存中的,可是一旦cpu緩存不夠用了,cpu緩存會執行清理工做,這時會把失去cpu時間片的線程工做內存移至內存中緩存,用時再加載會cpu緩存中,因此cpu緩存越大多線程性能越好,線程切換更少

還有另一個佐證:棧幀中的局部變量表也是垃圾回收機制重要的根節點。可見垃圾收集器是能訪問到棧內存的,棧內存要是常駐cpu告訴緩存中的話,垃圾回收器是訪問不到的

這裏我迷糊了很久,搞清這個問題我是花了不少功夫的...

寫到這裏我終於找到了明確的答案:

因爲操做數是存儲在內存中的,所以會頻繁的執行內存讀/寫操做,必然會影響執行速度。緯二路解決這個問題,Hotspot 虛擬機的設計者們提出了棧頂緩存技術,講棧頂元素也就是立刻要執行的棧幀(方法)緩存在cpu寄存器中,以此下降對內存的讀寫次數,提高執行引擎的效率

結合這段話,我是咱們能夠猜想下,棧頂的棧幀放入cpu緩存的優先級確定是:L1->L2->L3 的嗎,我估計即使線程失去cpu時間片,可能還會緩存該線程下一個棧幀到L3中,估計會結合java的鎖升級機制,經過鎖能夠知道哪一個線程將來執行機會大

本地方法棧

仍是有必要說一下,找到一段經典解析:

當某個線程調用一個本地方法時,她就進入了一個全新的而且再也不受虛擬機限制的世界,他和虛擬機擁有一樣的權限

  • 本地方法經過本地方法接口來訪問虛擬機內存的運行時數據區
  • 能夠直接使用cpu中的寄存器
  • 能夠直接從本地內存的堆中分配任意數量的內存

並非全部的JVM都支持本地方法棧,java 虛擬機規範並無明確規定本地方法棧使用的語言,具體實現等。若是JVM產品不打算直接native方法,能夠沒有本地方法站的

hotspot JVM 中,直接將本地方法棧和虛擬機棧合二爲一了

棧內存使執行 java 方法的,本地方法棧是執行 C/C++ 方法的,知道這麼多就行啦 o( ̄ヘ ̄o#) 等你研究 C++ 時能夠再深刻理解

相關文章
相關標籤/搜索