《深入理解JVM》-類的加載-張龍

第一部分:類加載 

java中類型的加載、連接和初始化都是在程序運行期間完成的:

  • 加載:查找並加載類的二進制數據
  • 連接:
  1. 驗證:確保被加載的類的正確性
  2. 準備:爲類的靜態變量分配內存,並將其初始化爲默認值
  3. 解析:把類中的符號引用轉換爲直接引用
  • 初始化:爲類的靜態變量賦予正確的初始值(會執行靜態代碼塊),每一個類只會被初始化一次

類的生命週期的五個階段:加載、連接、初始化、使用、卸載

java程序對類的使用方式分爲兩種:主動使用和被動使用

所有的java虛擬機實現必須在每個類或接口被java程序首次主動使用時才初始化他們

主動使用(七種):

  1. 創建類的實例
  2. 訪問某個類或接口的靜態變量(getstatic),或者對該靜態變量賦值(putstatic)
  3. 調用類的靜態方法(invokestatic)
  4. 反射
  5. 初始化一個類的子類
  6. java虛擬機啓動時被標明爲啓動類的類(包含main)
  7. jdk1.7開始提供的動態語言支持:java.lang.invoke.MethodHandle實例的解析結果REF_getStatic,REF_putStatic,REF_invokeStatic句柄對應的類沒有初始化,則初始化

除了上方七種情況,其他使用java類的方式都被看做是對類的被動使用,都不會導致類的初始化

 類的加載

類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然後在內存中創建一個java.lang.Class對象用來封裝類在方法區內的數據結構

java虛擬機結束生命的幾種方式:

  • 執行Sytem.exit()方法
  • 程序正常執行結束
  • 程序執行過程中遇到異常或者錯誤
  • 由於操作系統出現錯誤而導致JVM進程終止
package shengsiyuan.capter01;

/**
 * 1.靜態代碼塊只有在類被初始化的時候會執行
 * 2.對於靜態字段而言,使用靜態字段時,只有直接定義了該靜態字段的類纔會被初始化
 * 3.當一個類被初始化的時候,要求其父類都已經初始化完畢
 */
public class Test1 {
    public static void main(String[] args) {
        //Parent1初始化,Child1不會初始化
        System.out.println(Child1.name);
    }
}
class Parent1{
    public static String name = "parent";
    static {
        System.out.println("Parent1 init");
    }
}
class Child1 extends Parent1{
    static {
        System.out.println("Child1 init");
    }
}
package shengsiyuan.capter01;

/**
 * 常量(final)在編譯階段會存入到這個常量池的方法所在的類的常量池中
 * 本質上,調用類並沒有直接引用到這個定義常量的類,因此不會觸發定義常量
 * 類的初始化,
 * 注意:這裏指的是將常量存放到了Test2的常量池中,只有的Test2與Parent2沒有任何關係,甚至,我們可以將Parent2的class文件刪除
 */
public class Test2 {
    public static void main(String[] args) {
        System.out.println(Parent2.parentName);
    }
}

class Parent2{
    final static String parentName = "parent";
    static {
        System.out.println("Parent1 init");
    }
}

 

package shengsiyuan.capter01;

import java.util.UUID;

/**
 * 當一個常量的值並非編譯期間可以確定的,那麼其值不會放到調用方法的調用類的常量池中
 * 這時在程序運行時,會導致主動使用這個常量所在的類,顯然會導致這個類被初始化
 */
public class Test3 {
    public static void main(String[] args) {
        System.out.println(Parent3.parentName);
    }
}

class Parent3{
    final static String parentName = UUID.randomUUID().toString();
    static {
        System.out.println("Parent1 init");
    }
}

 

package shengsiyuan.capter01;

/**
 * 輸出的值是1,0
 * 原因:當調用Singleton.getInstance()是主動使用類,對Singleton類進行加載,連接,初始化
 * 在連接的準備階段,會對靜態變量進行賦初始值:count1=0,singleton=null,count2=0,
 * 然後進行初始化進行賦值階段,當執行到new Singleton()時,會執行構造方法,執行完構造方法後count1=1,count2=2,
 * 然後執行到count2=0進行賦值,導致1被覆蓋成0
 */
public class Test6 {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        System.out.println(Singleton.count1);
        System.out.println(Singleton.count2);
    }
}

class Singleton{

    public static int count1;

    private static Singleton singleton = new Singleton();

    public static int count2 = 0;
    private Singleton(){
        count1++;
        count2++;
    }
    public static Singleton getInstance(){
        return singleton;
    }
}

 

靜態變量的聲明語句,以及靜態代碼塊都被看做類的初始化語句,java虛擬機會按照初始化語句在類文件中的先後順序來依次執行它們。

package shengsiyuan.capter01;

/**
 * 調用classloader的loadClass方法只會加載類,不會對類進行初始化
 * 使用Class.forName()加載類會對類進行初始化
 */
public class Test7 {
    public static void main(String[] args) throws Exception {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        Class<?> clazz =  classLoader.loadClass("shengsiyuan.capter01.GL");
        System.out.println(clazz);
        clazz = Class.forName("shengsiyuan.capter01.GL");
        System.out.println(clazz);
    }
}
class GL{
    static {
        System.out.println("invoke static block");
    }
}

ClassLoader中對於數組類型的加載解釋:

Class objects for array classes are not created by class loaders, but are created automatically as required by the Java runtime. The class loader for an array class, as returned by Class.getClassLoader() is the same as the class loader for its element type; if the element type is a primitive type, then the array class has no class loader.

package shengsiyuan.capter01;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

/**
 * 自定義類加載器,具體加載過程參見ClassLoader java doc文檔
 */
public class MyClassLoader extends ClassLoader{

    private String classLoaderName;
    private static final String extention = ".class";

    public MyClassLoader(String classLoaderName){
        super();
        this.classLoaderName = classLoaderName;
    }

    public MyClassLoader(String classLoaderName,ClassLoader parent){
        super(parent);
        this.classLoaderName = classLoaderName;
    }

    @Override
    public String toString() {
        return "MyClassLoader{" +
                "classLoaderName='" + classLoaderName + '\'' +
                '}';
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] b = this.loadClassData(name);
        return this.defineClass(name,b,0,b.length);
    }

    private byte[] loadClassData(String name){
        // com.shengsiyuan.Test-->com/shengsiyuan/Test.class
        String path = name.replace(".","/")+extention;
        byte[] data = null;
        FileInputStream inputStream = null;
        ByteArrayOutputStream outputStream = null;
        try{
            inputStream = new FileInputStream(new File(path));
            int d ;
            while (-1 !=(d = inputStream.read())){
                outputStream.write(d);
            }
            data = outputStream.toByteArray();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if(null!=inputStream){
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(null!=outputStream){
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return data;
    }

    public void test(ClassLoader classLoader)throws Exception{
        Class<?> loadClass = classLoader.loadClass("shengsiyuan.capter01.Test1");
        Object instance = loadClass.newInstance();
        System.out.println(instance);
        ((Test1)instance).test();
    }

    public static void main(String[] args)throws Exception {
        MyClassLoader classLoader = new MyClassLoader("test1");
        classLoader.test(classLoader);
    }
}

關於命名空間的重要說明:

1.子加載器所加載的類能夠訪問父加載器所加載的類

2.父加載器所加載的類無法訪問子加載器所加載的類

類加載器的雙親委託模型的好處:

1.可以確保java核心庫的類型安全:所有java應用都至少會引用java.lang.Object類,也就是說在運行期,java.lang.Object這個類會被加載到java虛擬機中;如果這個加載過程是由java應用自己的類加載器所完成的,那麼很可能就會在JVM中存在多個版本的java.lang.Object類,而且這些類之間還是不兼容的,相互不可見的(正是命名空間在發揮作用)。

藉助與雙親委託機制,java核心類庫中的類的加載工作都是由啓動類加載器來統一完成,從而確保了java應用所使用的都是同一個版本的java核心類庫,他們之間是相互不兼容的。

2.可以確保java核心類庫所提供的類不會被自定義的類所替代。

3.不同的類加載器可以爲相同名稱(binary name)的類創建額外的命名空間,相同名稱的類可以並存在java虛擬機中,只需要用不用的類加載器來加載他們即可,不同類加載器所加載的類之間是不兼容的,這就相當於在java虛擬機內部創建了一個有一個相互兼容的java類空間,這類技術在很多框架中得到了實際應用。

在運行期,一個java類是由該類的完全限定名(binary name,二進制名)和用於加載該類的定義類加載器(defining loader)所共同決定的。如果同樣名字(即相同的完全限定名)的類是由兩個不同的加載器所加載,那麼這些類就是不同的,即便.class文件的字節碼完全一樣,並且從相同的位置加載亦如此

內建於JVM中的啓動類加載器會加載java.lang.ClassLoader以及其他java平臺類,當JVM啓動時,一塊特殊的機器碼會運行,他會加載擴展類加載器與系統類加載器,這塊特殊的機器碼叫做啓動類加載器。 啓動類加載器並不是java類,而其他的加載器則都是java類,啓動類加載器是特定於平臺的機器指令,它負責開啓整個加載過程。 所有類加載器(除了啓動類加載器)都被實現爲java類。不過,總歸要有一個組件來加載第一個java類加載器,從而讓整個加載過程能夠順利進行下去,加載第一個純java類加載器就是啓動類加載器的職責。 啓動類加載器還會負責加載供JRE正常運行所需要的基本組件,這包括java.util與java.lang包中的類等等

package shengsiyuan.capter01;

/*
    當前類加載器(Current Classloader)

    每個類都會使用自己的額類加載器(即加載自身的類加載器)來去加載其他類(指
    的是所依賴的類),如果ClassX引用了ClassY,那麼ClassX的類加載器就會去加載ClassY(前提是ClassY尚未被加載)
    
    線程上下文類加載器(Context Classloader)
    
    線程上下文類加載器是從JDK1.2開始引入的,類Thread中的getContextClassloader()與setContextClassLoader(ClassLoader cl)分
    別用來獲取和設置上下文類加載器
    
    如果沒有通過setContextClassLoader(ClassLoader cl)進行設置的話,線程將繼承其父線程的上下文類加載器。java應用運行時的初始線
    程的上下文類加載器是系統類加載器。在線程中運行的代碼可以通過該類加載器來加載類與資源。
    
    線程上下文類加載器的重要性:
    
    SPI(Service Provider Interface)
    
    父ClassLoader可以使用當前線程Thread.currentThread().getContextClassLoader()所指定的ClassLoader加載的類。這就改變的父
    ClassLoader不能使用子ClassLoader或是其他沒有直接父子關係的ClassLoader加載的類的情況,即改變了雙親委託模型。
    
    線程上下文類加載器就是當前線程的Current ClassLoader
    
    在雙親委託模型下,類加載是由下至上的,即下層的類加載器會委託上層進行加載。但是對於SPI來說,有些接口是java核心庫所提供的,
    而java核心庫是由啓動類加載器來加載的,而這些接口的實現卻來自於不同的jar包(廠商提供,如JDBC),java的啓動類加載器是不會加載
    其他來源的jar包,且類加載器加載的類下層能訪問上層,但是上層加載的類無法訪問到下層類加載器加載的類,這樣傳統的雙親委派模型就
    無法滿足SPI的要求。而通過給當前線程設置上下文類加載器,就可以由設置的上下文類加載器來實現對接口實現類的加載

線程上下文類加載器的一般使用模式(獲取-使用-還原)
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try{
    Thread.currentThread.setContextClassLoader(targetThreadContextClassLoader);
    myMethod()
}finally{
    Thread.currentThread.setContextClassLoader(classLoader);
}
myMethod裏面則調用了Thread.currentThread().getContextClassLoader(),獲取當前線程的上下文類加載器做某些事情。
如果一個類由類加載器A加載,那麼這個類的依賴類也是由相同的類加載器加載的(如果該依賴類之前沒有被加載過的話)。
ContextClassLoader的作用就是爲了破壞java的類加載委託機制。
當高層提供了統一的接口讓低層去實現,同時又要在高層加載(或實例化)低層類的時候,就必須要通過線程上下文類加載器來幫助高層的ClassLoader找到並加載該類。

java的雙親委託機制限制了只能沿着類加載層級由下向上訪問,線程上下文類加載相當於提供了由上向下訪問的入口


 */
public class Test8 {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getContextClassLoader());
        System.out.println(Thread.class.getClassLoader());
    }
}

/*
    此處體現了使用當前線程上下文classLoader加載數據庫接口實現類資源,如果設置當前線程的類加載器是擴展類加載器,則不會從classpath的jar包中查找實現類,則獲取到的Driver接口實現類爲空,如果使用系統類加載器進行加載,則默認會加載classpath的jar包中的實現類,則能夠加載到mysql驅動實現類
*/
class Test8 {
    public static void main(String[] args) {
//        Thread.currentThread().setContextClassLoader(Test8.class.getClassLoader().getParent());
        ServiceLoader<Driver> load = ServiceLoader.load(Driver.class);
        Iterator<Driver> iterator = load.iterator();
        while (iterator.hasNext()){
            System.out.println();
            Driver driver = iterator.next();
            System.out.println("driver: "+driver.getClass()+", load: "+driver.getClass().getClassLoader());
        }
        System.out.println("當前線程上下文加載器:"+Thread.currentThread().getContextClassLoader());
        System.out.println("serviceLoader的類加載器:"+load.getClass().getClassLoader());
    }
}

研究mysql加載過程:

Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("");

JVM設置參數規則:

-XX:+<option>   表示開啓option選項

-XX:-<option>    表示關閉option選項

-XX:<option>=<value>  表示將option選項的值設置爲true

JVM常見參數設置:

-XX:+TraceClassLoading,用於追蹤類的加載信息並打印出來

-XX:+TraceClassUnloading,用戶追蹤類的卸載信息並打印出來

 JVM中常用命令:

javap -c xxxx.class,反編譯class文件

JVM中助記符:

idc表示將int、float或是String類型的常量值從常量池中推送至棧頂

bipush表示將單字節(-128~127)的常量值推送至棧頂

sipush表示將一個短整型常量值(-32768~32767)推送至棧頂

iconst_1表示將int類型1對推送至棧頂(iconst_1~iconst_5)

anewarray:表示創建一個引用類型的(如類、接口、數組)數組,並將其引用值壓入棧頂

newarray:表示創建一個指定的原始類型(如int、float、char等)的數組,並將其引用值壓入棧頂

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章