JVM之类加载与字节码

这篇具有很好参考价值的文章主要介绍了JVM之类加载与字节码。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1.类文件结构

根据 Java 虚拟机规范,Class 文件通过 ClassFile 定义。ClassFile 的结构如下:

ClassFile {
    u4             magic; //Class 文件的标志
    u2             minor_version;//Class 的小版本号
    u2             major_version;//Class 的大版本号
    u2             constant_pool_count;//常量池中常量的数量
    cp_info        constant_pool[constant_pool_count-1];//常量池
    u2             access_flags;//Class 的访问标记
    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];//属性表集合
}

通过分析 ClassFile 的内容,我们便可以知道 class 文件的组成。

JVM之类加载与字节码,# JVM,jvm

下面这张图是通过 IDEA 插件 jclasslib 查看的,你可以更直观看到 Class 文件结构。使用 jclasslib 不光可以直观地查看某个类对应的字节码文件,还可以查看类的基本信息、常量池、接口、属性、函数等信息。下面详细介绍一下 Class 文件结构涉及到的一些组件。

JVM之类加载与字节码,# JVM,jvm

1.1 魔数

    u4             magic; //Class 文件的标志

每个 Class 文件的头 4 个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接收的 Class 文件。Java 规范规定魔数为固定值:0xCAFEBABE。如果读取的文件不是以这个魔数开头,Java 虚拟机将拒绝加载它。

1.2 Class 文件版本号

    u2             minor_version;//Class 的小版本号
    u2             major_version;//Class 的大版本号

紧接着魔数的四个字节存储的是 Class 文件的版本号:第 5 和第 6 个字节是次版本号,第 7 和第 8 个字节是主版本号

每当 Java 发布大版本(比如 Java 8,Java9)的时候,主版本号都会加 1。你可以使用 javap -v 命令来快速查看 Class 文件的版本号信息。

高版本的 Java 虚拟机可以执行低版本编译器生成的 Class 文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的 Class 文件。所以,我们在实际开发的时候要确保开发的的 JDK 版本和生产环境的 JDK 版本保持一致。

1.3 常量池

    u2             constant_pool_count;//常量池中常量的数量
    cp_info        constant_pool[constant_pool_count-1];//常量池

紧接着主次版本号之后的是常量池,常量池的数量是 constant_pool_count - 1(常量池计数器是从 1 开始计数的,将第 0 项常量空出来是有特殊考虑的,索引值为 0 代表“不引用任何一个常量池项”)。

常量池主要存放两大常量:字面量和符号引用。字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。而符号引用则属于编译原理方面的概念。包括下面三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

常量池中每一项常量都是一个表,这 14 种表有一个共同的特点:开始的第一位是一个 u1 类型的标志位 -tag 来标识常量的类型,代表当前这个常量属于哪种常量类型.

JVM之类加载与字节码,# JVM,jvm

.class 文件可以通过javap -v class类名 指令来看一下其常量池中的信息(javap -v class类名-> temp.txt:将结果输出到 temp.txt 文件)。

1.4 访问标志

    u2             access_flags;//Class 的访问标记

在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口,是否为 public 或者 abstract 类型,如果是类的话是否声明为 final 等等。

类访问和属性修饰符:

JVM之类加载与字节码,# JVM,jvm

我们定义了一个 Employee

package top.snailclimb.bean;
public class Employee {
   ...
}

通过 javap -v class 类名 指令来看一下类的访问标志。

JVM之类加载与字节码,# JVM,jvm

1.5 当前类、父类、接口索引集合

    u2             this_class;//当前类
    u2             super_class;//父类
    u2             interfaces_count;//接口数量
    u2             interfaces[interfaces_count];//一个类可以实现多个接口

Java 类的继承关系由类索引、父类索引和接口索引集合三项确定。类索引、父类索引和接口索引集合按照顺序排在访问标志之后,

类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于 Java 语言的单继承,所以父类索引只有一个,除了 java.lang.Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0。

接口索引集合用来描述这个类实现了那些接口,这些被实现的接口将按 implements (如果这个类本身是接口的话则是extends) 后的接口顺序从左到右排列在接口索引集合中。

1.5 字段表集合

u2             fields_count;//字段数量
field_info     fields[fields_count];//一个类会可以有个字段

字段表(field info)用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。

field info(字段表) 的结构:

JVM之类加载与字节码,# JVM,jvm

  • access_flags: 字段的作用域,也就是 public 、private、protected 修饰符,是实例变量还是类变量(static修饰符),可否被序列化(transient 修饰符),可变性(final),可见性(volatile 修饰符,是否强制从主内存读写)。

  • name_index: 对常量池的引用,表示的字段的名称;

  • descriptor_index: 对常量池的引用,表示字段和方法的描述符;

  • attributes_count: 一个字段还会拥有一些额外的属性,attributes_count 存放属性的个数;

  • attributes[attributes_count]: 存放具体属性具体内容。

上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字、字段被定义为什么数据类型这些都是无法固定的,只能引用常量池中常量来描述。

字段的 access_flag 的取值:

JVM之类加载与字节码,# JVM,jvm

1.6 方法表集合

    u2             methods_count;//方法数量
    method_info    methods[methods_count];//一个类可以有个多个方法

methods_count 表示方法的数量,而 method_info 表示方法表。

Class 文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。

method_info(方法表的) 结构:

JVM之类加载与字节码,# JVM,jvm

方法表的 access_flag 取值:

JVM之类加载与字节码,# JVM,jvm

注意:因为 volatile 修饰符和 transient 修饰符不可以修饰方法,所以方法表的访问标志中没有这两个对应的标志,但是增加了synchronized、native、abstract 等关键字修饰方法,所以也就多了这些关键字对应的标志。

1.7 属性表集合

u2             attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合

在 Class 文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。与 Class 文件中其它的数据项目要求的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写 入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性。

2.字节码指令

在 ClassFile 结构中,methods_count 字段指示了该类中方法的数量,而 methods 数组则包含了每个方法的 method_info 结构。每个 method_info 结构中包含了方法的相关信息,包括方法的名称、访问标记、参数列表、异常表和属性表等。其中,方法的字节码指令存储在 code 属性中。code 属性是 attribute_info 结构的一种,其中包含了字节码指令的长度和实际的字节码指令序列。

因此,字节码指令存储在 ClassFile 结构中的 method_info 结构的 code 属性中。每个方法都有对应的 method_info 结构,用于描述方法的详细信息,并存储方法的字节码指令。

2.1 入门

构造方法的字节码指令如下所示

2a b7 00 01 b1
  1. 2a => aload_0 加载 slot 0 的局部变量,即 this,做为下面的 invokespecial 构造方法调用的参数
  2. b7 => invokespecial 预备调用构造方法,哪个方法呢?
  3. 00 01 引用常量池中 #1 项,即【 Method java/lang/Object."":()V 】
  4. b1 表示返回

主方法的字节码指令如下所示

b2 00 02 12 03 b6 00 04 b1
  1. b2 => getstatic 用来加载静态变量,哪个静态变量呢?
  2. 00 02 引用常量池中 #2 项,即【Field java/lang/System.out:Ljava/io/PrintStream;】
  3. 12 => ldc 加载参数,哪个参数呢?
  4. 03 引用常量池中 #3 项,即 【String hello world】
  5. b6 => invokevirtual 预备调用成员方法,哪个方法呢?
  6. 00 04 引用常量池中 #4 项,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】
  7. b1 表示返回

上述这些指令都是Java虚拟机真正解释执行的,而aload_0、invokespecial等是一些助记符,是为了便于人类理解的。

2.2 Javap工具

自己分析类文件结构太麻烦了,Oracle 提供了 javap 工具来反编译 class 文件

将HelloWorld.class文件利用Javap工具反编译后的类文件结构如下所示:

[root@localhost ~]# javap -v HelloWorld.class
Classfile /root/HelloWorld.class
    Last modified Jul 7, 2019; size 597 bytes
    MD5 checksum 361dca1c3f4ae38644a9cd5060ac6dbc
    Compiled from "HelloWorld.java"
public class cn.itcast.jvm.t5.HelloWorld
    minor version: 0
    major version: 52
    flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
    #1 = Methodref #6.#21 // java/lang/Object."<init>":()V
    #2 = Fieldref #22.#23 //
java/lang/System.out:Ljava/io/PrintStream;
    #3 = String #24 // hello world
    #4 = Methodref #25.#26 // java/io/PrintStream.println:
(Ljava/lang/String;)V
    #5 = Class #27 // cn/itcast/jvm/t5/HelloWorld
    #6 = Class #28 // 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/t5/HelloWorld;
    #14 = Utf8 main
    #15 = Utf8 ([Ljava/lang/String;)V
    #16 = Utf8 args
    #17 = Utf8 [Ljava/lang/String;
    #18 = Utf8 MethodParameters
    #19 = Utf8 SourceFile
    #20 = Utf8 HelloWorld.java
    #21 = NameAndType #7:#8 // "<init>":()V
    #22 = Class #29 // java/lang/System
    #23 = NameAndType #30:#31 // out:Ljava/io/PrintStream;
    #24 = Utf8 hello world
    #25 = Class #32 // java/io/PrintStream
    #26 = NameAndType #33:#34 // println:(Ljava/lang/String;)V
    #27 = Utf8 cn/itcast/jvm/t5/HelloWorld
    #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 (Ljava/lang/String;)V
{
    public cn.itcast.jvm.t5.HelloWorld();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
        stack=1, locals=1, args_size=1
            0: aload_0
            1: invokespecial #1 // Method java/lang/Object."
<init>":()V
            4: return
    LineNumberTable:
      line 4: 0
    LocalVariableTable:
      Start Length Slot Name Signature
      0      5      0   this Lcn/itcast/jvm/t5/HelloWorld;
    public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
            stack=2, locals=1, args_size=1
                0: getstatic #2 // Field
java/lang/System.out:Ljava/io/PrintStream;
                3: ldc #3 // String hello world
                5: invokevirtual #4 // Method
java/io/PrintStream.println:(Ljava/lang/String;)V
                8: return
        LineNumberTable:
            line 6: 0
            line 7: 8
        LocalVariableTable:
        Start Length Slot Name Signature
          0     9     0   args [Ljava/lang/String;
    MethodParameters:
        Name Flags
        args
}

2.3 图解方法执行流程

1.原始 java 代码

package cn.itcast.jvm.t3.bytecode;
/**
* 演示 字节码指令 和 操作数栈、常量池的关系
*/
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.编译后的字节码文件

[root@localhost ~]# javap -v Demo3_1.class
Classfile /root/Demo3_1.class
    Last modified Jul 7, 2019; size 665 bytes
    MD5 checksum a2c29a22421e218d4924d31e6990cfc5
    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.#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        // cn/itcast/jvm/t3/bytecode/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         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         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         cn/itcast/jvm/t3/bytecode/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 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
    MethodParameters:
        Name                         Flags
        args
}

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

字节码中常量池的数据会被放入到运行时常量池

JVM之类加载与字节码,# JVM,jvm

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

方法的字节码会被放入到方法区中

JVM之类加载与字节码,# JVM,jvm

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

stack=2,locals=4

  • locals=4决定了局部变量表的槽位是4
  • stack=2决定了操作数栈的深度为2

JVM之类加载与字节码,# JVM,jvm

6.执行引擎开始执行字节码

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

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

JVM之类加载与字节码,# JVM,jvm

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

istore_1:将操作数栈顶数据弹出,存入局部变量表的 slot 1

JVM之类加载与字节码,# JVM,jvm

JVM之类加载与字节码,# JVM,jvm

ldc #3:从运行时常量池加载 #3 数据到操作数栈

JVM之类加载与字节码,# JVM,jvm

注意:Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的

istore_2:将操作数栈的栈顶元素弹出,存入到局部变量表中slot为2的位置

JVM之类加载与字节码,# JVM,jvm

JVM之类加载与字节码,# JVM,jvm

iload_1:将局部变量表中slot为1的变量值复制一份到操作数栈中

JVM之类加载与字节码,# JVM,jvm

iload_2:将局部变量表中slot为2的变量值复制一份到操作数栈中

JVM之类加载与字节码,# JVM,jvm

iadd:将操作数栈中的二个元素弹出相加后,重新存入操作数栈

JVM之类加载与字节码,# JVM,jvm

istore_3:将操作数栈中的栈顶元素弹出,存入到局部变量表中slot为3的位置

JVM之类加载与字节码,# JVM,jvm

JVM之类加载与字节码,# JVM,jvm

getstatic #4:根据运行时常量池取得 #4 在堆中的对象,并将对象引用存入到操作数栈中

JVM之类加载与字节码,# JVM,jvm

JVM之类加载与字节码,# JVM,jvm

 iload_3

JVM之类加载与字节码,# JVM,jvm

JVM之类加载与字节码,# JVM,jvm

invokevirtual #5

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

JVM之类加载与字节码,# JVM,jvm

清除 main 操作数栈内容

JVM之类加载与字节码,# JVM,jvm

return

  • 完成 main 方法调用,弹出 main 栈帧
  • 程序结束

2.4 图解 i++ 执行流程

目的:从字节码角度分析 i++ 相关题目

Java代码

package cn.itcast.jvm.t3.bytecode;
/**
* 从字节码角度分析 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);
    }
}

字节码:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) 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

分析:

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

图解main方法执行流程

bipush 10:将方法区中的字面量10加入到操作数栈中

JVM之类加载与字节码,# JVM,jvm

 istore 1:将操作数栈的栈顶元素弹出,加入到局部变量表中slot=1的位置

JVM之类加载与字节码,# JVM,jvm

iload 1:将局部变量表中 slot=1 处的变量值 10 复制一份到操作数栈中

JVM之类加载与字节码,# JVM,jvm

linc 1,1:直接将方法区中的1与局部变量表中slot=1处的变量值相加

JVM之类加载与字节码,# JVM,jvm

JVM之类加载与字节码,# JVM,jvm

iload 1:将局部变量表中slot=1处的变量值12复制一份到操作数栈中

JVM之类加载与字节码,# JVM,jvm

iadd:将操作数栈中的二个元素弹出相加后,重新存入操作数栈

JVM之类加载与字节码,# JVM,jvm

iload 1:将局部变量表中slot=1处的变量值12复制一份到操作数栈中

JVM之类加载与字节码,# JVM,jvm

linc 1,-1:将方法区的 -1 与局部变量表中slot = 1处的元素相加

JVM之类加载与字节码,# JVM,jvm

iadd:将操作数栈中的二个元素弹出相加后,重新存入操作数栈

JVM之类加载与字节码,# JVM,jvm

istore:将操作数栈的栈顶元素弹出加入到局部变量表中 slot = 2 处的位置

JVM之类加载与字节码,# JVM,jvm

总结:从字节码角度解读分析了 a ++ 和 ++ a 的区别

a++ 是先执行 iload_1,再执行 linc 1,1

++a 是先执行 linc 1,1,再执行 iload_1

a- - 是先执行 iload_1,再执行 linc 1,-1

2.5 条件判断指令

JVM之类加载与字节码,# JVM,jvm

几点说明:

  • byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节
  • goto 用来进行跳转到指定行号的字节码 

条件判断指令的示例:

Java代码

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
6: bipush 10
8: istore_1
9: goto 15
12: bipush 20
14: istore_1
15: return

注意:以上比较指令中没有 long,float,double 的比较,那么它们要比较怎 么办?

参考 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.lcmp

2.6 循环控制指令

1.while 循环

Java代码

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

字节码

0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14
8: iinc 1, 1
11: goto 2
14: return

2.do while 循环

Java代码

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

字节码

 0: iconst_0
 1: istore_1
 2: iinc 1, 1
 5: iload_1
 6: bipush 10
 8: if_icmplt 2
11: return

3.for 循环

Java代码

public class Demo3_6 {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
        }
    }
}

字节码

0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14
8: iinc 1, 1
11: goto 2
14: return

比较 while 和 for 的字节码,你发现它们是一模一样的,殊途也能同归😊

2.7 构造方法的原理

1.<init>()V

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

Java代码

字节码

public cn.itcast.jvm.t3.bytecode.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 // super.<init>()V
            4: aload_0
            5: ldc #2 // <- "s1"
            7: putfield #3 // -> this.a
            10: aload_0
            11: bipush 20 // <- 20
            13: putfield #4 // -> this.b
            16: aload_0
            17: bipush 10 // <- 10
            19: putfield #4 // -> this.b
            22: aload_0
            23: ldc #5 // <- "s2"
            25: putfield #3 // -> this.a
            28: aload_0 // ------------------------------
            29: aload_1 // <- slot 1(a) "s3" |
            30: putfield #3 // -> this.a |
            33: aload_0 |
            34: iload_2 // <- slot 2(b) 30 |
            35: putfield #4 // -> this.b --------------------
            38: return
       LineNumberTable: ...
       LocalVariableTable:
         Start Length Slot Name Signature
            0    39     0  this Lcn/itcast/jvm/t3/bytecode/Demo3_8_2;
            0    39     1   a   Ljava/lang/String;
            0    39     2   b   I
MethodParameters: ...

2.<cinit>()V

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

Java代码

public class Demo3_8_1 {
    static int i = 10;
    static {
        i = 20;
    }
    static {
        i = 30;
    }
}

字节码

0: bipush 10
2: putstatic #2      // Field i:I
5: bipush 20
7: putstatic #2      // Field i:I
10: bipush 30
12: putstatic #2     // Field i:I
15: return

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

2.8 方法调用的原理

通过以下几种不同的方法及其调用来查看对应的字节码指令

Java代码

public class Demo3_9 {

    public Demo3_9() { }
    private void test1() { }
    private final void test2() { }
    public void test3() { }
    public static void test4() { }

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

}

字节码

0: new #2            // class cn/itcast/jvm/t3/bytecode/Demo3_9
3: dup
4: invokespecial #3  // Method "<init>":()V
7: astore_1
8: aload_1
9: invokespecial #4  // Method test1:()V
12: aload_1
13: invokespecial #5 // Method test2:()V
16: aload_1
17: invokevirtual #6 // Method test3:()V
20: aload_1
21: pop
22: invokestatic #7  // Method test4:()V
25: invokestatic #7  // Method test4:()V
28: return

由字节码可知如下结论:

new 是创建对象,给对象分配堆内存,执行成功会将 对象引用 压入操作数栈

dup 是复制操作数栈栈顶的内容,本例即为对象引用,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 <init>:()V ,另一个要配合 astore_1 赋值给局部变量

最终方法(final),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静态绑定。此外,通过 super 调用父类方法也是执行 invokespecial 指令

普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态

普通成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】

比较有意思的是 d.test4(); 是通过【对象引用】调用一个静态方法,可以看到在调用 invokestatic 之前执行了 pop 指令,把【对象引用】从操作数栈弹掉了

2.9 多态的原理

接下来通过以下Java代码和 jps、HSDB工具来说明多态的原理。最后的总结部分是核心,如果总结部分没看懂,可以看看多态原理这部分的过程。

Java代码

package cn.itcast.jvm.t3.bytecode;
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 安装目录,执行以下命令打开 HSDB 的图形化界面

java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB

进入图形界面点击 file 选择下图所示的第一项,在输入进程 id

JVM之类加载与字节码,# JVM,jvm

在弹出的界面中输入通过 jps 获得的进程 id,连接到 Demo3_10 这个程序

JVM之类加载与字节码,# JVM,jvm

连接成功后

JVM之类加载与字节码,# JVM,jvm

3.查找某个对象

打开 Tools -> Find Object By Query

在弹出的窗口中输入 select d from cn.itcast.jvm.t3.bytecode.Dog d,点击 Execute 执行

JVM之类加载与字节码,# JVM,jvm

4.查看对象内存结构

点击超链接可以看到对象的内存结构,此对象没有任何属性,因此只有对象头的 16 字节,前 8 字节是 MarkWord,后 8 字节就是对象的 Class 指针,但目前看不到它的实际地址

堆内存中的对象分为对象头和对象中的成员变量二部分,对象头总共占16个字节,

前八个字节是对象的哈希码、将来对象加锁时的锁标记等,称为MarkWord;

后八个字节是对象的类型指针,可以根据类型指针找到对象的class类

5.查看对象的 Class 的内存地址

可以通过 Windows -> Console 进入命令行(Command Line)模式,执行

mem 0x00000001299b4978 2

mem 有两个参数,参数 1 是对象地址,参数 2 是查看 2 行(即 16 字节)

结果中第二行 0x000000001b7d4028 即为 Class 的内存地址

JVM之类加载与字节码,# JVM,jvm

6.查看类的 vtable(虚方法表)

点击 Tools 中的Inspector,在打开的窗口中输入 Class 的内存地址并回车,得到的结果便是字节码文件在方法区的完整内容,也就是类的全部信息。

JVM之类加载与字节码,# JVM,jvm

可以找到 Dog Class 的 vtable 长度为 6,意思就是 Dog 类有 6 个虚方法(虚方法都是多态相关的方法,final、static 方法不会列入)

那么这 6 个方法都是谁呢?根据一些资料可知,从 Class 的起始地址开始算,偏移 0x1b8 就是 vtable 的起始地址,进行计算得到:

0x000000001b7d4028
1b8 +
---------------------
0x000000001b7d41e0

通过HSDB中的 Windows -> Console 进入命令行模式,执行如下命令

mem 0x000000001b7d41e0 6

就得到了 6 个虚方法的入口地址

mem 0x000000001b7d41e0 6
0x000000001b7d41e0: 0x000000001b3d1b10
0x000000001b7d41e8: 0x000000001b3d15e8
0x000000001b7d41f0: 0x000000001b7d35e8
0x000000001b7d41f8: 0x000000001b3d1540
0x000000001b7d4200: 0x000000001b3d1678
0x000000001b7d4208: 0x000000001b7d3fa8

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() 方法是继承 String 类的
  • finalize() ,equals(),hashCode(),clone() 都是继承 Object 类的

        说明调用子类从父类那继承的方法,实际上还是调用的父类在方法区中的方法。这样,不同子类都继承到了父类的方法,但也都有自己独有的方法,这就是多态的原理。

多态原理总结

普通成员方法是由 invokevirtual 指令调用,当执行 invokevirtual 指令时

1. 先通过栈帧中的对象引用找到对象

2. 分析对象头,找到对象的实际 Class

3. Class 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了

4. 查表得到方法的具体地址

5. 执行方法的字节码

2.10 异常处理的原理

1.try-catch

Java代码

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

字节码

为了观察重点,下面的字节码省略了不重要的部分

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
            9: bipush 20
            11: istore_1
            12: return
        Exception table:
            from to target type
             2    5   8    Class java/lang/Exception
        LineNumberTable: ...
        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: ...
    MethodParameters: ...
}

结论

  • 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
  • 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置

2.多个catch 块的情况

Java代码

public class Demo3_11_2 {
    public static void main(String[] args) {
        int i = 0;
        try {
        //因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用
        multi-catch 的情况
            i = 10;
        } catch (ArithmeticException e) {
            i = 30;
        } catch (NullPointerException e) {
            i = 40;
        } catch (Exception e) {
            i = 50;
        }
    }
}

字节码

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
        StackMapTable: ...
    MethodParameters: ...

因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用

3.catch 中含有多个异常的情况

Java代码

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
            2: ldc #3
            4: iconst_0
            5: anewarray #4
            8: invokevirtual #5
            11: astore_1
            12: aload_1
            13: aconst_null
            14: iconst_0
            15: anewarray #6
            18: invokevirtual #7
            21: pop
            22: goto 30
            25: astore_1
            26: aload_1
            27: invokevirtual #11 // e.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
        LineNumberTable: ...
        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: ...
    MethodParameters: ...

4.finally

Java代码

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 Exceptin -> e ----------------------
            12: bipush 20 // |
            14: istore_1 // 20 -> i |
            15: bipush 30 // finally |
            17: istore_1 // 30 -> i |
            18: goto 27 // return -----------------------------------
            21: astore_3 // catch any -> slot 3 ----------------------
            22: bipush 30 // finally |
            24: istore_1 // 30 -> i |
            25: aload_3 // <- slot 3 |
            26: athrow // throw ------------------------------------
            27: return
        Exception table:
            from to target type
             2   5  11     Class java/lang/Exception
             2   5  21     any // 剩余的异常类型,比如 Error
             11  15 21     any // 剩余的异常类型,比如 Error
        LineNumberTable: ...
        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: ...
    MethodParameters: ..

总结:可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程

5.练习 - finally 面试题

Java代码

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

字节码

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
        LineNumberTable: ...
        StackMapTable: ...

由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以 finally 的为准

至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子

跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会 吞掉异常😱😱😱,可以试一下下面的代码

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

6.finally 对返回值影响

Java代码

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

字节码

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,目的是为了固定返回值
            5: bipush 20 // <- 20 放入栈顶
            7: istore_0 // 20 -> i
            8: iload_1 // <- slot 1(10) 载入 slot 1 暂存的值
            9: ireturn // 返回栈顶的 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
    LineNumberTable: ...
    LocalVariableTable:
        Start Length Slot Name Signature
         3      13    0    i     I
    StackMapTable: ...

2.11 synchronized

Java代码

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
            4: invokespecial #1 // invokespecial <init>:()V
            7: astore_1 // lock引用 -> lock
            8: aload_1 // <- lock (synchronized开始)
            9: dup
            10: astore_2 // lock引用 -> slot 2
            11: monitorenter // monitorenter(lock引用)
            12: getstatic #3 // <- System.out
            15: ldc #4 // <- "ok"
            17: invokevirtual #5 // invokevirtual println:
(Ljava/lang/String;)V
            20: aload_2 // <- slot 2(lock引用)
            21: monitorexit // monitorexit(lock引用)
            22: goto 30
            25: astore_3 // any -> slot 3
            26: aload_2 // <- slot 2(lock引用)
            27: monitorexit // monitorexit(lock引用)
            28: aload_3
            29: athrow
            30: return
        Exception table:
            from to target type
             12  22 25     any
             25  28 25     any
        LineNumberTable: ...
        LocalVariableTable:
            Start Length Slot Name Signature
             0    31     0    args [Ljava/lang/String;
             8    23     1    lock Ljava/lang/Object;
        StackMapTable: ...
    MethodParameters: ...

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

3. 编译期处理

什么是语法糖

所谓的 语法糖 ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成 和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利(给糖吃)

以下代码分析的前提

注意,以下代码的分析,借助了 javap 工具、idea 的反编译功能、idea 插件、jclasslib 等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。

3.1 默认构造器

public class Candy1 {
}

编译成class后的代码:

public class Candy1 {
    // 这个无参构造是编译器帮助我们加上的
    public Candy1() {
        super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object."
<init>":()V
    }
}

3.2 自动拆装箱

这个特性是 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();
    }
}

        显然之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是包装类型),因此这些转换的事情在 JDK 5 以后都由编译器在编译阶段完成。即 代码片段1 都会在编 译阶段被转换为 代码片段2

3.3 泛型集合取值

泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息 在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:

public class Candy3 {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(10); // 实际调用的是 List.add(Object e)
        Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);
    }
}

所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换的操作:

// 需要将 Object 转为 Integer
Integer x = (Integer)list.get(0)

如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是:

// 需要将 Object 转为 Integer, 并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();

擦除的是字节码上的泛型信息,可以看到 LocalVariableTypeTable 仍然保留了方法参数泛型的信息

public cn.itcast.jvm.t3.candy.Candy3();
    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/candy/Candy3;
    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: 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 8: 0
                line 9: 8
                line 10: 20
                line 11: 31
        LocalVariableTable:
            Start Length Slot Name Signature
             0     32     0   args [Ljava/lang/String;
             8     24     1   list Ljava/util/List;
        LocalVariableTypeTable:
            Start Length Slot Name Signature
              8    24     1   list Ljava/util/List<Ljava/lang/Integer;>;

使用反射,仍然能够获得这些信息:

public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
}
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("泛型参数[%d] - %s\n", i, arguments[i]);
        }
    }
}

输出

原始类型 - interface java.util.List
泛型参数[0] - class java.lang.String
原始类型 - interface java.util.Map
泛型参数[0] - class java.lang.Integer
泛型参数[1] - class java.lang.Object

3.4 可变参数

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

Java代码

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 ,从下面的代码中就可以看出来

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 进去

3.5 foreach 循环

仍是 JDK 5 开始引入的语法糖

1.数组的 foreach 循环

Java代码

public class Candy5_1 {
    public static void main(String[] args) {
        int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖哦
        for (int e : array) {
            System.out.println(e);
        }
    }
}

被编译器转换后的代码

public class Candy5_1 {
    public Candy5_1() {
    }

    public static void main(String[] args) {
        int[] array = new int[]{1, 2, 3, 4, 5};
        for(int i = 0; i < array.length; ++i) {
            int e = array[i];
            System.out.println(e);
        }
    }
}

2.集合的 foreach 循环

Java代码

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

被编译器转换后的代码

实际被编译器转换为对迭代器的调用

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 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用,其中 Iterable 用来获取集合的迭代器( Iterator )

3.6 switch

字符串从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖

1. switch 字符串

Java代码

public class Candy6_1 {
    public static void choose(String str) {
        switch (str) {
            case "hello": {
                System.out.println("h");
                break;
            }
            case "world": {
                System.out.println("w");
                break;
            }
        }
    }
}

注意 switch 配合 String 和枚举使用时,变量不能为null,原因分析完语法糖转换后的代码应当自然清楚

被编译器转换后的代码

public class Candy6_1 {
    public Candy6_1() {
    }
    public static void choose(String str) {
        byte x = -1;
        switch(str.hashCode()) {
        case 99162322: // 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");
        }
    }
}

可以看到,执行了两遍 switch,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应 byte 类型,第二遍才是利用 byte 执行进行比较。

为什么第一遍时必须既比较 hashCode,又利用 equals 比较呢?hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突,例如 BM 和 C,这两个字符串的hashCode值都是 2123 ,如果有如下Java代码:

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 值可能相同,需要进一步用 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");
            }
        }
    }
}

2.switch 枚举

Java代码

enum Sex {
    MALE, FEMALE
}
public class Candy7 {
    public static void foo(Sex sex) {
        switch (sex) {
            case MALE:
                System.out.println("男"); break;
            case FEMALE:
                System.out.println("女"); break;
        }
    }
}

被编译器转换后的代码

public class Candy7 {
    /**
     * 定义一个合成类(仅 jvm 使用,对我们不可见)
     * 用来映射枚举的 ordinal 与数组元素的关系
     * 枚举的 ordinal 表示枚举对象的序号,从 0 开始
     * 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
    */
    static class $MAP {
        // 数组大小即为枚举元素个数,里面存储case用来对比的数字
        static int[] map = new int[2];
        static {
            map[Sex.MALE.ordinal()] = 1;
            map[Sex.FEMALE.ordinal()] = 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.7 枚举类

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

Java代码

enum Sex {
    MALE, FEMALE
}

转换后代码

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

3.8 try-with-resources

JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources

语法格式

try(资源变量 = 创建资源对象){

} catch( ) {

}

Java代码

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

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

转换后代码

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

为什么要设计一个 addSuppressed(Throwable e) (添加被压制异常)的方法呢?是为了防止异常信息的丢失(想想 try-with-resources 生成的 fianlly 中如果抛出了异常):

Java代码

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

输出:

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.9 方法重写时的桥接方法

方法重写时对返回值分两种情况:

  • 父子类的返回值完全一致
  • 子类返回值可以是父类返回值的子类

Java代码

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

子类转换后的代码

class B extends A {
    public Integer m() {
        return 2;
    }

    // 此方法才是真正重写了父类 public Number m() 方法
    public synthetic bridge Number m() {
        // 调用 public Integer m()
        return m();
    }
}

其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突,可以用下面反射代码来验证:

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

会输出:

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

3.10 匿名内部类

Java代码

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

转换后的代码

// 额外生成的类
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();
    }
}

引用局部变量的匿名内部类的Java代码

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

转换后代码:

// 额外生成的类
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 的值赋值给了 Candy11$1 对象的 val$x 属性,所以 x 不应该再发生变 化了,如果变化,那么 val$x 属性没有机会再跟着一起变化。文章来源地址https://www.toymoban.com/news/detail-625950.html

到了这里,关于JVM之类加载与字节码的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • JVM——类加载与字节码技术—类加载器+运行期优化

    jdk的类加载器具有层级关系。 启动类加载器》扩展类加载器》应用程序类加载器》自定义类加载器  对应类加载器只会负责加载对应目录的类。 双亲委派上级机制 应用程序类加载器加载一个类之前会先查询上级加载器是否已经加载过了该类。然后再让上级询问上上级。都没

    2024年02月11日
    浏览(41)
  • JVM——类加载与字节码技术—编译期处理+类加载阶段

    编译期优化称为语法糖 java基本类型和包装类型之间的自动转换。     在字节码中可以看见,泛型擦除就是字节码中的执行代码不区分是String还是Integer了,统一用Object.  对于取出的Object,这个checkcast用于转换Object为Integer类型。  可以看见 局部变量类型表 里面包含了方法参

    2024年02月11日
    浏览(35)
  • JVM——类加载与字节码技术—类文件结构

      由源文件被编译成字节码文件,然后经过类加载器进行类加载,了解类加载的各个阶段,了解有哪些类加载器,加载到虚拟机中执行字节码指令,执行时使用解释器进行解释执行,解释时对热点代码进行运行期的编译处理。    根据下面规范,魔数,两个版本,常量池信息

    2024年02月11日
    浏览(38)
  • JVM学习笔记(四)类加载与字节码技术

    目录 一、类文件结构 二、字节码指令 2.3 图解方法执行流程 1)原始 java 代码 2)编译后的字节码文件 3)常量池载入运行时常量池 4)方法字节码载入方法区 5)main 线程开始运行,分配栈帧内存 6)执行引擎开始执行字节码★★★ 2.4 练习 - 分析 i++ 2.5 条件判断指令 2.6 循环控

    2024年04月22日
    浏览(39)
  • p7付费课程笔记:jvm基础知识、字节码、类加载器

    机器语言-编程语言-高级语言(java,c++,Go,Rust等) 面向过程–面向对象-面向函数 java是一种面向对象、静态类型、编译执行,有VM(虚拟机)/GC和运行时、跨平台的高级语言。重点:VM(虚拟机)/GC(Garbage Collector)和运行时、跨平台。 跨平台步骤:字节码文件被虚拟机加载(

    2024年02月10日
    浏览(39)
  • 【Java高级应用:深入探索Java编程的强大功能,JVM 类加载机制, JVM 内存模型,垃圾回收机制,JVM 字节码执行,异常处理机制】

    本人详解 作者:王文峰,参加过 CSDN 2020年度博客之星,《Java王大师王天师》 公众号:JAVA开发王大师,专注于天道酬勤的 Java 开发问题 中国国学、传统文化和代码爱好者的程序人生,期待你的关注和支持!本人外号:神秘小峯 山峯 转载说明:务必注明来源(注明:作者:

    2024年01月16日
    浏览(89)
  • 【JVM001】宋红康JVM字节码举例

    1 Integer 说明: 1 String package jvmT; 说明: 1 Object 执行结果: 说明: (1)成员变量(非静态)赋值过程:①默认初始化—②显示初始化/代码块中初始化—③构造器中初始化—④有了对象之后可以f.x或者f.method()设置属性值 (2)方法有多态,属性没有多态 main方法的字节码文件

    2024年02月06日
    浏览(32)
  • JVM(字节码文件详解)

    类加载器 运行时数据区域 执行引擎(主要理解垃圾回收器,及时编译器) 本地方法 在讲解字节码文件组成前,可以安装使用“ jclasslib ”工具或idea中“ jclasslib ”插件进行字节码文件查看 jclasslib的Github地址: https://github.com/ingokegel/jclasslib

    2024年01月23日
    浏览(64)
  • 【JVM】字节码文件的组成

    1. 魔数与Class文件版本  魔数是一个用于校验字节码文件是否有效的标识,位于文件开头的前四个字节,魔数之后是次版本号和主版本号,共同构成了字节码文件的版本号。 2.常量池 常量池是字节码文件中的一个结构,包含了该类的所有常量,如数字,字符串,符号常量等。

    2024年02月02日
    浏览(45)
  • 【JVM 基础】类字节码详解

    源代码通过编译器编译为字节码,再通过类加载子系统进行加载到JVM中运行。 计算机是不能直接运行java代码的,必须要先运行java虚拟机,再由java虚拟机运行编译后的java代码。这个编译后的java代码,就是本文要介绍的java字节码。 为什么jvm不能直接运行java代码呢,这是因为

    2024年02月01日
    浏览(56)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包