.Class
文件,文件須要有特定的標識(cafe babe
)。ClassLoader
只負責.Class
文件的加載,至於它是否能夠運行,由執行引擎決定。.Class
文件中常量池部分的內存映射).Class
文件被解析加載到 JVM,類的對象加載到堆區,類信息被加載到方法區(java8 中方法區的實現是「元空間」)。這部分工做是類加載子系統完成的假設定義了一個類,名爲HelloWorld
,運行其 Main 方法,流程如圖:
html
加載(Loading)是狹義上的加載,「類加載」中的加載是廣義上的。java
java.lang.Class
對象,做爲方法區這個類的各類數據的訪問入口。加載.class文件的方式:數據庫
連接可細分爲三步:驗證(verify)準備(prepare)、解析(resolve)編程
static int a = 2;
final
修飾的static
,由於final
修飾的變量再也不是變量而是不可更改的常量,在編譯期就已經分配內存,準備階段會將其顯示初始化。如上面的賦值語句,加上final
後,a的值就被初始化爲2。final static int a = 2;
<clinit>()
的過程。static int a = 1;
或者靜態代碼塊static {}
時纔會建立並執行該方法。<clinit>()
中指令按語句在源文件中出現的順序執行。具體表現就是一個靜態變量的最後的取值決定於最後一行它的賦值語句。public class ClassInitTest { private static int num = 1; static{ num = 2; number = 20; System.out.println(num); //System.out.println(number);//報錯:非法的前向引用。 } private static int number = 10; //linking之prepare: number = 0 --> initial: 20 --> 10 public static void main(String[] args) { System.out.println(ClassInitTest.num);//2 System.out.println(ClassInitTest.number);//10 } }
在以上代碼中,num和number的最終值分別爲2和10。
4. <clinit>()
不一樣於咱們常說的類的構造函數。類的構造函數在虛擬機中是<init>()
,在任什麼時候候都是會建立的,由於全部的類都至少有一個默認無參構造函數。
5. 若該類具備父類,JVM會保證子類的 <clinit>()
執行前,父類的 <clinit>()
已經執行完畢。
6. 虛擬機必須保證一個類的 <clinit>()
方法在多線程下被同步加鎖。保證同一個類只被虛擬機加載一次(只調用一次 <clinit>()
),後續其它線程再使用類,只須要在虛擬機的緩存中獲取便可。bootstrap
類加載分爲啓動類加載器、擴展類加載器、應用程序類加載器(系統類加載器)、自定義加載器。以下圖:
api
須要注意的是,它們四者並不是子父類的繼承關係。如下展現瞭如何獲取類加載器:數組
public class ClassLoaderTest { public static void main(String[] args) { //獲取系統類加載器 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 //獲取其上層:擴展類加載器 ClassLoader extClassLoader = systemClassLoader.getParent(); System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d //獲取其上層:獲取不到引導類加載器 ClassLoader bootstrapClassLoader = extClassLoader.getParent(); System.out.println(bootstrapClassLoader);//null //對於用戶自定義類來講:默認使用系統類加載器進行加載 ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 //String類使用引導類加載器進行加載的。---> Java的核心類庫都是使用引導類加載器進行加載的。 ClassLoader classLoader1 = String.class.getClassLoader(); System.out.println(classLoader1);//null } }
關於幾種類加載器具體的加載對象和雙親委派機制能夠參考:玩命學JVM-類加載機制緩存
步驟安全
java.lang.ClassLoade
r的方式,實現本身的類加載器。loadClass()
,而是重寫findClass()
。URLClassLoade
r,這樣能夠避免本身去編寫findClass()方法以及其獲取字節碼流的方式,使其自定義類加載器編寫更加簡潔。public class CustomClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] result = getClassFromCustomPath(name); if(result == null){ throw new FileNotFoundException(); }else{ return defineClass(name,result,0,result.length); } } catch (FileNotFoundException e) { e.printStackTrace(); } throw new ClassNotFoundException(name); } private byte[] getClassFromCustomPath(String name){ //從自定義路徑中加載指定類:細節略 //若是指定路徑的字節碼文件進行了加密,則須要在此方法中進行解密操做。 return null; } public static void main(String[] args) { CustomClassLoader customClassLoader = new CustomClassLoader(); try { Class<?> clazz = Class.forName("One",true,customClassLoader); Object obj = clazz.newInstance(); System.out.println(obj.getClass().getClassLoader()); } catch (Exception e) { e.printStackTrace(); } } }
它是一個抽象類,其後全部的類加載器都繼承自ClassLoader
(不包括啓動類加載器)。
API網絡
getParent()
loadClass(String name)
findClass(String name)
findLoaderClass(String name)
defineClass(String name, byte[] b, int off, int len)
resolveClass(Class<?>c)
詳見 http://www.javashuo.com/article/p-cloinoxk-mo.html
自定義的一個java.lang.String不能加載到的JVM中,緣由:
使用自定義的java.lang.String時,首先是應用類加載器向上委託到擴展類加載器,而後擴展類加載器向上委託給引導類加載器,引導類加載接收到類的信息,發現該類的路徑時「java.lang.String」,這在引導類加載器的加載範圍內,所以引導類加載器開始加載「java.lang.String」,只不過此時它加載的是jdk核心類庫裏的「java.lang.String」。這就是雙親委派機制中的向上委託。在完成向上委託以後,如到了引導類加載器,引導類加載器發現待加載的類不屬於本身加載的類範圍,就會再向下委託給擴展類加載器,讓下面的加載器進行類的加載。
優點
沙箱安全機制
上文中提到的java.lang.String就是沙箱安全機制的表現,保證了對java核心源代碼的保護。
首先來看一下百度百科的定義:
在計算機科學中, 字面量(literal)是用於表達源代碼中一個固定值的表示法(notation)。幾乎全部計算機編程語言都具備對基本值的字面量表示, 諸如: 整數, 浮點數以及字符串; 而有不少也對布爾類型和字符類型的值也支持字面量表示; 還有一些甚至對枚舉類型的元素以及像數組, 記錄和對象等複合類型的值也支持字面量表示法.
這段話不太好理解,咱們來拆解下(注意下面這段話純屬我的理解):
「字面量(literal)是用於表達源代碼中一個固定值的表示法(notation)」,這裏說明了兩點:第一,字面量是體如今源碼中的;第二,字面量是對一個固定值的表示。接下來它提到了「幾乎全部計算機編程語言都具備對基本值的字面量表示」,並以一些基本數據類型、枚舉、數組等數據類型舉例。它們都有一個特色,就是它們的賦值是能夠作到「代碼可視化的」。你能夠在代碼中給以上提到的類型進行賦值。而咱們賦值時所給出的「值」,更準確來講是一種「表示」(好比給數組賦值時,約定了須要用大括號括起來)就是字面量的含義。
舉個例子:
int i = 1; String s = "abs"; int[] a = {1, 3, 4}; // 以上 1,「abc」,{1,3,4}均是字面量
一樣,先來看一下書面定義:
符號引用:符號引用以一組符號來描述所引用的目標,符號引用能夠是任何形式的字面量,只要使用時可以無歧義的定位到目標便可。好比org.simple.People類引用了org.simple.Language類,在編譯時People類並不知道Language類的實際內存地址,所以只能使用符號org.simple.Language(假設是這個,固然實際中是由相似於CONSTANT_Class_info的常量來表示的)來表示Language類的地址。各類虛擬機實現的內存佈局可能有所不一樣,可是它們能接受的符號引用都是一致的,由於符號引用的字面量形式明肯定義在Java虛擬機規範的Class文件格式中。
直接引用: 直接引用能夠是
(1)直接指向目標的指針(好比,指向「類型」【Class對象】、類變量、類方法的直接引用多是指向方法區的指針)
(2)相對偏移量(好比,指向實例變量、實例方法的直接引用都是偏移量)
(3)一個能間接定位到目標的句柄
直接引用是和虛擬機的佈局相關的,同一個符號引用在不一樣的虛擬機實例上翻譯出來的直接引用通常不會相同。若是有了直接引用,那引用的目標一定已經被加載入內存中了。
說實話看這種書面化語言抽象晦澀,下面給出一些本身的理解吧。首先找到一個.class文件(來源:玩命學JVM(一))反編譯後的結果中常量池的部分:
Constant pool: #1 = Methodref #6.#15 // java/lang/Object."<init>":()V #2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #18 // Hello World #4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #21 // Main #6 = Class #22 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 main #12 = Utf8 ([Ljava/lang/String;)V #13 = Utf8 SourceFile #14 = Utf8 Main.java #15 = NameAndType #7:#8 // "<init>":()V #16 = Class #23 // java/lang/System #17 = NameAndType #24:#25 // out:Ljava/io/PrintStream; #18 = Utf8 Hello World #19 = Class #26 // java/io/PrintStream #20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V #21 = Utf8 Main #22 = Utf8 java/lang/Object #23 = Utf8 java/lang/System #24 = Utf8 out #25 = Utf8 Ljava/io/PrintStream; #26 = Utf8 java/io/PrintStream #27 = Utf8 println #28 = Utf8 (Ljava/lang/String;)V
爲何只看常量池呢,由於在「解析」的定義中提到了:解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。咱們來看看常量池中有什麼:
常量池:常量池指的是字節碼文件中的Constant pool部分。它是靜態的,當編譯生成字節碼文件直接就不變了。常量池中包括各類字面量和對類型、域和方法的符號引用。幾種在常量池內存儲的數據類型包括:數量值、字符串值、類引用、字段引用、方法引用。
由此咱們能夠看出,上面咱們給出的常量池中都屬於「符號引用」(符號引用自己就是一種字面量)或字面量。咱們不由要問了了,那直接引用在哪呢?
我找到了《深刻理解Java虛擬機》中的一句話:
對同一個符號引用進行屢次解析請求是很常見的事情,除invokedynamic指令外,虛擬機實現能夠對第一次解析的結果進行緩存(在運行時常量池中記錄直接引用,並把常量標識爲已解析狀態)從而避免解析動做重複進行。
關鍵是括號中話給了啓發,說明直接引用是放在運行時常量池中的,接下來咱們看看運行時常量池的一些定義或特性。
運行時常量池是方法區的一部分。常量池用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後存放到方法區的運行時常量池中。而加載類和接口到虛擬機後,就會建立對應的運行時常量池。
運行時常量池與常量池的不一樣點在於:
1. 運行時常量池中包括了編譯期就已經明確的數值字面量,也包括在運行期解析後才能得到的方法或字段引用。但,請注意,此時的方法或字段引用已經再也不是常量池中的「符號引用」,而是「直接引用」。
2. 運行時常量池具有「動態性」。
至此,關於「符號引用」和「直接引用」的解釋就差很少了。最後我再多說一句,再分析的時候,我一直在疑惑「直接引用」究竟是什麼,我能不能像看到常量池中的內容同樣看到「直接引用」。實際上,咱們並不能拿到這樣一個文件,裏面整齊地寫了直接引用的具體內容,由於直接引用不是所謂的「字面量」。但咱們能夠回到「直接引用」的最初定義:直接引用能夠是指向目標的指針、相對偏移量或是能間接定位到目標的句柄,能夠想象一下在運行時,在內存中存放的直接引用大概是什麼內容。
JVM 中兩個Class對象是否爲同一個類的必要條件
類的主動使用和被動使用
主動使用和被動使用的區別是,主動使用會致使類的初始化。
主動使用有如下七種狀況:
參考文獻:
https://blog.csdn.net/u011069294/article/details/107489721
http://www.javashuo.com/article/p-huylhhxc-mq.html