本文參考自:https://www.cnblogs.com/lishun1005/p/6019678.html 和 https://blog.csdn.net/albenxie/article/details/70145603 http://www.cnblogs.com/CongLollipop/p/6665606.html
一、java
1.java是一種技術
說起Java,人們首先想到的是Java編程語言,然而事實上,Java是一種技術,它由四方面組成:Java編程語言、Java類文件格式、Java虛擬機和Java應用程序接口(Java API)。它們的關系如下圖所示:
運行期環境代表着Java平台,開發人員編寫Java代碼(.java文件),然后將之編譯成字節碼(.class文件),JVM才能識別並運行它,JVM針對每個操作系統開發其對應的解釋器,所以只要其操作系統有對應版本的JVM,那么這份Java編譯后的代碼就能夠運行起來,這就是Java能一次編譯,到處運行的原因。
JVM運行字節碼:字節碼被裝入內存,一旦字節碼進入虛擬機,它就會被解釋器解釋執行,或者是被即時代碼發生器有選擇的轉換成機器碼執行。
利用Java API編寫的應用程序(application) 和小程序(Java applet) 可以在任何Java平台上運行而無需考慮底層平台, 就是因為有Java虛擬機(JVM)屏蔽了與具體操作系統平台相關的信息,實現了程序與操作系統的分離,從而實現了Java 的平台無關性,使Java程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就可以在多種平台上不加修改地運行。JVM在執行字節碼時,實際上最終還是把字節碼解釋成具體平台上的機器指令執行。
2.java平台:
Java平台由Java虛擬機和Java應用程序接口搭建,Java語言則是進入這個平台的通道,用Java語言編寫並編譯的程序可以運行在這個平台上。這個平台的結構如下圖所示:
在Java平台的結構中, 可以看出,Java虛擬機(JVM) 處在核心的位置,是程序與底層操作系統和硬件無關的關鍵。它的下方是移植接口,移植接口由兩部分組成:適配器和Java操作系統, 其中依賴於平台的部分稱為適配器;JVM 通過移植接口在具體的平台和操作系統上實現;在JVM 的上方是Java的基本類庫和擴展類庫以及它們的API, 利用Java API編寫的應用程序(application) 和小程序(Java applet) 可以在任何Java平台上運行而無需考慮底層平台, 就是因為有Java虛擬機(JVM)實現了程序與操作系統的分離,從而實現了Java 的平台無關性。
二、JVM
1.JVM概念:
JVM即 Java Virtual Machine ,Java虛擬機。它是由軟件技術模擬出計算機運行的一個虛擬的計算機。
Java虛擬機主要由字節碼指令集、寄存器、棧、垃圾回收堆和存儲方法域等構成。
2.JVM周期:
JVM在Java程序開始執行的時候,它才運行,程序結束的時它就停止。
一個Java程序會開啟一個JVM進程,如果一台機器上運行三個程序,那么就會有三個運行中的JVM進程。
JVM中的線程分為兩種:守護線程和普通線程
守護線程是JVM自己使用的線程,比如垃圾回收(GC)就是一個守護線程。
普通線程一般是Java程序的線程,只要JVM中有普通線程在執行,那么JVM就不會停止。
權限足夠的話,可以調用exit()方法終止程序。
3.JVM的結構體系:
·每個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代碼,初始化類變量為指定初始值
②執行引擎:負責執行包含在已裝載的類或接口中的指令 。
·每個JVM都包含:
方法區、Java堆、Java棧、本地方法棧、指令計數器及其他隱含寄存器
(1)Class文件
Class文件由Java編譯器生成,我們創建的.Java文件在經過編譯器后,會變成.Class的文件,這樣才能被JVM所識別並運行。
(2)方法區:(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虛擬機都會為他們產生一個方法列表,這個列表中保存了這個類可能調用的所有實例方法的引用,保存那些父類中調用的方法。
JVM 常量池
JVM常量池也稱之為運行時常量池,它是方法區(Method Area)的一部分。用於存放編譯期間生成的各種字面量和符號引用。運行時常量池不要求一定只有在編譯器產生的才能進入,運行期間也可以將新的常量放入池中,這種特性被開發人員利用比較多的就是String.intern()方法。
由“用於存放編譯期間生成的各種字面量和符號引用”這句話可見,常量池中存儲的是對象的引用而不是對象的本身。
常量池的好處
常量池是為了避免頻繁的創建和銷毀對象而影響系統性能,它也實現了對象的共享。
例如字符串常量池:在編譯階段就把所有字符串文字放到一個常量池中。
1、節省內存空間:常量池中如果有對應的字符串,那么則返回該對象的引用,從而不必再次創建一個新對象。
2、節省運行時間:比較字符串時,==比equals()快。對於兩個引用變量,==判斷引用是否相等,也就可以判斷實際值是否相等。
雙等號(==)的含義
基本數據類型之間使用雙等號,比較的是數值。
復合數據類型(類)之間使用雙等號,比較的是對象的引用地址是否相等。
八種基本類型的包裝類和常量池
Byte、Short、Integer、Long、Character、Boolean、String這7種包裝類都各自實現了自己的常量池。
//例子: Integer i1 = 20; Integer i2 = 20; System.out.println(i1=i2);//輸出TRUE
Byte、Short、Integer、Long、Character這5種包裝類都默認創建了數值[-128 , 127]的緩存數據。當對這5個類型的數據不在這個區間內的時候,將會去創建新的對象,並且不會將這些新的對象放入常量池中。
//IntegerCache.low = -128 //IntegerCache.high = 127 public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
//例子 Integer i1 = 200; Integer i2 = 200; System.out.println(i1==i2);//返回FALSE
Float 和Double 沒有實現常量池。
String包裝類與常量池
String str1 = "aaa";
當以上代碼運行時,JVM會到字符串常量池查找 "aaa" 這個字面量對象是否存在?
存在:則返回該對象的引用給變量 str1 。
不存在:則在堆中創建一個相應的對象,將創建的對象的引用存放到常量池中,同時將引用返回給變量 str1 。
String str1 = "aaa"; String str2 = "aaa"; 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() 方法后,檢查字符串常量池中是否有這個對象的引用,並做如下操作:
存在:直接返回對象引用給變量。
不存在:將這個對象引用加入到常量池,再返回對象引用給變量。
String interns = str3.intern(); System.out.println(interns == str1);//返回TRUE
假定常量池中都沒有以上字面量的對象,以下創建了多少個對象呢?
String str4 = "abc"+"efg"; String str5 = "abcefg"; 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常量池也稱之為運行時常量池,它是方法區(Method Area)的一部分。用於存放編譯期間生成的各種字面量和符號引用。運行時常量池不要求一定只有在編譯器產生的才能進入,運行期間也可以將新的常量放入池中,這種特性被開發人員利用比較多的就是String.intern()方法。
由“用於存放編譯期間生成的各種字面量和符號引用”這句話可見,常量池中存儲的是對象的引用而不是對象的本身。
常量池的好處
常量池是為了避免頻繁的創建和銷毀對象而影響系統性能,它也實現了對象的共享。
例如字符串常量池:在編譯階段就把所有字符串文字放到一個常量池中。
1、節省內存空間:常量池中如果有對應的字符串,那么則返回該對象的引用,從而不必再次創建一個新對象。
2、節省運行時間:比較字符串時,==比equals()快。對於兩個引用變量,==判斷引用是否相等,也就可以判斷實際值是否相等。
雙等號(==)的含義
基本數據類型之間使用雙等號,比較的是數值。
復合數據類型(類)之間使用雙等號,比較的是對象的引用地址是否相等。
八種基本類型的包裝類和常量池
Byte、Short、Integer、Long、Character、Boolean、String這7種包裝類都各自實現了自己的常量池。
//例子: Integer i1 = 20; Integer i2 = 20; System.out.println(i1=i2);//輸出TRUE
Byte、Short、Integer、Long、Character這5種包裝類都默認創建了數值[-128 , 127]的緩存數據。當對這5個類型的數據不在這個區間內的時候,將會去創建新的對象,並且不會將這些新的對象放入常量池中。
//IntegerCache.low = -128 //IntegerCache.high = 127 public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
//例子 Integer i1 = 200; Integer i2 = 200; System.out.println(i1==i2);//返回FALSE
Float 和Double 沒有實現常量池。
String包裝類與常量池
String str1 = "aaa";
當以上代碼運行時,JVM會到字符串常量池查找 "aaa" 這個字面量對象是否存在?
存在:則返回該對象的引用給變量 str1 。
不存在:則在堆中創建一個相應的對象,將創建的對象的引用存放到常量池中,同時將引用返回給變量 str1 。
String str1 = "aaa"; String str2 = "aaa"; 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() 方法后,檢查字符串常量池中是否有這個對象的引用,並做如下操作:
存在:直接返回對象引用給變量。
不存在:將這個對象引用加入到常量池,再返回對象引用給變量。
String interns = str3.intern(); System.out.println(interns == str1);//返回TRUE
假定常量池中都沒有以上字面量的對象,以下創建了多少個對象呢?
String str4 = "abc"+"efg"; String str5 = "abcefg"; 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();
(3)Java堆(JVM堆、Heap)---年輕代,老年代
當Java創建一個類的實例對象或者數組時,都在堆中為新的對象分配內存。
虛擬機中只有一個堆,程序中所有的線程都共享它。
堆占用的內存空間是最多的。
堆的存取類型為管道類型,先進先出。
在程序運行中,可以動態的分配堆的內存大小。
堆的內存資源回收是交給JVM GC進行管理的,詳情請參考:Java性能優化之JVM GC(垃圾回收機制) - 知乎專欄
(4)Java棧(JVM Stack)-用來加載方法的內存模型
java虛擬機棧:用來執行邏輯指令;執行方法運行時期的內存模型;線程內部獨有
虛擬機棧的內存空間就是:隊列;每一個單位:幀--》線程加載的每一個方法內存數據FILO(First in last out)
虛擬機棧-優化參數:定義隊列里有多少個幀->即定義虛擬機棧的大小
(遞歸會導致虛擬機棧爆掉嗎?如下圖:調用遞歸函數,執行后console:壓棧壓到6232次后,虛擬機棧爆掉了,所以不想浪費時間,不想壓那么多棧,要進行性能調優,如果不斷進行壓棧,這個線程會莫名占用一大撮的棧空間,其他線程占用的虛擬機棧的空間會變小,導致線程的數量會變少,因為每個線程必須要分配這么大的一個位置,其中一個線程不斷進行壓棧壓棧,需要的空間比其他線程多很多倍,所以導致整個操作系統的線程會變少,所以會有一個控制參數Xss--》來優化棧空間,)系統分配線程數量上限就會提升,並發量就會有提升。如圖:設置-Xss為128k(限制線程棧的空間,線程的高並發數量就會增多),再次執行main方法,調優后,從6000多次,到1036次就出現異常了,這樣就可以把線程申請的無線空間進行控制,頂多只能使用這么多。
(擴展:為什么JVM當中的虛擬機棧要設計為FILO?:例如java要執行MethodOne()方法,然后MethodOne()中調用方法MethodTwo(),如果先進先出的話,我們要拿到MethodTwo()的返回結果,這個時候MethodOne()已經退出了,MethodOne()在虛擬機棧已經不存在數據了,MethodTwo()的執行將沒有意義;所以必須要先進后出,后進來的執行完,先進來的才能出去)
(擴展:對於操作系統來說,為什么線程資源是有限的?(阿里面試):實際上我們的內存是根據我們的操作位,例如32位操作系統,我們能得到的最大物理內存是3G,操作系統本身數據會占有內存空間,實際給我們java的運行內存是2G,這其中還有java堆,方法區,都在2G內存中瓜分,剩下由虛擬機棧和本地方法棧去瓜分;虛擬機棧就是預留給我們成千上萬的線程去請求,比如虛擬機棧只有100MB,線程初始化會申請-Xss,初始化時有一個參數會控制復合棧的申請,通常是默認5MB ,所以線程的復合棧申請初始化越大,線程數量越少)
在Java棧中只保存基礎數據類型(參考:Java 基本數據類型 - 四類八種 - 知乎專欄)和自定義對象的引用,注意只是對象的引用而不是對象本身哦,對象是保存在堆區中的。
拓展知識:像String、Integer、Byte、Short、Long、Character、Boolean這六個屬於包裝類型,它們是存放於堆中的。
棧的存取類型為類似於水杯,先進后出。
棧內的數據在超出其作用域后,會被自動釋放掉,它不由JVM GC管理。
每一個線程都包含一個棧區,每個棧中的數據都是私有的,其他棧不能訪問。
每個線程都會建立一個操作棧,每個棧又包含了若干個棧幀,每個棧幀對應着每個方法的每次調用,每個棧幀包含了三部分:
局部變量區(方法內基本類型變量、變量對象指針)
操作數棧區(存放方法執行過程中產生的中間結果)
運行環境區(動態連接、正確的方法返回相關信息、異常捕捉)
(5)本地方法棧(Native Method Stack)
本地方法棧的功能和JVM棧非常類似,用於存儲本地方法的局部變量表,本地方法的操作數棧等信息。
棧的存取類型為類似於水杯,先進后出。
棧內的數據在超出其作用域后,會被自動釋放掉,它不由JVM GC管理。
每一個線程都包含一個棧區,每個棧中的數據都是私有的,其他棧不能訪問。
本地方法棧是在程序調用或JVM調用本地方法接口(Native)時候啟用。
本地方法都不是使用Java語言編寫的,比如使用C語言編寫的本地方法,本地方法也不由JVM去運行,所以本地方法的運行不受JVM管理。
HotSpot VM將本地方法棧和JVM棧合並了。
(6)程序計數器
執行引擎肯定是線程,線程執行類加載器,類加載器執行這段代碼,線程執行這段代碼之后,CUP設計屬於搶占式的,所以CPU對線程分配時間片--》執行這段代碼--》沒有執行完這段代碼,時間片沒有了,時間片分配給其他線程;重新拿到時間片時,線程為什么知道他上次執行到哪里,從哪里再次執行?--》因為有程序計數器:線程內部獨占的,每個線程抓住一個程序計數器,用來記錄當前字節碼的句柄,下次再搶到時間片時,就知道從哪里開始執行。
在JVM的概念模型里,字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令。分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。
JVM的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,為了各條線程之間的切換后計數器能恢復到正確的執行位置,所以每條線程都會有一個獨立的程序計數器。
程序計數器僅占很小的一塊內存空間。
當線程正在執行一個Java方法,程序計數器記錄的是正在執行的JVM字節碼指令的地址。如果正在執行的是一個Natvie(本地方法),那么這個計數器的值則為空(Underfined)。
程序計數器這個內存區域是唯一一個在JVM規范中沒有規定任何OutOfMemoryError(內存不足錯誤)的區域。
(7)JVM執行引擎
Java虛擬機相當於一台虛擬的“物理機”,這兩種機器都有代碼執行能力,其區別主要是物理機的執行引擎是直接建立在處理器、硬件、指令集和操作系統層面上的。而JVM的執行引擎是自己實現的,因此程序員可以自行制定指令集和執行引擎的結構體系,因此能夠執行那些不被硬件直接支持的指令集格式。
在JVM規范中制定了虛擬機字節碼執行引擎的概念模型,這個模型稱之為JVM執行引擎的統一外觀。JVM實現中,可能會有兩種的執行方式:解釋執行(通過解釋器執行)和編譯執行(通過即時編譯器產生本地代碼)。有些虛擬機只采用一種執行方式,有些則可能同時采用兩種,甚至有可能包含幾個不同級別的編譯器執行引擎。
輸入的是字節碼文件、處理過程是等效字節碼解析過程、輸出的是執行結果。在這三點上每個JVM執行引擎都是一致的。
(8)本地方法接口(JNI)
JNI是Java Native Interface的縮寫,它提供了若干的API實現了Java和其他語言的通信(主要是C和C++)。
JNI的適用場景
當我們有一些舊的庫,已經使用C語言編寫好了,如果要移植到Java上來,非常浪費時間,而JNI可以支持Java程序與C語言編寫的庫進行交互,這樣就不必要進行移植了。或者是與硬件、操作系統進行交互、提高程序的性能等,都可以使用JNI。需要注意的一點是需要保證本地代碼能工作在任何Java虛擬機環境。
JNI的副作用
一旦使用JNI,Java程序將丟失了Java平台的兩個優點:
1、程序不再跨平台,要想跨平台,必須在不同的系統環境下程序編譯配置本地語言部分。
2、程序不再是絕對安全的,本地代碼的使用不當可能會導致整個程序崩潰。一個通用規則是,調用本地方法應該集中在少數的幾個類當中,這樣就降低了Java和其他語言之間的耦合。
4.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方法裝載該類。
5.Java代碼編譯和執行的整個過程
也正如前面所說,Java代碼的編譯和執行的整個過程大概是:開發人員編寫Java代碼(.java文件),然后將之編譯成字節碼(.class文件),再然后字節碼被裝入內存,一旦字節碼進入虛擬機,它就會被解釋器解釋執行,或者是被即時代碼發生器有選擇的轉換成機器碼執行。
(1)Java代碼編譯是由Java源碼編譯器來完成,也就是Java代碼到JVM字節碼(.class文件)的過程。 流程圖如下所示:
(2)Java字節碼的執行是由JVM執行引擎來完成,流程圖如下所示:
Java代碼編譯和執行的整個過程包含了以下三個重要的機制:
·Java源碼編譯機制
·類加載機制
·類執行機制
(1)Java源碼編譯機制
Java 源碼編譯由以下三個過程組成:
①分析和輸入到符號表
②注解處理
③語義分析和生成class文件
流程圖如下所示:
最后生成的class文件由以下部分組成:
①結構信息:包括class文件格式版本號及各部分的數量與大小的信息
②元數據:對應於Java源碼中聲明與常量的信息。包含類/繼承的超類/實現的接口的聲明信息、域與方法聲明信息和常量池
③方法信息:對應Java源碼中語句和表達式對應的信息。包含字節碼、異常處理器表、求值棧與局部變量區大小、求值棧的類型記錄、調試符號信息
(2)類加載機制
JVM的類加載是通過ClassLoader及其子類來完成的,類的層次關系和加載順序可以由下圖來描述:
①Bootstrap ClassLoader
負責加載$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++實現,不是ClassLoader子類
②Extension ClassLoader
負責加載java平台中擴展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目錄下的jar包
③App ClassLoader
負責記載classpath中指定的jar包及目錄中class
④Custom ClassLoader
屬於應用程序根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據j2ee規范自行實現ClassLoader
加載過程中會先檢查類是否被已加載,檢查順序是自底向上,從Custom ClassLoader到BootStrap ClassLoader逐層檢查,只要某個classloader已加載就視為已加載此類,保證此類只所有ClassLoader加載一次。而加載的順序是自頂向下,也就是由上層來逐層嘗試加載此類。
(3)類執行機制
JVM是基於堆棧的虛擬機。JVM為每個新創建的線程都分配一個堆棧.也就是說,對於一個Java程序來說,它的運行就是通過對堆棧的操作來完成的。堆棧以幀為單位保存線程的狀態。JVM對堆棧只進行兩種操作:以幀為單位的壓棧和出棧操作。
JVM執行class字節碼,線程創建后,都會產生程序計數器(PC)和棧(Stack),程序計數器存放下一條要執行的指令在方法內的偏移量,棧中存放一個個棧幀,每個棧幀對應着每個方法的每次調用,而棧幀又是有局部變量區和操作數棧兩部分組成,局部變量區用於存放方法中的局部變量和參數,操作數棧中用於存放方法執行過程中產生的中間結果。棧的結構如下圖所示:
6.JVM內存管理及垃圾回收機制
JVM內存結構分為:方法區(method),棧內存(stack),堆內存(heap),本地方法棧(java中的jni調用),結構圖如下所示:
(1)堆內存(heap)
所有通過new創建的對象的內存都在堆中分配,其大小可以通過-Xmx和-Xms來控制。
操作系統有一個記錄空閑內存地址的鏈表,當系統收到程序的申請時,會遍歷該鏈表,尋找第一個空間大於所申請空間的堆結點,然后將該結點從空閑結點鏈表中刪除,並將該結點的空間分配給程序,另外,對於大多數系統,會在這塊內存空間中的首地址處記錄本次分配的大小,這樣代碼中的delete語句才能正確的釋放本內存空間。但由於找到的堆結點的大小不一定正好等於申請的大小,系統會自動的將多余的那部分重新放入空閑鏈表中。這時由new分配的內存,一般速度比較慢,而且容易產生內存碎片,不過用起來最方便。另外,在WINDOWS下,最好的方式是用VirtualAlloc分配內存,它不是在堆,也不是在棧,而是直接在進程的地址空間中保留一塊內存,雖然這種方法用起來最不方便,但是速度快,也是最靈活的。堆內存是向高地址擴展的數據結構,是不連續的內存區域。由於系統是用鏈表來存儲的空閑內存地址的,自然是不連續的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬內存。由此可見,堆獲得的空間比較靈活,也比較大。
(2)棧內存(stack)
在Windows下, 棧是向低地址擴展的數據結構,是一塊連續的內存區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在WINDOWS下,棧的大小是固定的(是一個編譯時就確定的常數),如果申請的空間超過棧的剩余空間時,將提示overflow。因此,能從棧獲得的空間較小。只要棧的剩余空間大於所申請空間,系統將為程序提供內存,否則將報異常提示棧溢出。 由系統自動分配,速度較快。但程序員是無法控制的。
堆內存與棧內存需要說明:
基礎數據類型直接在棧空間分配,方法的形式參數,直接在棧空間分配,當方法調用完成后從棧空間回收。引用數據類型,需要用new來創建,既在棧空間分配一個地址空間,又在堆空間分配對象的類變量 。方法的引用參數,在棧空間分配一個地址空間,並指向堆空間的對象區,當方法調用完成后從棧空間回收。局部變量new出來時,在棧空間和堆空間中分配空間,當局部變量生命周期結束后,棧空間立刻被回收,堆空間區域等待GC回收。方法調用時傳入的literal參數,先在棧空間分配,在方法調用完成后從棧空間收回。字符串常量、static在DATA區域分配,this在堆空間分配。數組既在棧空間分配數組名稱,又在堆空間分配數組實際的大小。
如:
(3)本地方法棧(java中的jni調用)
用於支持native方法的執行,存儲了每個native方法調用的狀態。對於本地方法接口,實現JVM並不要求一定要有它的支持,甚至可以完全沒有。Sun公司實現Java本地接口(JNI)是出於可移植性的考慮,當然我們也可以設計出其它的本地接口來代替Sun公司的JNI。但是這些設計與實現是比較復雜的事情,需要確保垃圾回收器不會將那些正在被本地方法調用的對象釋放掉。
(4)方法區(method)
它保存方法代碼(編譯后的java代碼)和符號表。存放了要加載的類信息、靜態變量、final類型的常量、屬性和方法信息。JVM用持久代(Permanet Generation)來存放方法區,可通過-XX:PermSize和-XX:MaxPermSize來指定最小值和最大值。
垃圾回收機制
堆里聚集了所有由應用程序創建的對象,JVM也有對應的指令比如 new, newarray, anewarray和multianewarray,然並沒有向 C++ 的 delete,free 等釋放空間的指令,Java的所有釋放都由 GC 來做,GC除了做回收內存之外,另外一個重要的工作就是內存的壓縮,這個在其他的語言中也有類似的實現,相比 C++ 不僅好用,而且增加了安全性,當然她也有弊端,比如性能這個大問題。
7.Java虛擬機的運行過程示例
上面對虛擬機的各個部分進行了比較詳細的說明,下面通過一個具體的例子來分析它的運行過程。
虛擬機通過調用某個指定類的方法main啟動,傳遞給main一個字符串數組參數,使指定的類被裝載,同時鏈接該類所使用的其它的類型,並且初始化它們。例如對於程序:
編譯后在命令行模式下鍵入: java HelloApp run virtual machine
將通過調用HelloApp的方法main來啟動java虛擬機,傳遞給main一個包含三個字符串"run"、"virtual"、"machine"的數組。現在我們略述虛擬機在執行HelloApp時可能采取的步驟。
開始試圖執行類HelloApp的main方法,發現該類並沒有被裝載,也就是說虛擬機當前不包含該類的二進制代表,於是虛擬機使用ClassLoader試圖尋找這樣的二進制代表。如果這個進程失敗,則拋出一個異常。類被裝載后同時在main方法被調用之前,必須對類HelloApp與其它類型進行鏈接然后初始化。鏈接包含三個階段:檢驗,准備和解析。檢驗檢查被裝載的主類的符號和語義,准備則創建類或接口的靜態域以及把這些域初始化為標准的默認值,解析負責檢查主類對其它類或接口的符號引用,在這一步它是可選的。類的初始化是對類中聲明的靜態初始化函數和靜態域的初始化構造方法的執行。一個類在初始化之前它的父類必須被初始化。整個過程如下: