JVM-ClassLoader

<譚鋒>整理 java

爲了支持跨平臺的特性,java語言採用源代碼編譯成中間字節碼,而後又各平臺的jvm解釋執行的方式。字節碼採用了徹底與平臺無關的方式進行描述,java只給出了字節碼格式的規範,並無規定字節碼最終來源是什麼,它能夠是除了java語言外的其餘語言產生,只要是知足字節碼規範的,均可以在jvm中很好的運行。正由於這個特性,極大的促進了各種語言的發展,在jvm平臺上出現了不少語言,如scala,groovy等 mysql

因爲字節碼來源並無作限制,所以jvm必須在字節碼正式使用以前,即在加載過程當中,對字節碼進行檢查驗證,以保證字節碼的可用性和安全性。 c++

1. jvm運行時內存結構劃分

在正式介紹以前,先看看jvm內存結構劃分: web

結合垃圾回收機制,將堆細化: spring

在加載階段主要用到的是方法區: sql

方法區是可供各條線程共享的運行時內存區域。存儲了每個類的結構信息,例如運行時常量池(Runtime Constant Pool)、字段和方法數據、構造函數和普通方法的字節碼內容、還包括一些在類、實例、接口初始化時用到的特殊方法 數據庫

若是把方法的代碼看做它的「靜態」部分,而把一次方法調用須要記錄的臨時數據看作它的「動態」部分,那麼每一個方法的代碼是隻有一份的,存儲於JVM的方法區中;每次某方法被調用,則在該調用所在的線程的的Java棧上新分配一個棧幀,用於存放臨時數據,在方法返回時棧幀自動撤銷。
tomcat

2. 類加載過程

jvm將類加載過程分紅加載,鏈接,初始化三個階段,其中鏈接階段又細分爲驗證,準備,解析三個階段。 安全

上述三個階段整體上會保持這個順序,可是有些特殊狀況,如加載階段與鏈接階段的部份內容(一部分字節碼的驗證工做)是交叉進行的。再如:解析階段能夠是推遲初次訪問某個類的時候,所以它可能出如今初始化階段以後。 服務器

2.1 裝載

裝載階段主要是將java字節碼以二進制的方式讀入到jvm內存中,而後將二進制數據流按照字節碼規範解析成jvm內部的運行時數據結構。java只對字節碼進行了規範,並無對內部運行時數據結構進行規定,不一樣的jvm實現能夠採用不一樣的數據結構,這些運行時數據結構是保存在jvm的方法區中(hotspot jvm的內部數據結構定義能夠參見撒迦的博文藉助HotSpot SA來一窺PermGen上的對象)。當一個類的二進制解析完畢後,jvm最終會在堆上生成一個java.lang.Class類型的實例對象,經過這個對象能夠訪問到該類在方法區的內容。

jvm規範並無規定從二進制字節碼數據應該如何產生,事實上,jvm爲了支持二進制字節碼數據來源的可擴展性,它提供了一個回調接口將經過一個類的全限定名來獲取描述此類的二進制字節碼的動做開放到jvm的外部實現,這就是咱們後面要講到的類加載器,若是有須要,咱們徹底能夠自定義一些類加載器,達到一些特殊應用場景。因爲有了jvm的支持,二進制流的產生的方式能夠是:

(1) 從本地文件系統中讀取

(2) 從網絡上加載(典型應用:java Applet)

(3) 從jar,zip,war等壓縮文件中加載

(4) 經過動態將java源文件動態編譯產生(jsp的動態編譯)

(5) 經過程序直接生成。

2.2 鏈接

鏈接階段主要是作一些加載完成以後的驗證工做,和初始化以前的準備一些工做,它細分爲三個階段。

2.2.1 驗證

驗證是鏈接階段的第一步,它主要是用於保證加載的字節碼符合java語言的規範,而且不會給虛擬機帶來危害。好比驗證這個類是否是符合字節碼的格式、變量與方法是否是有重複、數據類型是否是有效、繼承與實現是否合乎標準等等。按照驗證的內容不一樣又能夠細分爲4個階段:文件格式驗證(這一步會與裝載階段交叉進行),元數據驗證,字節碼驗證,符號引用驗證(這個階段的驗證每每會與解析階段交叉進行)。

2.2.2 準備

準備階段主要是爲類的靜態變量分配內存,並設置jvm默認的初始值。對於非靜態的變量,則不會爲它們分配內存。

在jvm中各種型的初始值以下:

int,byte,char,long,float,double 默認初始值爲0

boolean 爲false(在jvm內部用int表示boolean,所以初始值爲0)

reference類型爲null

對於final static基本類型或者String類型,則直接採用常量值(這其實是在編譯階段就已經處理好了)。

2.2.3 解析

解析過程就是查找類的常量池中的類,字段,方法,接口的符號引用,將他們替換成直接引用的過程。

a.解析過程主要針對於常量池中的CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info及CONSTANT_InterfaceMethodref_info四種常量。

b. jvm規範並無規定解析階段發生的時間,只是規定了在執行anewarray,checkcast,getfield,getstatic,instanceof,invokeinterface,invokespecial,invokespecial,invokestatic,invokevirtual,multinewaary,new,putfield,putstatic這13個指令應用於符號指令時,先對它們進行解析,獲取它們的直接引用.

c. jvm對於每一個加載的類都會有在內部建立一個運行時常量池(參考上面圖示),在解析以前是以字符串的方式將符號引用保存在運行時常量池中,在程序運行過程當中當須要使用某個符號引用時,就會促發解析的過程,解析過程就是經過符號引用查找對應的類實體,而後用直接引用替換符號引用。因爲符號引用已經被替換成直接引用,所以後面再次訪問時,無需再次解析,直接返回直接引用。

2.3 初始化

初始化階段是根據用戶程序中的初始化語句爲類的靜態變量賦予正確的初始值。這裏初始化執行邏輯最終會體如今類構造器方法<clinit>()方中。該方法由編譯器在編譯階段生成,它封裝了兩部份內容:靜態變量的初始化語句和靜態語句塊。

2.3.1 初始化執行時機

jvm規範明確規定了初始化執行條件,只要知足如下四個條件之一,就會執行初始化工做

(1) 經過new關鍵字實例化對象、讀取或設置類的靜態變量、調用類的靜態方法(對應new,getstatic,putstatic,invokespecial這四條字節碼指令)。

(2) 經過反射方式執行以上行爲時。

(3) 初始化子類的時候,會觸發父類的初始化。

(4) 做爲程序入口直接運行時的主類。

2.3.2 初始化過程

初始化過程包括兩步:

(1) 若是類存在直接父類,而且父類沒有被初始化則對直接父類進行初始化。

(2) 若是類當前存在<clinit>()方法,則執行<clinit>()方法。

須要注意的是接口(interface)的初始化並不要求先初始化它的父接口。(接口不能有static塊)

2.3.3 <clinit>()方法存在的條件

並非每一個類都有<clinit>()方法,以下狀況下不會有<clinit>()方法:

a. 類沒有靜態變量也沒有靜態語句塊

b.類中雖然定義了靜態變量,可是沒有給出明確的初始化語句。

c.若是類中僅包含了final static 的靜態變量的初始化語句,並且初始化語句採用編譯時常量表達時,也不會有<clinit>()方法。

例子:

public class ConstantExample {

    public static final int   a = 10;
    public static final float b = a * 2.0f;
}
編譯以後用 javap -verbose ConstantExample查看字節碼,顯示以下:
{
public static final int a;
  Constant value: int 10
public static final float b;
  Constant value: float 20.0f
public ConstantExample();
  Code:
   Stack=1, Locals=1, Args_size=1
   0:	aload_0
   1:	invokespecial	#15; //Method java/lang/Object."<init>":()V
   4:	return
  LineNumberTable: 
   line 12: 0

  LocalVariableTable: 
   Start  Length  Slot  Name   Signature
   0      5      0    this       LConstantExample;

}

這裏因爲編譯器直接10,看成常量來處理,看到是沒有<clinit>()方法存在的。能夠看成常量來處理的類型包括基本類型和String類型

對於其餘類型:

public class ConstantExample1 {

    public static final int   a = 10;
    public static final float b = a * 2.0f;
    public static final Date  c = new Date();
}
這裏雖然c被聲明成final,可是仍然會產生<clinit>()方法,以下所示:
{
public static final int a;
  Constant value: int 10
public static final float b;
  Constant value: float 20.0f
public static final java.util.Date c;

static {};
  Code:
   Stack=2, Locals=0, Args_size=0
   0:	new	#17; //class java/util/Date
   3:	dup
   4:	invokespecial	#19; //Method java/util/Date."<init>":()V
   7:	putstatic	#22; //Field c:Ljava/util/Date;
   10:	return
  LineNumberTable: 
   line 19: 0
   line 14: 10

2.3.4 併發性

在同一個類加載器域下,每一個類只會被初始化一次,當多個線程都須要初始化同一個類,這時只容許一個線程執行初始化工做,其餘線程則等待。當初始化執行完後,該線程會通知其餘等待的線程。

2.4 在使用過程當中類,對象在方法區和堆上的分佈狀態

先上代碼

public class TestThread extends Thread implements Cloneable {

    public static void main(String[] args) {
        TestThread t = new TestThread();
        t.start();
    }
}

上面這代碼中TestThread及相關類在jvm運行的存儲和引用狀況以下圖所示:



其中 t 做爲TestThread對象的一個引用存儲在線程的棧幀空間中,Thread對象及類型數據對應的Class對象實例都存儲在堆上,類型數據存儲在方法區,前面講到了,TestThread的類型數據中的符號引用在解析過程當中會被替換成直接引用,所以TestThread類型數據中會直接引用到它的父類Thread及它實現的接口Cloneable的類型數據。

在同一個類加載器空間中,對於全限定名相同的類,只會存在惟一的一份類的實例及類型數據。實際上類的實例數據和其對應的Class對象是相互引用的。

3. 類加載器

上面已經講到類加載器實際上jvm在類加載過程當中的裝載階段開放給外部使用的一個回調接口,它主要實現的功能就是:將經過一個類的全限定名來獲取描述此類的二進制字節碼。固然類加載器的優點遠不止如此,它是java安全體系的一個重要環節(java安全體系結構,後面會專門寫篇文章討論),同時經過類加載器的雙親委派原則等類加載器和class惟一性標識一個class的方式,能夠給應用程序帶來一些強大的功能,如hotswap。

3.1 雙親委派模型

在jvm中一個類實例的惟一性標識是類的全限定名和該類的加載器,類加載器至關於一個命名空間,將同名class進行了隔離。

從jvm的角度來講,只存在兩類加載器,一類是由c++實現的啓動類加載器,是jvm的一部分,一類是由java語言實現的應用程序加載器,獨立在jvm以外。

jkd中本身定義了一些類加載器:

(1).BootStrap ClassLoader:啓動類加載器,由C++代碼實現,負責加載存放在%JAVA_HOME%\lib目錄中的,或者通被-Xbootclasspath參數所指定的路徑中的,而且被java虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫,即便放在指定路徑中也不會被加載)類庫到虛擬機的內存中,啓動類加載器沒法被java程序直接引用。

(2).Extension ClassLoader:擴展類加載器,由sun.misc.Launcher$ExtClassLoader實現,負責加載%JAVA_HOME%\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的全部類庫,開發者能夠直接使用擴展類加載器。

(3).Application ClassLoader:應用程序類加載器,由sun.misc.Launcher$AppClassLoader實現,負責加載用戶類路徑classpath上所指定的類庫,是類加載器ClassLoader中的getSystemClassLoader()方法的返回值,開發者能夠直接使用應用程序類加載器,若是程序中沒有自定義過類加載器,該加載器就是程序中默認的類加載器。

參考ClassLoader源代碼會發現,這些Class之間並非採用繼承的方式實現父子關係,而是採用組合方式。

正常狀況下,每一個類加載在收到類加載請求時,會先調用父加載器進行加載,若父加載器加載失敗,則子加載器進行加載。

3.2 兩種主動加載方式

在java中有兩種辦法能夠在應用程序中主動加載類:

一種是Class類的forName靜態方法

public static Class<?> forName(String className) 
                throws ClassNotFoundException 
//容許指定是否初始化,而且指定類的類加載器
public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException

另外一種就是ClassLoader中的loadClass方法

protected synchronized Class<?> loadClass(String name, boolean resolve) //第二個參數表示是否在轉載完後進行鏈接(解析)
	throws ClassNotFoundException

public Class<?> loadClass(String name) throws ClassNotFoundException

上面這兩種方式是有區別的,以下例所示

public class InitialClass {

    public static int i;
    static {
        i = 1000;
        System.out.println("InitialClass is init");
    }

}
public class InitClassTest {

    public static void main(String[] args) throws MalformedURLException, ClassNotFoundException {
        Class classFromForName = Class.forName("com.alibaba.china.jianchi.example.InitialClass",
                                               true,
                                               new URLClassLoader(
                                                                  new URL[] { new URL(
                                                                                      "file:/home/tanfeng/workspace/springStudy/bin/") },
                                                                  InitClassTest.class.getClassLoader()));

        Class classFromClassLoader = (new URLClassLoader(
                                                         new URL[] { new URL(
                                                                             "file:/home/tanfeng/workspace/springStudy/bin/") },
                                                         InitClassTest.class.getClassLoader())).loadClass("com.alibaba.china.jianchi.example.InitialClass");

    }
}

經過運行能夠考到用Class.forName()方法會將裝載的類初始化,而ClassLoader.loadClass()方法則不會。

咱們常常會看到在數據庫操做時,會用Class.forName()的方式加載驅動類,而不是ClassLoader.loadClass()方法,爲什麼要這樣呢?

來看看mysql的驅動類實現,能夠看到在類的初始化階段,它會將本身註冊到驅動管理器中(static塊)。

package com.mysql.jdbc;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {

	static {
		try {
			java.sql.DriverManager.registerDriver(new Driver());
		} catch (SQLException E) {
			throw new RuntimeException("Can't register driver!");
		}
	}
      ... ...
}

3.3 自定義類加載器的應用

3.3.1 Tomcat中類加載器分析

3.3.1.1 tomcat中經過自定義一組類加載器,解決了如下幾個問題:

(1)部署在一個服務器上的兩個Web應用程序自身所使用的Java類庫是相互隔離的。

(2)部署在一個服務器上的兩個Web應用程序能夠共享服務器提供的java共用類庫。

(3)服務器儘量的保證自身安全不受部署的Web應用程序影響。

(4)支持對JSP的HotSwap功能。

3.3.1.2 tomcat的目錄結構

tomcat主要根據根據java類庫的共享範圍,分爲4組目錄:

(1)common目錄:能被Tomcat和全部Web應用程序共享。
(2)server目錄:僅能被Tomcat使用,其餘Web應用程序不可見。
(3)Shared目錄:能夠被全部Web應用程序共享,對Tomcat不可見。
(4)WEB-INF目錄:只能被當前Web應用程序使用,對其餘web應用程序不可見。

3.3.1.3 tomcat自定義類加載器

這幾個類加載器分別對應加載/common/*、/server/*、/shared/*和 /WEB-INF/*類庫, 其中Webapp類加載器和Jsp類加載器會存在多個,每一個Web應用對應一個Webapp類加載器。

CommonClassLoader加載的類能夠被CatalinaClassLoader和ShareClassLoader使用;CatalinaClassLoader加載的類和ShareClassLoader加載的類相互隔離; WebappClassLoader可使用ShareClassLoader加載的類,但各個WebappClassLoader間相互隔離;JspClassLoader僅能用JSP文件編譯的class文件。

相關文章
相關標籤/搜索