一篇筆記整理JVM工作原理


前言:

  想提高Java開發,了解jvm是必不可少的。它讓開發者了解他們的代碼,jvm是如何變異與運行。深入了解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,這個大小只是保存堆中一個沒有任何屬性的對象的大小。看下面語句:

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的內部體系結構分為三部分,

  (1)類裝載器(ClassLoader)子系統
     作用: 用來裝載.class文件
  (2)執行引擎
     作用:執行字節碼,或者執行本地方法
  (3)運行時數據區
     方法區,堆,java棧,PC寄存器,本地方法棧

 

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構造器的方法為:<init>)以及調用對象實例中的私有方法時。

二、反射機制是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)自適應優化:開始對所有的代碼都采取解釋執行的方式,並監視代碼執行情況,然后對那些經常調用的方法啟動一個后台線程,將其編譯為本地代碼,並進行仔細優化。若方法不再頻繁使用,則取消編譯過的代碼,仍對其進行解釋執行。

參考:http://www.cnblogs.com/binyue/p/4546205.html

參考:http://blog.csdn.net/aaa1117a8w5s6d/article/details/8254922

參考:http://www.cnblogs.com/alexlo/archive/2013/02/21/2920209.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM