首先我想從宏觀上介紹一下java虛擬機的工作原理。從最初的我們編寫的java源文件(.java文件)是如何一步步執行的,如下圖所示,首先java源文件經過前端編譯器(javac或ecj)將.java文件編譯為java字節碼文件,然后jre加載java字節碼文件,載入系統分配給jvm的內存區,然后執行引擎解釋或編譯類文件,再由即時編譯器將字節碼轉化為機器碼。主要介紹下圖中的類加載器和運行時數據區兩個部分。
類加載
類加載指將類的字節碼文件(.class)中的二進制數據讀入內存,將其放在運行時數據區的方法區內,然后在堆上創建java.lang.class對象,封裝類在方法區內的數據結構。類加載的最終產品是位于堆中的類對象,類對象封裝了類在方法區內的數據結構,并且向java程序提供了訪問方法區內數據結構的接口。如下是類加載器的層次關系圖。
啟動類加載器(bootstrapclassloader):在jvm運行時被創建,負責加載存放在jdk安裝目錄下的jre\lib的類文件,或者被-xbootclasspath參數指定的路徑中,并且能被虛擬機識別的類庫(如rt.jar,所有的java.*開頭的類均被bootstrap classloader加載)。啟動類無法被java程序直接引用。
擴展類加載器(extension classloader):該類加載器負責加載jdk安裝目錄下的\jre\lib\ext的類,或者由java.ext.dirs系統變量指定路徑中的所有類庫,開發者也可以直接使用擴展類加載器。
應用程序類加載器(appclassloader):負責加載用戶類路徑(classpath)所指定的類,開發者可以直接使用該類加載器,如果應用程序中沒有定義過自己的類加載器,該類加載器為默認的類加載器。
用戶自定義類加載器(user classloader):jvm自帶的類加載器是從本地文件系統加載標準的java class文件,而自定義的類加載器可以做到在執行非置信代碼之前,自動驗證數字簽名,動態地創建符合用戶特定需要的定制化構建類,從特定的場所(數據庫、網絡中)取得java class。
注意如上的類加載器并不是通過繼承的方式實現的,而是通過組合的方式實現的。而java虛擬機的加載模式是一種委派模式,如上圖中的1-7步所示。下層的加載器能夠看到上層加載器中的類,反之則不行。類加載器可以加載類但是不能卸載類。說了一大堆,還是感覺需要拿點代碼說事。
首先我們先定義自己的類加載器myclassloader,繼承自classloader,并覆蓋了父類的findclass(string name)方法,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
public class myclassloader extends classloader{ private string loadername; //類加載器名稱 private string path = "" ; //加載類的路徑 private final string filetype = ".class" ; public myclassloader(string name){ super (); //應用類加載器為該類的父類 this .loadername = name; } public myclassloader(classloader parent,string name){ super (parent); this .loadername = name; } public string getpath(){ return this .path;} public void setpath(string path){ this .path = path;} @override public string tostring(){ return this .loadername;} @override public class <?> findclass(string name) throws classnotfoundexception{ byte [] data = loaderclassdata(name); return this .defineclass(name, data, 0 , data.length); } //讀取.class文件 private byte [] loaderclassdata(string name){ inputstream is = null ; byte [] data = null ; bytearrayoutputstream baos = new bytearrayoutputstream(); try { is = new fileinputstream( new file(path + name + filetype)); int c = 0 ; while (- 1 != (c = is.read())){ baos.write(c); } data = baos.tobytearray(); } catch (exception e) { e.printstacktrace(); } finally { try { if (is != null ) is.close(); if (baos != null ) baos.close(); } catch (ioexception e) { e.printstacktrace(); } } return data; } } |
我們如何利用我們定義的類加載器加載指定的字節碼文件(.class)呢?如通過myclassloader加載c:\\users\\administrator\\下的test.class字節碼文件,代碼如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class client { public static void main(string[] args) { // todo auto-generated method stub //myclassloader的父類加載器為系統默認的加載器appclassloader myclassloader mycloader = new myclassloader( "myclassloader" ); //指定myclassloader的父類加載器為extclassloader //myclassloader mycloader = new myclassloader(classloader.getsystemclassloader().getparent(),"myclassloader"); mycloader.setpath( "c:\\users\\administrator\\" ); class <?> clazz; try { clazz = mycloader.loadclass( "test" ); field[] filed = clazz.getfields(); //獲取加載類的屬性字段 method[] methods = clazz.getmethods(); //獲取加載類的方法字段 system.out.println( "該類的類加載器為:" + clazz.getclassloader()); system.out.println( "該類的類加載器的父類為:" + clazz.getclassloader().getparent()); system.out.println( "該類的名稱為:" + clazz.getname()); } catch (classnotfoundexception e) { // todo auto-generated catch block e.printstacktrace(); } } } |
運行時數據區
字節碼的加載第一步,其后分別是認證、準備、解析、初始化,那么這些步驟又具體做了哪些工作,以及他們會對運行時數據區纏身什么影響呢?如下圖所示:
如下我們將介紹運行時數據區,主要分為方法區、java堆、虛擬機棧、本地方法棧、程序計數器。其中方法區和java堆一樣,是各個線程共享的內存區域,而虛擬機棧、本地方法棧、程序計數器是線程私有的內存區。
java堆:java堆是java虛擬機所管理的內存中最大的一塊,被進程的所有線程共享,在虛擬機啟動時被創建。該區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內存,隨著jit編譯器的發展與逃逸分支技術逐漸成熟,棧上分配、標量替換等優化技術使得對象在堆上的分配內存變得不是那么“絕對”。java堆是垃圾收集器管理的主要區域。由于現在的收集器基本都采用分代收集算法,所以java堆中還可以分為老年代和新生代(eden、from survivor、to survivor)。根據java虛擬機規范,java堆可以處于物理上不連續的內存空間,只要邏輯上連續即可。該區域的大小可以通過-xmx和-xms參數來擴展,如果堆中沒有內存完成實例分配,并且堆也無法擴展,將會拋出outofmemoryerror異常。
方法區:用于存儲被java虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。不同于java堆的是,java虛擬機規范對方法區的限制非常寬松,可以選擇不實現垃圾收集。但并非數據進入了方法區就“永久”存在了,這區域內存回收目標主要是針對常量池的回收和對類型的卸載。如果該區域內存不足也會拋出outofmemoryerror異常。
常量池:這個名詞可能大家也經常見,它是方法區的一部分。class文件除了有類的版本、字段、方法、接口等描述信息外,還有一項信息就是常量池,用于存放編譯期生成的各種字面量和符號引用。java虛擬機運行期間,也可能將新的常量放入常量池(如string類的intern()方法)。
虛擬機棧:線程私有,生命周期與線程相同。虛擬機棧描述的是java方法執行的內存模型:每個方法在執行時都會創建一個棧幀用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每個方法從調用直至執行完成的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程。如果請求的站深度大于虛擬機所允許的深度,將拋出stackoverflowerror異常,虛擬機棧在動態擴展時如果無法申請到足夠的內存,就會拋出outofmemoryerror異常。
本地方法棧:與虛擬機棧類似,不過虛擬機棧是為虛擬機執行java方法(字節碼)服務,而本地方法棧則是為虛擬機使用到的native方法服務。該區域同樣會報stackoverflowerror和outofmemoryerror異常。
程序計數器:一塊較小的內存空間,可以看作是當前線程所執行的字節碼的行號指示器。字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器完成。如果線程正在執行一個java方法,計數器記錄的是正在執行的虛擬機字節碼指令的地址,如果正在執行的是native方法,這個計數器值為空。此內存區域是唯一一個在java虛擬機規范中沒有規定任何outofmemoryerror情況的區域。
寫了這么多,感覺還是少一個例子。通過最簡單的一段代碼解釋一下,程序在運行時數據區個部分的變化情況。
1
2
3
4
5
6
7
8
9
|
public class test{ public static void main(string[] args){ string name = "best.lei" ; sayhello(name); } public static void sayhello(string name){ system.out.println( "hello " + name); } } |
通過編譯器將test.java文件編譯為test.class,利用javap -verbose test.class對編譯后的字節碼進行分析,如下圖所示:
我們在看看運行時數據區的變化:
以上就是本文的全部內容,希望本文的內容對大家的學習或者工作能帶來一定的幫助,同時也希望多多支持服務器之家!
原文鏈接:http://www.cnblogs.com/zhanglei93/p/6590609.html