天天看點

深入了解JVM運作機制與GC機制

作者:網際網路進階架構師

虛拟機結構

運作時資料區域(叫JVM 記憶體模型也可以)

按oracle的虛拟機規範介紹可以大緻分為下面幾類:

程式計數器(pc register)、Java虛拟機堆棧、堆、方法區、運作時常量池、本機方法堆棧(native方法堆棧)

JVM中寄存器用于存儲程式計數器的值,就是每個位元組碼指令左側的數字。看到寄存器我還蠻親切的,上學時在實驗室裡沒少“扣”寄存器,那真是用本子記再用機器“扣”着讀。那時還是寫彙編(現在全忘了),代碼向一個寄存器位址load一個1,move一個1在寄存器調試器上都可以立馬看到,非常有意思。

最後這個本機方法堆棧其實也并不陌生,它會産生兩個JVM的異常:

  • 如果線程中的計算需要的堆棧大小比允許本機方法堆棧更大,Java 虛拟機将抛出一個StackOverflowError.
  • 如果本機方法堆棧可以動态擴充并且嘗試本機方法堆棧擴充但可用記憶體不足,或者如果可用記憶體不足以為新線程建立初始本機方法堆棧,Java 虛拟機将抛出一個OutOfMemoryError

棧幀

這裡指虛拟機在安排方法執行時棧幀,一個方法進入執行前都會産生一個棧幀,多個方法則按照LIFO的模式執行。

在結構上棧幀包含本地變量表、操作棧兩個可具象化的塊兒。除此之外,在棧幀中還需要引用運作時常量池中的變量,方法,但他們在常量池中都是以符号形式存在,這就需要依賴 動态連結将它們轉化為具體引用來供棧幀讀取。

我還是更喜歡用圖形來記憶:

深入了解JVM運作機制與GC機制

從對象的建立分析JVM執行流程

準備一個簡單的java檔案,實作建立一個對象的代碼:

深入了解JVM運作機制與GC機制
  1. 在class加載前,先啟動虛拟機 。虛拟機啟動後,堆,方法區(及常量池)都會建立完成
  2. 建立一個線程棧來執行方法
  3. 外部需要一個類加載器,來加載Class到方法區
  4. 執行main方法前,将押入方法棧幀,設定好操作數棧,本地變量表大小。

完成準備工作後就像下圖的樣子:

深入了解JVM運作機制與GC機制

這裡的 Hello 就是類名字元串,其引用将通過動态連結引入到棧幀

5.執行main方法 需要一個外部的執行引擎,來修改程式計數器 以第一個指令 new 作圖 計數器 0: new : 建立一個對象,并将其引用值壓入棧頂

在堆區建立一個對象[配置設定記憶體],将其引用入棧 :

深入了解JVM運作機制與GC機制

後續就不用圖了因為準備工作都完成了,直接看指令

  • 計數器 3: dup: 複制棧頂數值并将複制值壓入棧頂
  • 計數器 4: invokespecial: 調用超類構造方法,執行個體初始化方法,私有方法 這裡會進入到構造器執行,也就需要建立新的棧幀。
深入了解JVM運作機制與GC機制

從構造方法指令可以看到得構造方法的 棧容積1,本地變量表大小1 ,押入新棧幀:

深入了解JVM運作機制與GC機制

0: aload_0 :将第0個引用類型本地變量推送至棧頂 1: invokespecial :調用超類構造方法 4: return :出棧

7: astore_1: 将棧頂引用型數值存入第1位本地變量 這裡将棧的引用存入1位變量中 到這裡才完成了hello = new Hello() 這行代碼的執行

深入了解JVM運作機制與GC機制

看來建立一個對象最少需要4步指令,如果要指派給另一個變量還需要額外增加2步

Class生命周期與對象生命周期(粗略了解)

JVM類加載過程:(裝載、校驗、準備、解析、初始化、使用、解除安裝)

Java對象在JVM中的生命周期:

1 建立階段(Created) 2 應用階段(In Use) 3 不可見階段(Invisible) 4 不可達階段(Unreachable) 5 收集階段(Collected) 6 終結階段(Finalized) 7 對象空間重配置設定階段(De-allocated)

GC(Automatic Garbage Collection)

垃圾回收設計原理的官方介紹文檔:Java Garbage Collection Basics

GC root的種類:Garbage Collection Roots

重點關注:

系統Class Thread Block:被存活線程引用的對象 Thread:已啟動但未停止的線程 Finalizable:在終結器隊列中的對象 Busy Monitor:作為同步螢幕或調用 wait() 或 notify() 的對象 Java Local:被線程棧引用的方法内的局部變量

在Heap中的對象,其組成結構中包含一個age塊,用于标記其存活年齡。

Heap 分區

整個垃圾回收機制要集合Heap分區與不同的GC算法來一起看。先看heap分區結構

深入了解JVM運作機制與GC機制

從左往右分别是:新生代、老年代與永久代。YG區域又細分為 eden,與兩個survivor space 區域,這裡比較好了解。

eden區都是新建立的對象,在eden記憶體區滿時會觸發小型GC,幸存的對象将移至S0,根據age累計不同逐漸移動到S1,當判定為長期存活的對象将移動到老年代。

小型GC往往會很快完成,但小型GC也是“Stop the world Event”即阻塞全部線程的執行。

老年代為主要垃圾回收,也是“Stop the world Event”,其影響時間會更長,主要由老年代的垃圾收集器決定。

永久代包含JVM所需的中繼資料,描述應用程式中使用的類,方法,跟随JVM運作時需要引入的類進行遞增填充。簡單可以了解為永久代是專門給JVM存儲類與方法的描述資訊用的,比如ClassLoader加載的類資訊應該就在這裡吧。這很容易誤以為永久代就是方法區(永久代是Heap中的塊,而方法區是與Heap同級的)。

永久代也會參與垃圾回收,如果 JVM 發現不再需要這些類并且其他類可能需要空間,則這些類可能會被收集(解除安裝)。

與Heap分區對應的GC算法

優秀的個人文章:圖解 Java 垃圾回收算法及詳細過程
  • 新生代取整體采用的是标記+複制清理法:

Eden GC 後将幸存者複制到S0,Eden再次GC後 如果S0滿了,則将 Eden存活的與S0存活的一起複制到S1區域,同時清空Eden與S0區域。

建立對象通常量比較小,是以Eden小型GC開銷相對較小,采用複制清理可以釋放出整塊的記憶體空間,避免記憶體碎片化。

深入了解JVM運作機制與GC機制
  • 老年代采取标記+壓縮算法(或者叫标記+整理更好了解)

在回收完被标記的對象後後産生較多的碎片記憶體,導緻無法釋放出整塊的記憶體用于建立大的對象。此時采取壓縮算法對存活的對象進行重排,釋放出整塊的記憶體區域。

開銷:當記憶體中存活對象多,并且都是一些微小對象,而垃圾對象少時,要移動大量的存活對象才能換取少量的記憶體空間,進而引發頻繁的GC,進而造成應用卡頓。

深入了解JVM運作機制與GC機制

到這裡,此次對JVM知識的回顧就結束了,一共花了整整兩天時間。相比較上一次這次資訊視野變大了很多,檢視了更多oracle的官方文檔,從原始資料上獲得了更多更深入的了解,配合一起的筆記,課程吸收更快,發現了不少以前模棱兩可的了解。

這種從裡往外學習技術本質的感覺着實讓人上瘾!就像武俠小說裡的進階心法一樣,往往是心法修煉到一定級别就會進入快速上升期,随之對外在招式也是一看就會。

另一個體會,英語已經逐漸成為我的學習的明顯阻礙了,直接讀英文了解會更到位,但是由于各種長句潛逃與生詞讀起來很慢。閱讀機翻的話往往會感到每個字都認識但就是看的不是特别明白,又不得不重新讀原文。

等新工作定好,要在英語學習上加大力了。其實自信心還是很足的,我從來不會覺得我學英語有困難,在Tandem上面經常跟老外聊日常是遠遠不足的,一個是因為時差等因素不能保證時長,其次是聊天的品質也不太能保證,想來想去還是配合GPT來做定時學習吧。

作者:橘子沒

連結:https://juejin.cn/post/7217054630295371813

來源:稀土掘金

繼續閱讀