1. 程式人生 > >阿里面試題,深入理解Java類載入機制

阿里面試題,深入理解Java類載入機制

類的生命週期

包括以下 7 個階段:

  • 載入(Loading)
  • 驗證(Verification)
  • 準備(Preparation)
  • 解析(Resolution)
  • 初始化(Initialization)
  • 使用(Using)
  • 解除安裝(Unloading)

其中解析過程在某些情況下可以在初始化階段之後再開始,這是為了支援 Java 的動態繫結。

類初始化時機

虛擬機器規範中並沒有強制約束何時進行載入,但是規範嚴格規定了有且只有下列五種情況必須對類進行初始化(載入、驗證、準備都會隨著發生):

  • 遇到 new、getstatic、putstatic、invokestatic 這四條位元組碼指令時,如果類沒有進行過初始化,則必須先觸發其初始化。最常見的生成這 4 條指令的場景是:使用 new 關鍵字例項化物件的時候;讀取或設定一個類的靜態欄位(被 final 修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候;以及呼叫一個類的靜態方法的時候。

  • 使用 java.lang.reflect 包的方法對類進行反射呼叫的時候,如果類沒有進行初始化,則需要先觸發其初始化。

  • 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。

  • 當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含 main() 方法的那個類),虛擬機器會先初始化這個主類;

  • 當使用 JDK 1.7 的動態語言支援時,如果一個 java.lang.invoke.MethodHandle 例項最後的解析結果為 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法控制代碼,並且這個方法控制代碼所對應的類沒有進行過初始化,則需要先觸發其初始化;

以上 5 種場景中的行為稱為對一個類進行主動引用。除此之外,所有引用類的方式都不會觸發初始化,稱為被動引用。被動引用的常見例子包括:

  • 通過子類引用父類的靜態欄位,不會導致子類初始化。
System.out.println(SubClass.value); // value 欄位在 SuperClass 中定義
  • 通過陣列定義來引用類,不會觸發此類的初始化。該過程會對陣列類進行初始化,陣列類是一個由虛擬機器自動生成的、直接繼承自 Object 的子類,其中包含了陣列的屬性和方法。
SuperClass[] sca = new SuperClass[10];
  • 常量在編譯階段會存入呼叫類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。
System.out.println(ConstClass.HELLOWORLD);

類載入過程

包含了載入、驗證、準備、解析和初始化這 5 個階段。

1. 載入

載入是類載入的一個階段,注意不要混淆。

載入過程完成以下三件事:

  • 通過一個類的全限定名來獲取定義此類的二進位制位元組流。
  • 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時儲存結構。
  • 在記憶體中生成一個代表這個類的 Class 物件,作為方法區這個類的各種資料的訪問入口。

其中二進位制位元組流可以從以下方式中獲取:

  • 從 ZIP 包讀取,這很常見,最終成為日後 JAR、EAR、WAR 格式的基礎。
  • 從網路中獲取,這種場景最典型的應用是 Applet。
  • 執行時計算生成,這種場景使用得最多得就是動態代理技術,在 java.lang.reflect.Proxy 中,就是用了 ProxyGenerator.generateProxyClass 的代理類的二進位制位元組流。
  • 由其他檔案生成,典型場景是 JSP 應用,即由 JSP 檔案生成對應的 Class 類。
  • 從資料庫讀取,這種場景相對少見,例如有些中介軟體伺服器(如 SAP Netweaver)可以選擇把程式安裝到資料庫中來完成程式程式碼在叢集間的分發。 ...

2. 驗證

確保 Class 檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。

  • 檔案格式驗證:驗證位元組流是否符合 Class 檔案格式的規範,並且能被當前版本的虛擬機器處理。
  • 元資料驗證:對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合 Java 語言規範的要求。
  • 位元組碼驗證:通過資料流和控制流分析,確保程式語義是合法、符合邏輯的。
  • 符號引用驗證:發生在虛擬機器將符號引用轉換為直接引用的時候,對類自身以外(常量池中的各種符號引用)的資訊進行匹配性校驗。

3. 準備

類變數是被 static 修飾的變數,準備階段為類變數分配記憶體並設定初始值,使用的是方法區的記憶體。

例項變數不會在這階段分配記憶體,它將會在物件例項化時隨著物件一起分配在 Java 堆中。(例項化不是類載入的一個過程,類載入發生在所有例項化操作之前,並且類載入只進行一次,例項化可以進行多次)

初始值一般為 0 值,例如下面的類變數 value 被初始化為 0 而不是 123。

public static int value = 123;

如果類變數是常量,那麼會按照表達式來進行初始化,而不是賦值為 0。

public static final int value = 123;

4. 解析

將常量池的符號引用替換為直接引用的過程。

5. 初始化

初始化階段才真正開始執行類中的定義的 Java 程式程式碼。初始化階段即虛擬機器執行類構造器 <clinit>() 方法的過程。

在準備階段,類變數已經賦過一次系統要求的初始值,而在初始化階段,根據程式設計師通過程式制定的主觀計劃去初始化類變數和其它資源。

<clinit>() 方法具有以下特點:

  • 是由編譯器自動收集類中所有類變數的賦值動作和靜態語句塊(static{} 塊)中的語句合併產生的,編譯器收集的順序由語句在原始檔中出現的順序決定。特別注意的是,靜態語句塊只能訪問到定義在它之前的類變數,定義在它之後的類變數只能賦值,不能訪問。例如以下程式碼:
public class Test {
    static {
        i = 0;                // 給變數賦值可以正常編譯通過
        System.out.print(i);  // 這句編譯器會提示“非法向前引用”
    }
    static int i = 1;
}
  • 與類的建構函式(或者說例項構造器 <init>())不同,不需要顯式的呼叫父類的構造器。虛擬機器會自動保證在子類的 <clinit>() 方法執行之前,父類的 <clinit>() 方法已經執行結束。因此虛擬機器中第一個執行 <clinit>() 方法的類肯定為 java.lang.Object。

  • 由於父類的 <clinit>() 方法先執行,也就意味著父類中定義的靜態語句塊要優於子類的變數賦值操作。例如以下程式碼:

static class Parent {
    public static int A = 1;
    static {
        A = 2;
    }
}

static class Sub extends Parent {
    public static int B = A;
}

public static void main(String[] args) {
     System.out.println(Sub.B);  // 輸出結果是父類中的靜態變數 A 的值,也就是 2。
}
  • <clinit>() 方法對於類或介面不是必須的,如果一個類中不包含靜態語句塊,也沒有對類變數的賦值操作,編譯器可以不為該類生成 <clinit>() 方法。

  • 介面中不可以使用靜態語句塊,但仍然有類變數初始化的賦值操作,因此介面與類一樣都會生成 <clinit>() 方法。但介面與類不同的是,執行介面的 <clinit>() 方法不需要先執行父介面的 <clinit>() 方法。只有當父介面中定義的變數使用時,父接口才會初始化。另外,介面的實現類在初始化時也一樣不會執行介面的 <clinit>() 方法。

  • 虛擬機器會保證一個類的 <clinit>() 方法在多執行緒環境下被正確的加鎖和同步,如果多個執行緒同時初始化一個類,只會有一個執行緒執行這個類的 <clinit>() 方法,其它執行緒都會阻塞等待,直到活動執行緒執行 <clinit>() 方法完畢。如果在一個類的 <clinit>() 方法中有耗時的操作,就可能造成多個執行緒阻塞,在實際過程中此種阻塞很隱蔽。

類載入器

實現類的載入動作。在 Java 虛擬機器外部實現,以便讓應用程式自己決定如何去獲取所需要的類。

類與類載入器

兩個類相等:類本身相等,並且使用同一個類載入器進行載入。這是因為每一個類載入器都擁有一個獨立的類名稱空間。

這裡的相等,包括類的 Class 物件的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回結果為 true,也包括使用 instanceof 關鍵字做物件所屬關係判定結果為 true。

類載入器分類

從 Java 虛擬機器的角度來講,只存在以下兩種不同的類載入器:

  • 啟動類載入器(Bootstrap ClassLoader),這個類載入器用 C++ 實現,是虛擬機器自身的一部分;

  • 所有其他類的載入器,這些類由 Java 實現,獨立於虛擬機器外部,並且全都繼承自抽象類 java.lang.ClassLoader。

從 Java 開發人員的角度看,類載入器可以劃分得更細緻一些:

  • 啟動類載入器(Bootstrap ClassLoader)此類載入器負責將存放在 <JAVA_HOME>\lib 目錄中的,或者被 -Xbootclasspath 引數所指定的路徑中的,並且是虛擬機器識別的(僅按照檔名識別,如 rt.jar,名字不符合的類庫即使放在 lib 目錄中也不會被載入)類庫載入到虛擬機器記憶體中。啟動類載入器無法被 Java 程式直接引用,使用者在編寫自定義類載入器時,如果需要把載入請求委派給啟動類載入器,直接使用 null 代替即可。

  • 擴充套件類載入器(Extension ClassLoader)這個類載入器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它負責將 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系統變數所指定路徑中的所有類庫載入到記憶體中,開發者可以直接使用擴充套件類載入器。

  • 應用程式類載入器(Application ClassLoader)這個類載入器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。由於這個類載入器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般稱為系統類載入器。它負責載入使用者類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。

雙親委派模型

應用程式都是由三種類載入器相互配合進行載入的,如果有必要,還可以加入自己定義的類載入器。

下圖展示的類載入器之間的層次關係,稱為類載入器的雙親委派模型(Parents Delegation Model)。該模型要求除了頂層的啟動類載入器外,其餘的類載入器都應有自己的父類載入器。這裡類載入器之間的父子關係一般通過組合(Composition)關係來實現,而不是通過繼承(Inheritance)的關係實現。

(一)工作過程

一個類載入器首先將類載入請求傳送到父類載入器,只有當父類載入器無法完成類載入請求時才嘗試載入。

(二)好處

使得 Java 類隨著它的類載入器一起具有一種帶有優先順序的層次關係,從而是的基礎類得到統一。

例如 java.lang.Object 存放在 rt.jar 中,如果編寫另外一個 java.lang.Object 的類並放到 ClassPath 中,程式可以編譯通過。因為雙親委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 優先順序更高,因為 rt.jar 中的 Object 使用的是啟動類載入器,而 ClassPath 中的 Object 使用的是應用程式類載入器。正因為 rt.jar 中的 Object 優先順序更高,因為程式中所有的 Object 都是這個 Object。

(三)實現

以下是抽象類 java.lang.ClassLoader 的程式碼片段,其中的 loadClass() 方法執行過程如下:先檢查類是否已經載入過,如果沒有則讓父類載入器去載入。當父類載入器載入失敗時丟擲 ClassNotFoundException,此時嘗試自己去載入。

public abstract class ClassLoader {
    // The parent class loader for delegation
    private final ClassLoader parent;

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
}

自定義類載入器實現

FileSystemClassLoader 是自定義類載入器,繼承自 java.lang.ClassLoader,用於載入檔案系統上的類。它首先根據類的全名在檔案系統上查詢類的位元組程式碼檔案(.class 檔案),然後讀取該檔案內容,最後通過 defineClass() 方法來把這些位元組程式碼轉換成 java.lang.Class 類的例項。

java.lang.ClassLoader 類的方法 loadClass() 實現了雙親委派模型的邏輯,因此自定義類載入器一般不去重寫它,而是通過重寫 findClass() 方法。

public class FileSystemClassLoader extends ClassLoader {

    private String rootDir;

    public FileSystemClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getClassData(String className) {
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private String classNameToPath(String className) {
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }
}

免費Java資料需要自己領取,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高併發分散式等教程,一共30G。 
傳送門: https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q

相關推薦

阿里試題深入理解Java載入機制

類的生命週期 包括以下 7 個階段: 載入(Loading) 驗證(Verification) 準備(Pr

BAT經典試題深入理解Java記憶體模型JMM

Java 記憶體模型 Java 記憶體模型(JMM)是一種抽象的概念,並不真實存在,它描述了一組規則或規範,通過這組規範定義了

深入理解Java載入機制(一)

1 前言: 在上一篇文章一文讓你明白 Java 位元組碼中, 我們瞭解了java位元組碼的解析過程,那麼在接下來的內容中,我們來了解一下類的載入機制。 2 題外話 Java的核心是什麼?當然是JVM了,所以說了解並熟悉JVM對於我們理解Java語言非常重要,不管你是做Java還是Andr

深入理解Java載入

本文目的: 深入理解Java類載入機制; 理解各個類載入器特別是執行緒上下文載入器; Java虛擬機器類載入機制 虛擬機器把描述類的資料從 Class 檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的 Java 型別,這就是虛擬機器的類載入機制。 在Java語言裡面,

深入理解JVM載入機制

深入理解JVM類載入機制 轉載自:https://blog.csdn.net/a724888/article/details/78396462 簡述:虛擬機器把描述類的資料從class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這就是虛

阿里巴巴試題java載入機制

首先很經典的阿里巴巴面試題 加上我自己的一些疑惑程式碼 public class Text { public static int k = 0; public final int k1 = 3; //自己加的 public static Text t1 = new Text("

阿里P7帶你深入理解Java虛擬機器總結——初始化過程

類的初始化過程 非法向前引用 編譯器手機的順序是由語句在原始檔中出現的順序決定的,靜態語句塊中只能訪問到定義在靜態語句之前的變數,定義它之後的變數,可以賦值,但不能訪問 public class Test{ static{ i=0; system.out.print(

深入理解Java型信息(Class對象)與反射機制

成員變量 字段 機制 () 程序 轉換 默認 數據 統一   深入理解Class對象    RRTI的概念以及Class對象作用    認識Class對象之前,先來了解一個概念,RTTI(Run-Time Type Identification)運行時類型識別,對於這個詞一

深入理解Java加載器(1):Java加載原理解析

排查 中比 失敗 bootstrap class 信息 dex jvm java類 轉自:http://blog.csdn.net/zhoudaxia/article/details/35824249 1 基本信息   每個開發人員對java.lang.ClassNot

深入理解Java加載器(2)

同時 例如 char account 而且 request 系統屬性 launch write 1 基本信息   每個開發人員對Java.lang.ClassNotFoundExcetpion這個異常肯定都不陌生,這背後就涉及到了java技術體系中的類加載。Java

深入理解Java加載器(1)

介紹 生命 等等 inf java_home col 原因 就是 校驗 類加載器概述:   java類的加載是由虛擬機來完成的,虛擬機把描述類的Class文件加載到內存,並對數據進行校驗,解析和初始化,最終形成能被java虛擬機直接使用的java類型,這就是虛擬機的類加載

深入理解Java加載器(二):線程上下文加載器

解決 介紹 chan undle 調用父類 攔截 decision native object 摘要:   博文《深入理解Java類加載器(一):Java類加載原理解析》提到的類加載器的雙親委派模型並不是一個強制性的約束模型,而是Java設計者推薦給開發者的類加載器的實現方

Java試題】之類載入:從試題分析Java載入機制

 “載入”(Loading)階段是“類載入”(Class Loading)過程的第一個階段,在此階段,虛擬機器需要完成以下三件事情:        1、 通過一個類的全限定名來獲取定義此類的二進位制位元組流。        2、 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結

Java基礎7——深入理解Java與包

java類與包 (文章部分內容轉載於How 2 Play Life ) *.Java檔案 問題:一個”.java”原始檔中是否可以包括多個類(不是內部類)?有什麼限制? 答案:可以有多個類,但只能有一個public的類,並且public的類名必須與檔名相一致。一個檔案

深入理解java版本衝突的問題

一:要解決的問題  我們在嚐鮮 JDK1.5 的時候,相信不少人遇到過 Unsupported major.minor version 49.0 錯誤,當時定會茫然不知所措。因為剛開始那會兒,網上與此相關的中文資料還不多,現在好了,網上一找就知道是如何解決,大多會告訴你

曹工雜談:一道阿里試題兩個執行緒交替列印奇偶數

一、前言 這些天忙著寫業務程式碼,曹工說Tomcat系列暫時沒時間寫,先隨便寫點其他的。 逛部落格園的時候,發現一篇園友的阿里面試文章,https://www.cnblogs.com/crossoverJie/p/9404789.html。 裡面提到了:兩個執行緒,交替列印奇偶數這道筆試題。 看了園友

深入理解 Java 垃圾回收機制

nbsp 循環引用 方式 不同的 整理 一個 復制 垃圾回收機制 提高 垃圾回收機制中的算法: 1.引用計數法:無法檢測出循環引用。如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數永遠不可能為0. 2 標記-清除算法:采用從根集合進行掃描,對存活

業余草分享試題JVM結構、GC工作機制詳解

影響 根節點 tac 關註 共享 產生 我想 tar 效果 題外話:最近在應聘阿裏2015暑期實習,感觸頗多。機會總是留給有準備的人的,所以平常一定要註意知識的鞏固和積累。知識的深度也要有一定的理解,不比別人知道的多,公司幹嘛選你?關於JVM和GC,我相信學java的絕大部

深入理解Java異常處理機制 (籠統篇)

throw 種類型 綜合 IV 算術 其它 wid all 作用 開篇 1.異常處理(Exception Handling):   就是一種解決這一問題的機制,能夠較好地處理程序不能正常運行的情況。 2.異常(Exception):   是程序在運行時可能出現的

深入理解java的反射機制(轉載)

今天將從以下4方面來系統的學習一下java的反射機制: java反射是什麼 java反射(Reflection)底層實現原理 java反射的簡單演示 java反射的應用場景 1,java反射是什麼 首先大家應該先了解兩個概念,編譯