書名: 深入解析Java虛擬機HotSpot作者名: 楊易本章字數: 2917字更新時間: 2021-01-07 11:18:27
2.1 類的加載
類加載過程先于虛擬機的絕大部分組件的加載過程,具體會在第4章講解。虛擬機初始化完成后做的第一件事情就是加載用戶指定的主類。類加載也是類可用機制的第一步,它負責定位并解析位于磁盤(通常)的字節碼文件,生成一個包含殘缺數據的用于在JVM內部表示類的數據結構,然后將該結構傳遞給下一步鏈接做后續工作。
2.1.1 字節碼
Java源碼通過javac(Java編譯器)編譯生成字節碼,然后將字節碼送入虛擬機運行。字節碼是Java源碼的一種緊湊的二進制表示,它相對于Java源碼來說比較低級,但是更符合機器模型,更容易被機器“理解”。以代碼清單2-1的Java代碼為例:
代碼清單2-1 加法示例源碼
public class Foo{ public static void main(String[] args){ int a· = 3; int b = a+2; System.out.println(b); } }
使用javac編譯Foo.java得到二進制字節碼文件Foo.class,但二進制的Foo.class難以被人類理解,為了直觀地查看編譯后的字節碼,可以使用JDK中的javap -verbose Foo.class輸出人類可讀的字節碼,部分輸出如代碼清單2-2所示:
代碼清單2-2 加法示例字節碼
0: iconst_3 1: istore_1 2: iload_1 3: iconst_2 4: iadd 5: istore_2 6: getstatic#2 // Field Ljava/io/PrintStream; 9: iload_2 10: invokevirtual#3 // Method java/io/PrintStream.println:(I)V 13: return
字節碼中的#2表示常量池索引2的位置,后面注釋說明了該位置表示被調用的方法,這樣后面的字節碼可以使用字節索引而不需要表示函數的字符串,在減少冗余的同時節省了空間。可以看到兩個變量相加被編譯成了棧操作:iconst_3壓入3到操作棧,istore_1讀取棧頂的3到變量a,然后iload_1讀取a并入棧,iconst_2壓入2,iadd彈出a和2并將結果入棧,istore_2將剛剛計算得到的結果即棧頂彈出放入b,最后輸出。
也正是由于字節碼對于源碼的描述是棧的形式,所以Java虛擬機屬于棧式機器(Stack Machine)。與之相對的是寄存器機器(Register Machine),如代碼清單2-3所示的Lua字節碼,它對上面加法的描述截然不同:
代碼清單2-3 luac -l -p生成的加法字節碼
-- lua源碼 a = 3 b = a + 2 io.write(b) -- lua字節碼 0+ params, 2 slots, 1 upvalue, 0 locals, 6 constants, 0 functions 1 [1] SETTABUP 0 -1 -2 ; _ENV "a" 3 2 [2] GETTABUP 0 0 -1 ; _ENV "a" 3 [2] ADD 0 0 -4 ; - 2 4 [2] SETTABUP 0 -3 0 ; _ENV "b" 5 [3] GETTABUP 0 0 -5 ; _ENV "io" 6 [3] GETTABLE 0 0 -6 ; "write" 7 [3] GETTABUP 1 0 -3 ; _ENV "b" 8 [3] CALL 0 2 1 9 [3] RETURN 0 1
寄存器機器的加法是直接使用add 0 0 -4指令完成的,它的操作數和指令組成一個整體,而棧式機器的iadd沒有操作數,它隱式地假設了一個操作數棧,用于存放iadd需要的數據,這是兩者的主要區別。
寄存器機器和棧式機器很大程度上是指虛擬機指令集(Instruction Set Architecture,ISA)的特點,與虛擬機本身如何實現并無關系。當然,這并不是說寄存器機器就是用寄存器執行指令的虛擬機,事實上,很多寄存器機器都是用數組模擬寄存器執行讀寫指令的。寄存器機器的指令集更緊湊,性能也可能更好;棧式機器的指令集易于編譯器生成,兩者各有千秋,并無絕對優勢的一方。
2.1.2 類加載器
在了解了Java字節碼的基本概念后,就可以步入類可用機制的世界了。前面提過,javac編譯器編譯得到字節碼,然后將字節碼送入虛擬機執行。實際上送入虛擬機的字節碼并不能立即執行,它與視頻文件、音頻文件一樣只是一串二進制序列,需要虛擬機加載并解析后才能執行,這個過程位于ClassLoader::load_class()。
ClassLoader是虛擬機內部使用的類加載器,即Bootstrap類加載器。除了Bootstrap類加載器外,HotSpot VM還有Platform類加載器和Application類加載器,它們三個依次構成父子關系(不是代碼意義上由繼承構造出來的父子關系,而是邏輯上的父子關系)。虛擬機使用雙親委派機制加載類。當需要加載類時,首先使用Application類加載器加載,由Application類加載器將這個任務委派給Platform類加載器,而Platform類加載器又將任務委派給Bootstrap類加載器,如果Bootstrap類加載器加載完成,那么加載任務就此終止。如果沒有加載完成,它會將任務返還給Platform類加載器等待加載,如果Platform類加載器也無法加載則又會將任務返還給Application類加載器加載。每個類加載器對應一些類的搜索路徑,如果所有類加載器都無法完成類的加載,則拋出ClassNotFoundException。雙親委派加載模型避免了類被重復加載,而且保證了諸如java.lang.Object、java.lang.Thread等核心類只能被Bootstrap類加載器加載。
在HotSpot VM中用ClassLoader表示類加載器,可以使用ClassLoader::load_class()加載磁盤上的字節碼文件,但是類加載器的相關數據卻是存放在ClassLoaderData,簡稱CLD。源碼中很多CLD字樣指的就是類加載器的數據。每個類加載器都有一個對應的CLD結構,這是一個重要的數據結構,如圖2-1所示。
CLD存放了所有被該ClassLoader加載的類、當前類加載器的Java對象表示、管理內存的metaspace等。另外CLD還指示了當前類加載器是否存活、是否需要卸載等。除此之外,CLD還有一個next字段指向下一個CLD,所有CLD連接起來構成一幅CLD圖,即ClassLoaderDataGraph。通過調用ClassLoaderDataGraph::classes_do可以在垃圾回收過程中很容易地遍歷該結構找到所有類加載器加載的所有類。
2.1.3 文件解析
ClassLoader::load_class()負責定位磁盤上字節碼文件的位置,讀取該文件的工作由類文件解析器ClassFileParser完成,如代碼清單2-4所示:
代碼清單2-4 類文件解析器
void ClassFileParser::parse_stream(...) { // 開始解析 stream->guarantee_more(8, CHECK); // 讀取字節碼文件開頭的魔數,即0xcafebabe const u4 magic = stream->get_u4_fast(); guarantee_property(magic == JAVA_CLASSFILE_MAGIC,...); // 讀取major/minor版本號 _minor_version = stream->get_u2_fast(); _major_version = stream->get_u2_fast(); // 讀取常量池 ... // 讀取this_class和super_class _this_class_index = stream->get_u2_fast(); Symbol* class_name_in_cp = cp->klass_name_at(_this_class_index); _class_name = class_name_in_cp; ... }
Java所有的類最終都繼承自Object類,每個類的常量池都會包含諸如“[java/lang/Object;”的字符串。為了節省內存,HotSpot VM用Symbol唯一表示常量池中的字符串,所有Symbol統一存放到SymbolTable中。SymbolTable是一個并發哈希表,虛擬機會根據該表中Symbol的哈希值判斷是返回已有的Symbol還是創建新的Symbol。
SymbolTable有個特別的地方:它使用引用計數管理Symbol。如果兩個類常量池都包含字符串“hello world”,當兩個類都卸載后該Symbol計數為0,且下一次垃圾回收的時候不會做可達性分析,而是直接清除。
在HotSpot VM中,SymbolTable還有個孿生兄弟StringTable。StringTable這個名字可能比較陌生,但是讀者一定見過String.intern(),如代碼清單2-5所示,String.intern()底層依托的正是StringTable:
代碼清單2-5 java.lang.String.intern()的實現
JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str)) JVMWrapper("JVM_InternString"); JvmtiVMObjectAllocEventCollector oam; if (str == NULL) return NULL; oop string = JNIHandles::resolve_non_null(str); oop result = StringTable::intern(string, CHECK_NULL); return (jstring) JNIHandles::make_local(env, result); JVM_END
String.intern()會返回一個字符串的標準表示。所謂標準表示是指對于相同字符串常量會返回唯一內存地址。StringTable則是用來存放這些標準表示的字符串的哈希容器。它沒有使用引用計數管理,是眾多類型的GC Root之一,在垃圾回收過程中會被當作根,以它為起點出發進行標記。虛擬機用戶可以使用參數-XX:+PrintStringTableStatistics在虛擬機退出時輸出StringTable和SymbolTable的統計信息,或者使用jcmd <pid> VM.stringtable在運行時輸出相關信息。
回到源碼的解析上,這個過程比較簡單,按照如代碼清單2-6所示的Java虛擬機規范中規定的字節碼文件格式讀取對應字節即可:
代碼清單2-6 字節碼文件格式
ClassFile { u4 magic; // 字節碼文件魔數,0xcafebabe u2 minor_version; // 主版本號 u2 major_version; // 次版本號 u2 constant_pool_count; // 常量池大小 cp_info constant_pool[constant_pool_count-1]; // 常量池 u2 access_flags; // 該類是否public,是否final u2 this_class; // 當前類在常量池的索引號 u2 super_class; // 父類在常量池的索引號 u2 interfaces_count; // 接口個數 u2 interfaces[interfaces_count]; // 接口 u2 fields_count; // 字段個數 field_info fields[fields_count]; // 字段 u2 methods_count; // 方法個數 method_info methods[methods_count]; // 方法 u2 attributes_count; // 屬性信息,比如是否內部類 attribute_info attributes[attributes_count]; // 屬性 }
Java虛擬機規范要求字節碼文件遵循大端序,并且要求字節碼文件最開始四個字節是魔數0xcafebabe,接下來兩個字節是主版本號等。類文件解析器根據Java虛擬機規范以大端的方式讀取四個字節并檢查其是否為正確的魔數,然后檢查主版本號,如此繼續即可。
類加載的最終任務是得到InstanceKlass對象。當parse_stream()解析完二進制的字節碼文件后,由類加載器為InstanceKlass分配所需內存,然后使用fill_instance_klass()結合解析得到的數據填充這片內存。InstanceKlass是HotSpot VM中一個非常重要的數據結構,java.lang.Class在Java層描述對象的類,而InstanceKlass在虛擬機層描述對象的類,它記錄類有哪些字段,名字是什么,類型是什么,類名是什么,解釋器如何執行它的方法等信息,關于InstanceKlass會在第3章詳細討論。類加載的一個完整流程如下:
1)分配InstanceKlass所需內存(InstanceKlass::allocate_instance_klass);
2)使用parse_stream()得到的數據填充InstanceKlass的字段,如major/minor version;
3)如果引入了miranda方法,設置對應flag(set_has_miranda_methods);
4)初始化itable(klassItable::setup_itable_offset_table);
5)初始化OopMapBlock(fill_oop_maps);
6)分配klass對應的java.lang.Class,在Java層描述類(java_lang_Class::create_mirror);
7)生成Java8的default方法(DefaultMethods::generate_default_methods);
8)得到完整的InstanceKlass。
- 多媒體CAI課件設計與制作導論(第二版)
- DevOps with Kubernetes
- 新手學Visual C# 2008程序設計
- 編程可以很簡單
- Java程序設計教程
- Learning D3.js 5 Mapping(Second Edition)
- Unity 3D UI Essentials
- 微服務設計
- Python Natural Language Processing
- OpenCV輕松入門:面向Python
- Hands-On Exploratory Data Analysis with Python
- Mastering Kali Linux for Advanced Penetration Testing(Second Edition)
- ASP.NET程序開發參考手冊
- 深入理解TypeScript
- Java程序設計項目教程(第二版)