JVM学习笔记(Ⅰ):Class类文件结构解析(带你读懂Java字节码,这一篇就够了)

JVM学习笔记(Ⅰ):Class类文件结构解析,带你读懂Java字节码

前言:本文属于博主个人的学习笔记,博主也是小白。如果有不对的地方希望各位帮忙指出。本文主要还是我的学习总结,因为网上的一些知识分布比较零散故作整理叙述。如果有不对的地方,还请帮忙指正。本文不产生任何收益,如有出现禁止转载、侵权的图片,亦或者文字内容请联系我进行修改。



前言

提示:本文分为两部分,第一部分中我们会先了解class类文件结构;第二部分我们会结合实例来进行解读。


参考:
  拭心:Java 基础巩固:内部类的字节码学习和实战使用场景:链接: link.

  昨夜星辰_zhangjg:深入理解Java Class文件格式(一):链接: link.

  祈祷ovo详解JVM常量池、Class常量池、运行时常量池、字符串常量池(心血总结):链接: link.

  Joy CR:Class类文件结构——访问标志:链接: link.

  亦山:《Java虚拟机原理图解》1.3、class文件中的访问标志、类索引、父类索引、接口索引集合:链接: link.

  四月葡萄 从一个class文件深入理解Java字节码结构:链接: link.

  波波烤鸭:Class文件结构介绍[属性表集合]:链接: link.

  图灵学院:全网最牛JVM字节码结构分析、Class类文件核心结构剖析、全网最清晰JVM常量池讲解、从字节码底层分析:链接: link.

  大宝11:Java字节码<init><clinit>的区别:链接: link.

食用建议:
   因为该博客写的又臭又长,所以各位看官结合自己需要查找的部分阅读即可,了解原理的盆友可以直接移步第二部分,对常量池、属性表不了解的盆友可以跳转相应部分。
   做好心理准备了么,我们开始了。
在这里插入图片描述

一、Class类文件结构

1.1 class文件格式

  首先我们要知道 任何一个Class文件都对应着唯一的一个类或接口的定义信息。
   当然你可能会疑问如果我可以在一个java程序中定义多个class,只声明一个public,这时候我的程序也是可以运行的,但里面有多个类,当我对这个程序编译的时候字节码文件怎么处理,还是只生成一个Class文件嘛?
   这里我们要区分清楚一个Java程序与一个类/接口的不同,那我们写些代码来看看当存在内部类和外部类的情况下,进行编译会发生什么事情。

1.1.1 内部类与外部类的字节码文件

   这分两种情况,第一种你定义的类是内部类,如下代码所示,总共有三个自定义类,其中两个为内部类。

public class InnerTest {
    private int n=0;

    static class Inner1{
        private static int i=0;
    }
    static class Inner2{
        private static int j=1;
    }

    public static void main(String[] args) {
        Inner1 inner1=new Inner1();
        Inner2 inner2=new Inner2();
        System.out.println(inner1.i);
        System.out.println(inner2.j);
    }
}

   javac编译结果如下,生成三个class字节码文件。
在这里插入图片描述
   如果是外部类,如下列代码,有一个外部定义的接口,以及该接口的实现类。

public class OuterDemo {
    public static void main(String[] args) {
        Outer1.method().show();
        Inter inter=()->{
            System.out.println("MyShow");
        };
        inter.show();
    }
}
interface Inter{
    void show();
}
class Outer1{
    static class InterImpl implements Inter{
        @Override
        public void show() {
            System.out.println("Hello World");
        }
    }
    public static InterImpl method(){
        return new InterImpl();
    }
}

   编译后的字节码文件如下,注意此时InterImpl为Outer1的静态内部类,所以文件名为Outer1$InterImpl。
在这里插入图片描述

1.1.2 字节码文件格式

   Java良好的向后兼容性,很大一部分归功于Class文件结构的稳定性,Class文件是一组以字节为基础单位的二进制流,中间没有分隔符,所有内容都是必须的,Class文件中存储的数据类型共两种:无符号数:用来表示个数;表:用来记录具体数据。
   class文件格式如下所示,u1,u2等分别表示1个字节、2个字节等占位长度。
   这里大家可能比较疑惑的就是,为什么常量池表对应的常量个数是constant_pool_count-1,而不是constant_pool_count,这是因为我们的常量池必须空出一位给JVM,用于做空常量池标识.

类型名称数量
u4magic (魔数)1
u2minor_version (次版本号)1
u2major_version(主版本号)1
u2constant_pool_count (常量池个数)1
cp_infoconstant_pool (常量池表)constant_pool_count-1
u2access_flags(访问标记符)1
u2this_class (本类)1
u2super_class (父类)1
u2interfaces_count (接口个数)1
u2interfaces (接口表)interfaces_count
u2fields_count (字段个数)1
field_infofields (字段表)field_count
u2methods_count (方法个数)1
method_infomethods (方法表)methods_count
u2attribute_count (属性个数)1
attribute_infoattributes(属性表)attributes_count

   我们依次来看一下表中的各数据介绍

1.2 魔数及Class文件版本号

   计算机一切数据都可以以二进制文件形式打开,包括你的各种文件。
   当你用执行程序打开后缀文件的时候,程序并非只根据文件名后缀来判别文件是否可执行,而是根据文件起头的魔数来判断。以java文件编译后的. class字节码文件为例,java虚拟机如果判断该文件是可执行的字节码文件?我们用Binary Viewer打开后,以16进制查看文件(4个二进制位),查看前8位(4字节,32位),就是class文件的魔数,CAFE BABE(咖啡宝贝)。这也是为什么Java语言图标是一杯咖啡的缘故。
   简单地来理解一下,魔数的作用就是来确定这个文件是否为能被虚拟机接受的Class文件。
在这里插入图片描述
                        图1
   跳过4个字节后,接下来的两个字节 00 00 是我们的次版本号,次版本号虽然是最初定义好的规范,但只在Java1.2版本之前被短暂使用过,一般固定为00 00.
   再跳两个字节 00 34 就是我们的主版本号转成10进制,值为52,对应着我们的JDK8.

1.3 常量池

   紧接着主次版本号之后的就是常量池入口,由于常量池中的常量个数并非固定的,所以我们用u2(两个字节 1个字节8bit 两个字节=4个16进制位,此后统一用u*表示)来记录我们的常量池个数,图1中 00 2E 表示该类共48个常量。
   注:由于u2最大值为65535,所以我们在Java程序中最多定义65535个常量。这个数在之后也会反复出现,因为诸如表示方法个数,接口个数等的占位大小也是u2.

   常量池的项目类型表如下.之后我们读常量池的时候会根据每个常量的tag值来查表判断是何种类型.

类型标志描述
CONSTANT_utf8_info1UTF-8编码的字符串
CONSTANT_Integer_info3整形字面量
CONSTANT_Float_info4浮点型字面量
CONSTANT_Long_info5长整型字面量
CONSTANT_Double_info6双精度浮点型字面量
CONSTANT_Class_info7类或接口的符号引用
CONSTANT_String_info8字符串类型字面量
CONSTANT_Fieldref_info9字段的符号引用
CONSTANT_Methodref_info10类中方法的符号引用
CONSTANT_InterfaceMethodref_info11接口中方法的符号引用
CONSTANT_NameAndType_info12字段或方法的符号引用
CONSTANT_MethodHandle_info15表示方法句柄
CONSTANT_MothodType_info16标志方法类型
CONSTANT_Dynamic_info17表示一个动态计算常量
CONSTANT_InvokeDynamic_info18表示一个动态方法调用点

  每个数据项类型都是一种数据结构,他们有着自己的结构体,拿CONSTANT_Utf8_info常量类型为例.

//CONSTANT_Utf8_info 类型的数据结构
class CONSTANT_Utf8_info{
    byte[] tag=new byte[1];  //u1 类型标识符,值固定为1
    byte[] length=new byte[2];  //u2 UTF-8编码的字符串占用的字节数
    byte[] bytes=new byte[1];  //u1 表示长度为length的UTF-8编码的字符串
}

常量池数据类型的结构总表如下,里面记录了每种类型的tag值,以及其对应的数据结构。
在这里插入图片描述

  此外常量池中常量可分作两大类:字面量(Literal)和符号引用(Symbolic References).字面量比较接近于Java语言层面的常量概念,如文本字符串,被声明为final的常量值等,而符号引用内部则是指向某些字面量型结构体的索引值.
  (因为涉及的表格以及图片太多了,摸鱼的博客选择采用他人的表格以及图片,引用文章的链接放在开头,侵删)
在这里插入图片描述

1.4 访问标记符

   常量池结束后,紧接着的2个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息.包括:这个Class是类还是接口,是否定义为public类,是否定义为abstract类型等等,同时**标志值是通过位运算得出的.**总共2字节16位,其中每位的取值如下所示。
在这里插入图片描述

标志名称标志值含义
ACC_PUBLIC0x0001是否为Public类型
ACC_FINAL0x0010是否被声明为final,只有类可以设置
ACC_SUPER0x0020是否允许使用invokespecial字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认
ACC_INTERFACE0x0200标志这是一个接口
ACC_ABSTRACT0x0400是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假
ACC_SYNTHETIC0x1000标志这个类并非由用户代码产生
ACC_ANNOTATION0x2000标志这是一个注解
ACC_ENUMx4000标志这是一个枚举

   比如ox0021=ox0020与ox0001进行位运算说的,查找下图可知,该类为public类型,同时该类允许使用invokespecial字节码指令的新语义.
   如果还是看不懂的朋友,建议去阅读一下这篇博客 《Java虚拟机原理图解》1.3、class文件中的访问标志、类索引、父类索引、接口索引集合(开头有博客链接) 此处不加赘述。
在这里插入图片描述

1.5 类索引、父类索引与接口索引集合

   类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件通过这三项数据来确定给类型的继承关系。类索引和父类索引指向的是常量池中的某个编号常量,通常指向的常量是UTF-8编码的字符串,用来描述全限定名,父类索引只有一个,所以Java语言类不支持多重继承,类的父类有且只有一个。同时,因为任何类都有父类,如果没有显示给出,编译器会默认继承Object作为该类的父类,所以所有Java类的父类索引都不为0
   接口索引集合就是用来描述这个类实现(implements)哪些接口的,如果这个Class文件表示的就是一个接口,则表示该接口可以继承(extends)的接口(类不能多重继承,接口可以)。按实现/继承的接口顺序从左到右排列在接口索引集合中。

1.6 字段表集合

1.6.1 字段表结构

   字段表(field_info)用于描述接口或者类中的声明的变量,Java语言中的字段(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。字段表结构如下,其中 name_index、descriptor_index 是对常量池项的引用。
                     字段表结构

类型名称数量
u2access_flags (权限修饰符)1
u2name_index (字段名称索引)1
u2descriptor_index (字段描述索引)1
u2attributes_count (属性表个数)1
attribute_infoattributes (属性表)attributes_count

1.6.2 字段访问标志

  字段可以包括的修饰符如下。修饰符有字段的作用域,是实例变量还是类变量(static),可变性(final),并发可见性(volatile,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。
                字段访问标志

标志名称标志值含义
ACC_PUBLIC0x0001字段是否为public
ACC_PRIVATE0x0002字段是否为private
ACC_PROTECTED0x0004字段是否为protected
ACC_STATIC0x0008字段是否为static
ACC_FINAL0x0010字段是否为final
ACC_VOLATILE0x0040字段是否为volatile
ACC_TRANSTENT0x0080字段是否为transient
ACC_SYNCHETIC0x1000字段是否为由编译器自动产生
ACC_ENUM0x4000字段是否为enum

  除了字段访问标志之外,还有修饰字段数据类型的描述符,比如L加对象全限定名描述对象类型,[[ 描述二维数组等。这里不展开赘述。而之后的属性表则用来存储一些额外的信息,字段表可以在属性表中附加描述0到多项的额外信息。

1.6.3 注意事项

  • 字段表集合中不会列出从父类或者父接口中继承而来的字段。
  • 内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
  • 在Java语言中字段是无法重载的,两个字段的数据类型,修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的

1.7 方法表集合

1.7.1 方法表结构

  Class文件存储格式中对方法的描述与对字段的描述采用了几乎完全一致的方式。并且这些数据项目的含义也与字段表极其相似,仅在访问标志及属性表集合的可选项上有所区别。
  读到这你可能会疑问,在下面这张结构表中怎么没有方法代码这一项,我写的Java源代码到底保存到哪了? 方法中的Java代码。经过Javac编译器编译成字节码指令之后,存放在方法属性表集合中一个名为Code的属性里面,之后我们会结合实例再做分析。

类型名称含义数量
u2access_flags访问标志1
u2name_index方法名索引1
u2descriptor_index描述符索引1
u2attributes_count属性计数器1
attribute_infoattributes属性集合attributes_count

1.7.2 注意事项

  • 如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现父类的方法。
  • 编译器可能会自动添加方法,最常见的便是类构造方法(类构造器)方法和(实例构造器)方法。
  • 在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名之中,因此Java语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在Class文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个class文件中。

1.8 属性表集合

1.8.1 属性表介绍

   属性表在之前已经提及很多次了,其实Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。属性表要求更加宽松,允许只要不与已有属性名重复即可。
                     虚拟机规范预定义的属性

属性名称使用位置含义
Code方法表Java代码编译成的字节码指令
ConstantValue字段表final关键字定义的常量池
Deprecated类,方法,字段表被声明为deprecated的方法和字段
Exceptions方法表方法抛出的异常
EnclosingMethod类文件仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法
InnerClass类文件内部类列表
LineNumberTableCode属性Java源码的行号与字节码指令的对应关系
LocalVariableTableCode属性方法的局部变量描述
StackMapTableCode属性JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配
Signature类,方法表,字段表用于支持泛型情况下的方法签名
SourceFile类文件记录源文件名称
SourceDebugExtension类文件用于存储额外的调试信息
Synthetic类,方法表,字段表标志方法或字段为编译器自动生成的
LocalVariableTypeTable使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
RuntimeVisibleAnnotations类,方法表,字段表为动态注解提供支持
RuntimeInvisibleAnnotations表,方法表,字段表用于指明哪些注解是运行时不可见的
RuntimeVisibleParameterAnnotation方法表作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法
RuntimeInvisibleParameterAnnotation方法表作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数

  

1.8.2 属性表结构

   对于每一个属性,它的名称都需从常量池中引用一个CONSTANT_Utf8_info类型的常量来进行表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性说明属性值所占用的位数即可。一个符合规则的属性表应该满足如下定义的结构。

类型名称数量含义
u2attribute_name_index1属性名索引
u2attribute_length1属性长度
u1infoattribute_length属性表

1.8.3 部分属性解读

1.8.3.1 Code属性

   Code属性是Class文件中最重要的属性,如果把Java程序信息分为代码(Java代码)和元数据(包括类、字段、方法及其他信息),那么Code属性就是用来描述代码的,而其他所有数据项目都是在描述元数据。

Code属性结构如下

类型名称数量含义
u2attribute_name_index1属性名索引
u4attribute_length1属性长度
u2max_stack1操作数栈深度的最大值
u2max_locals1局部变量表所需的存续空间
u4code_length1字节码指令的长度
u1codecode_length存储字节码指令
u2exception_table_length1异常表长度
exception_infoexception_tableexception_length异常表
u2attributes_count1属性集合计数器
attribute_infoattributesattributes_count属性集合

部分类型说明:
attribute_name_index:一项指向CONSTANT_Utf8_info型常量的索引,该常量固定为"Code",代表了该属性的名称。
max_stack:操作数栈深度最大值,在方法执行的任意时刻,操作数栈都不会超过这个深度。
max_locals: 局部变量表所需存储空间,单位为变量槽(Slot),Slot是JVM为局部变量分配内存的最小单位。
code_length:虽然是u4,但由于JVM规定方法不允许超过65535条字节码指令,所以只用了u2.

1.8.3.2 Exceptions属性

   Exceptions属性属于方法表,与Code属性平级,作用是列举出方法中可能抛出的受查异常,也就是方法描述时throws关键词之后的异常。结构如下。这和Code属性中的Exception table不一样,Code异常表是用来处理异常,实现finally处理机制的。

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2number_of_exceptions1
u2exception_index_tablenumber_of_exception
1.8.3.3 LineNumberTable属性

  LineNumberTable属性是用来描述Java源码行号与字节码行号之间的对应关系。通过两者的对应关系,当发生异常时,JVM可以准确的定义到是源码的哪行出现的错误。

类型名称数量含义
u2attribute_name_index1属性名索引
u4attribute_length1属性长度
u2line_number_table_length1行号表长度
line_number_infoline_number_tableline_number_table_length行号表
1.8.3.4 LocalVariableTable及LocalVariableTypeTable属性
1.8.3.5 SourceFile属性

  attribute_name_index属性名索引指向常量池中的”SourceFile“字符串常量,sourcefile_index数据项指向常量池中CONSTANT_Utf8_info型常量索引,常量值是源码文件的文件名,通常类名和文件名是一致的,但也有一些特殊情况,比如内部类(见1.1.1)。

类型名称数量含义
u2attribute_name_index1属性名索引
u4attribute_length1属性长度
u2sourcefile_index1源码文件索引

写到这里,博主已经顶不住了。相信看到这里的你也一样
在这里插入图片描述

二、示例:字节码文件阅读

2.1 源文件

2.1.1 文件代码

  本次示例的Java源码如下。

public class ByteCodeTest {
    private String userName;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }
}

2.1.2 字节码

  通常我们可以直接运行,然后在out文件夹中找到我们的class字节码(当然这个示例程序不行,因为没写main方法),也可以跳到文件夹,打开终端输入javac指令完成编译。

javac -encoding utf-8 ByteCodeTest.java

  然后我们将class文件拖入Binary Viewer中。它可以帮助我们读取二进制流文件,以16进制进行表示。
在这里插入图片描述
  现在我们可以看到class文件转成16进制之后的样子。
在这里插入图片描述

2.1.3 反编译

  把我们编译好的Class文件复制到out文件夹下,转到终端(这里博主使用的IDE是IDEA),利用javap指令进行反编译。
在这里插入图片描述
  反编译结果如下。我们可以很清楚的看到我们整个class文件的结构,比如主次版本号,常量池,方法表,同时方法表中还有一些我们比较熟悉的属性表,比如Code属性,再比如 LineNumberTable,SourceFile,因为该程序没有接口所以这里也就没有接口表。

Classfile /C:/Users/kouti/IdeaProjects/JVMDemo1/out/production/JVMDemo1/ByteCodeTest.class
  Last modified 2021-7-6; size 414 bytes
  MD5 checksum 3edd89f8dcb8cec0096512a634057689
  Compiled from "ByteCodeTest.java"
public class ByteCodeTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#17         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#18         // ByteCodeTest.userName:Ljava/lang/String;
   #3 = Class              #19            // ByteCodeTest
   #4 = Class              #20            // java/lang/Object
   #5 = Utf8               userName
   #6 = Utf8               Ljava/lang/String;
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               getUserName
  #12 = Utf8               ()Ljava/lang/String;
  #13 = Utf8               setUserName
  #14 = Utf8               (Ljava/lang/String;)V
  #15 = Utf8               SourceFile
  #16 = Utf8               ByteCodeTest.java
  #17 = NameAndType        #7:#8          // "<init>":()V
  #18 = NameAndType        #5:#6          // userName:Ljava/lang/String;
  #19 = Utf8               ByteCodeTest
  #20 = Utf8               java/lang/Object
{
  public ByteCodeTest();
    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 1: 0

  public java.lang.String getUserName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field userName:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 5: 0

  public void setUserName(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #2                  // Field userName:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 9: 0
        line 10: 5
}
SourceFile: "ByteCodeTest.java"

  还不够?那我们还可以安装jclasslib插件,用插件来查看反编译结果,这样结构会更加清晰。
在这里插入图片描述

2.2 魔数及版本号

  先看开头的几个,依次读取我们的编码。

{
  "magic_num": "占位:u4 编码:(CA FE BA BE) -->描述:魔数",
  "minor_version": "占位:u2 编码:(00 00) -->描述:次版本号",
  "major_version": "占位:u2 编码:(00 34) -->描述:主版本号 这里数值为52,对应JDK版本为1.8"
}

2.3 常量池

  这里只拿前几号常量进行举例分析。因为常量池中的常量数据类型频繁涉及到常量池内部其他常量的索引调用,所以我们把刚才反编译出的常量池表也复制一份在下面。

   紧接着常量池个数后的就是我们的第一个常量,“Methodref_info”: "占位:u5 编码:(0A 00 04 00 11) ,其中0A是tag,值为10对应常量池表中的Methodref_info,tag用于描述该常量是何种常量,只有知道了常量类型我们才知道这个常量到底有多长;00 04是指向声明方法的类的描述符的,这里转换成十进制是4,也就是指向常量池中4号索引,对应我们的#4 class;

   之后是第二个常量Fieldref(09 00 03 00 12) ,他也有两个索引,分别是#3#18,查看1.3中的数据表,得知#3为指向声明字段的类或接口描述符这里指向的是一个class,而这个class常量又指向#19字符串- ByteCodeTest,这个字符串就该字段的所属类,但我们现在还不知道这个字段到底是什么,我们就需要查看第二个索引#18,这个索引是指向常量池中的NameAndType类型常量的,我们找到#18 NameAndType,查表后发现它也有两个索引,#5,#6分别表示字段/方法名称,字段/方法描述符,找到#5:userName,#6:java/lang/String,这下JVM就知道了:1.这个字段属于ByteCodeTest类;2.这个字段的名称是username;3.这个字段的描述符是String,这是一个String类型的字段。


{
  "constant_pool_count": "占位:u2 编码:(00 15) -->描述:共21个常量,减去被JVM占用的,实际为20个",
  "Methodref_info": "占位:u5 编码:(0A 00 04 00 11) -->描述: #4.#17  // java/lang/Object.\"<init>\":()V\n",
  "Fieldref_info": "占位:u5 编码:(09 00 03 00 12) -->描述:#3.#18 // ByteCodeTest.userName:Ljava/lang/String;\n",
  
  "Class_info": "占位:u3 编码:(07 00 13) -->描述: #19 // ByteCodeTest",
  "Class_info": "占位:u3 编码:(07 00 14) -->描述: #19 // ByteCodeTest",
  "Utf8":占位:u11 编码(01  00 08   75 73 65 72 4E 61 6D 65):共8个UTF8编码组成的字符串 userName
  "Utf8":占位:u21 编码(01  00 12   4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B):18位字符串 Ljava/lang/String;
  "Utf8":占位:u9 编码(01  00 06   3C 69 6E 69 74 3E):6位字符串 <init>
  "Utf8":占位:u6 编码(01  00 03   28 29 56):3位字符串 ()V
  "Utf8":占位:u7 编码(01  00 04   43 6F 64 65):4位字符串 Code
  "Utf8":占位:u18 编码(01  00 0F   4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65):15位字符串  LineNumberTable
  "Utf8": 占位:u14 编码(01  00 0B   67 65 74 55 73 65 72 4E 61 6D 65):11位字符串 getUserName
  "Utf8": 占位:u23 编码(01  00 14   28 29 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B):20位字符串  ()Ljava/lang/String;
  "Utf8":占位:u14 编码(01  00 0B   73 65 74 55 73 65 72 4E 61 6D 65):11位字符串 setUserName
  "Utf8":占位:u24 编码(01  00 15   28 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56):21位字符串  (Ljava/lang/String;)V
  "Utf8":占位:u13 编码(01  00 0A   53 6F 75 72 63 65 46 69 6C 65):10位字符串 SourceFile
  "Utf8":占位:u20 编码(01  00 11   42 79 74 65 43 6F 64 65 54 65 73 74 2E 6A 61 76 61):17位字符串  ByteCodeTest.java
  "NameAndType":占位:u5 编码(0C   00 07   00 08)
  "NameAndType":占位:u5 编码(0C   00 05   00 06)
  "Utf8":占位:u15 编码(01  00 0C   42 79 74 65 43 6F 64 65 54 65 73 74):12位字符串 ByteCodeTest
  "Utf8":占位:u19 编码(01  00 10   6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74):16位字符串  java/lang/Object


  #1 = Methodref          #4.#17         // java/lang/Object."<init>":()V
  #2 = Fieldref           #3.#18         // ByteCodeTest.userName:Ljava/lang/String;
  #3 = Class              #19            // ByteCodeTest
  #4 = Class              #20            // java/lang/Object
  #5 = Utf8               userName
  #6 = Utf8               Ljava/lang/String;
  #7 = Utf8               <init>
  #8 = Utf8               ()V
  #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               getUserName
  #12 = Utf8               ()Ljava/lang/String;
  #13 = Utf8               setUserName
  #14 = Utf8               (Ljava/lang/String;)V
  #15 = Utf8               SourceFile
  #16 = Utf8               ByteCodeTest.java
  #17 = NameAndType        #7:#8          // "<init>":()V
  #18 = NameAndType        #5:#6          // userName:Ljava/lang/String;
  #19 = Utf8               ByteCodeTest
  #20 = Utf8               java/lang/Object
}

对其中几个名称索引进行说明:

  • 常量池中的UTF8字符串,诸如Code,LineNumberTable等都是属性名,为之后的属性引用预先定义好的。

JVM指令码与Java源码的对应关系:

  • 其中Code属性是拿来记录JVM指令的,LineNumberTable用将Code中JVM指令行数与Java源码中行数进行对应,这样JVM抛出异常的时候我们才知道是Java源程序中的哪行代码出错。

this关键字:

  • aload_0意味着将0号局部变量压入栈,但我们发现我们并没有在ByteCodeTest(),也就是构造器方法中放置任何局部变量,甚至我们都没有写这个构造器方法,而是编译器自动帮我们添加的,那么我们哪来的0号元素?其实这里的0号元素是我们的this关键字,java语言的一条潜规则就是:在任何实例方法里面,都可以通过this来访问此方法所属对象,它的实现便是由Javac编译器编译的时候将this关键字的访问转变成对一个普通方法参数的访问,所有实例方法的局部变量表中至少会有一个指向当前对象实例的局部变量。
{
public ByteCodeTest();
    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 1: 0

}

2.4 访问标志 、类索引、父类索引及接口表

{
  "access_flags":访问标志 占位:u2 编码:(00 21),0x0021是0020与0001位运算结果,1.4中有进行过分析,不再复述
  "this_class":类索引 占位:u2 编码:(00 03) 描述:-->#3Class -->#19 ByteCodeTest ,为类的全限定名
  "this_class":父类索引 占位:u2 编码:(00 04) 描述:-->#4Class -->#20 java/lang/Object,在java中没显示给出父类则默认继承Object
}

在这里插入图片描述
   再之后,紧接着的就是我们的接口个数,因为我们演示的程序并没有编写接口,所以此处为00 00,也就是没有接口。
   所以接口表为空,接下来的是字段个数以及我们的字段表。
   00 01:字段表个数,此时为1表示字段表中共一个字段。

在这里插入图片描述
   查询1.6.1的字段表结构,再依次分析上述编码含义。00 02:权限修饰符,private;00 05:常量池名称索引;#5 username ;00 06 常量池描述索引:#6 Ljava/lang/String String类型;00 00:属性表格个数 这里为0,意味着该字段属性表为空。

类型名称数量
u2access_flags (权限修饰符)1
u2name_index (字段名称索引)1
u2descriptor_index (字段描述索引)1
u2attributes_count (属性表个数)1
attribute_infoattributes (属性表)attributes_count

2.4 方法表

   简单读一个方法,字节码阅读就结束吧,这里我已经按照方法表结构以及占位大小为每个数据项做了分割,我们还是逐一来看一下各个红框的具体含义。

00 03:方法表个数,共3个。

00 01:access_flags 方法访问标志 因为方法表访问标志和字段表近似,我们可以查1.6.2的字段访问标志表 ,查询结果 0001表示该方法为public方法。

00 07:name_index 名称索引 对应常量池#7 此方法为编译器自动添加的方法,为我们的实例构造器,是实例化类时调用的方法,对非静态变量解析初始化,。

补充:因为此处只做了第一个方法的介绍,其实之后还有编译器自动添加的另一个方法 ,两者的区别在于是类在初始化时调用的方法,是class类构造器对静态变量,静态代码块进行初始化,子类的方法中会先对父类方法的调用,并且clinit优先于init。这个我们在之后的类加载机制中会提及。

00 08: descriptor_index 方法描述,参数:返回值 对应常量池中的#8 ()V 意味着空参数空返回值

00 09:attributes_count 属性表个数,共1个

类型名称含义数量
u2access_flags访问标志1
u2name_index方法名索引1
u2descriptor_index描述符索引1
u2attributes_count属性计数器1
attribute_infoattributes属性集合attributes_count

在这里插入图片描述
   再来进一步分析该方法所带的属性表中的唯一属性,

00 09:attribute_name_index 对应常量池中的索引#9 Code

00 00 00 1D:attribute_length 属性长度 29

00 01:max_stack :1

00 01:max_locals: 1 这里之所以是1,因为有this

00 00 00 05:code_length:code指令码长度为5

Code属性结构如下

类型名称数量含义
u2attribute_name_index1属性名索引
u4attribute_length1属性长度
u2max_stack1操作数栈深度的最大值
u2max_locals1局部变量表所需的存续空间
u4code_length1字节码指令的长度
u1codecode_length存储字节码指令
u2exception_table_length1异常表长度
exception_infoexception_tableexception_length异常表
u2attributes_count1属性集合计数器
attribute_infoattributesattributes_count属性集合

在这里插入图片描述
   然后我们来分析一下指令码2A B7 00 01 B1 的含义,以及找到它们的JVM助记符来理解JVM都执行了哪些指令:

{
Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0
}
{
0x2a	aload_0	将第一个引用类型本地变量
0xb7	invokespecial	调用超类构造方法,实例初始化方法,私有方法
0x00 01,指向常量池中的第1项
0xb1	return	从当前方法返回void
}

   后面的00 00是我们的exception_table_length因为这边没写try catch finally,所以长度为0,再之后的 00 01是我们Code属性的属性表个数(属性也是可以嵌套其他属性的),这里Code属性带的属性是LineNumberTable,刚才已经提过了就不再赘述了。

附录:

虚拟机规范预定义的属性

  由 波波烤鸭 整理,开头文章链接,推荐阅读。

属性名称使用位置含义
Code方法表中Java代码编译成的字节码指令(即:具体的方法逻辑字节码指令)
ConstantValue字段表中final关键字定义的常量值
Deprecated类中、方法表中、字段表中被声明为deprecated的方法和字段
Exceptions方法表中方法声明的异常
LocalVariableTableCode属性中方法的局部变量描述
LocalVariableTypeTable类中JDK1.5中新增的属性,它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
InnerClasses类中内部类列表
EnclosingMethod类中仅当一个类为局部类或者匿名类时,才能拥有这个属性,这个属性用于表示这个类所在的外围方法
LineNumberTableCode属性中Java源码的行号与字节码指令的对应关系
StackMapTableCode属性中JDK1.6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配
Signature类中、方法表中、字段表中JDK1.5新增的属性,这个属性用于支持泛型情况下的方法签名,在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。由于Java的泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息
SourceFile类中记录源文件名称
SourceDebugExtension类中JDK1.6中新增的属性,SourceDebugExtension用于存储额外的调试信息。如在进行JSP文件调试时,无法通过Java堆栈来定位到JSP文件的行号,JSR-45规范为这些非Java语言编写,却需要编译成字节码运行在Java虚拟机汇中的程序提供了一个进行调试的标准机制,使用SourceDebugExtension就可以存储这些调试信息。
Synthetic类中、方法表中、字段表中标识方法或字段为编译器自动产生的
RuntimeVisibleAnnotations类中、方法表中、字段表中JDK1.5中新增的属性,为动态注解提供支持。RuntimeVisibleAnnotations属性,用于指明哪些注解是运行时(实际上运行时就是进行反射调用)可见的。
RuntimeInvisibleAnnotations类中、方法表中、字段表中JDK1.5中新增的属性,作用与RuntimeVisibleAnnotations相反用于指明哪些注解是运行时不可见的。
RuntimeVisible ParameterAnnotations方法表中JDK1.5中新增的属性,作用与RuntimeVisibleAnnotations类似,只不过作用对象为方法的参数。
RuntimeInvisible ParameterAnnotations方法表中JDK1.5中新增的属性,作用与RuntimeInvisibleAnnotations类似,只不过作用对象为方法的参数。
AnnotationDefault方法表中JDK1.5中新增的属性,用于记录注解类元素的默认值
BootstrapMethods类中JDK1.7新增的属性,用于保存invokedynamic指令引用的引导方法限定符

Java虚拟机字节码指令表

   指令表由 四月葡萄 整理,完整指令表请查看原文,开头文章链接,推荐阅读。

字节码助记符指令含义
0x00nop什么都不做
0x01aconst_null将null推送至栈顶
0x02iconst_m1将int型-1推送至栈顶
0x03iconst_0将int型0推送至栈顶
0x04iconst_1将int型1推送至栈顶
0x05iconst_2将int型2推送至栈顶
0x06iconst_3将int型3推送至栈顶
0x07iconst_4将int型4推送至栈顶
0x08iconst_5将int型5推送至栈顶
0x09lconst_0将long型0推送至栈顶
0x0alconst_1将long型1推送至栈顶
0x0bfconst_0将float型0推送至栈顶
0x0cfconst_1将float型1推送至栈顶
0x0dfconst_2将float型2推送至栈顶
0x0edconst_0将do le型0推送至栈顶
0x0fdconst_1将do le型1推送至栈顶
0x10bipush将单字节的常量值(-128~127)推送至栈顶
0x11sipush将一个短整型常量值(-32768~32767)推送至栈顶
0x12ldc将int, float或String型常量值从常量池中推送至栈顶
0x13ldc_w将int, float或String型常量值从常量池中推送至栈顶(宽索引)
0x14ldc2_w将long或do le型常量值从常量池中推送至栈顶(宽索引)
0x15iload将指定的int型本地变量
0x16lload将指定的long型本地变量
0x17fload将指定的float型本地变量
0x18dload将指定的do le型本地变量
0x19aload将指定的引用类型本地变量
0x1aiload_0将第一个int型本地变量
0x1biload_1将第二个int型本地变量
0x1ciload_2将第三个int型本地变量
0x1diload_3将第四个int型本地变量
0x1elload_0将第一个long型本地变量
0x1flload_1将第二个long型本地变量
0x20lload_2将第三个long型本地变量
0x21lload_3将第四个long型本地变量
0x22fload_0将第一个float型本地变量
0x23fload_1将第二个float型本地变量
0x24fload_2将第三个float型本地变量
0x25fload_3将第四个float型本地变量
0x26dload_0将第一个do le型本地变量
0x27dload_1将第二个do le型本地变量
0x28dload_2将第三个do le型本地变量
0x29dload_3将第四个do le型本地变量
0x2aaload_0将第一个引用类型本地变量
0x2baload_1将第二个引用类型本地变量
0x2caload_2将第三个引用类型本地变量
0x2daload_3将第四个引用类型本地变量
0x2eiaload将int型数组指定索引的值推送至栈顶
0x2flaload将long型数组指定索引的值推送至栈顶
0x30faload将float型数组指定索引的值推送至栈顶
0x31daload将do le型数组指定索引的值推送至栈顶
0x32aaload将引用型数组指定索引的值推送至栈顶
0x33baload将boolean或byte型数组指定索引的值推送至栈顶
0x34caload将char型数组指定索引的值推送至栈顶
0x35saload将short型数组指定索引的值推送至栈顶
0x36istore将栈顶int型数值存入指定本地变量
0x37lstore将栈顶long型数值存入指定本地变量
0x38fstore将栈顶float型数值存入指定本地变量
0x39dstore将栈顶do le型数值存入指定本地变量
0x3aastore将栈顶引用型数值存入指定本地变量
0x3bistore_0将栈顶int型数值存入第一个本地变量
0x3cistore_1将栈顶int型数值存入第二个本地变量
0x3distore_2将栈顶int型数值存入第三个本地变量
0x3eistore_3将栈顶int型数值存入第四个本地变量
0x3flstore_0将栈顶long型数值存入第一个本地变量
0x40lstore_1将栈顶long型数值存入第二个本地变量
0x41lstore_2将栈顶long型数值存入第三个本地变量
0x42lstore_3将栈顶long型数值存入第四个本地变量
0x43fstore_0将栈顶float型数值存入第一个本地变量
0x44fstore_1将栈顶float型数值存入第二个本地变量
0x45fstore_2将栈顶float型数值存入第三个本地变量

总结

没有总结,写完了,溜了溜了。

等等!

不来个免费的收藏和点赞吗?(贴一个自己剪的表情包)
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值