Java 基本功03-JVM-內存結構和類加載原理機制

JVM

1. 什麼是 JVM

1.1 JVM(Java Virtual Machine,Java 虛擬機)

2. JVM 的組成

2.1 JVM 的組成分爲兩種:總體組成部分和運行時數據區組成部分

2.2 JVM 總體組成:

JVM 的總體組成可分爲如下四個部分:
	1、類加載器(ClassLodaer)
	2、運行時數據區(Runtime Data Area)
	3、執行引擎(Execution Engine)
	4、本地庫接口(Native interface) 各個組成部分的: -程序在執行以前先要把 Java 代碼轉換成字節碼文件(class 文件),JVM 首先須要把字節碼經過必定的方式 類加載器(ClassLoader)把文件加載到內存中 運行時數據區(Runtime Data Area),而字節碼文件是 JVM 的一套指定集規範,並不能直接交給底層操做系統去執行,所以須要特定的命令解析器 執行引擎(Execution Engine)將字節碼翻譯成底層系統指令再交由 CPU 去執行,而這個過程須要調用其餘語言的接口 本地庫接口(Native interface)來實現整個程序的功能 -這就是這4個主要組成部分的職責與功能 複製代碼

2.3 運行時數據區組成:

JVM 的運行時數據區,不一樣虛擬機實現可能略微有所不一樣,但都會聽從 Java 虛擬機規範,JDK 1.8 虛擬機規範規定,Java 虛擬機所管理的內存將包括如下幾個運行時數據區域:
	1、程序計數器(Program Counter Register)
	2、Java 虛擬機棧(Java Virtual Machine Stacks)
	3、本地方法棧(Native Method Stack)
	4、Java 堆(Java Heap)
	5、方法區(Methed Area)
	
接下來咱們分別介紹每一個區域的用途:
	1、Java 程序計數器
		程序計數器(Program Counter Register)是一塊較小的內存空間,它能夠看做是當前線程所執行的字節碼的信號指示器。在虛擬機的概念模型裏,字節碼解析器的工做是經過改變這個計數器的值來選取下一條須要執行的字節碼指令、分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。
		
		特性:內存私有。
			因爲 JVM 的多線程是經過線程輪流切換並分配處理器執行時間的方式來實現的,也就是任什麼時候刻,一個處理器(或者說一個內核)都只會執行一條線程中的指令。所以爲了線程切換後能恢復到正確的執行位置,每一個線程都有獨立的程序計數器。
		
		異常規定:無
			若是線程正在執行 Java 中的方法,程序計數器記錄的就是正在執行虛擬機字節碼指令的地址,若是是 Native 方法,這個計數器就位空(undefined),所以該內存區域是惟一一個在 Java 虛擬機規範中沒有規定 OutOfMemoryError 的區域。
		
	2、Java 虛擬機棧
		Java 虛擬機棧(Java Virtual Machine Stacks)描述的是 Java 方法執行的內存模型,每一個方法在執行的同時都會建立一個線幀(Stack Frame)用於存儲局部變量表、操做數棧、動態連接、方法出口等信息,每一個方法從調用直至執行完成的過程,都對應着一個線幀在虛擬機棧中入棧到出棧的過程。
		
		特性:內存私有,他的生命週期和線程相同。
		
		異常規定:StackOverflowError、OutOfMemoryError
			-若是線程請求的棧深度大於虛擬機所容許的棧深度就會拋出 StackOverFlow 異常
			-若是虛擬機是能夠動態擴展的,若是擴展時沒法申請到足夠的內存就會拋出 OutOfMemoryError 異常
		
	3、本地方法棧
		-本地方法棧(Native Method Stack)與虛擬機棧的做用是同樣的,只不過虛擬機棧是服務 Java 方法的,而本地方法棧是爲虛擬機調用的 Native 方法服務的
		-在 Java 虛擬機規範中對於本地方法棧沒有特殊的要求,虛擬機能夠自由的實現它,所以在 SunHotSpot 虛擬機直接把本地方法棧和虛擬機棧合二爲一了
		
		特性和異常:同虛擬機棧,請參考上面知識點。
		
	4、Java 堆
		Java 堆(Java Heap)是 Java 虛擬機中內存最大的一塊,是由年輕代和老年代組成,而年輕代又被分爲三部分,Eden 空間、From Survivor 空間、To Survivor 空間,是被全部線程共享的,在虛擬機啓動的時候建立,Java 堆惟一的目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存,隨着 JIT(Just-In-Time Compiler,即時編譯器) 編譯器的發展和逃逸分析技術的逐漸成熟,棧上分配、標量替換優化的技術會致使一些微妙的變化,全部的對象分都分配在堆上漸漸變得不那麼"絕對"了。
		
		特性:內存共享
		
		異常規定:OutOfMemoryError
			-若是在堆中沒有內存完成實例分配,而且堆不能夠擴展時,將會拋出 OutOfMemoryError
			-Java 虛擬機規範規定,Java 堆能夠處理在物理上不連續的內存空間中,只要邏輯上連續便可,就像咱們的磁盤空間同樣。在實現上也能夠是固定大小的,也能夠是可擴展的,不過當前主流的虛擬機都是克擴展的,經過 -Xmx 和 -Xms 控制。
			
	5、方法區
		方法區(Method Area)用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯後的代碼等數據
		
		誤區:方法區不等於永生代。
			不少人把方法區稱做"永生代"(Permanent Generation),本質上二者並不等價,只是 HotSpot 虛擬機垃圾回收器團隊把 GC 分代收集擴展到了方法區,或者說是用來永久代來實現方法區而已,這樣能省去專門爲方法區編寫內存管理的代碼,可是在 JDK1.8 也移除了"永久代",使用 Native Memory 來實現方區。
			
		特性:內存共享
		
		異常規定:OutOfMemoryError
			當方法沒法知足內存分配時會拋出 OutOfMemoryError 異常。
複製代碼

3. JVM 內存結構示意圖

4. 類加載原理機制

4.1 類加載過程圖解(類的生命週期):

4.2 類加載詳解

4.2.1 類的加載過程:
類的加載指的是將類的 .class 文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,而後在堆區建立一個 java.lang.Class 對象,用來封裝類在方法區內的數據結構。類的加載的最終產品是位於堆區中的 Class 對象,Class 對象封裝了類在方法區的數據結構,而且向 Java 程序員提供了訪問方法區內的數據結構的接口。

JVM 將類的加載過程分爲三個步驟:裝載(Load)、連接(Link)和初始化(Initialize)而鏈接又分爲三個步驟如上圖所示:
	1、裝載(Load):
		-查找並加載類的二進制數據
		
	2、連接(Link):
		-驗證:確保被加載類的正確性
		-準備:爲類中的符號引用轉換爲直接引用
		-解析:把類中的符號引用轉換爲直接引用
		
	3、初始化(Initialize):
		-爲類的靜態變量賦予正確的初始值
		
		-爲何咱們要有驗證這一步呢?首先若是由編譯器生成的 class 文件,它確定符合 JVM 字節碼格式的,可是萬一有高手本身寫了一個 class 文件,讓 JVM 加載運行,用於惡意用途,就不妙了,所以這個 class 文件要先過驗證這一關,不符合的話不會讓它繼續執行,爲了安全考慮 -準備階段和初始化階段看似有些矛盾,實際上是不矛盾的,若是類中有語句:private static int a = 10,它的執行過程是這樣的,首先字節碼文件被加載到內存後,先進行連接的驗證這一步驟,驗證經過後準備階段,給 a 分配內存,由於變量 a 是 static 的,因此此時 a 等於 int 類型的默認初始值 0,即 a = 0,而後到解析,到初始化這一步驟時,才把 a 真正的值 10 賦給 a,此時 a = 10
複製代碼
4.2.2 類的初始化:
類何時才被初始化:
	1、建立類的實例,也就是 new 一個對象
	2、訪問某個類或接口的靜態變量,或者對該靜態變量賦值
	3、調用類的靜態方法
	4、反射(Class.forName("com.gkedu.load"))
	5、初始化一個類的子類(會首先初始化子類的父類)
	6、JVM 啓動時標明的啓動類,即文件名和類名相同的那個類
	只有這 6 種狀況纔會致使類的初始化。
	
類的初始化步驟:
	1、若是這個類尚未被加載和連接,那麼先進行加載和連接
	2、假如這個類存在直接父類,而且這個類還沒被初始化(注意:在一個類加載器中,類只能初始化一次),那就初始化直接的父類(不適用於接口)
	3、加入類中存在初始化語句(如 static 變量和 static 塊),那就依次執行這些初始化語句
複製代碼
4.2.3 類的加載:
類的加載指的是將類的 .class 文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,而後將在堆內建立一個這個類的 Java.lang.Class 對象,用來封裝類在方法區的類的對象。看下圖:
複製代碼

類的加載的最終的產品是位於堆區中的 Class 對象。

Class 對象封裝了類在方法區的內存 數據結構,而且向 Java 程序員提供了訪問方法區的數據結構接口。

加載 .class 文件的幾種方式:
	1、從本地系統直接加載。
	2、經過網絡下載 .class 文件。
	3、從 zip jar 等歸檔案文件中加載 .class 文件。
	4、從專有 數據庫 中提取 .class 文件。
	5、將 Java 源文件動態編譯爲 .class 文件(服務器)。
  
加載類的方式:
	1、命令行啓動應用時候由JVM初始化加載
	2、經過Class.forName()方法動態加載
	3、經過ClassLoader.loadClass()方法動態加載
	
Class.forName()和ClassLoader.loadClass()的區別:
	1、Class.forName():將類的 .class 文件加載到 JVM 中以外,還會對類進行解釋,執行類中的 static 塊;
	2、ClassLoader.loadClass():只幹一件事情,就是將 .class 文件加載到 JVM 中,不會執行 static 中的內容,只有在 newInstance 纔會執行 static 塊;
	3、Class.forName(name, initialize, loader):帶參函數也能夠控制是否加載 static 塊。而且只有調用了 newInstance()方法採用調用構造函數,建立類的對象。
複製代碼
4.2.4 加載器:
JVM 的類加載是經過 ClassLoader 及子類來完成的,類的層次關係和加載順序能夠由下圖來描述:
複製代碼

1、Bootstrap ClassLoader(啓動類加載器):
	負責加載 $JAVA_HOME 中 jre/lib/rt.jar 裏全部的 class,由 C++ 實現,不是 ClassLoader 子類。 二、Extension ClassLoader(擴展類加載器): 負責加載 Java 平臺中擴展功能的一些 jar 包,包括 $JAVA_HOMEjre/lib/*.jar 或 -Djava.ext.dirs 指定目錄下的 jar 包。 三、App ClassLodaer(應用類加載器): 負責記載 classpath 中指定的 jar 包及目錄中 class。 四、Custom ClassLoader(自定義類加載器): 屬於應用程序根據自身須要定義的 ClassLoader,如 tomcatjboss 都會根據 j2ee 規範自行實現 ClassLoader。 加載過程當中會先檢查類是否被已加載,檢查順序是自底向上,從 Custom ClassLoaderBootStrap ClassLoader 逐層檢查,只要某個 ClassLoader 已加載就視爲已加載此類,保證此類只被全部 ClassLoader 加載一次。而加載的順序是自頂向下,也就是由上層來逐層嘗試加載此類。 複製代碼
4.2.5 委派機制:
JVM 在加載類時默認採用的是 雙親委派 機制。通俗的講,就是某個特定的類加載器在接到加載類的請求時,首先將加載任務委託給父類加載器,依次遞歸,若是父類加載器徹底能夠完成類加載任務,就成功返回;只有父類加載器沒法完成此加載任務時,才本身去加載。
	
雙親委派機制:
	1、當 AppClassLoader 加載一個 class 時,它首先不會本身去嘗試加載這個類,而是把類加載請求委派給父類加載器 ExtClassLoader 去完成。 二、當 ExtClassLoader 加載一個 class 時,它首先也不會本身去嘗試加載這個類,而是把類加載請求委派給 BootStrapClassLoader 去完成。 三、若是 BootStrapClassLoader 加載失敗(例如在 $JAVA_HOME/jre/lib 裏未查找到該 class),會使用 ExtClassLoader 來嘗試加載。 四、若 ExtClassLoader 也加載失敗,則會使用 AppClassLoader 來加載,若是 AppClassLoader 也加載失敗,則會報出異常 ClassNotFoundException複製代碼
相關文章
相關標籤/搜索