1. 程式人生 > 其它 >JVM體系和類載入機制,快來複習一下吧!

JVM體系和類載入機制,快來複習一下吧!

技術標籤:javajvm面試後端

大家好,今天來聊聊老生常談的 JVM,這也是面試必問的知識。
我是狂聊君,可以微信搜一搜公眾號「 狂聊Java 」第一時間閱讀。

1、JVM 是什麼?

1、Java 虛擬機器(Jvm)是可執行 Java 程式碼的假想計算機。

2、Jvm 充當著一個翻譯官的角色,我們平常所編寫出的 Java 程式,是不能夠被作業系統所直接識別的,這時候 JVM 的作用就體現出來了,它負責把我們的程式翻譯給系統“聽”,告訴它我們的程式需要做什麼操作。

3、Jvm 針對每個作業系統開發其對應的直譯器,所以只要其作業系統有對應版本的 Jvm,那麼這份 Java 編譯後的程式碼就能夠執行起來,這句話大家一定聽說過:「Java 能一次編譯到處執行」

2、Jvm 的體系架構?

Jvm 是這四部分組成:

  • 執行區資料
  • 類載入器
  • 執行引擎
  • 垃圾回收器

下面就聊聊這四個部分~~

2.1 執行區資料

Java 虛擬機器在執行 Java 程式的過程中會把它所管理的記憶體劃分為若干個不同的資料區域,這些區域各有各的作用,各有各的生命週期。

有些區域隨著虛擬機器程序的啟動而存在,有些區域則依賴使用者執行緒的啟動和結束建立和銷燬。

執行區資料的劃分:方法區、虛擬機器棧,本地方法棧、堆、程式計數器

java虛擬機器執行區資料

上面這張圖大家一定都見過,其實可以劃分的更細點,看下面的這兩張圖:
java8之前
java8之後

能看出 1.8 版本前後的差別麼,下面就看看這些區域都幹啥的~~

程式計數器

其實你可以把它看作是當前執行緒執行的位元組碼的行號指示器,在 Jvm 工作時,就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈,跳轉,異常處理,執行緒的恢復等工作都需要依賴程式計數器去完成。它就好像是一個路口的紅綠燈一樣。

特點:1、佔用很小的記憶體 2、各執行緒私有

就比如下面位元組碼一樣,每一行開頭的綠色數字,我們就可以認為它是程式計數器所儲存的內容:

 public void doSth1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #5
         2: dup
         3: astore_1
         4: monitorenter
         5: getstatic     #2
8: ldc

虛擬機器棧

虛擬機器棧,其描述的就是執行緒記憶體模型,也可以稱作執行緒棧,也是每個執行緒私有的,生命週期與執行緒保持一致。在每個方法執行的時候,jvm 都會同步建立一個棧幀去儲存區域性變量表,運算元棧,動態連線,方法出口等資訊。一個方法的生命週期就貫徹了一個棧幀從入棧到出棧的全部過程。

特點:1、隨執行緒而生、隨執行緒而死 2、先進後出

棧示意圖:

棧

本地方法棧

本地方法棧,和虛擬棧其實很相似的,我們知道,java 底層用了很多 c 的程式碼去實現,而其呼叫 c 端的方法上都會有 native 來代表本地方法,而本地方法棧就是為其服務的。

特點:1、各執行緒私有 2、和本地方法有關

native 修飾的方法

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

  1. 堆可以說是 jvm 中最大的一塊記憶體區域了,它是所有執行緒共享,幾乎所有的物件例項都會在這裡分配。
  2. java 堆是垃圾回收器主要回收的區域。從記憶體回收的角度來說,堆空間可以分為新生代和老年代,而新生代又可以分為伊甸區,servivor 區。

特點:1、所有執行緒共享 2、佔用大的記憶體空間 3、先進先出

堆的劃分:
堆

方法區

  1. 方法區,也是各個執行緒共享的記憶體區域,它是用來被儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯後的程式碼等資料。
  2. 從上面的圖,可以看到 1.8 之前和之後,方法區所在的位置是有差別的。在 Java 8 之前有個永久代的概念,實際上指的是 HotSpot 虛擬機器上的永久代,它用永久代實現了 JVM 規範定義的方法區功能,這部分由於是在堆中實現的,受 GC 的管理,不過由於永久代有 -XX:MaxPermSize 的上限,所以如果大量地呼叫 String.intern 方法 (將欄位串放入永久代中的常量區)或 動態生成類(將類資訊放入永久代),很容易造成 OOM。
  3. 所以,在 Java 8 中就把方法區的實現移到了本地記憶體中的元空間中,這樣方法區就不受 JVM 的控制了,這個區域也就不會進行 GC,也因此提升了效能,正因為放到了本地記憶體,也就不存在由於永久代限制大小而導致的 OOM 異常了。
  4. 另外,執行時常量池也是方法區的一部分,用來存放編譯期生成的各種字面量和符號引用,這部分內容在類載入後進入該常量池中。

特點:1、所有執行緒共享 2、1.8 之後移到了元空間 3、涉及到常量池

直接記憶體

從上面的圖中,看到有直接記憶體這個區域

直接記憶體,並不是虛擬機器執行時資料區的一部分,其實可以理解為堆外記憶體,在一些場景下,比如:NIO 類引入了一種基於通道(Channel)與緩衝區(Buffer)的 IO 方式,它可以使用 Native 函式庫直接分配堆外記憶體,然後通過一個儲存 Java 堆中的物件作為這塊記憶體的引用,這樣能夠顯著提高效能,因為避免了 Java 堆和 Native 堆中來回複製資料。

2.2 類載入器

1、什麼是類載入機制?

JVM 執行時,java 虛擬機器會把描述類的資料從 Class 檔案載入到記憶體,並對資料進行校驗、轉換、解析和初始化,最終形成可以被 jvm 可以直接使用的型別,這就是類載入機制。

2、說說類載入的過程?

開局一張圖:

在這裡插入圖片描述

這張圖說明了類從載入到虛擬機器記憶體開始,到卸載出記憶體為止,它的整個生命週期。

一般來說,我們把 Java 的類載入過程分為三個主要步驟:載入、連結、初始化,具體行為在 Java 虛擬機器規範裡有非常詳細的定義。

1、首先是載入階段

  1. 它是 Java 將位元組碼(jar 包)資料從不同的資料來源讀取到 JVM 中,並對映為 JVM 認可的資料結構(Class 物件),
  2. 重點:載入階段是使用者參與的階段,我們可以自定義類載入器,去實現自己的類載入過程。
  3. 通過位元組流將類的.class 檔案中的二進位制資料讀入到記憶體。然後在堆中建立 java.lang.class 物件,用來封裝類在方法區的資料結構
  4. 只會建立一個 Class 物件,該 Class 物件來描述有哪些構造方法,都有哪些成員變數

2、第二階段是連結,這是核心的步驟,簡單說是把原始的類定義資訊平滑地轉化入 JVM 執行的過程中。這裡可進一步細分為三個步驟:

① 驗證

  1. 這是虛擬機器安全的重要保障,JVM 需要核驗位元組資訊是符合 Java 虛擬機器規範的,否則就被認為是 VerifyError,這樣就防止了惡意資訊或者不合規的資訊危害 JVM 的執行,
  2. 驗證階段有可能觸發更多 class 的載入。

② 準備

  1. 建立類或介面中的靜態變數,並初始化靜態變數的初始值。
  2. 但這裡的“初始化”和下面的顯式初始化階段是有區別的,
  3. 測重點在於分配需要的記憶體空間,不會去執行更進一步的 JVM 指令

這裡的初始化是指:

1、8 種基本資料型別的預設初始值是 0。

2、引用型別預設的初始值是 null。

3、對於有 static final 修飾的常量會直接賦值,例如:static final int x=123;則 x 直接會初始化為 123。

③ 解析

  1. 在這一步會將常量池中的符號引用(symbolic reference)替換為直接引用。
  2. 符號引用就是唯一的字串,直接引用可以理解為一個地址值和偏移量

在這裡插入圖片描述

3、最後是初始化階段

這一步真正去執行類初始化的程式碼邏輯,包括靜態欄位動作,以及執行類定定義中的靜態初始化塊內的邏輯編譯器在編譯階段就會把這部分邏輯整理好,父型別的初始化邏輯優先於當前型別的邏輯。

初始化順序:

  • 先是父類靜態域(靜態成員變數)或者靜態程式碼庫塊
  • 然後是子類靜態域或者子類靜態程式碼塊
  • 所以最先初始化的總是 java.lang.Object 類

3、什麼時候會對類進行初始化?

  1. 通過 new 關鍵字例項化物件、讀取或設定類的靜態變數、呼叫類的靜態方法
  2. 通過反射發生上面的三種行為
  3. 初始化子類時,會觸發父類的初始化
  4. 作為程式入口執行,就是指的 main 方法

4、類載入器有哪些?

  1. 啟動類載入器:負責載入環境變數下 jre/lib 下面的 jar 檔案
  2. 擴充套件類載入器:負責載入環境變數下 jre/lib/ext 目錄下面的 jar 包
  3. 應用類載入器:就是載入我們熟悉的 classpath 的內容
  4. 自定義載入器:繼承 ClassLoader 就可以實現

5、瞭解雙親委派模型嗎?

雙親委派模型

這是一張很經典的圖,通常情況下,各個類載入器的協作關係就是這樣的。

概念:就是說一個類載入器收到了類載入的請求,不會自己先載入,而是把它交給自己的父類去載入,層層迭代。

用上圖來說明就是如果應用程式類載入器收到了一個類載入的請求,會先給擴充套件類載入器,然後再給啟動類載入器,如果啟動類載入器無法完成這個類載入的請求,再返回給擴充套件類載入器,如果擴充套件類載入器也無法完成,最後才會到應用類載入器。

好處:1、避免重複載入 Java 型別 2、保證核心的類不會被篡改。

6、classLoader與class.forName區別

  • class.forName()除了將類的.class 檔案載入到 jvm 中之外,還會對類進行解釋,執行類中的 static 塊,當然你可以指定是否執行靜態塊。
  • classLoader 只幹一件事情,就是將.class 檔案載入到 jvm 中,不會執行 static 中的內容,只有在 newInstance 才會去執行 static 塊。

今天就寫到這裡了,給大家介了JVM、執行區資料、類載入機制。
希望大家面試前能掌握和Jvm有關的知識。

垃圾回收器下篇寫~~