1. 類文件結構
根據jvm規范,類文件結構如下:
ClassFile{
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count - 1];
u2 access_flags; //訪問修飾
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];
}
1.1 魔數
0~3 字節,表示它是否是 class 類型的文件
0000000 ca fe ba be
00 00 00 34 00 23 0a 00 06 00 15 09
1.2 版本
4~7字節,表示類的版本 (major version)00 34 (52),表示java8
0000000 ca fe ba be 00 00 00 34
00 23 0a 00 06 00 15 09
1.3 常量池
Constant Type | Value |
---|---|
CONSTANT_Class | 7 |
CONSTANT_Fieldref | 9 |
CONSTANT_Methodref | 10 |
CONSTANT_InterfaceMethodref | 11 |
CONSTANT_String | 8 |
CONSTANT_Integer | 3 |
CONSTANT_Float | 4 |
CONSTANT_Long | 5 |
CONSTANT_Double | 6 |
CONSTANT_NameAndType | 12 |
CONSTANT_utf8 | 1 |
CONSTANT_MethodHandle | 15 |
CONSTANT_MethodType | 16 |
CONSTANT_InvokeDynamic | 18 |
8~9字節,表示常量池長度,00 23(35)表示常量池有 #1~#34 項,注意 #0 項不計入,也沒有值
0000000 ca fe ba be 00 00 00 34 00 23
0a 00 06 00 15 09
第#1項 0a 表示一個Method信息,00 06 和 00 15(21) 表示它引用了常量池中 #6 和 #21 項來獲得這個方法的 【所屬類】 和 【方法名】
0000000 ca fe ba be 00 00 00 34 00 230a 00 06 00 15
09
第#2項 09 表示一個Field信息,00 16 (22) 和 00 17(23)表示它引用了常量池中的 #22 #23項來獲得這個成員變量的【所屬類】和 【成員變量名】
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
0000020 00 16 00 17
08 00 18 0a 00 19 00 1a 07 00 1b 07
......
2. 字節碼指令
構造方法的字節碼指令
2a b7 00 01 b1
2a -> aload_0
- 加載 slot0的局部變量,即this,作為下面的invokespecial 構造方法調用的參數
b7->invokespecial
- 預備調用構造方法,哪個方法呢?
00 01 表示引用常量池中的第#1項,即【Method java/lang/Object."<init>":()V】
b1->return
- 表示返回
主方法main的字節碼指令
b2 00 02 12 03 b6 00 04 b1
b2->getstatic
- 用來加載靜態變量,哪個靜態變量呢?
00 02 引用常量池中的第#2項,即【Field java/lang/System:out:Ljava/io/PrintStream;】
12->ldc
- 加載參數
03 引用常量池中的第#3項,即【String hello world】
b6 ->invokevirtual
- 預備調用成員方法,哪個方法呢?
00 04 引用常量池的第#4項,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】
b1->return
- 表示返回
2.1 javap 工具
javap -v HelloWorld.class
Classfile /C:/D/code/juc/target/classes/com/lily/jvm/HelloWorld.class
Last modified 2021-9-13; size 559 bytes
MD5 checksum edfc1b40953449a97c3a448c0d9f6620
Compiled from "HelloWorld.java"
public class com.lily.jvm.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // Hello World
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // com/lily/jvm/HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/lily/jvm/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 Hello World
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 com/lily/jvm/HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public com.lily.jvm.HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/lily/jvm/HelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
line 6: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"
2.2 方法執行流程
2.2.1 原始Java 代碼
public class Test {
public static void main(String[] args) {
int a = 10;
//short 范圍內的數字跟字節碼指令存儲在一起,大于的數字存儲在常量池
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}
2.2.2 編譯后的字節碼文件
public class com.lily.jvm.Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#25 // java/lang/Object."<init>":()V
#2 = Class #26 // java/lang/Short
#3 = Integer 32768
#4 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #29.#30 // java/io/PrintStream.println:(I)V
#6 = Class #31 // com/lily/jvm/Test
#7 = Class #32 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcom/lily/jvm/Test;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 a
#20 = Utf8 I
#21 = Utf8 b
#22 = Utf8 c
#23 = Utf8 SourceFile
#24 = Utf8 Test.java
#25 = NameAndType #8:#9 // "<init>":()V
#26 = Utf8 java/lang/Short
#27 = Class #33 // java/lang/System
#28 = NameAndType #34:#35 // out:Ljava/io/PrintStream;
#29 = Class #36 // java/io/PrintStream
#30 = NameAndType #37:#38 // println:(I)V
#31 = Utf8 com/lily/jvm/Test
#32 = Utf8 java/lang/Object
#33 = Utf8 java/lang/System
#34 = Utf8 out
#35 = Utf8 Ljava/io/PrintStream;
#36 = Utf8 java/io/PrintStream
#37 = Utf8 println
#38 = Utf8 (I)V
{
public com.lily.jvm.Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/lily/jvm/Test;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: ldc #3 // int 32768
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
17: return
LineNumberTable:
line 6: 0
line 7: 3
line 8: 6
line 9: 10
line 10: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
3 15 1 a I
6 12 2 b I
10 8 3 c I
}
2.2.3 執行流程
1.常量池載入運行時常量池
2.方法字節碼載入方法區
3.main
線程開始運行,分配棧幀內存,操作數棧和本地變量表(stack = 2, locals = 4)
4.執行引擎開始執行字節碼
bipush 10
- 將一個byte壓入操作數棧,其長度會補齊四個字節
類似指令有:
sipush 將一個short壓入操作數棧,其長度會補齊四個字節
ldc 將一個Int 壓入操作數棧
ldc2_w將一個long 壓入操作數棧,分兩次壓入 ,long是8個字節
小的數字都和字節碼指令在一起,超過short范圍的數字存入常量池
istore_1
- 將操作數棧頂數據彈出,存入局部變量表 slot_1
ldc
// int 32768
- 從常量池加載數據,到操作數棧
- 注意:Short.MAX_VALUE = 32767, 這里的32768在編譯期計算好了
istore_2
- 將操作數棧頂數據彈出,存入局部變量表 slot_2
iload_1
- 將局部變量表slot_1中的數據,讀取到操作數棧上
iload_2
- 將局部變量表slot_2中的數據,讀取到操作數棧上
iadd
- 執行 add 操作并把結果放到操作數棧頂
istore_3
- 將操作數棧頂數據彈出,存入局部變量表 slot_3
getstatic
// Field java/lang/System.out:Ljava/io/PrintStream;
- 通過常量池先找到
堆
中的System.out 對象 - getstatic 把System.out 對象的引用值放入操作數棧
iload_3
- 將局部變量表slot_3中的數據,讀取到操作數棧上
invokevirtual
// Method java/io/PrintStream.println:(I)V
- 在常量池中定位方法 java/io/PrintStream.println:(I)V
- 生成新的棧幀
- 傳遞參數,執行新棧幀中的字節碼
- 執行完畢,彈出棧幀
- 清除main 操作數棧內容
return
- 完成main 方法調用,彈出main 棧幀
- 程序結束
2.3 構造方法
2.3.1 <cinit>()V
public class Test {
static int i = 10;
static {
i = 20;
}
static {
i = 30;
}
}
編譯器會按照從上至下的順序,收集所有static 靜態代碼塊和靜態成員變量的代碼,合并成一個特殊的方法<cinit>()V
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 10
2: putstatic #2 // Field i:I
5: bipush 20
7: putstatic #2 // Field i:I
10: bipush 30
12: putstatic #2 // Field i:I
15: return
LineNumberTable:
line 5: 0
line 8: 5
line 12: 10
line 13: 15
<cinit>()V 方法會在類加載的初始化階段被調用。
2.3.2 <init>()V
編譯器會按照從上到下的順序,收集所有 {} 代碼塊 和 成員變量賦值的代碼塊,形成新的構造方法,原始構造方法內的代碼總是在最后
2.4 方法調用
public class Test {
private void test01() {}
private final void test02() {}
public void test03() {}
public static void test04() {}
public static void main(String[] args) {
Test test = new Test();
test.test01();
test.test02();
test.test03();
test.test04();
Test.test04();
}
}
{
public com.lily.jvm.Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/lily/jvm/Test;
public void test03();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/lily/jvm/Test;
public static void test04();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 11: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/lily/jvm/Test
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokespecial #4 // Method test01:()V
12: aload_1
13: invokespecial #5 // Method test02:()V
16: aload_1
17: invokevirtual #6 // Method test03:()V
20: aload_1
21: pop
22: invokestatic #7 // Method test04:()V
25: invokestatic #7 // Method test04:()V
28: return
}
invokespecial 和 invokestatic 屬于靜態綁定
。在字節碼指令生成的時候就知道要調用哪個方法
invokevirtual 屬于動態綁定
。public 方法在編譯期間不能確定調用哪個方法,可能有方法重寫。在運行期間確定方法的入口地址。
一個對象的初始化
0: new #2 // class com/lily/jvm/Test
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
new
- 使用new關鍵字在堆中分配內存, 分配成功后將該對象的引用放入操作數棧
dup
- 將棧頂的地址在復制一份。此時操作數棧中有兩個對象引用
invokespecial
- 根據棧頂的對象引用地址調用相應的構造方法,調用完畢后,棧頂只有一個對象引用了
astore_1
- 將操作數棧中剩余的對象引用存入本地變量表 slot_1
2.5 多態的原理
當執行 invokevirtual指令時
1.先通過棧幀中的對象引用找到對象
2.分析對象頭,找到對象的實際class
3.Class結構中有vtable, 它在類加載的連接階段就已經根據方法的重寫規則生成好了
4.查表得到方法的具體地址
5.執行方法的字節碼
3. 語法糖-編譯期處理
所謂語法糖,其實就是指java編譯器把 *.java 源碼編譯為 *.class 字節碼過程中,自動生成和轉化的一些代碼,主要是為了減輕程序員負擔,算是Java編譯器給我們的一個額外福利(糖)
注意,以下代碼的分析,借助了javap工具,idea的反編譯功能,idea 插件 jclasslib 工具等。編譯器轉換的結果直接就是 *.class文件。以下給出偽代碼
3.1 默認構造器
public class Candy1{
}
編譯成 .class后的代碼
public class Candy1{
//這個默認構造器是編譯器自動幫我們加上的
public Candy1() {
//調用父類Object 的無參構造方法 java/lang/Object."<init>":()V
super();
}
}
3.2 自動拆裝箱
這個特性是jdk 5開始加入的
public class Candy2{
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}
這段代碼在jdk 5之前是無法編譯的,必須改為如下代碼
public class Candy2{
public static void main(String[] args) {
Integer x = Integer.valueOf(1);
int y = x.intValue();
}
}
這些基本類型和包裝類型的轉換在jdk 5之后,由編譯器在編譯階段完成
3.3 泛型集合取值
泛型也是在jdk 5開始加入的特性,但java在編譯泛型代碼后,會執行 泛型擦除
的動作。即泛型信息在編譯為字節碼之后就丟失了。實際的類型都當做了 Object 類型來做處理
public class Candy3{
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
//實際調用的是 list.add(Object o)
list.add(10);
//實際調用的是 Object obj = List.get(index);
Integer x = list.get(0);
}
}
所以在取值時,編譯器真正生成的字節碼中,還要額外做一個類型轉換的操作
//需要將 Object 轉換為Integer
Integer x = (Integer)list.get(0);
擦除的是字節碼的泛型信息,可以看到LocalVariableTypeTable仍然保留方法的參數的泛型信息
使用反射仍然能獲得泛型信息
3.4 可變參數
可變參數也是jdk 5開始加入的新特性
public class Candy4{
public static void foo(String...args){
String[] array = args;
System.out.Println(array);
}
public static void main(String[] args) {
foo("hello","world");
}
}
可變參數 String... args 其實是一個String[] args.
Java編譯器會在編譯期間將上述代碼變為:
public class Candy4{
public static void foo(String...args){
String[] array = args;
System.out.Println(array);
}
public static void main(String[] args) {
foo(new String[]{"hello","world"});
}
}
注意,如果調用了 foo(), 不會把 null 傳進去,而是傳入一個空的數組 foo(new String[]{});
3.5 foreach 循環
jdk 5開始加入的新特性
public class Candy5_1{
public static void main(String[] args) {
int[] array = {1,2,3,4,5}; //數組賦初值的簡化寫法也會語法糖
for(int e : array){
System.out.Println(e);
}
}
}
會被編譯器轉換為:
public class Candy5_1{
public Candy5_1(){}
public static void main(String[] args) {
int[] array = new int[]{1,2,3,4,5}; //數組賦初值的簡化寫法也會語法糖
for(int i = 0; i < array.length; ++i){
int e = array[i];
System.out.Println(e);
}
}
}
而集合的循環
public class Candy5_2{
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1,2,3,4,5);
for(Integer e : list){
System.out.Println(e);
}
}
}
實際被編譯器轉換為:
public class Candy5_2{
public Candy5_2(){}
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1,2,3,4,5);
Iterator iter = list.interarot();
while(iter.hasNext()){
Integer e = (Integer)iter.next();
System.out.Println(e);
}
}
}
3.6 switch 字符串
jdk 7開始 switch 可以作用于字符串和枚舉類,這個功能其實也是語法糖。
public class Candy6_1{
public static void choose(String str) {
switch (str) {
case "hello" : {
System.out.Println("h");
break;
}
case "world" : {
System.out.Println("w");
break;
}
}
}
}
switch 配合 String 和枚舉使用時,變量不能為null,
會被編譯器轉換為:
public class Candy6_1{
public Candy6_1() {
}
public static void choose(String str) {
byte x = -1;
switch (str.hashCode()) {
case 99162322:
if (str.equals("hello")) {
x = 0;
}
case 113318802:
if (str.equals("world")){
x = 1;
}
}
switch (x) {
case 0 :
System.out.Println("h");
break;
case 1 :
System.out.Println("w");
break;
}
}
}
可以看到執行了兩遍 switch , 為什么用兩遍?
hashCode 是為了提高效率,減少可能的比較。而equals 是為了防止hash 沖突。
3.7 switch 枚舉
enum Sex{
MALE, FEMALE
}
public class Candy7{
public static void foo(Sex sex) {
switch (sex) {
case MALE :
System.out.Println("男");
break;
case FEMALE :
System.out.Println("女");
break;
}
}
}
編譯器轉換后代碼
public class Candy7{
//定義一個合成類,僅jvm可見
//用來映射枚舉的 ordinal 與數組元素的關系
//枚舉的 ordinal 表示枚舉對象的序號,從 0 開始
//即 MALE 的 ordinal()=0, FEMALE 的 ordinal()=1
static class $MAP{
//數組大小即為枚舉元素個數,這里存儲 case 用來比對的數字
static int[] map = new int[2];
static{
map[Sex.MALE.ordinal()] = 1;
map[Sex.FEMALE.ordinal()] = 1;
}
}
public static void foo(Sex sex) {
int x = $MAP.map[sex.ordinal()];
switch (x) {
case 1 :
System.out.Println("男");
break;
case 2 :
System.out.Println("女");
break;
}
}
}
3.8 枚舉類
jdk 7新增了枚舉類
enum Sex{
MALE, FEMALE
}
轉換后代碼:
//枚舉類不能被繼承
public final class Sex extends Enum<Sex>{
public static final Sex MALE;
public static final Sex FEMALE;
public static final Sex[] $VALUES;
static{
MALE = new Sex("MALE", 0);
FEMALE = new Sex("FEMALE", 1);
$VALUES = new Sex[]{MALE, FEMALE};
}
private Sex(String name, int ordinal) {
super(name, ordinal);
}
public static Sex[] values() {
return $VALUES.clone();
}
public static Sex valueOf(String name) {
return Enum.valueOf(Sex.class, name);
}
}
3.9 try-with-resource
jdk 7 新增了對需要關閉的資源處理
try(資源變量 = 創建資源對象) {
} catch() {
}
其中資源對象需要實現 AutoCloseable 接口,例如 FileInputStream、FileOutputStream、Connection等接口都實現了 AutoCloseable .
使用 try-with-resource 可以不用寫 finally 語句塊,編譯器會幫忙生成資源關閉代碼
public class Candy9{
public static void main(String[] args) {
try (InputStream is = new FileInputStream("c:\\ll.txt")) {
System.out.Println(is);
} catch(IOException e) {
}
}
}
編譯器轉換為:
public class Candy9{
public Candy9() {
}
public static void main(String[] args) {
try {
InputStream is = new FileInputStream("c:\\ll.txt");
Throwable t = null;
try {
System.out.Println(is);
} catch (Throwable e1) {
//t 是代碼出現異常
t = e1;
throw e1;
} finally {
//判斷了資源不為空
if (is != null) {
//如果代碼有異常
if (t != null) {
try{
is.close();
} catch (Throwable e2) {
//如果close出現異常,作為被壓制異常添加
//如此設計防止異常信息丟失
t.addSuppressed(e2);
}
} else {
//代碼沒有異常
is.close();
}
}
}
} catch(IOException e) {
}
}
}
3.10 方法重寫時的橋接方法
方法重寫時對返回值分兩種情況
- 父子類的返回值完全一致
- 子類的返回值可以是父類返回值的子類。
class A {
public Number m() {
return 1;
}
}
class B extends A{
//子類的返回值是 Integer 是父類的返回值 Number的子類
@Override
public Integer m() {
return 2;
}
}
對于子類編譯器轉換為
class B extends A{
public Integer m() {
return 2;
}
//此方法才是真正重寫了父類的 m 方法
public synthetic bridge Number m() {
return m();
}
}
其中橋接方法比較特殊,僅jvm可見,并且與原來的 public Integer m()沒有命名沖突
3.11 匿名內部類
public class Candy11{
public static void test(final int x) {
Runnable runnable = new Runnable(){
@Override
public void run() {
System.out.Println("ok" + x);
}
}
}
}
轉換后代碼
//額外生成類
public class Candy11$1 implements Runnable{
int val$x;
public Candy11$1(int x) {
this.val$x = x;
}
public void run() {
System.out.Println("ok" + this.val$x);
}
}
public class Candy11{
public static void test(final int x) {
Runnable runnable = new Candy11$1(x);
}
}
為什么匿名內部類引用局部變量時,局部變量必須是 final?
因為在創建Candy11$1
對象時,將x值賦值給 Candy11$1
對象的 val$x
屬性,所以 x 不應該再發生變化。如果變化了,那么val$x
的值沒有機會跟著一起變化
4. 類加載階段
4.1 加載
將類的字節碼載入方法區中,內部采用 c++ 的數據結構 instanceKlass 描述類,它的重要 field 有:
- _java_mirror 即java 類鏡像,例如 String.class,作用是把Klass暴露給Java使用
- _super 父類
- _fields 成員變量
- _methods 方法
- _constants 常量池
- _class_loader 類加載器
- _vtable 虛方法表
- _itable 接口方法表
如果這個類還有父類沒有加載,先加載父類
加載和鏈接可能是交替運行的
類的加載是懶惰的
注意:instanceKlass 這樣的元數據是存儲的方法區(元空間),_java_mirror存儲在堆中。
4.2 鏈接
1.驗證:類是否復合 jvm 規范,進行安全性檢查
2.準備:為static 變量分配內存空間
,設置默認值
- static 變量在 jdk 7之前存儲在 instanceKlass 末層,從jdk 7 開始存儲于 _java_mirror 末尾(堆中類對象)。
- static 變量分配空間和賦值是兩個步驟,
分配空間在準備階段完成,賦值在初始化階段完成。
- 如果 static 變量是 final 的
基本類型
(包括String類型),那么編譯階段值就確定了,賦值在準備階段完成。 - 如果 static 變量不是 final 的,但屬于引用類型,那么賦值也會在初始化階段完成。
3.解析:將常量池中的符號引用解析為直接引用
public class ClassParseTest {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classLoader = ClassParseTest.class.getClassLoader();
//loadClass() 方法不會導致類的解析和初始化
classLoader.loadClass("com.lily.jvm.C");
}
}
class C {
D d = new D();
}
class D {
}
4.3 初始化
初始化其實就是執行類的構造方法
4.3.1 <cinit>()V方法
初始化即調用<cinit>()V方法, 虛擬機會保證這個類的構造方法線程安全
4.3.2 發生的時機
類初始化是懶惰的
類初始化的時機
- main方法所在的類,總是會被首先初始化
- 首次訪問這個類的靜態變量或靜態方法時會引發類的初始化
- 子類初始化,如果父類還沒有初始化,會引發
- 子類訪問父類的靜態變量,只會觸發父類的初始化
- Class.forName
默認情況
會觸發類的初始化 - new 會導致初始化
不會導致初始化的情況
- 訪問類的 final static 靜態常量(基本類型和字符串),不會觸發初始化。在類的鏈接階段完成
- 類對象 .class 不會觸發初始化(類加載時生成_java_mirror對象)
- 創建該類的數組時,不會觸發初始化
- 調用類加載器的
loadClass()
方法,不會觸發類的初始化 - Class.forName() 第二個參數為false時,不會觸發類的初始化
5. 類加載器
以jdk 8為例
名稱 | 加載哪的類 | 說明 |
---|---|---|
Bootstrap ClassLoader | JAVA_HOME/jre/lib | 無法直接訪問 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上級為 Bootstrap,顯示為 null |
Application ClassLoader | classpath | 上級為 Extension |
自定義類加載 | 自定義 | 上級為 Application |
public class F {
static {
System.out.println("bootstrap F init");
}
}
public class Load1 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("com.lily.jvm.F");
System.out.println(aClass.getClassLoader());
}
}
輸出
bootstrap F init
sun.misc.Launcher$AppClassLoader@18b4aac2
5.1 雙親委派模式
雙親委派模式指,調用類加載器的 loadClass方法時,查找類的規則。
注意:這里的雙親并沒繼承關系
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
//1.檢查本類加載器 是否已經加載該類
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//2.有上級委派上級loadClass
c = parent.loadClass(name, false);
} else {
//3.沒有上級了(ExtClassLoader),則委派Bootstrap ClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
//4.每一層找不到,調用findClass方法(每個類加載器自己擴展),來加載
c = findClass(name);
//5. 記錄耗時
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
5.2 自定義類加載器
什么時候需要自定義類加載器?
想加載非 classpath ,隨意路徑下的類文件
都是通過接口來使用實現,希望解耦時,常用在框架設計
這些類希望予以隔離,不同應用的同名類都可以加載,不沖突,常見tomcat
步驟
1.繼承ClassLoader父類
2.遵從雙親委派機制,重寫findClass()
方法
3.讀取類文件的字節碼
4.調用父類的 defineClass 方法來加載類
5.使用者調用該類加載器的 loadClass方法
6. 運行期優化
6.1 即時編譯
6.1.1 分層編譯
public class JITTest {
public static void main(String[] args) {
for (int i = 0; i < 200; i++) {
long start = System.nanoTime();
for (int j = 0; j < 1000; j++) {
new Object();
}
long end = System.nanoTime();
System.out.println(end - start);
}
}
}
運行結果發現剛開始占用時間很多,后面突然時間變短了
原因是什么呢?
JVM將字節碼執行分為 5 個層次:
- 0層,解釋執行 Interpreter
- 1層,使用C1即時編譯器編譯執行(不帶
profiling
) - 2層,使用C1即時編譯器編譯執行(帶基本的
profiling
) - 3層,使用C1即時編譯器編譯執行(帶完全的
profiling
) - 4層,使用C2即時編譯器編譯執行
profiling
是指在運行過程中,收集一些程序執行狀態的數據,例如【方法的調用次數】,【循環的回邊次數】等
即時編譯器JIT 和 解釋器 的區別
- 解釋器是將字節碼解釋為機器碼,下次即使遇到相同的字節碼,仍會執行重復的解釋
- JIT 是將一些反復執行的字節碼編譯為機器碼,存入 code cache, 下次遇到相同的代碼,直接執行,無需再編譯
- 解釋器是將字節碼解釋為針對所有平臺通用的機器碼
- JIT會根據平臺類型,生成平臺特定的機器碼
對于占據大部分的不常用代碼,我們無需耗費時間將其編譯成機器碼,而是采取解釋執行。
另一方面,對于僅占據小部分的熱點代碼,我們則可以將其編譯成機器碼,以達到理想的運行速度。
執行效率上簡單比較一下 Interpreter < C1 < C2
JIT總的目標是發現熱點代碼,并優化。
6.1.2 方法內聯
public class JITTest {
public static void main(String[] args) {
System.out.println(square(9));
}
private static int square(final int i) {
return i * i;
}
}
如果發現 square 是熱點方法,并且長度不太長時,會進行內聯。
內聯:就是把方法內代碼拷貝、粘貼到調用者的位置。
System.out.println(9 * 9);
//還可以進行常量折疊優化
System.out.println(81);