JVM学习笔记——类加载和字节码技术篇

JVM学习笔记——类加载和字节码技术篇

在本系列内容中我们会对JVM做一个系统的学习,本片将会介绍JVM的类加载和字节码技术部分

我们会分为以下几部分进行介绍:

  • 类文件结构
  • 字节码指令
  • 编译期处理
  • 类加载阶段
  • 类加载器
  • 运行期优化

类文件结构

这一小节我们将简单介绍一下类的文件结构部分,简单阅读一下以下内容即可

整体文件展示

首先我们通过一个简单的HelloWorld文件来进行类文件结构介绍

首先我们给出Java文件代码:

package cn.itcast.jvm.t5;
// HelloWorld 示例
public class HelloWorld {
	public static void main(String[] args) {
		System.out.println("hello world");
}

我们如果想要获取底层二进制代码,需要在out文件下输入以下命令:

// 获得底层二进制代码
javac -parameters -d . HellowWorld.java

然后我们就可以获得二进制代码:

// 当然目前你是完全看不懂的,我们这里只需要大概了解结构即可,不需要解读
[root@localhost ~]# od -t xC HelloWorld.class
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07
0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63
0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01
0000140 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63
0000160 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f
0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16
0000220 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72
0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13
0000260 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69
0000300 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61
0000320 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46
0000340 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e
0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64
0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74
0000440 63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c
0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61
0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61
0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f
0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72
0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76
0000600 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d
0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a
0000640 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01
0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00
0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00
0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00
0001000 0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00
0001020 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a
0001040 00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b
0001060 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00
0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00
0001120 00 00 02 00 14

类文件结构展示

首先我们给出类文件结构的整体展示:

ClassFile {
    // 魔数
    u4 magic;
    
    // 类文件版本
    u2 minor_version;
    u2 major_version;
    
    // 类文件常量池
    u2 constant_pool_count;
    cp_info constant_pool[constant_pool_count-1];
    
    // 类文件的类型(public或private)
    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];
}

魔数信息

首先我们给出魔数定义:

  • 0~3 字节,表示它是否是【class】类型的文件

我们给出实例展示:

  • 0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

版本信息

首先我们给出版本定义:

  • 4~7 字节,表示类的版本 00 34(52) 表示是 Java 8

我们给出实例展示:

  • 0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

常量池信息

首先我们给出常量池定义:

  • 8~9 字节,表示常量池长度,00 23 (35) 表示常量池有 #1~#34项,注意 #0 项不计入,也没有值

我们给出实例展示:

  • 0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09

我们给出常量池的类型对应的十六进制标号:

Constant Type Value
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

然后下述的34项全部都是常量内容,我们的常量通常分为以下几种:

信息表示 信息表示名 位数 含义
0a Method 信息 3 信息表示:调用类名:调用方法名
09 Field 信息 3 信息表示:调用类名:调用静态方法名
08 字符串常量名称 2 信息表示:调用常量池位置
07 Class 信息 2 信息表示:调用常量池位置
01 utf8 串 3 信息表示:字符长度:字符意义

我们给出一些实例:

// 第#1项 0a 表示一个 Method 信息,00 06 和 00 15(21) 
// 表示它引用了常量池中 #6 和 #21 项来获得这个方法的【所属类】和【方法名】
0a 00 06 00 15

// 第#2项 09 表示一个 Field 信息,00 16(22)和 00 17(23) 
//表示它引用了常量池中 #22 和 # 23 项来获得这个成员变量的【所属类】和【成员变量名】
09 00 16 00 17
    
// 第#3项 08 表示一个字符串常量名称,00 18(24)表示它引用了常量池中 #24 项
08 00 18 
    
// 第#5项 07 表示一个 Class 信息,00 1b(27) 表示它引用了常量池中 #27 项
07 00 1b
    
// 第#7项 01 表示一个 utf8 串,00 06 表示长度,3c 69 6e 69 74 3e 是【 <init> 】
00 06 3c 69 6e 69 74 3e

访问标识与继承信息

首先我们给出访问标识与继承信息定义:

  • 一个字节表示该 class 是一个类的信息:00 21 公共的
  • 一个字节表示根据常量池中位置找到本类全限定名:00 05 表示常量池#5
  • 一个字节表示根据常量池中位置找到父类全限定名 :00 06 表示常量池#6
  • 一个字节表示接口的数量: 00 00 表示接口数为0

我们给出访问标识与继承信息的一些信息列表:

Flag Name Value Interpretation
ACC_PUBLIC 0x0001 Declared public ; may be accessed from outside its package.
ACC_FINAL 0x0010 Declared final ; no subclasses allowed.
ACC_SUPER 0x0020 Treat superclass methods specially when invoked by the invokespecial instruction.
ACC_INTERFACE 0x0200 Is an interface, not a class.
ACC_ABSTRACT 0x0400 Declared abstract ; must not be instantiated.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.
ACC_ANNOTATION 0x2000 Declared as an annotation type.
ACC_ENUM 0x4000 Declared as an enum type.

成员变量信息

首先我们给出成员变量信息定义:

  • 表示成员变量数量,本类为 0

我们给出实例展示:

  • 0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

我们给出成员变量的一些信息列表:

FieldType Type Interpretation
B byte signed byte
C char Unicode character code point in the Basic Multilingual Plane, encoded with UTF-16
D double double-precision floating-point value
F float single-precision floating-point value
I int integer
J long long integer
L ClassName ; reference an instance of class ClassName
S short signed short
Z boolean true or false
[ reference one array dimension

方法信息

首先我们给出方法信息定义:

  • 表示方法数量,本类为 2

我们给出实例展示:

  • 0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01

一个方法由 访问修饰符,名称,参数描述,方法属性数量,方法属性组成,由于过于复杂这里不做展示

附加属性

首先我们给出方法信息定义:

  • 00 01 表示附加属性数量
  • 00 13 表示引用了常量池 #19 项,即【SourceFile】
  • 00 00 00 02 表示此属性的长度
  • 00 14 表示引用了常量池 #20 项,即【HelloWorld.java】

我们给出实例展示:

  • 00 01 00 13 00 00 00 02 00 14

字节码指令

这一节我们将详细介绍字节码指令以及分析Java底层代码

字节码指令介绍

我们首先对之前的HelloWorld中的两个指令进行介绍

第一个指令是:

  • 构造方法的字节码指令 :public cn.itcast.jvm.t5.HelloWorld();

其二进制代码为:

  • 2a b7 00 01 b1

我们对其进行解释:

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

第二个指令是:

  • 主方法的字节码指令:public static void main(java.lang.String[]);

其二进制代码为:

  • b2 00 02 12 03 b6 00 04 b1

我们对其进行解释:

  • b2 => getstatic 用来加载静态变量,哪个静态变量呢?

  • 00 02 引用常量池中 #2 项,即【Field java/lang/System.out:Ljava/io/PrintStream;】

  • 12 => ldc 加载参数,哪个参数呢?

  • 03 引用常量池中 #3 项,即 【String hello world】

  • b6 => invokevirtual 预备调用成员方法,哪个方法呢?

  • 00 04 引用常量池中 #4 项,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】

  • b1 表示返回

Javap工具介绍

我们如果采用二进制代码来查看其底层数据就会显得繁杂且麻烦

所以Java为我们提供了具体的工具,隶属于JVM的工具,可以直接在out文件下使用:

// javap 反编译工具
[root@localhost ~]# javap -v HelloWorld.class

然后我们就可以得到HelloWorld的反编译文件:

// 虽然依旧是底层代码,但这种阅读方式就比较舒服

// 魔数
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
}

图解方法执行流程

我们首先给出一串简单的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);
    }
}

我们再给出javap反编译后的代码:

Classfile /E:/编程内容/JVM/资料-解密JVM/代码/jvm/out/production/jvm/cn/itcast/jvm/t3/bytecode/Demo3_1.class
  Last modified 2022-11-2; size 635 bytes
  MD5 checksum 1a6413a652bcc5023f130b392deb76a1
  Compiled from "Demo3_1.java"
public class cn.itcast.jvm.t3.bytecode.Demo3_1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#25         // java/lang/Object."<init>":()V
   #2 = Class              #26            // java/lang/Short
   #3 = Integer            32768
   #4 = Fieldref           #27.#28        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #29.#30        // java/io/PrintStream.println:(I)V
   #6 = Class              #31            // cn/itcast/jvm/t3/bytecode/Demo3_1
   #7 = Class              #32            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Lcn/itcast/jvm/t3/bytecode/Demo3_1;
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               args
  #18 = Utf8               [Ljava/lang/String;
  #19 = Utf8               a
  #20 = Utf8               I
  #21 = Utf8               b
  #22 = Utf8               c
  #23 = Utf8               SourceFile
  #24 = Utf8               Demo3_1.java
  #25 = NameAndType        #8:#9          // "<init>":()V
  #26 = Utf8               java/lang/Short
  #27 = Class              #33            // java/lang/System
  #28 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;
  #29 = Class              #36            // java/io/PrintStream
  #30 = NameAndType        #37:#38        // println:(I)V
  #31 = Utf8               cn/itcast/jvm/t3/bytecode/Demo3_1
  #32 = Utf8               java/lang/Object
  #33 = Utf8               java/lang/System
  #34 = Utf8               out
  #35 = Utf8               Ljava/io/PrintStream;
  #36 = Utf8               java/io/PrintStream
  #37 = Utf8               println
  #38 = Utf8               (I)V
{
  public cn.itcast.jvm.t3.bytecode.Demo3_1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t3/bytecode/Demo3_1;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10
         2: istore_1
         3: ldc           #3                  // int 32768
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: istore_3
        10: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: iload_3
        14: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        17: return
      LineNumberTable:
        line 8: 0
        line 9: 3
        line 10: 6
        line 11: 10
        line 12: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  args   [Ljava/lang/String;
            3      15     1     a   I
            6      12     2     b   I
           10       8     3     c   I
}
SourceFile: "Demo3_1.java"

下面我们通过图解来执行底层结构变化:

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

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

  1. main 线程开始运行,分配栈帧内存
这里的绿色块块是局部变量表,里面用于存放局部变量,大小为4,前面javap有标记
这里的青色块块是操作数栈,我们的操作数的任何操作都要在里面进行,大小为2,前面javap有标记

  1. 执行方法bipush 10
将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有:

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

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

  1. 执行方法istore_1
将操作数栈顶数据弹出,存入局部变量表的 slot 1

  1. 执行方法ldc #3
从常量池加载 #3 数据到操作数栈
注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的

  1. 执行方法istore_2

  1. 执行方法iload_1 iload_2

  1. 执行方法iadd

  1. 执行方法istore_3

  1. 执行方法getstatic #4
这里需要注意getstatic引用了常量池里存放的System.out的引用对象地址
然后我们到堆里去寻找该对象,找到该对象后将该对象的引用放到操作数栈中进行操作

  1. 执行方法iload_3

  1. 执行方法invokevirtual #5
第一步操作:
找到常量池 #5 项
定位到方法区 java/io/PrintStream.println:(I)V 方法
生成新的栈帧(分配 locals、stack等)
传递参数,执行新栈帧中的字节码

第二步操作:
执行完毕,弹出栈帧
清除 main 操作数栈内容

  1. 执行方法return
完成 main 方法调用,弹出 main 栈帧
程序结束

方法i++底层实现

目的:

  • 从字节码角度分析 a++ 相关题目

源码:

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

字节码:

Classfile /E:/编程内容/JVM/资料-解密JVM/代码/jvm/out/production/jvm/cn/itcast/jvm/t3/bytecode/Demo3_2.class
  Last modified 2022-11-2; size 610 bytes
  MD5 checksum 5f6a35e5b9bb88d08249958a8d2ab043
  Compiled from "Demo3_2.java"
public class cn.itcast.jvm.t3.bytecode.Demo3_2
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#22         // java/lang/Object."<init>":()V
   #2 = Fieldref           #23.#24        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #25.#26        // java/io/PrintStream.println:(I)V
   #4 = Class              #27            // cn/itcast/jvm/t3/bytecode/Demo3_2
   #5 = Class              #28            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               Lcn/itcast/jvm/t3/bytecode/Demo3_2;
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               args
  #16 = Utf8               [Ljava/lang/String;
  #17 = Utf8               a
  #18 = Utf8               I
  #19 = Utf8               b
  #20 = Utf8               SourceFile
  #21 = Utf8               Demo3_2.java
  #22 = NameAndType        #6:#7          // "<init>":()V
  #23 = Class              #29            // java/lang/System
  #24 = NameAndType        #30:#31        // out:Ljava/io/PrintStream;
  #25 = Class              #32            // java/io/PrintStream
  #26 = NameAndType        #33:#34        // println:(I)V
  #27 = Utf8               cn/itcast/jvm/t3/bytecode/Demo3_2
  #28 = Utf8               java/lang/Object
  #29 = Utf8               java/lang/System
  #30 = Utf8               out
  #31 = Utf8               Ljava/io/PrintStream;
  #32 = Utf8               java/io/PrintStream
  #33 = Utf8               println
  #34 = Utf8               (I)V
{
  public cn.itcast.jvm.t3.bytecode.Demo3_2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcn/itcast/jvm/t3/bytecode/Demo3_2;

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

相关知识点补充:

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

相关图示展示:

条件判断指令

首先我们给出条件判断的相关指令集合:

指令 助记符 含义
0x99 ifeq 判断是否 == 0
0x9a ifne 判断是否 != 0
0x9b iflt 判断是否 < 0
0x9c ifge 判断是否 >= 0
0x9d ifgt 判断是否 > 0
0x9e ifle 判断是否 <= 0
0x9f if_icmpeq 两个int是否 ==
0xa0 if_icmpne 两个int是否 !=
0xa1 if_icmplt 两个int是否 <
0xa2 if_icmpge 两个int是否 >=
0xa3 if_icmpgt 两个int是否 >
0xa4 if_icmple 两个int是否 <=
0xa5 if_acmpeq 两个引用是否 ==
0xa6 if_acmpne 两个引用是否 !=
0xc6 ifnull 判断是否 == null
0xc7 ifnonnull 判断是否 != null

我们对上述内容做简单说明:

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

源码:

package cn.itcast.jvm.t3.bytecode;

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

我们将重要的字节码内容单独调下来讲解:

// 产生一个0
0: iconst_0
// 放入到局部变量池
1: istore_1
// 取出0    
2: iload_1
// ifne:判断是否 != 0
// 进行判断是否不为0,如果不为0直接跳转12,如果不是继续执行
3: ifne 12
// 放入一个10,然后存入到原本a的位置
6: bipush 10
8: istore_1
// 直接跳转到return
9: goto 15
// 这里是如果不为0的逻辑:放入一个20然后存到a的位置
12: bipush 20
14: istore_1
// 代码结束
15: return

循环控制指令

我们循环控制指令实际上还是采用条件判断语句的指令进行操作

while源码:

package cn.itcast.jvm.t3.bytecode;

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

我们将while中重要的字节码内容单独调下来讲解:

0: iconst_0
1: istore_1
// 首先取出a的值,再放入一个10,然后采用if_icmpge判断a是否>=10,如果是跳转return结束,如果不是执行下述操作
2: iload_1
3: bipush 10
5: if_icmpge 14
// 这里是自加操作,注意是在局部变量中执行,这时局部变量的a+1,但是操作数栈的a值未发生变化
8: iinc 1, 1
// 我们回到2操作,重新取a,取10,并再次进行比较
11: goto 2
14: return

dowhile源码:

package cn.itcast.jvm.t3.bytecode;

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

我们将dowhile中重要的字节码内容单独调下来讲解:

0: iconst_0
1: istore_1
// 我们首先对a的值进行一次自加操作,然后再取a和10
2: iinc 1, 1
5: iload_1
6: bipush 10
// 在这里进行进行判断,如果符合条件就回到第二步不断重复
8: if_icmplt 2
11: return

for源码:

package cn.itcast.jvm.t3.bytecode;

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

        }
    }
}

我们将for中重要的字节码内容单独调下来讲解:

// 我们会发现for和while的源码是完全一致的~
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

构造方法

我们先来介绍构造方法的构造原理:

  • 编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 <cinit>()V
  • <cinit>()V 方法会在类加载的初始化阶段被调用 ,但原始构造方法<init>()V 内的代码总是在最后

首先我们来介绍类的构造方法:

package cn.itcast.jvm.t3.bytecode;

public class Demo3_8_1 {

	static int i = 10;
    
    static {
        i = 20;
    }


    static {
        i = 30;
    }

    

    public static void main(String[] args) {
        System.out.println(Demo3_8_1.i);
    }
}

然后我们查看重要的字节码部分:

// 这里首先将最上面的构造语句static int i = 10;读取,并输入10为i的值
0: bipush 10
2: putstatic #2 // Field i:I
// 这里将中间的构造方法读取,i=20
5: bipush 20
7: putstatic #2 // Field i:I
// 这里将最后的构造方法读取,i=30
10: bipush 30
12: putstatic #2 // Field i:I
// 所以最后我们的类中的i为30
15: return

然后同理我们来查看存在原始构造方法的类:

package cn.itcast.jvm.t3.bytecode;

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

我们来查看重要的字节码部分:

// 首先他们调用了 super.<init>()V,分别按顺序给ab进行赋值
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"
    
// 但是后面他们调用了原始构造方法,将传进来的参数值作为a,b的值赋值
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

方法调用

我们首先给出一些方法和方法调用示例:

package cn.itcast.jvm.t3.bytecode;

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

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

    // 无法改变的私有方法
    private final void test2() { }

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

    // 静态公开方法
    public static void test4() { }

    // 继承toString方法
    @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();
    }

}

我们来查看其字节码:

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 指令来调用,属于静态绑定

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

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

  • d.test4()是通过对象引用调用一个静态方法,但在调用invokestatic 之前执行了 pop 指令,把对象引用从操作数栈弹掉了

  • 还有一个执行 invokespecial 的情况是通过 super 调用父类方法

异常处理

我们同样首先给出异常处理的代码:

package cn.itcast.jvm.t3.bytecode;

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
         // 首先这里是try,将10赋给i
         2: bipush        10
         4: istore_1
         // 再往下是catch代码,我们不希望执行,所以直接采用goto跳转到return
         5: goto          12
         // 这里存放catch代码,如果检测到错误,就会跳转到这里执行
         8: astore_2
         9: bipush        20
        11: istore_1
        12: return
      // 首先这个地方多了一个异常处理机制from和to是作用范围,这里是[)形式的
      // 此外target表示跳转行数,type表示遇到什么样的错误
      // 系统会自动检测,不需要在上述重复书写
      Exception table:
         from    to  target type
             2     5     8   Class java/lang/Exception
      // Exception也会被看作对象,这里需要进行存放
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           23       3     2     e   Ljava/lang/Exception;
            0      27     0  args   [Ljava/lang/String;
            2      25     1     i   I
}

当然针对多个异常处理,我们同样采用这种方式:

package cn.itcast.jvm.t3.bytecode;

public class Demo3_11_2 {

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

}

我们给出部分重要字节码:

  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
      // 同时局部变量表中也会有异常Exception的存放位置,这里由于异常在同一处,不会同时出现,所以放在同一个局部变量即可
      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
}

针对multi-catch 的情况系统的处理方法也大同小异:

package cn.itcast.jvm.t3.bytecode;

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

我们给出部分重要字节码:

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: ldc           #2                  // class cn/itcast/jvm/t3/bytecode/Demo3_11_3
         2: ldc           #3                  // String test
         4: iconst_0
         5: anewarray     #4                  // class java/lang/Class
         8: invokevirtual #5                  // Method java/lang/Class.getMethod:(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
        11: astore_1
        12: aload_1
        13: aconst_null
        14: iconst_0
        15: anewarray     #6                  // class java/lang/Object
        18: invokevirtual #7                  // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
        21: pop
        22: goto          30
        25: astore_1
        26: aload_1
        27: invokevirtual #11                 // Method java/lang/ReflectiveOperationException.printStackTrace:()V
        30: return
      Exception table:
         from    to  target type
             0    22    25   Class java/lang/NoSuchMethodException
             0    22    25   Class java/lang/IllegalAccessException
             0    22    25   Class java/lang/reflect/InvocationTargetException
      // 这里注意只标有一个坑位了,所以某种意义上是节省内存了~
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           12      10     1  test   Ljava/lang/reflect/Method;
           26       4     1     e   Ljava/lang/ReflectiveOperationException;
            0      31     0  args   [Ljava/lang/String;
}

最后我们介绍一下finally操作:

package cn.itcast.jvm.t3.bytecode;

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
         // 前端的代码显得简单粗暴
         // 就是在try阶段执行一次finally操作,直接赋值一份在catch操作里面执行finally操作,最后再放一份finally在最后
         // 也就是说finally一共有三份,第一份在try,第二份在catch,第三份单独存放防止前面出现异常则直接执行
         0: iconst_0
         1: istore_1
         2: bipush        10
         4: istore_1
         5: bipush        30
         7: istore_1
         8: goto          27
        11: astore_2
        12: bipush        20
        14: istore_1
        15: bipush        30
        17: istore_1
        18: goto          27
        21: astore_3
        22: bipush        30
        24: istore_1
        25: aload_3
        26: athrow
        27: return
      // 重点在这里!因为try和catch操作都有可能出现异常,所以添加了两个异常,如果发现异常,直接跳到独有的fianlly操作里执行
      Exception table:
         from    to  target type
             2     5    11   Class java/lang/Exception
             2     5    21   any
            11    15    21   any
		// 这里同样是异常占位
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           12       3     2     e   Ljava/lang/Exception;
            0      28     0  args   [Ljava/lang/String;
            2      26     1     i   I
}

加锁处理

最后我们介绍一下synchronized的处理问题:

package cn.itcast.jvm.t3.bytecode;

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
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0  args   [Ljava/lang/String;
            8      23     1  lock   Ljava/lang/Object;
}

编译期处理

这一节我们将详细介绍编译期JVM为我们做的简单处理

语法糖介绍

首先我们简单介绍一下语法糖的概念:

  • 所谓的语法糖,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码
  • 相当于在JVM的原版本的一些插件,帮助我们快速编译

我们下面所介绍的编译期处理基本都是语法糖的内容,我们需要注意:

  • 以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。
  • 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了等价 的 java 源码方式

默认构造器

首先我们都知道,如果我们的类没有书写构造器,那么系统会自动为我们补充一个构造器

首先这是我们的源码:

public class Candy1 {
}

然后由编译器在编译期所做的处理如下:

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

自动拆装箱

我们在最开始的版本中其实包是一个很普遍的概念,我们需要进行手动拆箱装箱操作:

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

但是在JDK5之后,系统为我们自动添加了自动拆装箱功能,我们就可以节省掉这一步:

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

泛型集合取值

泛型也是在 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);

如果我们前面的类型是int类型,那么还会追加一层自动拆装箱功能:

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

可变参数

可变参数 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("hello", "world");
    }
}

但是在底层中系统会为我们完成一些操作:

public class Candy4 {
    
    // 这里接受的参数直接由args可变参数变为String类型的数组参数
    public static void foo(String[] args) {
        String[] array = args; // 直接赋值
        System.out.println(array);
    } 
    
    public static void main(String[] args) {
        // 相当于我们直接创建了String类型的数组,并且将值封装进去,这个数组的大小就是我们传入的大小
        foo(new String[]{"hello", "world"});
    }
}

foreach循环

我们的foreach循环操作也是由最基本的for循环来演变过来的,只不过是系统为我们进行了处理而已:

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

下面我们来展示由系统编译后的java代码:

public class Candy5_1 {
    
    public Candy5_1() {
    } 
    
    public static void main(String[] args) {
        
        // 这里的数组赋值实际上还是调用了new int[],但是系统帮你补充,所以你可以省略
        int[] array = new int[]{1, 2, 3, 4, 5};
        
        // 这里依旧采用的是for循环,但是系统为你封装好了for循环的开头与结束条件,并且帮你把数组中的元素取出来
        for(int i = 0; i < array.length; ++i) {
            int e = array[i];
            System.out.println(e);
        }
    }
}

对于集合也是同样的概念:

public class Candy5_2 {
    public static void main(String[] args) {
        
        List<Integer> list = Arrays.asList(1,2,3,4,5);
        
        for (Integer 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);
        }
    }
}

switch字符串

我们JDK7之后的switch可以直接采用字符串来进行判定,这同样也是底层编译的语法糖内容:

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

我们的底层是采用hashCode和equal方法来进行判定的:

public class Candy6_1 {
    
    public Candy6_1() {
    }
        
    public static void choose(String str) {
        
        byte x = -1;
        
        // 首先进行hashCode比较,因为hashCode是唯一值,比较速度快,可以进行大规模比较
        switch(str.hashCode()) {
                
            // 然后我们再采用eq方法确定,因为有的值的hashCode是一致的,我们需要判定是否符合我们的条件 
            // 我们根据条件再设置一个x的参数用于另一个switch来执行方法
            case 99162322: // hello 的 hashCode
                if (str.equals("hello")) {
                    x = 0;
                } 
                break;
            case 113318802: // world 的 hashCode
                if (str.equals("world")) {
                    x = 1;
                }
            } 
        
            // 执行方法的switch
        	switch(x) {
                case 0:
                    System.out.println("h");
                    break;
                case 1:
                    System.out.println("w");
        }
    }
}

switch枚举

switch和枚举一同使用也是由系统底层进行了改造:

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

我们底层修改如下:

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

枚举类

在JDK7中甚至为我们直接设计了枚举类:

enum Sex {
    MALE, FEMALE
}

底层直接为我们创造了一个类:

public final class Sex extends Enum<Sex> {
    
    // 首先将属性进行定义,并定义一个VALUE数组存放这些属性
    public static final Sex MALE;
    public static final Sex FEMALE;
    private static final Sex[] $VALUES;
    
    // 设置一个静态方法,将数据设置好顺序号,并存入数组
    static {
        MALE = new Sex("MALE", 0);
        FEMALE = new Sex("FEMALE", 1);
        $VALUES = new Sex[]{MALE, FEMALE};
    } 

    // 私有方法防止修改枚举
    private Sex(String name, int ordinal) {
        super(name, ordinal);
    } 
    
    // 获得所有枚举数
    public static Sex[] values() {
        return $VALUES.clone();
    } 
    
    // 根据名称获得枚举
    public static Sex valueOf(String name) {
        return Enum.valueOf(Sex.class, name);
    }
}

try-with-resources

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

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

但是我们需要注意:

  • 其中资源对象需要实现 AutoCloseable 接口, 才能够自动进行资源关闭
  • 例如 InputStream 、 OutputStream 、Connection 、 Statement 、 ResultSet 等接口都实现了 AutoCloseable ,

我们可以这样书写代码:

public class Candy9 {
    public static void main(String[] args) {
        // 书写代码时将需要关闭的资源放在try条件中,最后系统会自动关闭
        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();
        }
    }
}

方法重写时的桥接方法

我们都知道,方法重写时对返回值分两种情况:

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

我们给出一个简单例子:

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

匿名内部类

我们平时所使用的匿名内部类其实是系统为我们自动创建的一个类:

public class Candy11 {
    public static void test(final int x) {
        
        // 例如我们这里采用一个匿名内部类
        // 注意这里如果为匿名内部类添加参数,需要添加final固定参数,因为我们的类中的参数是无法修改的!
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("ok:" + x);
                }
            };
    }
}

系统为我们直接创造了一个类:

// application

public class Candy11 {
    
    public static void test(final int x) {
        Runnable runnable = new Candy11$1(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);
    }
}

类加载阶段

这一节我们将详细介绍JVM的类加载阶段

加载阶段

加载阶段是类加载阶段的第一阶段

加载阶段的用处是:

  • 将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类

由于我们无法直接使用C++语言的instanceKlass,所以我们需要借助镜像来完成加载:

  • java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用

除此之外我们来介绍instanceKlass的其他重要部分:

  • _super 即父类
  • _fields 即成员变量
  • _methods 即方法
  • _constants 即常量池
  • _class_loader 即类加载器
  • _vtable 虚方法表
  • _itable 接口方法表

除此之外我们强调两个注意点:

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

链接阶段

连接阶段是类加载阶段的第二阶段

链接主要分为三个部分:验证,准备,解析

验证阶段

我们简单介绍一下验证的含义:

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

准备阶段

我们简单介绍一下解释的含义:

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

其中我们需要注意:

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

解析阶段

我们简单介绍一下解析的含义:

  • 将常量池中的符号引用解析为直接引用
  • 这个阶段对我们程序员来说几乎是透明的,不需要过多了解

初始化阶段

首先我们先来简单介绍一下<cinit>方法:

  • 即static方法,会将所有的赋值按顺序堆积在static方法中,在第一次构造类时执行
  • 初始化即调用<cinit>()V ,虚拟机会保证这个类的『构造方法』的线程安全

初始化时机:

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

不会初始化的时间点:

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

我们给出简单验证的代码:

package cn.itcast.jvm.t3.load;

import java.io.IOException;

public class Load3 {
    
    static {
        System.out.println("main init");
    }
    
    // 每次选择一个测试~
    public static void main(String[] args) throws ClassNotFoundException, IOException {
//        // 1. 静态常量不会触发初始化
//        System.out.println(B.b);
//        // 2. 类对象.class 不会触发初始化
//        System.out.println(B.class);
//        // 3. 创建该类的数组不会触发初始化
//        System.out.println(new B[0]);
//        // 4. 不会初始化类 B,但会加载 B、A
//        ClassLoader cl = Thread.currentThread().getContextClassLoader();
//        cl.loadClass("cn.itcast.jvm.t3.load.B");
//        // 5. 不会初始化类 B,但会加载 B、A
//        ClassLoader c2 = Thread.currentThread().getContextClassLoader();
//        Class.forName("cn.itcast.jvm.t3.load.B", false, c2);
        System.in.read();


//        // 1. 首次访问这个类的静态变量或静态方法时
//        System.out.println(A.a);
//        // 2. 子类初始化,如果父类还没初始化,会引发
//        System.out.println(B.c);
//        // 3. 子类访问父类静态变量,只触发父类初始化
//        System.out.println(B.a);
//        // 4. 会初始化类 B,并先初始化类 A
//        Class.forName("cn.itcast.jvm.t3.load.B");
    }
}

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

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

类加载器

这一节我们将详细介绍JVM的类加载器

四种基本类加载器

我们先来介绍四种基本类加载器:

名称 加载哪的类 说明
Bootstrap ClassLoader JAVA_HOME/jre/lib 无法直接访问
Extension ClassLoader JAVA_HOME/jre/lib/ext 上级为 Bootstrap,显示为 null
Application ClassLoader classpath 上级为 Extension
自定义类加载器 自定义 上级为 Application

我们简单介绍一下运行机制:

  • 首先我们需要知道Bootstrap ClassLoader 是不可访问的,当我们查找到该层级时会显示null
  • 我们如果需要加载一个类,会先检测是否有上级,如果有上级就到上级中去,如果没有就在本层查找是否有该类
  • 意思就是以最高级的加载器中的类为最高标准,如果同时存在多个类,我们会选择最高级的类加载器中的类来运行

启动类加载器

用 Bootstrap 类加载器加载类:

package cn.itcast.jvm.t3.load;

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

执行 :

package cn.itcast.jvm.t3.load;

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

输出:

// 我们会发现最后输出的结果为null,这就是启动类加载器,因为我们无法使用,所以JVM索性给我们定义一个null返回

// 运行~
E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:.
    
// 运行结果
cn.itcast.jvm.t3.load.Load5
bootstrap F init
null

我们对上述的运行语句做简单解释:

  • -Xbootclasspath 表示设置 bootclasspath
  • 其中 /a:. 表示将当前目录追加至 bootclasspath 之后

扩展类加载器

用Extension ClassLoader 类加载器加载类:

package cn.itcast.jvm.t3.load;

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

执行:

package cn.itcast.jvm.t3.load;

/**
 * 演示 扩展类加载器
 * 在 C:\Program Files\Java\jdk1.8.0_91 下有一个 my.jar
 * 里面也有一个 G 的类,观察到底是哪个类被加载了
 */
public class Load5_2 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
        System.out.println(aClass.getClassLoader());
    }
}

输出:

// 目前是由Application ClassLoader 加载器运行的!(其实我们目前使用的基本都是Application类加载器)
classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2

但是如果我们将该程序打成jar包之后并放在Extension ClassLoader的对应目录下之后我们再来运行:

// 我们就会发现类加载器目录变为了Extension ClassLoader
ext G init
sun.misc.Launcher$ExtClassLoader@29453f44

双亲委派模式

首先我们来简单介绍一下双亲委派模式:

  • 所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则
  • 意思就是我们要遵从双亲(上级)的指令,以双亲为主,先调用双亲的类引用,再考虑自身

我们给出双亲委派模式的代码:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 1. 检查该类是否已经加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        // 2. 有上级的话,委派上级 loadClass
                        c = parent.loadClass(name, false);
                    } else {
                        // 3. 如果没有上级了(ExtClassLoader),则委派BootstrapClassLoader
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
					//	就算出现异常,这里捕获不做任何处理,继续运行程序
                }

                if (c == null) {
                    // 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
                    long t1 = System.nanoTime();
                    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;
        }
    }

自定义类加载器

首先我们简单介绍一下自定义类加载器:

  • 由我们自己设置什么情况下在什么文件夹下取得类的本体

那么我们给出我们需要自定义类加载器的情况:

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

我们这里给出简单步骤:

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

运行期优化

这一节我们将详细介绍JVM的运行期优化

即时编译

首先我们来简单介绍一下即时编译:

  • 即时编译就是在编译的过程中调整编译的细节状态,例如内部引用内部构造等

我们给出一个简单的例子:

package cn.itcast.jvm.t3.jit;

public class JIT1 {

    // -XX:+PrintCompilation -XX:-DoEscapeAnalysis
    public static void main(String[] args) {
        for (int i = 0; i < 200; i++) {
            long start = System.nanoTime();
            for (int j = 0; j < 1000; j++) {
                new Object();
            }
            long end = System.nanoTime();
            System.out.printf("%d\t%d\n",i,(end - start));
        }
    }
}

我们给出运行阶段的部分内容:

// 前面是第几次,后面是具体时间

// 我们会发现随着次数的增多,我们的运行时间越来越少,这其实就是即时编译的功劳

// 第一阶段:非常大~
0 96426
1 52907
2 44800
3 119040
4 65280
5 47360
6 45226
7 47786
8 48640
9 60586
10 42667

// 第二阶段:相对减少~
82 18774
83 17067
84 21760
85 23467
86 17920
87 17920
88 18774
89 18773
90 19200

// 第三阶段:非常少~
190 1280
191 853
192 853
193 853
194 853
195 854
196 853
197 853
198 853
199 854

我们首先来解释一个概念:

  • profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等

其实我们的即时编译会被分为五个时间段:

  • 0 层,解释执行(Interpreter)
  • 1 层,使用 C1 即时编译器编译执行(不带 profiling)
  • 2 层,使用 C1 即时编译器编译执行(带基本的 profiling)
  • 3 层,使用 C1 即时编译器编译执行(带完全的 profiling)
  • 4 层,使用 C2 即时编译器编译执行

那么我们就来介绍即时编译和解释器的区别:

  • 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
  • JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
  • 解释器是将字节码解释为针对所有平台都通用的机器码
  • JIT 会根据平台类型,生成平台特定的机器码

可是即时编译也会有部分缺陷,我们也不能将所有代码都使用即时编译来进行:

  • 对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;
  • 另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。

我们将这种思想称为逃逸分析:

  • 逃逸分析就是判断新建的对象是否逃逸。
  • 可以使用 -XX:-DoEscapeAnalysis 关闭逃逸分析

方法内联

我们来简单介绍一下方法内联:

  • 当方法的篇幅较短时,我们会直接将方法的内容替换到引用方法的位置

我们举一个简单的例子:

// 对于这么一个简单的返回方法
private static int square(final int i) {
	return i * i;
}

// 如果我们调用下述语句:
System.out.println(square(9));

// 在编译器中就会这样处理:
System.out.println(9 * 9);

// 甚至还可以做常量折叠的优化:
System.out.println(81);

字段优化

我们简单介绍一下字段优化:

  • 字段优化就是将之前所使用的字段存储在一个我们所看不到的默认数组中,我们再次调用或其他操作时会直接在数组中获取

我们给出简单例子:

package test;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

@Warmup(iterations = 2, time = 1)
@Measurement(iterations = 5, time = 1)
@State(Scope.Benchmark)
public class Benchmark1 {

    // 简单来说就是,我们会调用下述三个方法1000次,我们会判断其执行顺序
    
    // 我们不做示例直接给出结果:三者基本一致,因为他们都借助了数组来储存数据
    
    int[] elements = randomInts(1_000);

    private static int[] randomInts(int size) {
        Random random = ThreadLocalRandom.current();
        int[] values = new int[size];
        for (int i = 0; i < size; i++) {
            values[i] = random.nextInt();
        }
        return values;
    }

    // 这是我们平时会书写的代码,但是系统在for之前为我们做了一个数组,用来存放elements的值,就和test2一样
    @Benchmark
    public void test1() {
        for (int i = 0; i < elements.length; i++) {
            doSum(elements[i]);
        }
    }

    // 这是标准格式
    @Benchmark
    public void test2() {
        int[] local = this.elements;
        for (int i = 0; i < local.length; i++) {
            doSum(local[i]);
        }
    }

    // 这是foreeach循环,我们在前面通过字节码分析可以知道foreach是完全基于for循环制作的,所以他也存在数组存放数据
    @Benchmark
    public void test3() {
        for (int element : elements) {
            doSum(element);
        }
    }

    static int sum = 0;

    @CompilerControl(CompilerControl.Mode.DONT_INLINE)
    static void doSum(int x) {
        sum += x;
    }


    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(Benchmark1.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();
    }
}

反射优化

反射优化也是类似于即时编译的优化方法:

  • 在我们的运行期间会存在一个阈值
  • 在运行次数未到达阈值之前,我们会采用反射提供的方法或者类来运行
  • 但是当运行次数到达阈值之后,我们就会视情况采用我们运行过程中提供的方法或者类来进行余下的运行

我们给出简单示例:

package cn.itcast.jvm.t3.reflect;

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 NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
        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,而是直接使用方法中的foo

结束语

到这里我们JVM的类加载和字节码技术篇就结束了,希望能为你带来帮助~

附录

该文章属于学习内容,具体参考B站黑马程序员满老师的JVM完整教程

这里附上视频链接:01-类加载-概述_哔哩哔哩_bilibili

posted @ 2022-11-06 07:59  秋落雨微凉  阅读(298)  评论(0编辑  收藏  举报