1. 程式人生 > >Java JVM 執行機制及基本原理

Java JVM 執行機制及基本原理

將知識用文字記錄下來,供以後溫故知新。

JVM的基礎概念

JVM的中文名稱叫Java虛擬機器,它是由軟體技術模擬出計算機執行的一個虛擬的計算機。

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

我們都知道Java的程式需要經過編譯後,產生.Class檔案,JVM才能識別並執行它,JVM針對每個作業系統開發其對應的直譯器,所以只要其作業系統有對應版本的JVM,那麼這份Java編譯後的程式碼就能夠執行起來,這就是Java能一次編譯,到處執行的原因。

JVM的生命週期

JVM在Java程式開始執行的時候,它才執行,程式結束的時它就停止。

一個Java程式會開啟一個JVM程序,如果一臺機器上執行三個程式,那麼就會有三個執行中的JVM程序。

JVM中的執行緒分為兩種:守護執行緒和普通執行緒

守護執行緒是JVM自己使用的執行緒,比如垃圾回收(GC)就是一個守護執行緒。

普通執行緒一般是Java程式的執行緒,只要JVM中有普通執行緒在執行,那麼JVM就不會停止。

許可權足夠的話,可以呼叫exit()方法終止程式。

JVM的結構體系

JVM的啟動過程

1、JVM的裝入環境和配置

在學習這個之前,我們需要了解一件事情,就是JDK和JRE的區別。

JDK是面向開發人員使用的SDK,它提供了Java的開發環境和執行環境,JDK中包含了JRE。

JRE是Java的執行環境,是面向所有Java程式的使用者,包括開發者。

JRE = 執行環境 = JVM。

如果安裝了JDK,會發現電腦中有兩套JRE,一套位於/Java/jre.../下,一套位於/Java/jdk.../jre下。那麼問題來了,一臺機器上有兩套以上JRE,誰來決定執行那一套呢?這個任務就落到java.exe身上,java.exe的任務就是找到合適的JRE來執行java程式。

java.exe按照以下的順序來選擇JRE:

1、自己目錄下有沒有JRE

2、父目錄下有沒有JRE

3、查詢登錄檔: HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Java Runtime Environment\"當前JRE版本號"\JavaHome

這幾步的主要核心是為了找到JVM的絕對路徑。

jvm.cfg的路徑為:JRE路徑\lib\"CPU架構"\jvm.fig

jvm.cfg的內容大致如下:

-client KNOWN  -server KNOWN  -hotspot ALIASED_TO -client  -classic WARN  -native ERROR  -green ERROR  KNOWN 表示存在 、IGNORE 表示不存在 、ALIASED_TO 表示給別的JVM去一個別名 WARN 表示不存在時找一個替代 、ERROR 表示不存在丟擲異常 2、裝載JVM

通過第一步找到JVM的路徑後,Java.exe通過LoadJavaVM來裝入JVM檔案。 LoadLibrary裝載JVM動態連線庫,然後把JVM中的到處函式JNI_CreateJavaVM和JNI_GetDefaultJavaVMIntArgs 掛接到InvocationFunction 變數的CreateJavaVM和GetDafaultJavaVMInitArgs 函式指標變數上。JVM的裝載工作完成。 3、初始化JVM,獲得本地呼叫介面

呼叫InvocationFunction -> CreateJavaVM也就是JVM中JNI_CreateJavaVM方法獲得JNIEnv結構的例項。 4、執行Java程式

JVM執行Java程式的方式有兩種:jar包 與 Class 執行jar 的時候,Java.exe呼叫GetMainClassName函式,該函式先獲得JNIEnv例項然後呼叫JarFileJNIEnv類中getManifest(),從其返回的Manifest物件中取getAttrebutes("Main-Class")的值,即jar 包中檔案:META-INF/MANIFEST.MF指定的Main-Class的主類名作為執行的主類。之後main函式會呼叫Java.c中LoadClass方法裝載該主類(使用JNIEnv例項的FindClass)。 執行Class的時候,main函式直接呼叫Java.c中的LoadClass方法裝載該類。

Class檔案

Class檔案由Java編譯器生成,我們建立的.Java檔案在經過編譯器後,會變成.Class的檔案,這樣才能被JVM所識別並執行。

類載入子系統

類載入子系統也可以稱之為類載入器,JVM預設提供三個類載入器:

1、BootStrap ClassLoader :稱之為啟動類載入器,是最頂層的類載入器,負責載入JDK中的核心類庫,如 rt.jar、resources.jar、charsets.jar等。

2、Extension ClassLoader:稱之為擴充套件類載入器,負責載入Java的擴充套件類庫,預設載入$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs指定目錄下的jar包。

3、App ClassLoader:稱之為系統類載入器,負責載入應用程式classpath目錄下所有jar和class檔案。

除了Java預設提供的三個ClassLoader(載入器)之外,我們還可以根據自身需要自定義ClassLoader,自定義ClassLoader必須繼承java.lang.ClassLoader 類。除了BootStrap ClassLoader 之外的另外兩個預設載入器都是繼承自java.lang.ClassLoader 。BootStrap ClassLoader 不是一個普通的Java類,它底層由C++編寫,已嵌入到了JVM的核心當中,當JVM啟動後,BootStrap ClassLoader 也隨之啟動,負責載入完核心類庫後,並構造Extension ClassLoader 和App ClassLoader 類載入器。  

類載入器子系統不僅僅負責定位並載入類檔案,它還嚴格按照以下步驟做了很多事情:

1、載入:尋找並匯入Class檔案的二進位制資訊
2、連線:進行驗證、準備和解析
     1)驗證:確保匯入型別的正確性
     2)準備:為型別分配記憶體並初始化為預設值
     3)解析:將字元引用解析為直接引用
3、初始化:呼叫Java程式碼,初始化類變數為指定初始值

方法區(Method Area)

在JVM中,型別資訊和類靜態變數都儲存在方法區中,型別資訊是由類載入器在類載入的過程中從類檔案中提取出來的資訊。

需要注意的一點是,常量池也存放於方法區中。

程式中所有的執行緒共享一個方法區,所以訪問方法區的資訊必須確保執行緒是安全的。如果有兩個執行緒同時去載入一個類,那麼只能有一個執行緒被允許去載入這個類,另一個必須等待。

在程式執行時,方法區的大小是可以改變的,程式在執行時可以擴充套件。

方法區也可以被垃圾回收,但條件非常嚴苛,必須在該類沒有任何引用的情況下,詳情可以參考另一篇文章:Java效能優化之JVM GC(垃圾回收機制) - 知乎專欄

型別資訊包括什麼?

1、型別的全名(The fully qualified name of the type)

2、型別的父型別全名(除非沒有父型別,或者父型別是java.lang.Object)(The fully qualified name of the typeís direct superclass)

3、該型別是一個類還是介面(class or an interface)(Whether or not the type is a class )

4、型別的修飾符(public,private,protected,static,final,volatile,transient等)(The typeís modifiers)

5、所有父介面全名的列表(An ordered list of the fully qualified names of any direct superinterfaces)

6、型別的欄位資訊(Field information)

7、型別的方法資訊(Method information)

8、所有靜態類變數(非常量)資訊(All class (static) variables declared in the type, except constants)

9、一個指向類載入器的引用(A reference to class ClassLoader)

10、一個指向Class類的引用(A reference to class Class)

11、基本型別的常量池(The constant pool for the type)

方法列表(Method Tables)

為了更高效的訪問所有儲存在方法區中的資料,在方法區中,除了儲存上邊的這些型別資訊之外,還有一個為了加快存取速度而設計的資料結構:方法列表。每一個被載入的非抽象類,Java虛擬機器都會為他們產生一個方法列表,這個列表中儲存了這個類可能呼叫的所有例項方法的引用,儲存那些父類中呼叫的方法。

Java堆(JVM堆、Heap)

當Java建立一個類的例項物件或者陣列時,都在堆中為新的物件分配記憶體。

虛擬機器中只有一個堆,程式中所有的執行緒都共享它。

堆佔用的記憶體空間是最多的。

堆的存取型別為管道型別,先進先出。

在程式執行中,可以動態的分配堆的記憶體大小。

Java棧(JVM棧、Stack)

在Java棧中只儲存基礎資料型別(參考:Java 基本資料型別 - 四類八種 - 知乎專欄)和自定義物件的引用,注意只是物件的引用而不是物件本身哦,物件是儲存在堆區中的。

拓展知識:像String、Integer、Byte、Short、Long、Character、Boolean這六個屬於包裝型別,它們是存放於堆中的。

棧的存取型別為類似於水杯,先進後出。

棧內的資料在超出其作用域後,會被自動釋放掉,它不由JVM GC管理。

每一個執行緒都包含一個棧區,每個棧中的資料都是私有的,其他棧不能訪問。

每個執行緒都會建立一個操作棧,每個棧又包含了若干個棧幀,每個棧幀對應著每個方法的每次呼叫,每個棧幀包含了三部分:

區域性變數區(方法內基本型別變數、變數物件指標)

運算元棧區(存放方法執行過程中產生的中間結果)

執行環境區(動態連線、正確的方法返回相關資訊、異常捕捉)

本地方法棧

本地方法棧的功能和JVM棧非常類似,用於儲存本地方法的區域性變量表,本地方法的運算元棧等資訊。

棧的存取型別為類似於水杯,先進後出。

棧內的資料在超出其作用域後,會被自動釋放掉,它不由JVM GC管理。

每一個執行緒都包含一個棧區,每個棧中的資料都是私有的,其他棧不能訪問。

本地方法棧是在程式呼叫或JVM呼叫本地方法介面(Native)時候啟用。

本地方法都不是使用Java語言編寫的,比如使用C語言編寫的本地方法,本地方法也不由JVM去執行,所以本地方法的執行不受JVM管理。

HotSpot VM將本地方法棧和JVM棧合併了。

程式計數器

在JVM的概念模型裡,位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令。分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。

JVM的多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式來實現的,為了各條執行緒之間的切換後計數器能恢復到正確的執行位置,所以每條執行緒都會有一個獨立的程式計數器。

程式計數器僅佔很小的一塊記憶體空間。

當執行緒正在執行一個Java方法,程式計數器記錄的是正在執行的JVM位元組碼指令的地址。如果正在執行的是一個Natvie(本地方法),那麼這個計數器的值則為空(Underfined)。

程式計數器這個記憶體區域是唯一一個在JVM規範中沒有規定任何OutOfMemoryError(記憶體不足錯誤)的區域。

JVM執行引擎

Java虛擬機器相當於一臺虛擬的“物理機”,這兩種機器都有程式碼執行能力,其區別主要是物理機的執行引擎是直接建立在處理器、硬體、指令集和作業系統層面上的。而JVM的執行引擎是自己實現的,因此程式設計師可以自行制定指令集和執行引擎的結構體系,因此能夠執行那些不被硬體直接支援的指令集格式。

在JVM規範中制定了虛擬機器位元組碼執行引擎的概念模型,這個模型稱之為JVM執行引擎的統一外觀。JVM實現中,可能會有兩種的執行方式:解釋執行(通過直譯器執行)和編譯執行(通過即時編譯器產生原生代碼)。有些虛擬機器只採用一種執行方式,有些則可能同時採用兩種,甚至有可能包含幾個不同級別的編譯器執行引擎。

輸入的是位元組碼檔案、處理過程是等效位元組碼解析過程、輸出的是執行結果。在這三點上每個JVM執行引擎都是一致的。

本地方法介面(JNI)

JNI是Java Native Interface的縮寫,它提供了若干的API實現了Java和其他語言的通訊(主要是C和C++)。

JNI的適用場景

當我們有一些舊的庫,已經使用C語言編寫好了,如果要移植到Java上來,非常浪費時間,而JNI可以支援Java程式與C語言編寫的庫進行互動,這樣就不必要進行移植了。或者是與硬體、作業系統進行互動、提高程式的效能等,都可以使用JNI。需要注意的一點是需要保證原生代碼能工作在任何Java虛擬機器環境。

JNI的副作用

一旦使用JNI,Java程式將丟失了Java平臺的兩個優點:

1、程式不再跨平臺,要想跨平臺,必須在不同的系統環境下程式編譯配置本地語言部分。

2、程式不再是絕對安全的,原生代碼的使用不當可能會導致整個程式崩潰。一個通用規則是,呼叫本地方法應該集中在少數的幾個類當中,這樣就降低了Java和其他語言之間的耦合。

JVM GC(垃圾回收機制)

JVM 常量池

JVM常量池也稱之為執行時常量池,它是方法區(Method Area)的一部分。用於存放編譯期間生成的各種字面量和符號引用。執行時常量池不要求一定只有在編譯器產生的才能進入,執行期間也可以將新的常量放入池中,這種特性被開發人員利用比較多的就是String.intern()方法。

由“用於存放編譯期間生成的各種字面量和符號引用”這句話可見,常量池中儲存的是物件的引用而不是物件的本身。

常量池的好處

常量池是為了避免頻繁的建立和銷燬物件而影響系統性能,它也實現了物件的共享。

例如字串常量池:在編譯階段就把所有字串文字放到一個常量池中。

1、節省記憶體空間:常量池中如果有對應的字串,那麼則返回該物件的引用,從而不必再次建立一個新物件。

2、節省執行時間:比較字串時,==比equals()快。對於兩個引用變數,==判斷引用是否相等,也就可以判斷實際值是否相等。

雙等號(==)的含義

基本資料型別之間使用雙等號,比較的是數值。

複合資料型別(類)之間使用雙等號,比較的是物件的引用地址是否相等。

八種基本型別的包裝類和常量池

Byte、Short、Integer、Long、Character、Boolean、String這7種包裝類都各自實現了自己的常量池。

  1. //例子:

  2. Integer i1 = 20;

  3. Integer i2 = 20;

  4. System.out.println(i1=i2);//輸出TRUE

Byte、Short、Integer、Long、Character這5種包裝類都預設建立了數值[-128 , 127]的快取資料。當對這5個型別的資料不在這個區間內的時候,將會去建立新的物件,並且不會將這些新的物件放入常量池中。

  1. //IntegerCache.low = -128

  2. //IntegerCache.high = 127

  3. public static Integer valueOf(int i) {

  4. if (i >= IntegerCache.low && i <= IntegerCache.high)

  5. return IntegerCache.cache[i + (-IntegerCache.low)];

  6. return new Integer(i);

  7. }

  1. //例子

  2. Integer i1 = 200;

  3. Integer i2 = 200;

  4. System.out.println(i1==i2);//返回FALSE

Float 和Double 沒有實現常量池。

String包裝類與常量池

String str1 = "aaa";

當以上程式碼執行時,JVM會到字串常量池查詢 "aaa" 這個字面量物件是否存在?

存在:則返回該物件的引用給變數 str1 。

不存在:則在堆中建立一個相應的物件,將建立的物件的引用存放到常量池中,同時將引用返回給變數 str1 。

  1. String str1 = "aaa";

  2. String str2 = "aaa";

  3. System.out.println(str1 == str2);//返回TRUE

因為變數str1 和str2 都指向同一個物件,所以返回true。

String str3 = new String("aaa");
System.out.println(str1 == str3);//返回FALSE

當我們使用了new來構造字串物件的時候,不管字串常量池中是否有相同內容的物件的引用,新的字串物件都會建立。因為兩個指向的是不同的物件,所以返回FALSE 。

String.intern()方法

對於使用了new 建立的字串物件,如果想要將這個物件引用到字串常量池,可以使用intern() 方法。

呼叫intern() 方法後,檢查字串常量池中是否有這個物件的引用,並做如下操作:

存在:直接返回物件引用給變數。

不存在:將這個物件引用加入到常量池,再返回物件引用給變數。

  1. String interns = str3.intern();

  2. System.out.println(interns == str1);//返回TRUE

假定常量池中都沒有以上字面量的物件,以下建立了多少個物件呢?

  1. String str4 = "abc"+"efg";

  2. String str5 = "abcefg";

  3. System.out.println(str4 == str5);//返回TRUE

答案是三個。第一個:"abc" ,第一個:"efg",第三個:"abc"+"efg"("abcefg")

String str5 = "abcefg"; 這句程式碼並沒有建立物件,它從常量池中找到了"abcefg" 的引用,所有str4 == str5 返回TRUE,因為它們都指向一個相同的物件。

什麼情況下會將字串物件引用自動加入字串常量池?

//只有在這兩種情況下會將物件引用自動加入到常量池
String str1 = "aaa";
String str2 = "aa"+"a";

//其他方式下都不會將物件引用自動加入到常量池,如下:
String str3 = new String("aaa");
String str4 = New StringBuilder("aa").append("a").toString();
StringBuilder sb = New StringBuilder();
sb.append("aa");
sb.append("a");
String str5 = sb.toString();

好了,JVM的基本原理就寫到這裡,以後更深入的瞭解後會再來補充。