JVM学习笔记(四)类加载与字节码技术

目录

一、类文件结构

二、字节码指令

[2.3 图解方法执行流程](#2.3 图解方法执行流程)

[1)原始 java 代码](#1)原始 java 代码)

2)编译后的字节码文件

3)常量池载入运行时常量池

4)方法字节码载入方法区

[5)main 线程开始运行,分配栈帧内存](#5)main 线程开始运行,分配栈帧内存)

6)执行引擎开始执行字节码★★★

[2.4 练习 - 分析 i++](#2.4 练习 - 分析 i++)

[2.5 条件判断指令](#2.5 条件判断指令)

[2.6 循环控制指令](#2.6 循环控制指令)

[2.7 练习 - 判断结果](#2.7 练习 - 判断结果)

[2.8 构造方法★★★](#2.8 构造方法★★★)

[1) ()V 类构造方法](#1) ()V 类构造方法)

[2) ()V 实例构造方法](#2) ()V 实例构造方法)

[2.9 方法调用](#2.9 方法调用)

[2.10 多态的原理](#2.10 多态的原理)

多态原理小结★★★

[2.11 异常处理](#2.11 异常处理)

1)​​​​​​​try-catch

2)多个single-catch块的情况

[3)multi-catch 的情况](#3)multi-catch 的情况)

4)finally

[5)finally 面试题一](#5)finally 面试题一)

[5)finally 面试题二](#5)finally 面试题二)

[2.12 synchronized](#2.12 synchronized)

三、编译期处理

[1. 自动生成无参构造方法](#1. 自动生成无参构造方法)

[2. 自动拆装箱的转换](#2. 自动拆装箱的转换)

[3. 泛型集合取值](#3. 泛型集合取值)

[4. 可变参数](#4. 可变参数)

[5. foreach 循环](#5. foreach 循环)

[6. switch 字符串](#6. switch 字符串)

[7. switch 枚举](#7. switch 枚举)

[8. 枚举](#8. 枚举)

[9. try-with-resources](#9. try-with-resources)

[10. 方法重写时的桥接方法](#10. 方法重写时的桥接方法)

[11. 匿名内部类](#11. 匿名内部类)

四、类加载阶段

[4.1 加载](#4.1 加载)

[4.2 链接](#4.2 链接)

2)准备★★★

[4.3 初始化](#4.3 初始化)

练习一:

[练习二:典型应用 - 完成懒惰初始化单例模式](#练习二:典型应用 - 完成懒惰初始化单例模式)

五、类加载器

[5.1 双亲委派机制](#5.1 双亲委派机制)

[5.2 线程上下文类加载器](#5.2 线程上下文类加载器)

[5.3 自定义类加载器](#5.3 自定义类加载器)

六、运行期优化

[6.1 即时编译](#6.1 即时编译)

[6.2 反射优化](#6.2 反射优化)


这篇文章很不错,尤其是讲到类加载阶段那一块的时候:(没有本人这篇博文好,哈哈)

认识 .class 文件的字节码结构-CSDN博客

一、类文件结构

根据 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];

}

二、字节码指令

反编译命令:javap -v Xxx.class

2.3 图解方法执行流程

1)原始 java 代码

java 复制代码
package org.wuya.test;

/**
 * 演示 字节码指令 和 操作数栈、常量池的关系
 */
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)编译后的字节码文件

Classfile /D:/JavaTools/springbootRedisDemo/springbootRedisDemo/target/classes/org/wuya/test/Demo3_1.class

Last modified 2024-4-21; size 641 bytes

MD5 checksum 87a2a6a3e3f7d22289041a5d50f4c0dd

Compiled from "Demo3_1.java"

public class org.wuya.test.Demo3_1

minor version: 0

major version: 52

flags: ACC_PUBLIC, ACC_SUPER

Constant pool:

#1 = Methodref #7.#26 // java/lang/Object."<init>":()V

#2 = Class #27 // java/lang/Short

#3 = Integer 32768

#4 = Fieldref #28.#29 // java/lang/System.out:Ljava/io/PrintStream;

#5 = Methodref #30.#31 // java/io/PrintStream.println:(I)V

#6 = Class #32 // org/wuya/test/Demo3_1

#7 = Class #33 // java/lang/Object

#8 = Utf8 <init>

#9 = Utf8 ()V

#10 = Utf8 Code

#11 = Utf8 LineNumberTable

#12 = Utf8 LocalVariableTable

#13 = Utf8 this

#14 = Utf8 Lorg/wuya/test/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 MethodParameters

#24 = Utf8 SourceFile

#25 = Utf8 Demo3_1.java

#26 = NameAndType #8:#9 // "<init>":()V

#27 = Utf8 java/lang/Short

#28 = Class #34 // java/lang/System

#29 = NameAndType #35:#36 // out:Ljava/io/PrintStream;

#30 = Class #37 // java/io/PrintStream

#31 = NameAndType #38:#39 // println:(I)V

#32 = Utf8 org/wuya/test/Demo3_1

#33 = Utf8 java/lang/Object

#34 = Utf8 java/lang/System

#35 = Utf8 out

#36 = Utf8 Ljava/io/PrintStream;

#37 = Utf8 java/io/PrintStream

#38 = Utf8 println

#39 = Utf8 (I)V

{

public org.wuya.test.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 Lorg/wuya/test/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

MethodParameters:

Name Flags

args

}

SourceFile: "Demo3_1.java"

3)常量池载入运行时常量池

4)方法字节码载入方法区

5)main 线程开始运行,分配栈帧内存

(stack=2,locals=4)

6)执行引擎开始执行字节码★★★

bipush 10

istore_1

ldc #3 // int 32768

istore_2

iload_1

iload_2

iadd

istore_3

getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;

iload_3

invokevirtual #5 // Method java/io/PrintStream.println:(I)V

return
说明:具体每一步的图片看讲义。非常明晰。

bipush 10

将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有

  • sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
  • ldc 将一个 int 压入操作数栈
  • ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)

这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池。

  • bipush 10 将一个 byte 压入操作数栈(其长度会补齐 4 个字节)
  • istore_1 将操作数栈顶数据弹出,存入局部变量表的 slot 1,结果就是a被赋值为10 【可见,bipush 10和istore_1就对应java源代码中的int a = 10;操作】

(接下来就是对b变量赋值:由于b变量对应的值是32768,已经超过了short的范围了,所以它存储的位置是运行时常量池中)

  • ldc 从常量池加载#3数据到操作数栈(注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的)
  • istore_2 将操作数栈顶数据弹出,存入局部变量表的slot 2,结果就是b被赋值为32767

(接下来该执行a+b赋值给c了。局部变量表中不能执行a+b这个操作,它一定是在操作数栈中完成,所以执行引擎要对a、b这两个变量进行读取)

  • iload_1 把局部变量1槽位的值读入操作数栈上
  • iload_2 把局部变量2槽位的值读入操作数栈上
  • iadd 相加,把结果存入操作数栈
  • istore_3 将操作数栈顶数据弹出,存入局部变量表的slot 3
  • getstatic 从常量池#4找成员变量(System类的out成员变量)的引用,把堆中System.out 对象的引用值放入操作数栈

(接下来该进行打印,打印的话,需要一些参数)

  • iload_3 把局部变量2槽位的值32768读入操作数栈上
  • invokevirtual 分析见下面
  • return 完成main方法调用,弹出main栈帧,程序结束

invokevirtual #5

  • 找到常量池 #5 项
  • 定位到方法区 java/io/PrintStream.println:(I)V 方法
  • 生成新的栈帧(分配 locals、stack等)
  • 传递参数,执行新栈帧中的字节码
  • 执行完毕,弹出栈帧
  • 清除 main 操作数栈内容

2.4 练习 - 分析 i++

1)源码如下:

java 复制代码
package org.wuya.test;

/**
 * 从字节码角度分析 a++  相关题目
 */
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);
    }
}

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 表示对1号槽位自增,自增1

7: iinc 1, 1

10: iload_1

11: iadd 表示将操作数栈中的两个数(10+12)进行了相加,把结果22保存在了操作数栈

12: iload_1 执行a--操作,将局部变量表中slot 1的值12先读入操作数栈,再自减

13: iinc 1, -1

16: iadd 将22+12的结果34保存在操作数栈

17: istore_2 将操作数栈顶数据弹出,存入局部变量表的slot 2,即将34赋值给了b

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

MethodParameters:

Name Flags

args

}

分析:

  • 注意 iinc 指令是直接在局部变量 slot 上进行运算
  • a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc

a++ 是先iload再自增(iinc),++a 是先自增(iinc)再iload。

具体分析见上面。图解见讲义。

2.5 条件判断指令

讲义有一张指令代表的含义,略。

几点说明:

  • byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节
  • goto 用来进行跳转到指定行号的字节码
  • 比较小的整数(从-1到5)是用iconst来表示的。

源码:

java 复制代码
package org.wuya.test;

public class Demo3_3 {
    public static void main(String[] args) {
        int a = 0;
        if(a == 0) {
            a = 10;
        } else {
            a = 20;
        }
    }
}

关键的字节码:

0: iconst_0

1: istore_1

2: iload_1

3: ifne 12 ifne是判断是否 != 0,如果不等于0的话就跳到12行,否则顺序执行

6: bipush 10

8: istore_1

9: goto 15 goto表示跳到15行执行

12: bipush 20

14: istore_1

15: return

2.6 循环控制指令

其实循环控制还是前面介绍的那些指令,例如 while 循环,do while 循环,字节码略。

java 复制代码
public class Demo3_4 {
   public static void main(String[] args) {
       int a = 0;
       while (a < 10) {
           a++;
      }
  }
}



public class Demo3_5 {
   public static void main(String[] args) {
       int a = 0;
       do {
           a++;
      } while (a < 10);
  }
}

再看看 for 循环,比较 while 和 for 的字节码,你发现它们是一模一样的。

2.7 练习 - 判断结果

请从字节码角度分析,下列代码运行的结果:

java 复制代码
package org.wuya.test;

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);// 结果是 0
    }
}

关键字节码:

0: iconst_0

1: istore_1

2: iconst_0

3: istore_2

4: iload_1

5: bipush 10

7: if_icmpge 21

10: iload_2

11: iinc 2, 1

14: istore_2 关键是这个赋值操作,将操作数栈中的0又赋值给了x

15: iinc 1, 1

18: goto 4

21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;

24: iload_2

25: invokevirtual #3 // Method java/io/PrintStream.println:(I)V

28: return

2.8 构造方法★★★

1) <cinit>()V 类构造方法

源码:

java 复制代码
package org.wuya.test;

public class Demo3_8_1 {

    static {
        i = 20;
    }


    static {
        i = 30;
    }

    static int i = 10;

}

反编译后的字节码:

Classfile /D:/JavaTools/.../src/main/java/org/wuya/test/Demo3_8_1.class

Last modified 2024-4-21; size 305 bytes

MD5 checksum 88505d650a05ea6c2869fd5eae46f394

Compiled from "Demo3_8_1.java"

public class org.wuya.test.Demo3_8_1

minor version: 0

major version: 52

flags: ACC_PUBLIC, ACC_SUPER

Constant pool:

#1 = Methodref #4.#14 // java/lang/Object."<init>":()V

#2 = Fieldref #3.#15 // org/wuya/test/Demo3_8_1.i:I

#3 = Class #16 // org/wuya/test/Demo3_8_1

#4 = Class #17 // java/lang/Object

#5 = Utf8 i

#6 = Utf8 I

#7 = Utf8 <init>

#8 = Utf8 ()V

#9 = Utf8 Code

#10 = Utf8 LineNumberTable

#11 = Utf8 <clinit>

#12 = Utf8 SourceFile

#13 = Utf8 Demo3_8_1.java

#14 = NameAndType #7:#8 // "<init>":()V

#15 = NameAndType #5:#6 // i:I

#16 = Utf8 org/wuya/test/Demo3_8_1

#17 = Utf8 java/lang/Object

{

static int i;

2: putstatic #2 // Field i:I

5: bipush 30

7: putstatic #2 // Field i:I

10: bipush 10

12: putstatic #2 // Field i:I

15: return

LineNumberTable:

line 6: 0

line 11: 5

line 14: 10

}

SourceFile: "Demo3_8_1.java"

编译器会按(源码中)从上至下的顺序,收集所有 static 静态代码块静态成员赋值的代码,合并为一个特殊的方法 <cinit>()V :

static int i;

2: putstatic #2 // Field i:I

5: bipush 30

7: putstatic #2 // Field i:I

10: bipush 10

12: putstatic #2 // Field i:I

15: return

<cinit>()V 方法会在类加载的初始化阶段被调用。

2) <init>()V 实例构造方法

源码:

java 复制代码
package org.wuya.test;

public class Demo3_8_2 {


    private String a = "s1";

    {
        b = 20;
    }

    private int b = 10;

    {
        a = "s2";
    }

    public Demo3_8_2(String a, int b) {
        this.a = a;
        this.b = b;
    }

    public static void main(String[] args) {
        Demo3_8_2 d = new Demo3_8_2("s3", 30);
        System.out.println(d.a);
        System.out.println(d.b);
    }
}

下面是反编译后的字节码:

java 复制代码
PS D:\JavaTools\...\target\classes\org\wuya\test> javap -v .\Demo3_8_2.class
Classfile /D:/JavaTools/.../target/classes/org/wuya/test/Demo3_8_2.class
  Last modified 2024-4-21; size 859 bytes                                                
  MD5 checksum 987873efec13bf5b024a1af1a2d71201                                          
  Compiled from "Demo3_8_2.java"                                                         
public class org.wuya.test.Demo3_8_2                                                     
  minor version: 0                                                                       
  major version: 52                                                                      
  flags: ACC_PUBLIC, ACC_SUPER                                                           
Constant pool:                                                                           
   #1 = Methodref          #12.#32        // java/lang/Object."<init>":()V               
   #2 = String             #33            // s1                                          
   #3 = Fieldref           #6.#34         // org/wuya/test/Demo3_8_2.a:Ljava/lang/String;
   #4 = Fieldref           #6.#35         // org/wuya/test/Demo3_8_2.b:I                 
   #5 = String             #36            // s2                                          
   #6 = Class              #37            // org/wuya/test/Demo3_8_2                     
   #7 = String             #38            // s3                                          
   #8 = Methodref          #6.#39         // org/wuya/test/Demo3_8_2."<init>":(Ljava/lang/String;I)V
   #9 = Fieldref           #40.#41        // java/lang/System.out:Ljava/io/PrintStream;
  #10 = Methodref          #42.#43        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #11 = Methodref          #42.#44        // java/io/PrintStream.println:(I)V
  #12 = Class              #45            // java/lang/Object
  #13 = Utf8               a
  #14 = Utf8               Ljava/lang/String;
  #15 = Utf8               b
  #16 = Utf8               I
  #17 = Utf8               <init>
  #18 = Utf8               (Ljava/lang/String;I)V
  #19 = Utf8               Code
  #20 = Utf8               LineNumberTable
  #21 = Utf8               LocalVariableTable
  #22 = Utf8               this
  #23 = Utf8               Lorg/wuya/test/Demo3_8_2;
  #24 = Utf8               MethodParameters
  #25 = Utf8               main
  #26 = Utf8               ([Ljava/lang/String;)V
  #27 = Utf8               args
  #28 = Utf8               [Ljava/lang/String;
  #29 = Utf8               d
  #30 = Utf8               SourceFile
  #31 = Utf8               Demo3_8_2.java
  #32 = NameAndType        #17:#46        // "<init>":()V
  #33 = Utf8               s1
  #34 = NameAndType        #13:#14        // a:Ljava/lang/String;
  #35 = NameAndType        #15:#16        // b:I
  #36 = Utf8               s2
  #37 = Utf8               org/wuya/test/Demo3_8_2
  #38 = Utf8               s3
  #39 = NameAndType        #17:#18        // "<init>":(Ljava/lang/String;I)V
  #40 = Class              #47            // java/lang/System
  #41 = NameAndType        #48:#49        // out:Ljava/io/PrintStream;
  #42 = Class              #50            // java/io/PrintStream
  #43 = NameAndType        #51:#52        // println:(Ljava/lang/String;)V
  #44 = NameAndType        #51:#53        // println:(I)V
  #45 = Utf8               java/lang/Object
  #46 = Utf8               ()V
  #47 = Utf8               java/lang/System
  #48 = Utf8               out
  #49 = Utf8               Ljava/io/PrintStream;
  #50 = Utf8               java/io/PrintStream
  #51 = Utf8               println
  #52 = Utf8               (Ljava/lang/String;)V
  #53 = Utf8               (I)V
{
  public org.wuya.test.Demo3_8_2(java.lang.String, int);
    descriptor: (Ljava/lang/String;I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: ldc           #2                  // String s1
         7: putfield      #3                  // Field a:Ljava/lang/String;
        10: aload_0
        11: bipush        20
        13: putfield      #4                  // Field b:I
        16: aload_0
        17: bipush        10
        19: putfield      #4                  // Field b:I
        22: aload_0
        23: ldc           #5                  // String s2
        25: putfield      #3                  // Field a:Ljava/lang/String;
        28: aload_0
        29: aload_1
        30: putfield      #3                  // Field a:Ljava/lang/String;
        33: aload_0
        34: iload_2
        35: putfield      #4                  // Field b:I
        38: return
      LineNumberTable:
        line 18: 0
        line 6: 4
        line 9: 10
        line 12: 16
        line 15: 22
        line 19: 28
        line 20: 33
        line 21: 38
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      39     0  this   Lorg/wuya/test/Demo3_8_2;
            0      39     1     a   Ljava/lang/String;
            0      39     2     b   I
    MethodParameters:
      Name                           Flags
      a
      b

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=2, args_size=1
         0: new           #6                  // class org/wuya/test/Demo3_8_2
         3: dup
         4: ldc           #7                  // String s3
         6: bipush        30
         8: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
        11: astore_1
        12: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: aload_1
        16: getfield      #3                  // Field a:Ljava/lang/String;
        19: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        22: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
        25: aload_1
        26: getfield      #4                  // Field b:I
        29: invokevirtual #11                 // Method java/io/PrintStream.println:(I)V
        32: return
      LineNumberTable:
        line 24: 0
        line 25: 12
        line 26: 22
        line 27: 32
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      33     0  args   [Ljava/lang/String;
           12      21     1     d   Lorg/wuya/test/Demo3_8_2;
    MethodParameters:
      Name                           Flags
      args
}
SourceFile: "Demo3_8_2.java"

编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后。

【从字节码可以看出来,新的构造方法是Demo3_8_2(java.lang.String, int)】

在这个构造方法中,aload_0 表示把this加载到操作数栈;

2.9 方法调用

看一下几种不同的方法调用对应的字节码指令
1)如果源代码是这样的:

java 复制代码
package org.wuya.test;

public class Demo3_9 {
    public Demo3_9() { }

    private void test1() { }

    private final void test2() { }

    public void test3() { }

    public static void test4() { }

}

则javac命令编译后,再执行javap反编译后,方法表集合的字节码是这样的:

java 复制代码
{
  public org.wuya.test.Demo3_9();
    descriptor: ()V
    flags: ACC_PUBLIC

  public static void test4();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 12: 0
}

可以看到在类加载阶段,只加载了构造方法和static方法。

2)如果源代码是这样的:

java 复制代码
package org.wuya.test;

public class Demo3_9 {
    public Demo3_9() { } //构造方法

    private void test1() { } //私有方法

    private final void test2() { }  //final方法

    public void test3() { } //public方法

    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();
        d.test2();
        d.test3();
        d.test4();//通过对象调用的静态方法
        Demo3_9.test4();//通过类名调用的静态方法
        d.toString();
    }

}

则javac命令编译后,再执行javap反编译后,方法表集合的字节码是这样的:

java 复制代码
{
  public org.wuya.test.Demo3_9();
    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

  public void test3();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 10: 0

  public static void test4();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 12: 0

  public java.lang.String toString();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #2                  // Method java/lang/Object.toString:()Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 16: 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           #3                  // class org/wuya/test/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
        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
      LineNumberTable:
        line 20: 0
        line 21: 8
        line 22: 12
        line 23: 16
        line 24: 20
        line 25: 25
        line 26: 28
        line 27: 33
}

final的方法,不管修饰符是private,还是public,还是什么,对应的字节码指令是一样的。

可以看到:

  • 构造方法、final方法、private方法都是调用的invokespecial;
  • 普通public方法调用的是invokevirtual;
  • 静态方法调用的是invokestatic;

其中,invokestatic和invokespecial都属于静态绑定,也就是在.class字节码指令生成的时候,就知道如何找到哪个类的哪个方法了。构造方法、私有方法、静态方法都是能唯一确定的;只有普通的public方法在编译期间并不能确定是调用哪个类的哪个方法,也许是子类的也许是父类的,所以invokevirtual叫动态绑定,在运行的时候确定。

  • new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈;
  • dup 是复制操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 "<init>":()V (**会消耗掉栈顶一个引用,就是说调用完构造方法后,复制的这个引用就清除了。**延伸:这里就是我们所说的new对象的时候会调用构造方法),另一个要配合 astore_1 赋值给局部变量;(注意:赋值完成后,操作数栈就没有数据了哦)
  • 最终方法(final),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静态绑定;
  • 普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态;
  • 成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】;
  • 比较有意思的是 d.test4(); 是通过【对象引用】调用一个静态方法,可以看到在调用invokestatic 之前执行了 pop 指令,把【对象引用】从操作数栈弹掉了;(因为调用静态方法不需要对象调用)
  • 还有一个执行 invokespecial 的情况是通过 super 调用父类方法 。

2.10****多态的原理

前面介绍了虚拟机指令中的方法调用的多种方式,其中有一种方式是invokevirtual ,它是实现一个方法的多态调用,它的工作方法更为复杂,这里研究invokevirtual指令的执行流程,也就是多态的原理。

java 复制代码
package org.wuya.test;

import java.io.IOException;

/**
 * 演示多态原理,注意加上下面的 JVM 参数,禁用指针压缩
 * -XX:-UseCompressedOops -XX:-UseCompressedClassPointers
 */
public class Demo3_10 {

    public static void test(Animal animal) {
        animal.eat();
        System.out.println(animal.toString());
    }

    public static void main(String[] args) throws IOException {
        test(new Cat());
        test(new Dog());
        System.in.read();
    }
}

abstract class Animal {
    public abstract void eat();

    @Override
    public String toString() {
        return "我是" + this.getClass().getSimpleName();
    }
}

class Dog extends Animal {

    @Override
    public void eat() {
        System.out.println("啃骨头");
    }
}

class Cat extends Animal {

    @Override
    public void eat() {
        System.out.println("吃鱼");
    }
}

1)运行代码
停在System.in.read()方法上,这时运行jps获取进程id
2)运行 HSDB 工具
进入JDK安装目录,执行相关命令。
3)查找某个对象
4)查看对象内存结构
点击超链接可以看到对象的内存结构,此对象没有任何属性,因此只有对象头的16字节,前8字节是MarkWord,后8字节就是对象的Class指针,但目前看不到它的实际地址。
5)查看对象Class的内存地址
6)查看类的vtable

找到Dog Class的vtable长度为6,意思就是Dog类有6个虚方法(多态相关的,final,static不会列入)。 那么这 6 个方法都是谁呢?
7 )验证方法地址
通过 Tools -> Class Browser 查看每个类的方法定义,比较可知

Dog - public void eat() @0x000000001b7d3fa8
Animal - public java.lang.String toString() @0x000000001b7d35e8;
Object - protected void finalize() @0x000000001b3d1b10;
Object - public boolean equals(java.lang.Object) @0x000000001b3d15e8;
Object - public native int hashCode() @0x000000001b3d1540;
Object - protected native java.lang.Object clone() @0x000000001b3d1678;
对号入座,发现:

  • eat() 方法是 Dog 类自己的
  • toString() 方法是继承Animal 类的
  • finalize() ,equals(),hashCode(),clone() 都是继承 Object 类的

多态原理小结★★★

当执行 invokevirtual 指令时,

  1. 先通过栈帧中的对象引用找到对象
  2. 分析对象头,找到对象的实际 Class
  3. Class 结构中有 vtable ,它在类加载的链接阶段就已经根据方法的重写规则生成好了
  4. 查表得到方法的具体地址
  5. 执行方法的字节码

2.11 异常处理

1)​​​​​​​ try-catch

源码:

java 复制代码
package org.wuya.test;

public class Demo3_11_1 {

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

运行上面的代码,生成.class文件,再执行反编译命令后的关键的字节码摘录:

(不运行代码直接javac编译,与运行代码编译,反编译后的字节码格式上有一点差异)

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 这两行执行try中的代码块,如果发生异常,怎么进入catch的呢?

5: goto 12

8: astore_2 把那些异常对象的引用地址存入到局部变量表2号槽位上,槽位名字是e

9: bipush 20

11: istore_1

12: return

Exception table: Exception table对上面[2,5)行代码进行监控,一旦有异常发生,会进行匹配,如果与声明的java/lang/Exception类型一致或者是它的子类,则进入到target指示的第8行执行。
from to target type
2 5 8 Class java/lang/Exception

LineNumberTable:

line 6: 0

line 8: 2

line 11: 5

line 9: 8

line 10: 9

line 12: 12

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 */

MethodParameters:

Name Flags

args

2)多个single-catch块的情况

3)multi-catch****的情况

这是JDK1.7后的新语法

源码:

java 复制代码
package org.wuya.test;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

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

4)finally

java 复制代码
package org.wuya.test;

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

分析字节码知道:finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程。

5)finally****面试题一

finally 出现了 return

问:下面程序输出结果是什么?

  • 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以 finally 的为准
  • 至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子
  • 跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常,可以试一下下面的代码

finally 中出现了 return,会吞掉异常,所以日常编码一定不要在finally中写return代码!

java 复制代码
package org.wuya.test;

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

    public static int test() {
        try {
            //int i = 1/0;//虽然有异常,但该程序执行时不会发生任何异常,因为吞掉了。
            return 10;
        } finally {
            return 20;
        }
    }
}

5)finally****面试题二

finally 对返回值影响

java 复制代码
package org.wuya.test;

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

    public static int test() {
        int i = 10;
        try {
            return i;
        } finally {
            i = 20;
        }
    }
}

分析字节码知道:如果在try中进行了return,之后又在finally中进行了修改,是不会影响返回结果的。因为它在return之前先做了暂存,然后执行了finally中的代码,然后再把暂存的值恢复到栈顶,返回的还是暂存的这个值。所以结果已经在return时确定了,再在finally中修改已经无效了。

延伸:

2.12 synchronized

从字节码角度分析:为什么synchronized能够保证加锁/释放锁的成对出现呢?

java 复制代码
package org.wuya.test;

public class Demo3_13 {
    public static void main(String[] args) {
        Object lock = new Object();
        synchronized (lock) {
            System.out.println("ok");
        }
    }
}
java 复制代码
  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                  // class java/lang/Object
         3: dup
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: astore_1
         8: aload_1
         9: dup
        10: astore_2
        11: monitorenter
        12: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: ldc           #4                  // String ok
        17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        20: aload_2
        21: monitorexit
        22: goto          30
        25: astore_3
        26: aload_2
        27: monitorexit
        28: aload_3
        29: athrow
        30: return
      Exception table:
         from    to  target type
            12    22    25   any
            25    28    25   any
      LineNumberTable:
        line 5: 0
        line 6: 8
        line 7: 12
        line 8: 20
        line 9: 30
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0  args   [Ljava/lang/String;
            8      23     1  lock   Ljava/lang/Object;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 25
          locals = [ class "[Ljava/lang/String;", class java/lang/Object, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4
    MethodParameters:
      Name                           Flags
      args

注意:方法级别的 synchronized 不会在字节码指令中有所体现。

**三、**编译期处理

  • 所谓的 语法糖 ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利(给糖吃嘛)
  • 注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。

1. 自动生成无参构造方法

2. 自动拆装箱的转换

java 复制代码
package org.wuya.test;

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


这个特性是 JDK 5 开始加入的。由于是编译期完成的,所以代码怎么写都行哦,不存在效率高低之分。即使下面这样写也不代表效率高哦。

java 复制代码
public static void main(String[] args) {
    Integer x = Integer.valueOf(1);
    int y = x.intValue();
}

**3.**泛型集合取值

  • 泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理,所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换(checkcast)的操作。
  • 擦除的是字节码上的泛型信息,可以看到 LocalVariableTypeTable 仍然保留了方法参数泛型的信息。但是有限制:虽然没有被擦除,但局部变量上的泛型信息没有办法通过反射机制拿到;只有在方法参数和返回值上带有泛型信息,才能用反射的方法获取到。

4. 可变参数

​​​​​​​ 可变参数也是 JDK 5 开始加入的新特性。

java 复制代码
package org.wuya.test;

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 编译器会在编译期间将上述代码变换为:

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() 则等价代码为 foo(new String[]{}) ,创建了一个空的数组,而不会传递 null 进去。

5. foreach****循环

也是 JDK 5 开始引入的语法糖。

  • 数组的循环会被编译器转换为:带下标的fori循环;
  • 集合的循环实际被编译器转换为对迭代器的调用

注意
foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用,其中
Iterable 用来获取集合的迭代器( Iterator )

6. switch****字符串

从 JDK 7 开始, switch 可以作用于字符串和枚举类,这个功能其实也是语法糖。
字符串的switch,实际被拆分成两个整数的switch分支。

7. switch****枚举

java 复制代码
package org.wuya.test;

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

转换后代码:

java 复制代码
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()] = 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;
        }
    }
}

8. 枚举

JDK 7 新增了枚举类,以前面的性别枚举为例:

java 复制代码
enum Sex {
    MALE, FEMALE
}

转换后代码:

java 复制代码
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};
    }
    /**
     * Sole constructor. Programmers cannot invoke this constructor.
     * It is for use by code emitted by the compiler in response to
     * enum type declarations.
     *
     * @param name - The name of this enum constant, which is the identifier
     * used to declare it.
     * @param ordinal - The ordinal of this enumeration constant (its position
     * in the enum declaration, where the initial constant is
    assigned
     */
    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);
    }
}

枚举类的本质也是一个Class,它里面的 MALE, FEMALE 实际上就是Class的两个实例对象。枚举类和普通类的最大区别就是,普通类的实例对象可以是无穷多个,而枚举类的实例对象是有限的,比如该例中实例对象只有MALE, FEMALE两个。通过上面的字节码可以分析出来。

9. try-with-resources

jdk1.7新增,用于简化资源的关闭。语法:

java 复制代码
try (资源变量 =创建资源对象) {
    
} catch () {
    
}

其中资源对象需要实现 AutoCloseable 接口,例如 InputStream 、 OutputStream 、
Connection 、 Statement 、 ResultSet 等接口都实现了 AutoCloseable ,使用 try - with
resources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码,例如:

java 复制代码
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();
        }
    }
}

会被转换为:

java 复制代码
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 是我们代码出现的异常
                t = e1;
                throw e1;
            } finally {
                // 判断了资源不为空
                if (is != null) {
                    // 如果我们代码有异常
                    if (t != null) {
                        try {
                            is.close();
                        } catch (Throwable e2) {
                            // 如果 close 出现异常,作为被压制异常添加
                            t.addSuppressed(e2);
                        }
                    } else {
                        // 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e
                        is.close();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

10. 方法重写时的桥接方法

11. 匿名内部类

引用局部变量的匿名内部类,源代码:

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

转换后代码:

java 复制代码
// 额外生成的类
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);
    }
}

注意:

这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是final的:因为在创建Candy11$1对象时,将x的值赋值给了Candy111对象的valx属性,所以x不应该再发生变化了,如果变化,那么val$x属性没有机会再跟着一起变化。

四、类加载阶段

三大步:加载;链接(验证、准备、解析);初始化。

4.1 加载

将类的字节码载入 方法区 中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:

  • _java_mirror 即java的类镜像,例如对String来说,就是String.class,作用是把klass暴露给 java 使用
  • _super 即父类
  • _fields 即成员变量
  • _methods 即方法
  • _constants 即常量池
  • _class_loader 即类加载器
  • _vtable 虚方法表
  • _itable 接口方法表

如果这个类还有父类没有加载,先加载父类。
加载和链接可能是交替运行的。

注意

  • instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror是存储在堆中
  • 可以通过前面介绍的 HSDB 工具查看

4.2 链接

1)验证

验证类是否符合 JVM规范,安全性检查。

2)准备★★★

为 static 变量分配空间,设置默认值;

  • static 变量在JDK7之前存储于instanceKlass末尾,从JDK7开始,存储于_java_mirror末尾;
  • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段(第三大步)完成
  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了赋值在准备阶段完成;
  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成;

3)解析

将常量池中的符号引用解析为直接引用。(举例说明的)

4.3 初始化

<cinit>()V 方法

初始化即调用 <cinit>()V ,虚拟机会保证这个类的『构造方法』的线程安全。
发生初始化的时机
概括得说,类初始化是【懒惰的】

  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

不会导致类初始化的情况

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
  • 类对象.class 不会触发初始化
  • 创建该类的数组不会触发初始化
  • 类加载器的 loadClass 方法
  • Class.forName 的参数 2 为 false 时

练习一:

从字节码分析,使用 a,b,c 这三个常量是否会导致 E 初始化

java 复制代码
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;
}

练习二:典型应用 - 完成懒惰初始化单例模式

java 复制代码
public final class Singleton {
    private Singleton() { }
    // 内部类中保存单例
    private static class LazyHolder {
        static final Singleton INSTANCE = new Singleton();
    }
    // 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

静态内部类的好处:它可以访问外部类的资源。

以上的实现特点是:

  • 懒惰实例化
  • 初始化时的线程安全是有保障的

五、类加载器

以 JDK 8 为例:

java 复制代码
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("java.lang.String");
        ClassLoader classLoader = aClass.getClassLoader();
        System.out.println(classLoader);//null
    }

代码如上,如果获取某个类的类加载器是null时,表示该类是由启动类加载器Bootstrap加载的。因为Bootstrap加载器的代码是c语言写的,java直接获取不到。

5.1 双亲委派机制

所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则。

  • Bootstrap 启动类加载器
  • ExtClassLoader 扩展类加载器
  • AppClassLoader 应用程序类加载器

1)启动类加载器优先级最高。

2)如我们自己写的类(classpath路径下),首先经由AppClassLoader时,必须经过【上级】加载器的允许,即会逐层向上询问,上级的两个加载器没有加载时,才能轮到AppClassLoader加载。(确保了类的唯一性、安全性)

3)首先检查本类加载器有没有加载过,如果还没有,就看它有没有上级,有上级就调用递归方法委派给上级,源码如下:(老师用的断点跟踪,非常清晰)

java 复制代码
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),则委派BootstrapClassLoader
                    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 线程上下文类加载器

我们在使用JDBC时,都需要加载Driver驱动,不知道你注意到没有,不写Class.forName("com.mysql.jdbc.Driver")也是可以让 com.mysql.jdbc.Driver 正确加载的,怎么做的呢?
大名鼎鼎的 Service Provider Interface (SPI)
SPI约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称

这样就可以使用

java 复制代码
ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
while(iter.hasNext()) {
    iter.next();
}

来得到实现类,体现的是【面向接口编程 + 解耦】的思想,在下面一些框架中都运用了此思想:

  • JDBC
  • Servlet 初始化器
  • Spring 容器
  • Dubbo(对 SPI 进行了扩展)
    接着看 ServiceLoader.load 方法:
java 复制代码
public static <S> ServiceLoader<S> load(Class<S> service) {
    // 获取线程上下文类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器AppClassLoader,它内部又是由 Class.forName 调用了线程上下文类加载器完成类加载,具体代码略。

5.3 自定义类加载器

什么时候需要自定义类加载器?

1)想加载非 classpath 随意路径中的类文件
2)都是通过接口来使用实现,希望解耦时,常用在框架设计
3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤:

  1. 继承 ClassLoader 父类;
  2. 要遵从双亲委派机制,重写 findClass 方法;
    注意不是重写 loadClass 方法,否则不会走双亲委派机制
  3. 读取类文件的字节码;
  4. 调用父类的 defineClass 方法来加载类;
  5. 使用者调用该类加载器的 loadClass 方法;

六、运行期优化

6.1 即时编译

JVM 将执行状态分成了 5 个层次:

  • 0 层,解释执行(Interpreter)

  • 1 层,使用 C1 即时编译器编译执行(不带 profiling)

  • 2 层,使用 C1 即时编译器编译执行(带基本的 profiling)

  • 3 层,使用 C1 即时编译器编译执行(带完全的 profiling)

  • 4 层,使用 C2 即时编译器编译执行
    profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等。
    即时编译器(JIT)与解释器的区别:

  • 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释;

  • JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译;

  • 解释器是将字节码解释为针对所有平台都通用的机器码;

  • JIT 会根据平台类型,生成平台特定的机器码。

对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由来),优化之。
即时编译的三种体现形式:

  • 逃逸分析:刚才的一种优化手段称之为【逃逸分析】,发现新建的对象是否逃逸。可以使用 -XX:-DoEscapeAnalysis 关闭逃逸分析,再运行刚才的示例观察结果。
  • 方法内联
  • 字段优化

6.2 反射优化

java 复制代码
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Reflect1 {
   public static void foo() {
       System.out.println("foo...");
  }
   public static void main(String[] args) throws Exception {
       Method foo = Reflect1.class.getMethod("foo");
       for (int i = 0; i <= 16; i++) {
           System.out.printf("%d\t", i);
           foo.invoke(null);
      }
       System.in.read();
  }
}

foo.invoke 前面 0 ~ 15 次调用使用的是 MethodAccessor 的 NativeMethodAccessorImpl 实现

java 复制代码
class NativeMethodAccessorImpl extends MethodAccessorImpl {
    private final Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;

    NativeMethodAccessorImpl(Method var1) {
        this.method = var1;
    }

    public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
        // inflationThreshold 膨胀阈值,默认 15
        if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
            // 使用 ASM 动态生成的新实现代替本地实现,速度较本地实现快 20 倍左右
            MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
            this.parent.setDelegate(var3);
        }

        // 调用本地native实现,效率低下
        return invoke0(this.method, var1, var2);
    }

    void setParent(DelegatingMethodAccessorImpl var1) {
        this.parent = var1;
    }

    private static native Object invoke0(Method var0, Object var1, Object[] var2);
}

当调用到第 16 次(从0开始算)时,会采用运行时生成的类代替掉最初的实现,可以通过 debug 得到类名为 sun.reflect.GeneratedMethodAccessor1

可以使用阿里的 arthas 工具:

java -jar arthas-boot.jar
[INFO] arthas-boot version: 3.1.1
[INFO] Found existing java process, please choose one and hit RETURN.
* [1]: 13065 cn.itcast.jvm.t3.reflect.Reflect1
选择 1 回车表示分析该进程
(此处省略......)
注意
通过查看 ReflectionFactory 源码可知,sun.reflect.noInflation 可以用来禁用膨胀(直接生成 GeneratedMethodAccessor1,但首次生成比较耗时,如果仅反射调用一次,不划算)
sun.reflect.inflationThreshold 可以修改膨胀阈值。

相关推荐
东阳马生架构19 分钟前
JVM实战—2.JVM内存设置与对象分配流转
jvm
撸码到无法自拔2 小时前
深入理解.NET内存回收机制
jvm·.net
吴冰_hogan16 小时前
JVM(Java虚拟机)的组成部分详解
java·开发语言·jvm
东阳马生架构1 天前
JVM实战—1.Java代码的运行原理
jvm
ThisIsClark1 天前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
王佑辉1 天前
【jvm】内存泄漏与内存溢出的区别
jvm
大G哥1 天前
深入理解.NET内存回收机制
jvm·.net
泰勒今天不想展开1 天前
jvm接入prometheus监控
jvm·windows·prometheus
东阳马生架构2 天前
JVM简介—3.JVM的执行子系统
jvm