圖解jvm--(三)類加載與字節(jié)碼技術(shù)

類加載與字節(jié)碼技術(shù)

1.類文件結(jié)構(gòu)

根據(jù) JVM 規(guī)范,類文件結(jié)構(gòu)如下

ClassFile { 
    u4 magic; //魔數(shù)
    u2 minor_version; //小版本號(hào)
    u2 major_version; //java 主版本號(hào)
    u2 constant_pool_count; //常量池
    cp_info constant_pool[constant_pool_count-1];
    u2 access_flags;  //訪問標(biāo)識(shí)與繼承信息
    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]; 
}

2.字節(jié)碼指令

指令 作用
iconst_1 int型常量值1進(jìn)棧
bipush 將一個(gè)byte型常量值推送至棧頂
iload_1 第二個(gè)int型局部變量進(jìn)操作數(shù)棧,從0開始計(jì)數(shù)
istore_1 將操作數(shù)棧棧頂 int 型數(shù)值存入第二個(gè)局部變量郭变,從0開始計(jì)數(shù)
iadd 棧頂兩int型數(shù)值相加,并且結(jié)果進(jìn)棧
return 當(dāng)前方法返回void
getstatic 獲取指定類的靜態(tài)域,并將其值壓入棧頂
putstatic 為指定的類的靜態(tài)域賦值
invokevirtual 調(diào)用實(shí)例方法
invokespecial 調(diào)用超類構(gòu)造方法、實(shí)例初始化方法萨蚕、私有方法
invokestatic 調(diào)用靜態(tài)方法
invokeinterface 調(diào)用接口方法
new 創(chuàng)建一個(gè)對(duì)象,并且其引用進(jìn)棧
newarray 創(chuàng)建一個(gè)基本類型數(shù)組蹄胰,并且其引用進(jìn)棧
iinc 1,1 局部變量表的值自加
iinc 1,-1 局部變量表的值自減

2.1 javap 工具

自己分析類文件結(jié)構(gòu)太麻煩了岳遥,Oracle 提供了 javap 工具來反編譯 class 文件 javap -v

javap -v Demo1_22.class
Classfile /D:/IDEAworkplace/jvm/out/production/jvm/cn/itcast/jvm/t1/stringtable/Demo1_22.class
  Last modified 2020-1-30; size 534 bytes
  MD5 checksum 5c4213b2f1defff2bb24bf7cbd5ff183
  Compiled from "Demo1_22.java"
public class cn.itcast.jvm.t1.stringtable.Demo1_22
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
      //常量池
Constant pool:
   #1 = Methodref          #6.#24         // java/lang/Object."<init>":()V
   #2 = String             #25            // a
   #3 = String             #26            // b
   #4 = String             #27            // ab
   #5 = Class              #28            // cn/itcast/jvm/t1/stringtable/Demo1_22
   #6 = Class              #29            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcn/itcast/jvm/t1/stringtable/Demo1_22;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               s1
  #19 = Utf8               Ljava/lang/String;
  #20 = Utf8               s2
  #21 = Utf8               s3
  #22 = Utf8               SourceFile
  #23 = Utf8               Demo1_22.java
  #24 = NameAndType        #7:#8          // "<init>":()V
  #25 = Utf8               a
  #26 = Utf8               b
  #27 = Utf8               ab
  #28 = Utf8               cn/itcast/jvm/t1/stringtable/Demo1_22
  #29 = Utf8               java/lang/Object
{
 //----------------------------------------構(gòu)造方法
  public cn.itcast.jvm.t1.stringtable.Demo1_22();
    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
     //源代碼的第幾行對(duì)應(yīng)字節(jié)碼的第幾行           
      LineNumberTable:
        line 4: 0
            //本地變量表
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t1/stringtable/Demo1_22;
//-------------------------------
    //-------------------------------main方法
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    //執(zhí)行指令代碼
    Code:
      stack=1, locals=4, args_size=1
         0: ldc           #2                  // String a
         2: astore_1
         3: ldc           #3                  // String b
         5: astore_2
         6: ldc           #4                  // String ab
         8: astore_3
         9: return
       //源代碼的第幾行對(duì)應(yīng)字節(jié)碼的第幾行           
      LineNumberTable:
        line 11: 0
        line 12: 3
        line 13: 6
        line 26: 9
      //局部變量表            
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  args   [Ljava/lang/String;
            3       7     1    s1   Ljava/lang/String;
            6       4     2    s2   Ljava/lang/String;
            9       1     3    s3   Ljava/lang/String;
}
SourceFile: "Demo1_22.java"

2.2 圖解方法執(zhí)行流程

(1)原始java代碼

/*** 演示 字節(jié)碼指令 和 操作數(shù)棧、常量池的關(guān)系 */ 
public class Demo3_1 {
     public static void main(String[] args) {
     int a = 10;
     int b = Short.MAX_VALUE + 1;
     int c = a + b;
     System.out.println(c); 
     }
 }

(2)編譯后的字節(jié)碼文件

D:\IDEAworkplace\jvm\out\production\jvm\cn\itcast\jvm\t3\bytecode> javap -v Demo3_1.class
Classfile /D:/IDEAworkplace/jvm/out/production/jvm/cn/itcast/jvm/t3/bytecode/Demo3_1.class
  Last modified 2020-1-28; size 635 bytes
  MD5 checksum 1a6413a652bcc5023f130b392deb76a1
  Compiled from "Demo3_1.java"
public class cn.itcast.jvm.t3.bytecode.Demo3_1
  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
   a/io/PrintStream.println:(I)V
   #6 = Class              #31            // cn/itcast/jvm/t3/bytecode/Demo3_1
   #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               Lcn/itcast/jvm/t3/bytecode/Demo3_1;
  #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               Demo3_1.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               cn/itcast/jvm/t3/bytecode/Demo3_1
  #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 cn.itcast.jvm.t3.bytecode.Demo3_1();
    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 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t3/bytecode/Demo3_1;

  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 8: 0
        line 9: 3
        line 10: 6
        line 11: 10
        line 12: 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
}
SourceFile: "Demo3_1.java"

(3)常量池載入運(yùn)行時(shí)常量池

image

(4)方法字節(jié)碼載入方法區(qū)

image

(5)main線程開始運(yùn)行裕寨,分配棧幀內(nèi)存

(stack=2(棧的深度)浩蓉,locals=4(局部變量表))

image

(6)執(zhí)行引擎開始執(zhí)行字節(jié)碼

bipush 10

  • 將一個(gè) byte 壓入操作數(shù)棧(操作數(shù)棧默認(rèn)都是4字節(jié))(不夠4字節(jié)派继,其長(zhǎng)度會(huì)補(bǔ)齊 4 個(gè)字節(jié)),類似的指令還有

  • sipush 將一個(gè) short 壓入操作數(shù)棧(其長(zhǎng)度會(huì)補(bǔ)齊 4 個(gè)字節(jié))

  • ldc 將一個(gè) int 壓入操作數(shù)棧

  • ldc2_w 將一個(gè) long 壓入操作數(shù)棧(分兩次壓入捻艳,因?yàn)?long 是 8 個(gè)字節(jié))

  • 這里小的數(shù)字都是和字節(jié)碼指令存在一起驾窟,超過 short 范圍的數(shù)字存入了常量池

image

istore_1

  • 將操作數(shù)棧頂數(shù)據(jù)彈出,存入局部變量表的 slot 1
image
image

ldc #3

  • 從常量池加載 #3 數(shù)據(jù)到操作數(shù)棧

  • 注意 Short.MAX_VALUE 是 32767认轨,所以 32768 = Short.MAX_VALUE + 1 實(shí)際是在編譯期間計(jì)算好的

image

istore_2

image
image

iload_1

image

iload_2

image

iadd

image
image

istore_3

image
image

getstatic #4

image
image

iload_3

image
image

invokevirtual #5

  • 找到常量池 #5 項(xiàng)

  • 定位到方法區(qū) java/io/PrintStream.println:(I)V 方法

  • 生成新的棧幀(分配 locals绅络、stack等)

  • 傳遞參數(shù),執(zhí)行新棧幀中的字節(jié)碼

image
  • 執(zhí)行完畢嘁字,彈出棧幀

  • 清除 main 操作數(shù)棧內(nèi)容

image

return

  • 完成 main 方法調(diào)用恩急,彈出 main 棧幀

  • 程序結(jié)束

2.3 練習(xí)-分析 a++

目的:從字節(jié)碼角度分析 a++ 相關(guān)題目

源碼:

/**
 * 從字節(jié)碼角度分析 a++  相關(guān)題目
 */
public class Demo3_2 {
    public static void main(String[] args) {
        int a = 10;
        int b = a++ + ++a + a--;
        System.out.println(a);
        System.out.println(b);
    }
}

字節(jié)碼:

D:\IDEAworkplace\jvm\out\production\jvm\cn\itcast\jvm\t3\bytecode> javap -v Demo3_2.class
Classfile /D:/IDEAworkplace/jvm/out/production/jvm/cn/itcast/jvm/t3/bytecode/Demo3_2.class
  Last modified 2020-1-28; size 610 bytes
  MD5 checksum 5f6a35e5b9bb88d08249958a8d2ab043
  Compiled from "Demo3_2.java"
public class cn.itcast.jvm.t3.bytecode.Demo3_2
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#22         // java/lang/Object."<init>":()V
   #2 = Fieldref           #23.#24        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #25.#26        // java/io/PrintStream.println:(I)V
   #4 = Class              #27            // cn/itcast/jvm/t3/bytecode/Demo3_2
   #5 = Class              #28            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               Lcn/itcast/jvm/t3/bytecode/Demo3_2;
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               args
  #16 = Utf8               [Ljava/lang/String;
  #17 = Utf8               a
  #18 = Utf8               I
  #19 = Utf8               b
  #20 = Utf8               SourceFile
  #21 = Utf8               Demo3_2.java
  #22 = NameAndType        #6:#7          // "<init>":()V
  #23 = Class              #29            // java/lang/System
  #24 = NameAndType        #30:#31        // out:Ljava/io/PrintStream;
  #25 = Class              #32            // java/io/PrintStream
  #26 = NameAndType        #33:#34        // println:(I)V
  #27 = Utf8               cn/itcast/jvm/t3/bytecode/Demo3_2
  #28 = Utf8               java/lang/Object
  #29 = Utf8               java/lang/System
  #30 = Utf8               out
  #31 = Utf8               Ljava/io/PrintStream;
  #32 = Utf8               java/io/PrintStream
  #33 = Utf8               println
  #34 = Utf8               (I)V
{
  public cn.itcast.jvm.t3.bytecode.Demo3_2();
    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 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t3/bytecode/Demo3_2;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: bipush        10
         2: istore_1
         3: iload_1
         4: iinc          1, 1
         7: iinc          1, 1
        10: iload_1
        11: iadd
        12: iload_1
        13: iinc          1, -1
        16: iadd
        17: istore_2
        18: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        21: iload_1
        22: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        25: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        28: iload_2
        29: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        32: return
      LineNumberTable:
        line 8: 0
        line 9: 3
        line 10: 18
        line 11: 25
        line 12: 32
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      33     0  args   [Ljava/lang/String;
            3      30     1     a   I
           18      15     2     b   I
}
SourceFile: "Demo3_2.java"


分析:

  • 注意 iinc 指令是直接在局部變量 slot 上進(jìn)行運(yùn)算

  • a++ 和 ++a 的區(qū)別是先執(zhí)行 iload 還是 先執(zhí)行 iinc

image
image

a++

image
image

++a

image
image

add

image

a--

image
image

add

image
image

2.4 練習(xí) - 判斷結(jié)果

從字節(jié)碼角度分析,先 istore 0 纪蜒,進(jìn)操作數(shù)棧衷恭,然后,再將x 的局部變量表執(zhí)行 iinc 加1霍掺,然后匾荆,把操作數(shù)棧的數(shù) istore 回去 x 所在的局部變量表伞访,所以绝页,x一直為0

public class Demo3_6_1 {
    public static void main(String[] args) {
        int i = 0;
        int x = 0;
        while (i < 10) {
            x = x++;
            i++;
        }
        System.out.println(x);
    }
}

//結(jié)果 為 0

2.5 方法調(diào)用

public class Demo3_9 {
    public Demo3_9() { }

    private void test1() { }

    private final void test2() { }

    public void test3() { }

    public static void test4() { }

    @Override
    public String toString() {
        return super.toString();
    }

    public static void main(String[] args) {
        Demo3_9 d = new Demo3_9();
        d.test1();  //invokespecial
        d.test2();  //invokespecial
        d.test3();  //invokevirtual
        d.test4(); //invokestatic
        Demo3_9.test4(); //invokestatic
        d.toString();
    }

}

 stack=2, locals=2, args_size=1
         0: new           #3                  // class cn/itcast/jvm/t3/bytecode/Demo3_9
         3: dup
         4: invokespecial #4                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokespecial #5                  // Method test1:()V
        12: aload_1
        13: invokespecial #6                  // Method test2:()V
        16: aload_1
        17: invokevirtual #7                  // Method test3:()V
        20: aload_1
        21: pop             //說明通過對(duì)象調(diào)用靜態(tài)方法灭忠,會(huì)彈出棧,直接調(diào)用
        22: invokestatic  #8                  // Method test4:()V
        25: invokestatic  #8                  // Method test4:()V
        28: aload_1
        29: invokevirtual #9                  // Method toString:()Ljava/lang/String;
        32: pop
        33: return

通過對(duì)象調(diào)用靜態(tài)方法兔魂,實(shí)際上是load 后,又出棧了举娩,說明通過對(duì)象調(diào)用靜態(tài)方法析校,會(huì)彈出棧,直接調(diào)用铜涉。

靜態(tài)方法是不需要對(duì)象的智玻,直接使用

2.6 new 關(guān)鍵字原理

         0: new           #3                  // class cn/itcast/jvm/t3/bytecode/Demo3_9
         3: dup
         4: invokespecial #4                  // Method "<init>":()V
         7: astore_1

new 有兩步,第一步是先分配一個(gè)對(duì)象在堆空間需要的內(nèi)存芙代,分配成功吊奢,會(huì)把對(duì)象的引用放入操作數(shù)棧,然后 執(zhí)行 dup 復(fù)制一份對(duì)象引用纹烹? 為什么页滚? 復(fù)制一份對(duì)象,來執(zhí)行init方法铺呵,構(gòu)造方法裹驰,執(zhí)行完后,這個(gè)對(duì)象就出棧了片挂,所以需要復(fù)制一份幻林,然后會(huì)執(zhí)行 store 把此時(shí)棧頂?shù)膶?duì)象引用存儲(chǔ)到局部變量表中的(即賦值給 new 出這個(gè)對(duì)象的引用 )

image

2.7 多態(tài)原理

在類的鏈接階段贞盯,就確定了虛方法表(vtable) ,確定下一個(gè)類加載時(shí)執(zhí)行方法的先后順序

當(dāng)執(zhí)行一個(gè)對(duì)象的 invokevirtual指令時(shí)沪饺,

  1. 先通過棧幀中的對(duì)象引用找到對(duì)象
  2. 分析對(duì)象頭躏敢,找到對(duì)象的實(shí)際class
  3. class結(jié)構(gòu)中有 vtable ,它在類加載的鏈接階段就已經(jīng)根據(jù)方法的重寫規(guī)則生成好了
  4. 查表得到方法的具體地址
  5. 執(zhí)行方法的字節(jié)碼

2.8 異常處理

try - catch

public class Demo3_11_1 {

    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (Exception e) {
            i = 20;
        }
    }
}

字節(jié)碼

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        10
         4: istore_1
         5: goto          12
         8: astore_2         //把異常對(duì)象的局部變量存到e中
         9: bipush        20
        11: istore_1
        12: return
                  
      //異常表随闽,檢測(cè)第2行到第5行范圍內(nèi)的異常父丰,進(jìn)行異常類型匹配,一旦有異常,則會(huì)到第執(zhí)行第8行            
      Exception table:
         from    to  target type
             2     5     8   Class java/lang/Exception
   
      //局部變量表
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            9       3     2     e   Ljava/lang/Exception;
            0      13     0  args   [Ljava/lang/String;
            2      11     1     i   I
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 8
          locals = [ class "[Ljava/lang/String;", int ]
          stack = [ class java/lang/Exception ]
        frame_type = 3 /* same */
}

  • 可以看到多出來一個(gè) Exception table 的結(jié)構(gòu)掘宪,[from, to) 是前閉后開的檢測(cè)范圍蛾扇,一旦這個(gè)范圍

? 內(nèi)的字節(jié)碼執(zhí)行出現(xiàn)異常,則通過 type 匹配異常類型魏滚,如果一致镀首,進(jìn)入 target 所指示行號(hào)

  • 8 行的字節(jié)碼指令 astore_2 是將異常對(duì)象引用存入局部變量表的 slot 2 位置

多個(gè) try-catch塊

public class Demo3_11_2 {

    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (ArithmeticException e) {
            i = 30;
        } catch (NullPointerException e) {
            i = 40;
        } catch (Exception e) {
            i = 50;
        }
    }

}


字節(jié)碼:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        10
         4: istore_1
         5: goto          26
         8: astore_2
         9: bipush        30
        11: istore_1
        12: goto          26
        15: astore_2
        16: bipush        40
        18: istore_1
        19: goto          26
        22: astore_2
        23: bipush        50
        25: istore_1
        26: return
      Exception table:
         from    to  target type
             2     5     8   Class java/lang/ArithmeticException
             2     5    15   Class java/lang/NullPointerException
             2     5    22   Class java/lang/Exception
      LineNumberTable:
       LocalVariableTable:
        Start  Length  Slot  Name   Signature
            9       3     2     e   Ljava/lang/ArithmeticException;
           16       3     2     e   Ljava/lang/NullPointerException;
           23       3     2     e   Ljava/lang/Exception;
            0      27     0  args   [Ljava/lang/String;
            2      25     1     i   I

  • 因?yàn)楫惓3霈F(xiàn)時(shí),只能進(jìn)入 Exception table 中一個(gè)分支鼠次,所以局部變量表 slot 2 位置被共用

multi-catch的情況

public class Demo3_11_3 {

    public static void main(String[] args) {
        try {
            Method test = Demo3_11_3.class.getMethod("test");
            test.invoke(null);
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    public static void test() {
        System.out.println("ok");
    }
}

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: ldc           #2                  // class cn/itcast/jvm/t3/bytecode/Demo3_11_3
         2: ldc           #3                  // String test
         4: iconst_0
         5: anewarray     #4                  // class java/lang/Class
         8: invokevirtual #5                  // Method java/lang/Class.getMethod:(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
        11: astore_1
        12: aload_1
        13: aconst_null
        14: iconst_0
        15: anewarray     #6                  // class java/lang/Object
        18: invokevirtual #7                  // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
        21: pop
        22: goto          30
        25: astore_1
        26: aload_1
        27: invokevirtual #11                 // Method java/lang/ReflectiveOperationException.printStackTrace:()V
        30: return
      Exception table:
         from    to  target type
             0    22    25   Class java/lang/NoSuchMethodException
             0    22    25   Class java/lang/IllegalAccessException
             0    22    25   Class java/lang/reflect/InvocationTargetException
   
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           12      10     1  test   Ljava/lang/reflect/Method;
           26       4     1     e   Ljava/lang/ReflectiveOperationException;
            0      31     0  args   [Ljava/lang/String;
      StackMapTable: number_of_entries = 2

finally

public class Demo3_11_4 {

    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (Exception e) {
            i = 20;
        } finally {
            i = 30;
        }
    }
}
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=4, args_size=1
         0: iconst_0
         1: istore_1           // 0 -> i
         2: bipush        10      //try---
         4: istore_1              // 10 -> i
         5: bipush        30      //finally
         7: istore_1             //30 -> i
         8: goto          27     //return 
        11: astore_2             //catch Exception -> e 
        12: bipush        20
        14: istore_1
        15: bipush        30        //finally
        17: istore_1
        18: goto          27
        21: astore_3
        22: bipush        30       //finally
        24: istore_1
        25: aload_3
        26: athrow
        27: return
      Exception table:
         from    to  target type
             2     5    11   Class java/lang/Exception
             2     5    21   any
            11    15    21   any

      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           12       3     2     e   Ljava/lang/Exception;
            0      28     0  args   [Ljava/lang/String;
            2      26     1     i   I
      StackMapTable: number_of_entries = 3

}

可以看到 finally 中的代碼被復(fù)制了 3 份更哄,分別放入 try 流程,catch 流程以及 catch 剩余的異常類型流

2.9 finally練習(xí)

題目一:

public class Demo3_12_2 {
    public static void main(String[] args) {
        int result = test();
        System.out.println(result);
    }

    public static int test() {

        try {
            return 10;
        } finally {
            return  20;
        }
    }
}

//結(jié)果是 20 
  public static int test();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=0
         0: bipush        10  // <- 10放入棧頂
         2: istore_0          // 10 -> slot 0 (從棧頂移除了)
         3: bipush        20   // <- 20 放入棧頂 
         5: ireturn            // 返回棧頂 int(20)
         6: astore_1           // catch any --> slot 1
         7: bipush        20     // <- 20 放入棧頂
         9: ireturn              //返回棧頂 int(20)
      Exception table:
         from    to  target type
             0     3     6   any
}

  • 由于 fifinally 中的 ireturn 被插入了所有可能的流程腥寇,因此返回結(jié)果肯定以 finally 的為準(zhǔn)

  • 至于字節(jié)碼中第 2 行成翩,似乎沒啥用,且留個(gè)伏筆赦役,看下個(gè)例子

  • 跟上例中的 finally 相比麻敌,發(fā)現(xiàn)沒有 athrow 了,這告訴我們:如果在 finally 中出現(xiàn)了 return掂摔,會(huì)吞掉異常术羔,最好不要在finally 里面 return

例如:下面的代碼,不會(huì)出現(xiàn)除零異常

    public static int test() {

        try {
            return 10/0;
        } finally {
            return  20;
        }
    }
}

題目二:

public class Demo3_12_2 {
    public static void main(String[] args) {
        int result = test();
        System.out.println(result);
    }

    public static int test() {

        int i = 10;
        try {
            return i;
        } finally {
            i = 20;
        }
    }
}

//結(jié)果是 10
 public static int test();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=0
         0: bipush        10       // <- 10 放入棧頂
         2: istore_0               // 10 -> i
         3: iload_0                // <- i(10)
         4: istore_1               // 10 -> slot 1 ,暫存至 slot 1, 目的時(shí)為了固定返回值
         5: bipush        20       // <- 20 放入棧頂
         7: istore_0               // 20 ->i
         8: iload_1                // <- slot 1(10) 載入 slot 1 暫存的值
         9: ireturn                //返回棧頂?shù)?int(10)
        10: astore_2
        11: bipush        20
        13: istore_0
        14: aload_2
        15: athrow
      Exception table:
         from    to  target type
             3     5    10   any
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            3      13     0     i   I


2.10 synchronized

public class Demo3_13 {

    public static void main(String[] args) {
        Object lock = new Object();
        synchronized (lock) {
            System.out.println("ok");
        }
    }
}
 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: new           #2                  // new Object
         3: dup                              //復(fù)制一份對(duì)象引用
         4: invokespecial #1                  // 執(zhí)行構(gòu)造方法
         7: astore_1                         //lock引用 -> lock     
         8: aload_1                        //對(duì)象加載到操作數(shù)棧 <-lock (synchronized開始)
         9: dup 
        10: astore_2                      //lock 引用 -> slot2 
        11: monitorenter                   // monitorenter (lock引用) 加鎖操作
        12: getstatic     #3                  //System.out
        15: ldc           #4                  // String ok
        17: invokevirtual #5                   // System.out
        20: aload_2                          // <- slot2  (lock引用)
        21: monitorexit                   // monitorexit 解鎖
        22: goto          30
                  //--------------------出現(xiàn)異常-----
        25: astore_3                     // any -> slot 3
        26: aload_2                      //<- slot 2(lock 引用)
        27: monitorexit                  // monitorexit 解鎖
        28: aload_3
        29: athrow
                  /------------------
        30: return
      Exception table:
         from    to  target type
            12    22    25   any
            25    28    25   any

      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0  args   [Ljava/lang/String;
            8      23     1  lock   Ljava/lang/Object;

}

3.編譯器處理

所謂的 語法糖 乙漓,其實(shí)就是指 java 編譯器把 *.java 源碼編譯為 *.class 字節(jié)碼的過程中级历,自動(dòng)生成

和轉(zhuǎn)換的一些代碼,主要是為了減輕程序員的負(fù)擔(dān)叭披,算是 java 編譯器給我們的一個(gè)額外福利(給糖吃

嘛)

注意寥殖,以下代碼的分析,借助了 javap 工具趋观,idea 的反編譯功能扛禽,idea 插件 jclasslib 等工具。另外皱坛,

編譯器轉(zhuǎn)換的結(jié)果直接就是 class 字節(jié)碼编曼,只是為了便于閱讀,給出了 幾乎等價(jià) 的 java 源碼方式剩辟,并

不是編譯器還會(huì)轉(zhuǎn)換出中間的 java 源碼掐场,切記往扔。

3.1 默認(rèn)構(gòu)造器

public class Candy1 { 
 }
public class Candy1 {
// 這個(gè)無參構(gòu)造是編譯器幫助我們加上的 
  public Candy1() {
    super(); // 即調(diào)用父類 Object 的無參構(gòu)造方法,即調(diào)用 java/lang/Object." <init>":()V 
  } 
}

3.2 自動(dòng)拆裝箱

這個(gè)特性是 JDK 5 開始加入的熊户, 代碼片段1 :

public class Candy2 { 
    public static void main(String[] args) {
        Integer x = 1; 
        int y = x; 
    } 
}

這段代碼在 JDK 5 之前是無法編譯通過的萍膛,必須改寫為 代碼片段2 :

public class Candy2 { 
    public static void main(String[] args) { 
        Integer x = Integer.valueOf(1); 
        int y = x.intValue();
    } 
}

3.3 泛型集合取值

泛型也是在 JDK 5 開始加入的特性,但 java 在編譯泛型代碼后會(huì)執(zhí)行 泛型擦除 的動(dòng)作嚷堡,即泛型信息

在編譯為字節(jié)碼之后就丟失了蝗罗,實(shí)際的類型都當(dāng)做了 Object 類型來處理:

public class Candy3 { 
    public static void main(String[] args) { 
        List<Integer> list = new ArrayList<>(); 
        list.add(10); // 實(shí)際調(diào)用的是 List.add(Object e) 
        Integer x = list.get(0); // 實(shí)際調(diào)用的是 Object obj = List.get(int index); 
    } 
}

所以在取值時(shí),編譯器真正生成的字節(jié)碼中蝌戒,還要額外做一個(gè)類型轉(zhuǎn)換的操作:

// 需要將 Object 轉(zhuǎn)為 
Integer Integer x = (Integer)list.get(0);

如果前面的 x 變量類型修改為 int 基本類型那么最終生成的字節(jié)碼是:

// 需要將 Object 轉(zhuǎn)為 Integer, 并執(zhí)行拆箱操作 
int x = ((Integer)list.get(0)).intValue();

擦除的是字節(jié)碼上的泛型信息串塑,可以看到 LocalVariableTypeTable 仍然保留了方法參數(shù)泛型的信息

  public static void main(java.lang.String[]) throws java.lang.Exception;
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
         7: astore_1
         8: aload_1
         9: bipush        10
        11: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        14: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        19: pop
        20: aload_1
        21: iconst_0
        22: invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
        27: checkcast     #7                  // class java/lang/Integer
        30: astore_2
        31: return
      LineNumberTable:
        line 13: 0
        line 14: 8
        line 15: 20
        line 31: 31
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      32     0  args   [Ljava/lang/String;
            8      24     1  list   Ljava/util/List;
           31       1     2     x   Ljava/lang/Integer;
    //局部變量類型表                                 
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            8      24     1  list   Ljava/util/List<Ljava/lang/Integer;>;
    Exceptions:
      throws java.lang.Exception
}

使用反射,仍然能夠獲得這些信息:

public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
        return null;
    }
Method test = Candy3.class.getMethod("test", List.class, Map.class);
Type[] types = test.getGenericParameterTypes();
for (Type type : types) {
    if (type instanceof ParameterizedType) {
        ParameterizedType parameterizedType = (ParameterizedType) type;
        System.out.println("原始類型 - " + parameterizedType.getRawType());
        Type[] arguments = parameterizedType.getActualTypeArguments();
        for (int i = 0; i < arguments.length; i++) {
            System.out.printf("泛型參數(shù)[%d] - %s\n", i, arguments[i]);
        }
    }

}

輸出:
原始類型 - interface java.util.List
泛型參數(shù)[0] - class java.lang.String
原始類型 - interface java.util.Map
泛型參數(shù)[0] - class java.lang.Integer
泛型參數(shù)[1] - class java.lang.Object

3.4 可變參數(shù)

可變參數(shù)也是 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");
    }
}

可變參數(shù) String... args 其實(shí)是一個(gè) String[] args 北苟,從代碼中的賦值語句中就可以看出來桩匪。 同

樣 java 編譯器會(huì)在編譯期間將上述代碼變換為:

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"});
    }
}

注意 如果調(diào)用了 foo() 則等價(jià)代碼為 foo(new String[]{}) ,創(chuàng)建了一個(gè)空的數(shù)組友鼻,而不會(huì)

傳遞 null 進(jìn)去

3.5 foreach 循環(huán)

仍是 JDK 5 開始引入的語法糖傻昙,數(shù)組的循環(huán):

public class Candy5_1 {
    public static void main(String[] args) {
        int[] array = {1, 2, 3, 4, 5}; // 數(shù)組賦初值的簡(jiǎn)化寫法也是語法糖哦
        for (int e : array) {
            System.out.println(e);
        }
    }
}

會(huì)被編譯器轉(zhuǎn)換為:

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); 
        } 
    }
}

而集合循環(huán):

public class Candy5_2 {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        for (Integer i : list) {
            System.out.println(i);
        }
    }
}

實(shí)際被編譯器轉(zhuǎn)換為對(duì)迭代器的調(diào)用:

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.iterator(); //獲取迭代器
        while(iter.hasNext()) { 
            Integer e = (Integer)iter.next(); 
            System.out.println(e); 
        } 
    } 
}

注意 foreach 循環(huán)寫法,能夠配合數(shù)組彩扔,以及所有實(shí)現(xiàn)了 Iterable 接口的集合類一起使用妆档,其

中 Iterable 用來獲取集合的迭代器( Iterator )

3.6 switch 字符串

從 JDK 7 開始,switch 可以作用于字符串和枚舉類虫碉,這個(gè)功能其實(shí)也是語法糖过吻,例如:

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;
            }
        }
    }
}

public class Candy6_1 { 
    public Candy6_1() { 
    }
    public static void choose(String str) {
         byte x = -1;
         switch(str.hashCode()) {
         case 99162322: // hello 的 hashCode
              if (str.equals("hello")) { 
                   x = 0; 
               }
               break; 
           case 113318802: // world 的 hashCode 
               if (str.equals("world")) {
                   x = 1; 
               } 
        }
        switch(x) {
            case 0: 
                System.out.println("h"); 
                break; 
            case 1:
                System.out.println("w"); 
        } 
    }
}

可以看到,執(zhí)行了兩遍 switch蔗衡,第一遍是根據(jù)字符串的 hashCode 和 equals 將字符串的轉(zhuǎn)換為相應(yīng)

byte 類型,第二遍才是利用 byte 執(zhí)行進(jìn)行比較乳绕。

為什么第一遍時(shí)必須既比較 hashCode绞惦,又利用 equals 比較呢?hashCode 是為了提高效率洋措,減少可

能的比較济蝉;而 equals 是為了防止 hashCode 沖突,例如 BM 和 C. 這兩個(gè)字符串的hashCode值都是

2123 菠发,如果有如下代碼:

public class Candy6_2 {
    public static void choose(String str) {
        switch (str) { 
            case "BM": { 
                System.out.println("h");
                break; 
            }
            case "C.": {
                System.out.println("w");
                break; 
            } 
        }
    } 
}
                

編譯后:

public class Candy6_2 { 
    public Candy6_2() {
}
    public static void choose(String str) { 
        byte x = -1; switch(str.hashCode()) {
            case 2123: // hashCode 值可能相同王滤,需要進(jìn)一步用 equals 比較 
                if (str.equals("C.")) {
                    x = 1; 
                } else if (str.equals("BM")) {
                    x = 0; 
                } 
            default:
                switch(x) { 
                   case 0: 
                        System.out.println("h");
                        break; 
                    case 1: 
                        System.out.println("w");
                } 
        }
    } 
}

3.7 switch 枚舉類

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; 
        } 
    } 
}

轉(zhuǎn)換后:

public class Candy7 { 
    /**
    * 定義一個(gè)合成類(僅 jvm 使用滓鸠,對(duì)我們不可見) 
    * 用來映射枚舉的 ordinal 與數(shù)組元素的關(guān)系
    * 枚舉的 ordinal 表示枚舉對(duì)象的序號(hào)雁乡,從 0 開始 
    * 即 MALE 的 ordinal()=0,F(xiàn)EMALE 的 ordinal()=1 
    */ 
    static class $MAP { 
        // 數(shù)組大小即為枚舉元素個(gè)數(shù)糜俗,里面存儲(chǔ)case用來對(duì)比的數(shù)字 
        static int[] map = new int[2]; 
        static {
            map[Sex.MALE.ordinal()] = 1; 
            map[Sex.FEMALE.ordinal()] = 2; 
        } 
    }
    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 
}

轉(zhuǎn)換后代碼:

public final class Sex extends Enum<Sex> { 
    public static final Sex MALE;
    public static final Sex FEMALE; 
    private 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-resourses

JDK 7 開始新增了對(duì)需要關(guān)閉的資源處理的特殊語法 try-with-resources`:

try(資源變量 = 創(chuàng)建資源對(duì)象){
} 
catch( ) { 
    
}

其中資源對(duì)象需要實(shí)現(xiàn) AutoCloseable 接口曲饱,例如 InputStreamOutputStream 珠月、

Connection 扩淀、 StatementResultSet 等接口都實(shí)現(xiàn)了 AutoCloseable 啤挎,使用 try-with-

resources 可以不用寫 finally 語句塊驻谆,編譯器會(huì)幫助生成關(guān)閉資源代碼,例如:

public class Candy9 { 
    public static void main(String[] args) {
        try(InputStream is = new FileInputStream("d:\\1.txt")) {
            System.out.println(is); 
        } catch (IOException e) { 
            e.printStackTrace();
        } 
    }
}

會(huì)被轉(zhuǎn)換為:

public class Candy9 { 
    public Candy9() {
    }
    public static void main(String[] args) { 
        try {
            InputStream is = new FileInputStream("d:\\1.txt");
            Throwable t = null; 
            try {
                System.out.println(is); 
            } 
            catch (Throwable e1) { 
                // t 是我們代碼出現(xiàn)的異常 
                t = e1; 
                throw e1; 
            } finally { 
                // 判斷了資源不為空
                if (is != null) {
                    // 如果我們代碼有異常
                    if (t != null) {
                        try {
                            is.close();
                            }
                        catch (Throwable e2) {
                            // 如果 close 出現(xiàn)異常庆聘,作為被壓制異常添加
                            t.addSuppressed(e2); 
                        } 
                    } else { 
                        // 如果我們代碼沒有異常胜臊,close 出現(xiàn)的異常就是最后 catch 塊中的 e 
                        is.close();
                    }
                } 
            }
        }
        catch (IOException e) { 
            e.printStackTrace(); 
        }
    } 
}

為什么要設(shè)計(jì)一個(gè) addSuppressed(Throwable e) (添加被壓制異常)的方法呢?是為了防止異常信

息的丟失(想想 try-with-resources 生成的 fifianlly 中如果拋出了異常):

public class Test6 {
    public static void main(String[] args) {
        try (MyResource resource = new MyResource()) { 
            int i = 1/0; 
        } catch (Exception e) { 
            e.printStackTrace(); 
        } 
    } 
}

class MyResource implements AutoCloseable {
    public void close() throws Exception { 
        throw new Exception("close 異常"); 
    } 
}

輸出:異常都不會(huì)丟

java.lang.ArithmeticException: / by zero 
    at test.Test6.main(Test6.java:7) 
    Suppressed: java.lang.Exception: close 異常
        at test.MyResource.close(Test6.java:18)
        at test.Test6.main(Test6.java:6)

3.10 方法重新時(shí)的橋接方法

我們都知道掏觉,方法重寫時(shí)對(duì)返回值分兩種情況:

  • 父子類的返回值完全一致

  • 子類返回值可以是父類返回值的子類(比較繞口区端,見下面的例子)

class A { 
    public Number m() { 
        return 1; 
    } 
}

class B extends A { 
    @Override // 子類 m 方法的返回值是 Integer 是父類 m 方法返回值 Number 的子類
    public Integer m() {
        return 2; 
    } 
}

對(duì)于子類,java 編譯器會(huì)做如下處理:

class B extends A { 
    public Integer m() {
        return 2; 
    }
    // 此方法才是真正重寫了父類 public Number m() 方法 
    public synthetic bridge Number m() { 
        // 調(diào)用 public Integer m() 
        return m(); 
    }
}

其中橋接方法比較特殊澳腹,僅對(duì) java 虛擬機(jī)可見织盼,并且與原來的 public Integer m() 沒有命名沖突,可以

用下面反射代碼來驗(yàn)證:

for (Method m : B.class.getDeclaredMethods()) {
    System.out.println(m); 
}

會(huì)輸出:

public java.lang.Integer test.candy.B.m()
public java.lang.Number test.candy.B.m()

3.11 匿名內(nèi)部類

源代碼:

public class Candy11 { 
    public static void main(String[] args) {
        Runnable runnable = new Runnable() { 
            @Override
            public void run() { 
                System.out.println("ok"); 
            } 
        }; 
    } 
}

轉(zhuǎn)換后的代碼:

// 額外生成的類
final class Candy11$1 implements Runnable { 
    Candy11$1() { 
    }
    public void run() {
        System.out.println("ok");
    } 
}
public class Candy11 { 
    public static void main(String[] args) {
        Runnable runnable = new Candy11$1();
    } 
}

引用局部變量的匿名內(nèi)部類酱塔,源代碼:

public class Candy11 { 
    public static void test(final int x) {
        Runnable runnable = new Runnable() { 
            @Override 
            public void run() {
                System.out.println("ok:" + x); 
            } 
        };
    } 
}

轉(zhuǎn)換后代碼:

// 額外生成的類 
final class Candy11$1 implements Runnable { 
    int val$x; 
    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); 
    }
}

注意 這同時(shí)解釋了為什么匿名內(nèi)部類引用局部變量時(shí)沥邻,局部變量必須是 final 的:

因?yàn)樵趧?chuàng)建 Candy11 $1 對(duì)象時(shí),將 x 的值賦值給了 Candy11 $1 對(duì)象的 val

屬 性 羊娃, 不 應(yīng) 該 再 發(fā) 生 變 化 了 唐全, 如 果 變 化, 那 么 val$x 屬性沒有機(jī)會(huì)再發(fā)生變化

4.類加載階段

4.1 加載

將類的字節(jié)碼載入方法區(qū)中蕊玷,內(nèi)部采用 C++ 的 instanceKlass 描述 java 類邮利,它的重要 field 有:

  • _java_mirror 即 java 的類鏡像,例如對(duì) String 來說垃帅,就是 String.class延届,作用是把 Kclass 暴露給 java 使用

  • _super 即父類

  • _fields 即成員變量

  • _methods 即方法

  • _constants 即常量池

  • _class_loader 即類加載器

  • _vtable 虛方法表

  • _itable 接口方法表

如果這個(gè)類還有父類沒有加載,先加載父類

加載和鏈接可能是交替運(yùn)行的

  • 注意

    instanceKlass 這樣的【元數(shù)據(jù)】是存儲(chǔ)在方法區(qū)(1.8 后的元空間內(nèi))贸诚,但 _java_mirror是存儲(chǔ)在堆中

    可以通過前面介紹的 HSDB 工具查看

image

4.2 鏈接

驗(yàn)證

驗(yàn)證類是否符合 JVM規(guī)范方庭,安全性檢查

用 UE 等支持二進(jìn)制的編輯器修改 HelloWorld.class 的魔數(shù)( 修改 ca fe ba by ),在控制臺(tái)運(yùn)行

image

準(zhǔn)備

為 static 變量分配空間酱固,設(shè)置默認(rèn)值

  • static 變量在 JDK 7 之前存儲(chǔ)于 instanceKlass 末尾械念,從 JDK 7 開始,存儲(chǔ)于 _java_mirror 末尾(在堆)

  • static 變量分配空間和賦值是兩個(gè)步驟运悲,分配空間在準(zhǔn)備階段完成龄减,賦值在初始化階段完成

  • 如果 static 變量是 final 的基本類型,以及字符串常量扇苞,那么編譯階段值就確定了欺殿,賦值在準(zhǔn)備階段完成

  • 如果 static 變量是 final 的寄纵,但屬于引用類型,那么賦值也會(huì)在初始化階段完成

public class Load8 {

    static int a;
    static int b = 10;
    static final int c = 20;
    static final String d = "hello";
    static final Object e = new Object();
}

字節(jié)碼

{
  static int a;
    descriptor: I
    flags: ACC_STATIC

  static int b;
    descriptor: I
    flags: ACC_STATIC   //準(zhǔn)備階段先分配空間脖苏,然后在初始化階段才賦值

  static final int c;
    descriptor: I
    flags: ACC_STATIC, ACC_FINAL
    ConstantValue: int 20    //final 直接賦值

  static final java.lang.String d;
    descriptor: Ljava/lang/String;
    flags: ACC_STATIC, ACC_FINAL
    ConstantValue: String hello   //finale 直接負(fù)責(zé)

  static final java.lang.Object e;
    descriptor: Ljava/lang/Object;
    flags: ACC_STATIC, ACC_FINAL   //引用類型程拭,那么賦值也會(huì)在初始化階段完成

  public cn.itcast.jvm.t3.load.Load8();
    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 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t3/load/Load8;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: bipush        10
         2: putstatic     #2                  // Field b:I
         5: new           #3                  // class java/lang/Object
         8: dup
         9: invokespecial #1                  // Method java/lang/Object."<init>":()V
        12: putstatic     #4                  // Field e:Ljava/lang/Object;
        15: return
      LineNumberTable:
        line 7: 0
        line 10: 5
}

解析

將常量池中的符號(hào)引用解析為直接引用

/**
 * 解析的含義
 */
public class Load2 {
    public static void main(String[] args) throws ClassNotFoundException, IOException {          ClassLoader classloader = Load2.class.getClassLoader();
       //loadClass 方法不會(huì)導(dǎo)致類的解析和初始化                                                 
       Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C");
       // new C();
      //只有通過new 方式,才能實(shí)現(xiàn)對(duì)類的解析
        System.in.read();
    }
}

class C {
    D d = new D();
}

class D {

}

4.3 初始化

<cinit> ()V 方法

初始化即調(diào)用 <cinit>()V 棍潘,虛擬機(jī)會(huì)保證這個(gè)類的『構(gòu)造方法』的線程安全

發(fā)生的時(shí)機(jī)

概括得說恃鞋,類初始化是【懶惰的】

導(dǎo)致類初始化的情況

  • main 方法所在的類,總會(huì)被首先初始化

  • 首次訪問這個(gè)類的靜態(tài)變量或靜態(tài)方法時(shí)

  • 子類初始化亦歉,如果父類還沒初始化恤浪,會(huì)引發(fā)

  • 子類訪問父類的靜態(tài)變量,只會(huì)觸發(fā)父類的初始化

  • Class.forName

  • new 會(huì)導(dǎo)致初始化

不會(huì)導(dǎo)致類初始化的情況

  • 訪問類的 static final 靜態(tài)常量(基本類型和字符串)不會(huì)觸發(fā)初始化

  • 類對(duì)象.class 不會(huì)觸發(fā)初始化

  • 創(chuàng)建該類的數(shù)組不會(huì)觸發(fā)初始化

  • 類加載器的 loadClass 方法

  • Class.forName 的參數(shù) 2 為 false 時(shí)

public class Load3 {
    static {
        System.out.println("main init");
    }
    public static void main(String[] args) throws ClassNotFoundException, IOException {
//        // 1. 靜態(tài)常量不會(huì)觸發(fā)初始化
//        System.out.println(B.b);
//        // 2. 類對(duì)象.class 不會(huì)觸發(fā)初始化
//        System.out.println(B.class);
//        // 3. 創(chuàng)建該類的數(shù)組不會(huì)觸發(fā)初始化
//        System.out.println(new B[0]);
        // 4. 不會(huì)初始化類 B肴楷,但會(huì)加載 B水由、A
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        cl.loadClass("cn.itcast.jvm.t3.load.B");
//        // 5. 不會(huì)初始化類 B,但會(huì)加載 B赛蔫、A
//        ClassLoader c2 = Thread.currentThread().getContextClassLoader();
//        Class.forName("cn.itcast.jvm.t3.load.B", false, c2);
        System.in.read();


//        // 1. 首次訪問這個(gè)類的靜態(tài)變量或靜態(tài)方法時(shí)
//        System.out.println(A.a);
//        // 2. 子類初始化砂客,如果父類還沒初始化,會(huì)引發(fā)
//        System.out.println(B.c);
//        // 3. 子類訪問父類靜態(tài)變量呵恢,只觸發(fā)父類初始化
//        System.out.println(B.a);
//        // 4. 會(huì)初始化類 B鞠值,并先初始化類 A
//        Class.forName("cn.itcast.jvm.t3.load.B");


    }
}

class A {
    static int a = 0;
    static {
        System.out.println("a init");
    }
}

class B extends A {
    final static double b = 5.0;
    static boolean c = false;
    static {
        System.out.println("b init");
    }
}

4.4 練習(xí)

從字節(jié)碼分析,使用 a渗钉,b彤恶,c 這三個(gè)常量是否會(huì)導(dǎo)致 E 初始化

public class Load4 {
    public static void main(String[] args) {
        System.out.println(E.a);
        System.out.println(E.b);
        System.out.println(E.c);

    }
}

class E {
    public static final int a = 10;
    public static final String b = "hello";
    public static final Integer c = 20;  // Integer.valueOf(20) 會(huì)導(dǎo)致初始化
    static {
        System.out.println("init E");
    }
}


//結(jié)果
10
hello
init E
20

典型應(yīng)用 - 完成懶惰初始化單例模式

public class Load9 {
    public static void main(String[] args) {
//        Singleton.test();    //結(jié)果,不會(huì)調(diào)用初始化
        
        //結(jié)果時(shí)執(zhí)行了一次初始化鳄橘,第二次調(diào)用類以及初始化声离,直接返回
        Singleton.getInstance();
        Singleton.getInstance();
    }

}

class Singleton {

    public static void test() {
        System.out.println("test");
    }

    private Singleton() {}

    private static class LazyHolder{
        private static final Singleton SINGLETON = new Singleton();
        static {
            System.out.println("lazy holder init");
        }
    }

    public static Singleton getInstance() {
        return LazyHolder.SINGLETON;
    }
}

5.類加載器

以 JDK 8 為例:

名稱 加載那些類 說明
Bootstrap ClassLoader JAVA_HOME/jre/lib 無法直接訪問
Extension ClassLoader JAVA_HOME/jre/lib/ext 上級(jí)為 Bootstrap,顯示為 null
Application ClassLoader classpath 上級(jí)為 Extension
自定義類加載器 自定義 上級(jí)為 Application

5.1 啟動(dòng)類加載器

用Boorstrap 類加載器加載類:

public class F {
    static {
        System.out.println("bootstrap F init");
    }
}

執(zhí)行

public class Load5_1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F");
        System.out.println(aClass.getClassLoader()); // AppClassLoader  ExtClassLoader
    }
}

輸出

E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:.
cn.itcast.jvm.t3.load.Load5
bootstrap F init
null   //由啟動(dòng)類加載器加載的

-Xbootclasspath 表示設(shè)置 bootclasspath

其中 /a:. 表示將當(dāng)前目錄追加至 bootclasspath 之后

可以用這個(gè)辦法替換核心類

  • java -Xbootclasspath:<new bootclasspath>

  • java -Xbootclasspath/a:<追加路徑>

  • java -Xbootclasspath/p:<追加路徑>

5.2 拓展類加載器

public class G { 
    static { 
        System.out.println("classpath G init");
    } 
}
/**
 * 演示 擴(kuò)展類加載器
 * 在 C:\Program Files\Java\jdk1.8.0_91 下有一個(gè) my.jar
 * 里面也有一個(gè) G 的類瘫怜,觀察到底是哪個(gè)類被加載了
 */
public class Load5_2 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
        System.out.println(aClass.getClassLoader());
    }
}

輸出

classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2

寫一個(gè)同名的類

public class G { 
    static { 
        System.out.println("ext G init"); 
    } 
}

打個(gè) jar 包

E:\git\jvm\out\production\jvm>jar -cvf my.jar cn/itcast/jvm/t3/load/G.class 
已添加清單 
正在添加: cn/itcast/jvm/t3/load/G.class(輸入 = 481) (輸出 = 322)(壓縮了 33%)

將 jar 包拷貝到 JAVA_HOME/jre/lib/ext

重新執(zhí)行 Load5_2

輸出

ext G init
sun.misc.Launcher$ExtClassLoader@29453f44

5.3 雙親委派機(jī)制

所謂的雙親委派抵恋,就是指調(diào)用類加載器的 loadClass 方法時(shí),查找類的規(guī)則

注意

這里的雙親宝磨,翻譯為上級(jí)似乎更為合適纹坐,因?yàn)樗鼈儾]有繼承關(guān)系

ClassLoader類中的 loadClass 方法

   protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            //1.檢查該類是否已經(jīng)加載
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        //2.有上級(jí)的話灭美,委派上級(jí) loadClass
                        c = parent.loadClass(name, false);
                    } else {
                        //3.如果沒有上級(jí)了(ExtClassLoader),則委派BootstrapClassLoader
                        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.
                    long t1 = System.nanoTime();
                    //4.每一層找不到痕惋,調(diào)用findClass 方法(每個(gè)類加載器直接拓展)來加載
                    c = findClass(name);

                    //5.記錄耗時(shí)
                    // this is the defining class loader; record the stats
                    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.4 線程上下文類加載器

我們?cè)谑褂?JDBC 時(shí)私恬,都需要加載 Driver 驅(qū)動(dòng)拙吉,不知道你注意到?jīng)]有脊僚,不寫

Class.forName("com.mysql.jdbc.Driver")

也是可以讓 com.mysql.jdbc.Driver 正確加載的顶燕,你知道是怎么做的嗎庇麦?

讓我們追蹤一下源碼:

public class DriverManager {


    
    //注冊(cè)驅(qū)動(dòng)的集合
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();


    private DriverManager(){}


    //初始化驅(qū)動(dòng)
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

先不看別的蝙寨,看看 DriverManager 的類加載器:

System.out.println(DriverManager.class.getClassLoader());

打印 null晒衩,表示它的類加載器是 Bootstrap ClassLoader嗤瞎,會(huì)到 JAVA_HOME/jre/lib 下搜索類,但

JAVA_HOME/jre/lib 下顯然沒有 mysql-connector-java-5.1.47.jar 包听系,這樣問題來了贝奇,在

DriverManager 的靜態(tài)代碼塊中,怎么能正確加載 com.mysql.jdbc.Driver 呢靠胜?

繼續(xù)看 loadInitialDrivers() 方法:

private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
 
        // 1)使用 ServiceLoader 機(jī)制加載驅(qū)動(dòng)掉瞳,即 SPI
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

   
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);
         // 2)使用 jdbc.drivers 定義的驅(qū)動(dòng)名加載驅(qū)動(dòng)
        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                
                // 這里的 ClassLoader.getSystemClassLoader() 就是應(yīng)用程序類加載器
                Class.forName(aDriver, true,ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

先看 2)發(fā)現(xiàn)它最后是使用 Class.forName 完成類的加載和初始化,關(guān)聯(lián)的是應(yīng)用程序類加載器浪漠,因此

可以順利完成類加載

再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)

約定如下陕习,在 jar 包的 META-INF/services 包下,以接口全限定名名為文件址愿,文件內(nèi)容是實(shí)現(xiàn)類名稱

image

這樣就可以使用

ServiceLoader<接口類型> allImpls = ServiceLoader.load(接口類型.class); 
Iterator<接口類型> iter = allImpls.iterator(); 
while(iter.hasNext()) { 
    iter.next(); 
}

來得到實(shí)現(xiàn)類该镣,體現(xiàn)的是【面向接口編程+解耦】的思想,在下面一些框架中都運(yùn)用了此思想:

  • JDBC

  • Servlet 初始化器

  • Spring 容器

  • Dubbo(對(duì) SPI 進(jìn)行了擴(kuò)展)

接著看 ServiceLoader.load 方法:

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 獲取線程上下文類加載器
    ClassLoader cl = Thread.currentThread().getContextClassLoader(); 
    return ServiceLoader.load(service, cl);
}

線程上下文類加載器是當(dāng)前線程使用的類加載器响谓,默認(rèn)就是應(yīng)用程序類加載器损合,它內(nèi)部又是由Class.forName 調(diào)用了線程上下文類加載器完成類加載,具體代碼在 ServiceLoader 的內(nèi)部類LazyIterator 中:

 private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

5.5 自定義類加載器

問問自己歌粥,什么時(shí)候需要自定義類加載器

  • 1)想加載非 classpath 隨意路徑中的類文件

  • 2)都是通過接口來使用實(shí)現(xiàn)塌忽,希望解耦時(shí),常用在框架設(shè)計(jì)

  • 3)這些類希望予以隔離失驶,不同應(yīng)用的同名類都可以加載土居,不沖突,常見于 tomcat 容器

步驟:

  1. 繼承 ClassLoader 父類

  2. 要遵從雙親委派機(jī)制嬉探,重寫 findClass 方法

? 注意不是重寫 loadClass 方法擦耀,否則不會(huì)走雙親委派機(jī)制

  1. 讀取類文件的字節(jié)碼

  2. 調(diào)用父類的 defineClass 方法來加載類

  3. 使用者調(diào)用該類加載器的 loadClass 方法

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市涩堤,隨后出現(xiàn)的幾起案子眷蜓,更是在濱河造成了極大的恐慌,老刑警劉巖胎围,帶你破解...
    沈念sama閱讀 212,884評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吁系,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡白魂,警方通過查閱死者的電腦和手機(jī)汽纤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來福荸,“玉大人蕴坪,你說我怎么就攤上這事。” “怎么了背传?”我有些...
    開封第一講書人閱讀 158,369評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵呆瞻,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我径玖,道長(zhǎng)痴脾,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,799評(píng)論 1 285
  • 正文 為了忘掉前任挺狰,我火速辦了婚禮明郭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘丰泊。我一直安慰自己薯定,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,910評(píng)論 6 386
  • 文/花漫 我一把揭開白布瞳购。 她就那樣靜靜地躺著话侄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪学赛。 梳的紋絲不亂的頭發(fā)上年堆,一...
    開封第一講書人閱讀 50,096評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音盏浇,去河邊找鬼变丧。 笑死,一個(gè)胖子當(dāng)著我的面吹牛绢掰,可吹牛的內(nèi)容都是我干的痒蓬。 我是一名探鬼主播,決...
    沈念sama閱讀 39,159評(píng)論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼滴劲,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼攻晒!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起班挖,我...
    開封第一講書人閱讀 37,917評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤鲁捏,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后萧芙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體给梅,經(jīng)...
    沈念sama閱讀 44,360評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,673評(píng)論 2 327
  • 正文 我和宋清朗相戀三年双揪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了破喻。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,814評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡盟榴,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出婴噩,到底是詐尸還是另有隱情擎场,我是刑警寧澤羽德,帶...
    沈念sama閱讀 34,509評(píng)論 4 334
  • 正文 年R本政府宣布,位于F島的核電站迅办,受9級(jí)特大地震影響宅静,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜站欺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,156評(píng)論 3 317
  • 文/蒙蒙 一姨夹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧矾策,春花似錦磷账、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蓬豁,卻和暖如春绰咽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背地粪。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評(píng)論 1 267
  • 我被黑心中介騙來泰國(guó)打工取募, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蟆技。 一個(gè)月前我還...
    沈念sama閱讀 46,641評(píng)論 2 362
  • 正文 我出身青樓玩敏,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親付魔。 傳聞我的和親對(duì)象是個(gè)殘疾皇子聊品,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,728評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容