文章目錄
- 📕1. JVM簡介
- 📕2. JVM運行流程
- 📕3. JVM運行時數據區
- 📕4. JVM類加載
- ??4.1 類加載過程
- ??4.2 雙親委派模型
- ??4.3 破壞雙親委派模型
- 📕5. JVM垃圾回收機制(GC機制)
- ??5.1 判斷死亡對象的算法
- ??5.2 垃圾回收算法
📕1. JVM簡介
JVM 是 Java Virtual Machine 的簡稱,意為 Java虛擬機。 虛擬機是指通過軟件模擬的具有完整硬件功能的、運?在?個完全隔離的環境中的完整計算機系統。JVM 是?臺被定制過的現實當中不存在的計算機。
📕2. JVM運行流程
JVM 是 Java 運?的基礎,也是實現?次編譯到處執?的關鍵,那么 JVM 是如何執?的呢?
程序在執?之前先要把java代碼轉換成字節碼(class?件),JVM ?先需要把字節碼通過?定的?式把?件加載到內存中,?字節碼?件是 JVM 的?套指令集規范,并不能直接交個底層操作系統去執?,因此需要特定的命令解析器將字節碼翻譯成底層系統指令再交由CPU去執?,?這個過程中需要調?其他語?的接?來實現整個程序的功能,這就是這4個主要組成部分的職責與功能。
總結來看, JVM 主要通過分為以下 4 個部分,來執? Java 程序的,它們分別是:
- 類加載器(ClassLoader)
- 運?時數據區(Runtime Data Area)
- 執?引擎(Execution Engine)
- 本地庫接?(Native Interface)
📕3. JVM運行時數據區
JVM 運?時數據區域也叫內存布局,但需要注意的是它和 Java 內存模型((Java Memory Model,簡稱 JMM)完全不同,屬于完全不同的兩個概念,它由以下 5 ?部分組成:
🌈 1. 堆
堆的作?:程序中創建的所有對象都在保存在堆中(除了對象的實例屬性外還有對象的頭信息)
堆??分為兩個區域:新?代和??代,新?代放新建的對象,當經過?定 GC 次數之后還存活的對象會放???代。新?代還有 3 個區域:?個 Eden + 兩個 Survivor(S0/S1)。
垃圾回收的時候會將 Eden 中存活的對象放到?個未使?的 Survivor 中,并把當前的 Endn 和正在使?的Survivor 清除。
🌈 2. 虛擬機棧
Java 虛擬機棧的作?:Java 虛擬機棧的?命周期和線程相同,Java 虛擬機棧描述的是 Java ?法執?的內存模型:每個?法在執?的同時都會創建?個棧幀(Stack Frame)?于存儲局部變量表、操作數棧、動態鏈接、?法出?等信息。咱們常說的堆內存、棧內存中,棧內存指的就是虛擬機棧。
通俗來說,棧幀里面包括方法的參數,方法中的局部變量,方法結束后返回值結果,方法結束后跳轉回的地址。
🌈 3. 線程計數器
程序計數器的作?:描述Java程序要運行的下一個字節碼的位置。
程序計數器是?塊?較?的內存空間,可以看做是當前線程所執?的字節碼的?號指?器。
如果當前線程正在執?的是?個Java?法,這個計數器記錄的是正在執?的虛擬機字節碼指令的地址;如果正在執?的是?個Native?法,這個計數器值為空。
🌈 4. 元數據區(Java8之前被稱為方法區)
我們在Java代碼中會創建類,基于類創建對象,創建的對象會有內存空間進行保存,同理類也要有內存空間進行保存。Java中提出了類對象的概念,通過一個特殊的對象,表示一個類的基本信息。在元數據區中,存放的就是類的對應指令。
在元數據區中會保存:
1.類的名字是啥
2.類繼承的父類是啥
3.實現的接口是啥
4.有啥屬性(屬性的名字,類型,限定修飾符)
5.有啥方法(方法的名字,參數列表,返回值,限定修飾符)
6.類靜態成員
7.靜態常量
元數據區的內容Java代碼干預不了,在Java代碼中寫多少類,元數據區的內容就確定了。
🌈 5. 總結
📕4. JVM類加載
類加載是JVM從最開始讀取的 .class文件,到最終構造完成類對象的整個過程。是把“類”從硬盤上加載到內存中。
??4.1 類加載過程
對于?個類來說,它的?命周期是這樣的:
其中前 5 步是固定的順序并且也是類加載的過程,其中中間的 3 步我們都屬于連接,所以對于類加載來說總共分為以下?個步驟:
- 🌈加載
在加載 Loading 階段,Java虛擬機需要完成以下三件事情:
1.通過?個類的全限定類名(包名+類名)來獲取定義此類的?進制字節流
2.將這個字節流所代表的靜態存儲結構轉化為方法區的運?時數據結構。
3.在內存中?成?個代表這個類的Class對象,作為?法區這個類的各種數據的訪問??
- 🌈驗證
驗證是連接階段的第?步,校驗上一步讀出來的.class文件是否是合法的。 這?階段的?的是確保Class?件的字節流中包含的信息符合《Java虛擬機規范》的全部約束要求,保證這些信息被當作代碼運?后不會危害虛擬機??的安全。如果驗證過程中發現某個地方的格式出現問題,就需要及時報錯,告知給程序員。
- 🌈準備
準備階段是正式為類中定義的變量(即靜態變量,被static修飾的變量)分配內存并設置類變量初始值的階段。此處申請的空間是一個“未初始化”的空間,空間上的每個字節都是0。
//比如此時有這樣一行代碼:
private static int value = 123;
//此時value的值是0,而不是123
- 🌈解析
解析過程是針對代碼中的常量進行初始化,也就是把.class文件中的常量也加載到內存中。
對于上述代碼中value,此時value的值就是123了。
- 🌈初始化
初始化階段,Java 虛擬機真正開始執?類中編寫的 Java 程序代碼,執?類構造器?法。
??4.2 雙親委派模型
雙親委派模型是用于類加載過程中的第一個環節,根據類的全限定類名(包名+類名)找到對應的.class文件。
🚩什么是雙親委派模型?
如果?個類加載器收到了類加載的請求,它?先不會??去嘗試加載這個類,?是把這個請求委派給?類加載器(每一個類加載器都有一個parent屬性,記錄了自己的父親是誰,這個是無法篡改的,寫死在JVM源碼的)去完成,每?個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到最頂層的啟動類加載器中,只有當?加載器反饋???法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,?加載器才會嘗試??去完成加載。
JVM自帶了三種類加載器:
- Bootstrap ClassLoader :啟動類加載器,負責在Java的標準庫中進行查找
- Extension ClassLoader:擴展類加載器,負責在Java的擴展庫中進行查找
- Application ClassLoader:應用程序類加載器,負責在Java的第三方庫或者項目中進行查找
🚩雙親委派模型的優點:
- 避免重復加載類:比如 A 類和 B 類都有?個?類 C 類,那么當 A 啟動時就會將 C 類加載起來,那么在 B 類進?加載時就不需要在重復加載 C 類了。
- 安全性:使?雙親委派模型也可以保證了 Java 的核? API 不被篡改。比如我自己寫了一個String類,和標準庫的String類正好重復了,此時JVM加載的仍然是標準庫的String類,而不是我寫的String類。保證了安全性。
??4.3 破壞雙親委派模型
雙親委派模型是可以被打破的,程序員在特定的條件下可以實現自己的類加載器,自己實現的類加載器可以讓它遵守雙親委派模型,也可以不遵守雙親委派模型。這種情況會在編寫庫或者框架類代碼時出現,平時我們寫業務代碼并不會遇見。
📕5. JVM垃圾回收機制(GC機制)
上面我們介紹了JVM運行時的數據區,對于程序計數器,虛擬機棧,本地方法棧的生命周期與其對應的線程有關,隨著線程的結束進而銷毀。因此GC主要是回收堆上的對象。在 Java 中,所有的對象都是要存在內存中的(也可以說內存中存儲的是?個個對象),因此我們將內存回收,也可以叫做死亡對象的回收。GC回收并不是以字節為單位進行回收,而是以對象為單位進行回收。
??5.1 判斷死亡對象的算法
- 🚩 引用計數算法
給對象增加?個引?計數器,每當有?個地?引?它時,計數器就+1;當引?失效時,計數器就-1;
任何時刻計數器為0的對象就是不能再被使?的,即對象已"死"。
引?計數法實現簡單,判定效率也?較?,在?部分情況下都是?個不錯的算法。?如Python語?就
采?引?計數法進?內存管理。
但是,引入計數算法有兩個弊端:
a) 消耗額外的空間大 : 假如對象只有4字節,計數器有2字節,額外浪費50%的內存空間,造成內存浪費。
b) 循環引用問題 :
- 🚩可達性分析算法(Java所使用的方案)
此算法的核?思想為 : 通過?系列稱為"GC Roots"的對象作為起始點,從這些節點開始向下搜索,搜
索?過的路徑稱之為"引?鏈",當?個對象到GC Roots沒有任何的引?鏈相連時(從GC Roots到這個對
象不可達)時,證明此對象是不可?的。以下圖為例:
對象Object5-Object7之間雖然彼此還有關聯,但是它們到GC Roots是不可達的,因此他們會被判定為可回收對象。
在Java語?中,可作為GC Roots的對象包含下??種:
- 虛擬機棧(棧幀中的本地變量表)中引?的對象
- ?法區中類靜態屬性引?的對象
- ?法區中常量引?的對象
- 本地?法棧中 JNI(Native?法)引?的對象
??5.2 垃圾回收算法
- 🚩標記-清除算法
“標記-清除"算法是最基礎的收集算法。算法分為"標記"和"清除"兩個階段 : ?先標記出所有需要回收的對象,在標記完成后統?回收所有被標記的對象。后續的收集算法都是基于這種思路并對其不?加以改進?已。
"標記-清除"算法的不?主要有兩個 :
- 效率問題:標記(標記就是可達性分析找到碎片的過程)和清除(直接釋放這部分的內存)這兩個過程的效率都不?
- 空間問題:標記清除后會產??量不連續的內存碎?,空間碎?太多可能會導致以后在程序運?中需要分配較?對象時,?法找到?夠連續內存?不得不提前觸發另?次垃圾收集。
- 🚩復制算法
"復制"算法是為了解決"標記-清理"的效率問題。它將可?內存按容量劃分為??相等的兩塊,每次只使?其中的?塊。當這塊內存需要進?垃圾回收時,會將此區域還存活著的對象復制到另?塊上?,然后再把已經使?過的內存區域?次清理掉。這樣做的好處是每次都是對整個半區進?內存回收,內存分配時也就不需要考慮內存碎?等復雜情況,只需要移動堆頂指針,按順序分配即可。此算法實現簡單,運??效。算法的執?流程如下圖 :
復制算法很好的解決了內存碎片問題,但仍然存在兩個弊端:
- 內存浪費比較多
- 如果存活的對象比較多,復制的開銷會非常大
- 🚩標記-整理算法
這個方案類似于順序表刪除元素(我們會采用搬運元素的方法),這個方案雖然能解決內存碎片問題,也能避免復制算法的內存浪費,但是,搬運對象的成本也是比較高的。
- 🚩分代算法
分代算法和上?講的 3 種算法不同,分代算法是通過區域劃分,實現不同區域和不同的垃圾回收策略,從?實現更好的垃圾回收。這就好?中國的?國兩制?針?樣,對于不同的情況和地域設置更符合當地的規則,從?實現更好的管理,這就是分代算法的設計思想。
當前 JVM 垃圾收集都采?的是"分代收集(Generational Collection)"算法,這個算法并沒有新思想,只是根據對象存活周期的不同將內存劃分為?塊。?般是把Java堆分為新?代和?年代。在新?代中,每次垃圾回收都有?批對象死去,只有少量存活,因此我們采?復制算法;??年代中對象存活率?、沒有額外空間對它進?分配擔保,就必須采?"標記-清理"或者"標記-整理"算法
我們將整個堆空間,分為新生代和老生代。新?代中98%的對象都是"朝??死"的,所以并不需要按照1 : 1的?例來劃分內存空間,?是將內存(新?代內存)分為?塊較?的Eden(伊甸園)空間和兩塊較?的Survivor(幸存者)空間,每次使?Eden和其中?塊Survivor(兩個Survivor區域?個稱為From區,另?個稱為To區域)。當回收時,將Eden和Survivor中還存活的對象?次性復制到另?塊Survivor空間上,最后清理掉Eden和剛才?過的Survivor空間。當Survivor空間不夠?時,需要依賴其他內存(?年代)進?分配擔保。
HotSpot默認Eden與Survivor的???例是8 : 1,也就是說Eden:Survivor From : Survivor To =8:1:1
HotSpot實現的復制算法流程如下:
- 當Eden區滿的時候,會觸發第?次Minor gc,把還活著的對象拷?到Survivor From區;當Eden區再
次觸發Minor gc的時候,會掃描Eden區和From區域,對兩個區域進?垃圾回收,經過這次回收后還存
活的對象,則直接復制到To區域,并將Eden和From區域清空。 - 當后續Eden?發?Minor gc的時候,會對Eden和To區域進?垃圾回收,存活的對象復制到From區域,
并將Eden和To區域清空。 - 部分對象會在From和To區域中復制來復制去,如此交換15次(由JVM參數MaxTenuringThreshold決
定,這個參數默認是15),最終如果還是存活,就存?到?年代
哪些對象會進?新?代?哪些對象會進??年代?
? 新?代:?般創建的對象都會進?新?代;
? ?年代:?對象和經歷了 N 次(?般情況默認是 15 次)垃圾回收依然存活下來的對象會從新?代
移動到?年代。
🌰舉個例子:
當我們投簡歷時,簡歷會進入伊甸區,此時會收到非常多的簡歷。但是只有少數簡歷會經過挑選進入面試(從伊甸區進去幸存區),每一輪面試都會淘汰掉很多人,每次進入一輪面試就相當于存活過一輪GC,進入幸存區,當我們經過所有的面試拿到offer進入公司,就意味著我們進入了老生代。但是進入公司后也會有年終考核之類的評比,不過是考核時間長了。但是也有一類人能量非常強大,不用面試直接就能進入公司入職。