詳解JVM的基本知識和運行原理

下面介紹jvm的基本知識

>>數據類型

Java虛擬機中,數據類型可以分為兩類:基本類型和引用類型

基本類型的變量保存原始值,即:他代表的值就是數值本身;而引用類型的變量保存引用值。

“引用值”代表了某個對象的引用,而不是對象本身,對象本身存放在這個引用值所表示的地址的位置。

基本類型包括:byte,boolean(1 byte),short,char(2 bytes),int,float(4 bytes),long,double(8 bytes),returnAddress(不確定是否是基本類型)

引用類型包括:類類型,接口類型和數組。

>>堆與棧

棧是運行時的單位,而堆是存儲的單位。

棧解決程序的運行問題,即程序如何執行,或者說如何處理數據;堆解決的是數據存儲的問題,即數據怎麼放、放在哪兒。

在Java中一個線程就會相應有一個線程棧與之對應,這點很容易理解,因為不同的線程執行邏輯有所不同因此需要一個獨立的線程棧。而堆則是所有線程共享的。棧因為是運行單位,因此裡面存儲的信息都是跟當線程(或程序)相關信息的。包括局部變量、程序運行狀態、方法返回值等等;而堆只負責存儲對象信息。

堆中存的是對象。棧中存的是基本數據類型和堆中對象的引用。

堆和棧中,棧是程序運行最根本的東西。程序運行可以沒有堆,但是不能沒有棧。而堆是為棧進行數據存儲服務,說白了堆就是一塊共享的內存。不過,正是因為堆和棧的分離的思想,才使得Java的垃圾回收成為可能。

Java中,棧的大小通過-Xss來設置,當棧中存儲數據比較多時,需要適當調大這個值,否則會出現java.lang.StackOverflowError異常。常見的出現這個異常的是無法返回的遞歸,因為此時棧中保存的信息都是方法返回的記錄點。

>>Java中的參數傳遞時傳值呢?還是傳引用?

要說明這個問題,先要明確兩點:

1. 不要試圖與C進行類比,Java中沒有指針的概念。

2. 程序運行永遠都是在棧中進行的,因而參數傳遞時,只存在傳遞基本類型和對象引用的問題。不會直接傳對象本身。

明確以上兩點後。Java在方法調用傳遞參數時,因為沒有指針,所以它都是進行傳值調用(這點可以參考C的傳值調用)。因此,很多書裡面都說Java是進行傳值調用,這點沒有問題,而且也簡化的C中複雜性。

傳值傳引用都不夠準確,可以理解成傳引用變量的副本值。引用變量分為字面值引用變量(即基本數據類型引用變量)和對象引用變量 。 詳情需要了解數據類型使用機制和堆棧的概念:http://www.cnblogs.com/alexlo/archive/2013/02/21/2920209.html

對象引用變量:即普通java對象的引用變量 ,如 String a = "abc" , a就是對象引用變量。java 是不能直接操作對象的,只能通過對“對象引用的操作”來操作對象。而對象的引用的表示就是對象變量。可以多個對象引用變量指向同一個對象。

字面值引用變量:即普通數據類型的引用變量 ,如 int b = 1 , b就是字面值引用變量。可以有多個字面值引用變量指向同一字面值,但其中一個引用修改字面值,不會影響另一個引用字面值,這點要與對象引用區別開。

>>Java對象的大小

基本數據的類型的大小是固定的,Java基本數據類型與位運算,這裡就不多說了。對於非基本類型的Java對象,其大小就值得商榷。在Java中,一個空Object對象的大小是8byte,這個大小隻是保存堆中一個沒有任何屬性的對象的大小。看下面語句:

1

Object ob = new Object();

這樣在程序中完成了一個Java對象的生命,但是它所佔的空間為:4byte(棧)+8byte(堆)。4byte是上面部分所說的Java棧中保存引用的所需要的空間。而那8byte則是Java堆中對象的信息。因為所有的Java非基本類型的對象都需要默認繼承Object對象,因此不論什麼樣的Java對象,其大小都必須是大於8byte。

Class NewObject {

int count;

boolean flag;

Object ob;

}

其大小為:空對象大小(8byte)+int大小(4byte)+Boolean大小(1byte)+空Object引用的大小(4byte)=17byte。

但是因為Java在對對象內存分配時都是以8的整數倍來分,因此大於17byte的最接近8的整數倍的是24,因此此對象的大小為24byte。

這裡需要注意一下基本類型的包裝類型的大小。因為這種包裝類型已經成為對象了,因此需要把他們作為對象來看待。包裝類型的大小至少是12byte(聲明一個空Object至少需要的空間),而且12byte沒有包含任何有效信息,同時,因為Java對象大小是8的整數倍,因此一個基本類型包裝類的大小至少是16byte。這個內存佔用是很恐怖的,它是使用基本類型的N倍(N>2),有些類型的內存佔用更是誇張(隨便想下就知道了)。

因此,可能的話應儘量少使用包裝類。在JDK5.0以後,因為加入了自動類型裝換,因此,Java虛擬機會在存儲方面進行相應的優化。

>>引用類型

對象引用類型分為強引用、軟引用、弱引用和虛引用。

強引用:就是我們一般聲明對象時虛擬機生成的引用,強引用環境下,垃圾回收時需要嚴格判斷當前對象是否被強引用,如果被強引用,則不會被垃圾回收。

軟引用:軟引用一般被做為緩存來使用。與強引用的區別是,軟引用在垃圾回收時,虛擬機會根據當前系統的剩餘內存來決定是否對軟引用進行回收。如果剩餘內存比較緊張,則虛擬機會回收軟引用所引用的空間;如果剩餘內存相對富裕,則不會進行回收。換句話說,虛擬機在發生OutOfMemory時,肯定是沒有軟引用存在的。

弱引用:弱引用與軟引用類似,都是作為緩存來使用。但與軟引用不同,弱引用在進行垃圾回收時,是一定會被回收掉的,因此其生命週期只存在於一個垃圾回收週期內。

強引用不用說,我們系統一般在使用時都是用的強引用。而“軟引用”和“弱引用”比較少見。他們一般被作為緩存使用,而且一般是在內存大小比較受限的情況下做為緩存。因為如果內存足夠大的話,可以直接使用強引用作為緩存即可,同時可控性更高。因而,他們常見的是被使用在桌面應用系統的緩存。

JVM的生命週期

一、首先分析兩個概念

JVM實例和JVM執行引擎實例

(1)JVM實例對應了一個獨立運行的java程序,它是進程級別。

(2)JVM執行引擎實例則對應了屬於用戶運行程序的線程,它是線程級別的。

二、JVM的生命週期

(1)JVM實例的誕生:當啟動一個Java程序時,一個JVM實例就產生了,任何一個擁有public static void main(String[] args)函數的class都可以作為JVM實例運行的起點。

(2)JVM實例的運行 main()作為該程序初始線程的起點,任何其他線程均由該線程啟動。JVM內部有兩種線程:守護線程和非守護線程,main()屬於非守護線程,守護線程通常由JVM自己使用,java程序也可以標明自己創建的線程是守護線程。

(3)JVM實例的消亡:當程序中的所有非守護線程都終止時,JVM才退出;若安全管理器允許,程序也可以使用Runtime類或者System.exit()來退出。

JVM的體系結構

詳解JVM的基本知識和運行原理

一、JVM的內部體系結構分為三部分,

(1)類裝載器(ClassLoader)子系統

作用: 用來裝載.class文件

(2)執行引擎

作用:執行字節碼,或者執行本地方法

(3)運行時數據區

方法區,堆,java棧,PC寄存器,本地方法棧

JVM類加載器

詳解JVM的基本知識和運行原理

一、 JVM將整個類加載過程劃分為了三個步驟:

(1)裝載

裝載過程負責找到二進制字節碼並加載至JVM中,JVM通過類名、類所在的包名通過ClassLoader來完成類的加載,同樣,也採用以上三個元素來標識一個被加載了的類:類名+包名+ClassLoader實例ID。

(2)鏈接

鏈接過程負責對二進制字節碼的格式進行校驗、初始化裝載類中的靜態變量以及解析類中調用的接口、類。在完成了校驗後,JVM初始化類中的靜態變量,並將其值賦為默認值。最後一步為對類中的所有屬性、方法進行驗證,以確保其需要調用的屬性、方法存在,以及具備應的權限(例如public、private域權限等),會造成NoSuchMethodError、NoSuchFieldError等錯誤信息。

(3)初始化

初始化過程即為執行類中的靜態初始化代碼、構造器代碼以及靜態屬性的初始化,在四種情況下初始化過程會被觸發執行:調用了new;反射調用了類中的方法;子類調用了初始化;JVM啟動過程中指定的初始化類。

二、JVM兩種類裝載器包括:啟動類裝載器和用戶自定義類裝載器:

啟動類裝載器是JVM實現的一部分,用戶自定義類裝載器則是Java程序的一部分,必須是ClassLoader類的子類。

主要分為以下幾類:

(1) Bootstrap ClassLoader

這是JVM的根ClassLoader,它是用C++實現的,JVM啟動時初始化此ClassLoader,並由此ClassLoader完成$JAVA_HOME中jre/lib/rt.jar(Sun JDK的實現)中所有class文件的加載,這個jar中包含了java規範定義的所有接口以及實現。

(2) Extension ClassLoader

JVM用此classloader來加載擴展功能的一些jar包

(3) System ClassLoader

JVM用此classloader來加載啟動參數中指定的Classpath中的jar包以及目錄,在Sun JDK中ClassLoader對應的類名為AppClassLoader。

(4) User-Defined ClassLoader

User-DefinedClassLoader是Java開發人員繼承ClassLoader抽象類自行實現的ClassLoader,基於自定義的ClassLoader可用於加載非Classpath中的jar以及目錄

三、ClassLoader抽象類提供了幾個關鍵的方法:

(1)loadClass

此方法負責加載指定名字的類,ClassLoader的實現方法為先從已經加載的類中尋找,如沒有則繼續從parent ClassLoader中尋找,如仍然沒找到,則從System ClassLoader中尋找,最後再調用findClass方法來尋找,如要改變類的加載順序,則可覆蓋此方法

(2)findLoadedClass

此方法負責從當前ClassLoader實例對象的緩存中尋找已加載的類,調用的為native的方法。

(3) findClass

此方法直接拋出ClassNotFoundException,因此需要通過覆蓋loadClass或此方法來以自定義的方式加載相應的類。

(4) findSystemClass

此方法負責從System ClassLoader中尋找類,如未找到,則繼續從Bootstrap ClassLoader中尋找,如仍然為找到,則返回null。

(5)defineClass

此方法負責將二進制的字節碼轉換為Class對象

(6) resolveClass

此方法負責完成Class對象的鏈接,如已鏈接過,則會直接返回。

四、簡單的classLoader例子


/*

* 重寫ClassLoader類的findClass方法,將一個字節數組轉換為 Class 類的實例

*/

public Class> findClass(String name) throws ClassNotFoundException {

byte[] b = null;

try {

b = loadClassData(AutoClassLoader.FormatClassName(name));

} catch (Exception e) {

e.printStackTrace();

}

return defineClass(name, b, 0, b.length);

}

/*

* 將指定路徑的.class文件轉換成字節數組

*/

private byte[] loadClassData(String filepath) throws Exception {

int n =0;

BufferedInputStream br = new BufferedInputStream(new FileInputStream(new File(filepath)));

ByteArrayOutputStream bos= new ByteArrayOutputStream();

while((n=br.read())!=-1){

bos.write(n);

}

br.close();

return bos.toByteArray();

}

/*

* 格式化文件所對應的路徑

*/

public static String FormatClassName(String name){

FILEPATH= DEAFAULTDIR + name+".class";

return FILEPATH;

}

/*

* main方法測試

*/

public static void main(String[] args) throws Exception {

AutoClassLoader acl = new AutoClassLoader();

Class c = acl.findClass("testClass");

Object obj = c.newInstance();

Method m = c.getMethod("getName",new Class[]{String.class ,int.class});

m.invoke(obj,"你好",123);

System.out.println(c.getName());

System.out.println(c.getClassLoader());

System.out.println(c.getClassLoader().getParent());

}

JVM執行引擎

一、JVM通過執行引擎來完成字節碼的執行,在執行過程中JVM採用的是自己的一套指令系統

每個線程在創建後,都會產生一個程序計數器(pc)和棧(Stack),其中程序計數器中存放了下一條將要執行的指令,Stack中存放Stack Frame,棧幀,表示的為當前正在執行的方法,每個方法的執行都會產生Stack Frame,Stack Frame中存放了傳遞給方法的參數、方法內的局部變量以及操作數棧,操作數棧用於存放指令運算的中間結果,指令負責從操作數棧中彈出參與運算的操作數,指令執行完畢後再將計算結果壓回到操作數棧,當方法執行完畢後則從Stack中彈出,繼續其他方法的執行。

在執行方法時JVM提供了invokestatic、invokevirtual、invokeinterface和invokespecial四種指令來執行

(1)invokestatic:調用類的static方法

(2) invokevirtual: 調用對象實例的方法

(3) invokeinterface:將屬性定義為接口來進行調用

(4) invokespecial: JVM對於初始化對象(Java構造器的方法為:

)以及調用對象實例中的私有方法時。

二、反射機制是Java的亮點之一,基於反射可動態調用某對象實例中對應的方法、訪問查看對象的屬性等

而無需在編寫代碼時就確定需要創建的對象,這使得Java可以實現很靈活的實現對象的調用,代碼示例如下:

Class actionClass=Class.forName(外部實現類);

Method method=actionClass.getMethod(“execute”,null);

Object action=actionClass.newInstance();

method.invoke(action,null);

反射的關鍵:要實現動態的調用,最明顯的方法就是動態的生成字節碼,加載到JVM中並執行。

(1)Class actionClass=Class.forName(外部實現類);

調用本地方法,使用調用者所在的ClassLoader來加載創建出Class對象;

(2)Method method=actionClass.getMethod(“execute”,null);

校驗此Class是否為public類型的,以確定類的執行權限,如不是public類型的,則直接拋出SecurityException;調用privateGetDeclaredMethods來獲取到此Class中所有的方法,在privateGetDeclaredMethods對此Class中所有的方法的集合做了緩存,在第一次時會調用本地方法去獲取;

掃描方法集合列表中是否有相同方法名以及參數類型的方法,如有則複製生成一個新的Method對象返回;

如沒有則繼續掃描父類、父接口中是否有此方法,如仍然沒找到方法則拋出NoSuchMethodException;

(3) Object action=actionClass.newInstance();

第一步:校驗此Class是否為public類型,如權限不足則直接拋出SecurityException;

第二步:如沒有緩存的構造器對象,則調用本地方法獲取到構造器,並複製生成一個新的構造器對象,放入緩存,如沒有空構造器則拋出InstantiationException;

第三步:校驗構造器對象的權限;

第四步:執行構造器對象的newInstance方法;構造器對象的newInstance方法判斷是否有緩存的ConstructorAccessor對象,如果沒有則調用sun.reflect.ReflectionFactory生成新的ConstructorAccessor對象;

第五步:sun.reflect.ReflectionFactory判斷是否需要調用本地代碼,可通過sun.reflect.noInflation=true來設置為不調用本地代碼,在不調用本地代碼的情況下,就轉交給MethodAccessorGenerator來處理了;

第六步:MethodAccessorGenerator中的generate方法根據Java Class格式規範生成字節碼,字節碼中包括了ConstructorAccessor對象需要的newInstance方法,此newInstance方法對應的指令為invokespecial,所需的參數則從外部壓入,生成的Constructor類的名字以:sun/reflect/GeneratedSerializationConstructorAccessor或sun/reflect/GeneratedConstructorAccessor開頭,後面跟隨一個累計創建的對象的次數;

第七步:在生成了字節碼後將其加載到當前的ClassLoader中,並實例化,完成ConstructorAccessor對象的創建過程,並將此對象放入構造器對象的緩存中;

最後一步:執行獲取的constructorAccessor.newInstance,這步和標準的方法調用沒有任何區別。

(4) method.invoke(action,null);

這步執行的過程和上一步基本類似,只是在生成字節碼時生成的方法改為了invoke,其調用的目標改為了傳入的對象的方法,同時生成的類名改為了:sun/reflect/GeneratedMethodAccessor。

注:但是getMethod是非常耗性能的,一方面是權限的校驗,另外一方面所有方法的掃描以及Method對象的複製,因此在使用反射調用多的系統中應緩存getMethod返回的Method對象

2、執行技術

主要的執行技術有:解釋,即時編譯,自適應優化、芯片級直接執行

(1)解釋屬於第一代JVM,

(2)即時編譯JIT屬於第二代JVM,

(3)自適應優化(目前Sun的HotspotJVM採用這種技術)則吸取第一代JVM和第二代JVM的經驗,採用兩者結合的方式

(4)自適應優化:開始對所有的代碼都採取解釋執行的方式,並監視代碼執行情況,然後對那些經常調用的方法啟動一個後臺線程,將其編譯為本地代碼,並進行仔細優化。若方法不再頻繁使用,則取消編譯過的代碼,仍對其進行解釋執行。


分享到:


相關文章: