JVM 第6章类文件结构

1 概述

代码编译的结果从本地机器码(Native Code)转变为字节码,字节码与操作系统和机器指令集无关。

2 无关性的基石

字节码是构成无关性的基石:

  • 平台无关性
    虚拟机载入和执行同一种平台无关性的字节码,从而实现程序的“一次编写,到处运行”。
  • 语言无关性
    其他语言运行也可以运行在JVM上(Scala,Jython等)。JVM不和任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联。

Class文件包含:

  • java虚拟机指令集
  • 符号表
  • 其他辅助信息

使用java编译器可以把java代码编译为存储字节码的Class文件,使用JRuby等其他语言的编译器可以把程序编译为Class文件,虚拟机执行Class文件,不关心Class文件是来源于哪种语言。

3 Class文件的结构

任何一个Class文件都对应着唯一一个类或接口的定义信息,但类或接口并不一定都得定义在文件里(类或接口也可通过类加载器直接生成)。类或接口应当满足“Class文件格式”,并不一定以磁盘文件的形式存在。
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目按照顺序紧凑地排列在Class文件中,中间没分隔符。

Class文件包含2中数据类型:

  • 无符号数
    无符号数属于基本的数据类型,以u1, u2, u4, u8分别代表1个字节、2个字节…的无符号数。无符号数用于描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。

  • 表是由多个无符号数或者其他表构成的复合数据类型。表用于描述有层次关系的复合结构的数据,整个Class文件本质是一张表。表习惯性地以“_info”结尾。

无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,使用一个前置的容量计数器+若干个连续的数据项的形式,这也称为某一类型的集合。

3.1 魔数与Class文件的版本

★魔数:Class文件的头4个字节。

  • 作用:唯一作用是:确定这个Class文件能否被虚拟机接受。
  • 为什么使用魔数而不是使用扩展名进行身份认证:基于安全性考虑。因为扩展名可以随便改动。魔数存到了Class文件中,而扩展名是我们可以看到的。

★次版本号、主版本号:均为2个字节。依次对应5、6字节,7、8字节。

  • 作用:二者联合起来,表示此Class文件能运行的最低JDK版本号。如第5,6字节为0x0000,7、8字节为0x0032。查找版本号对照表,发现十六进制版本号00000032对应的十进制版本号为50.0(从45开始),对应的JDK版本号为1.6。故这个Class文件可以被JDK1.6或以上版本的虚拟机执行。

3.2 常量池

  • 地位(4个):是Class文件的资源仓库;是Class文件结构中与其他项目关联最多的数据类型;是占用Class文件空间最大的数据项目之一;是Class文件中第一个出现的表类型数据项目。
  • 格式:u2类型的容量计数值+计数值个常量。
  • 存放内容的类型:字面量、符号引用2类。
    • 字面量:就是java语言层面的常量概念,比如文本字符串or声明为final的变量等。
    • 符号引用:有3类:类和接口的全限定名;字段的名称和描述符;方法的名称和描述符。
      • 类和接口的全限定名:类的绝对路径
      • 字段的名称和描述符:对于字段就是:没有类型的字段名;对于方法就是:没有返回类型、没有参数的方法名。
      • 方法的名称和描述符:对于字段:就是字段类型;对于方法就是:返回类型和参数列表(包括数量、类型、顺序)。
  • 常量池中内容的存在意义:java动态链接机制使得:在Class文件中不会保存各个方法、字段的最终内存布局信息,故无法直接被虚拟机使用。所有当虚拟机运行时,则需要从常量池中获得对应的符号引用,再在类创建活着运行时解析、翻译到具体的内存地址。
  • 常量存储结构:每一项常量都是一张表。共有14中常量类型,每种常量类型对应的表结构都不一样。但所有表唯一共同点就是:表第一位是一个u1类型的标志位(tag),表示了当前这个常量是哪种常量类型。
  • Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称。所以CONSTANT_Utf8_info型常量的最大长度也就是java中方法、字段名的最大长度。如果java程序中定义了超过64KB英文字符的变量或者方法名,将会无法编译。
  • 自动生成的常量:在进行javac编译后,查看字节码,会看到生成了一些在java源程序里面没有定义过的常量。这些常量因为其不方便使用“固定字节”进行表达,但可能会被方法表、字段表、属性表引用到。这些常量可能是:方法返回值,方法参数个数,参数类型等等。所以当你看到这些变量时,不要因为在源码中找不到而困惑就好。

3.3 访问标志

  • 长度:占2字节,共16个标志位可以使用,没有用到的标志位用0表示。
  • 作用:用于识别一些类或者接口层次的访问信息。包括:这个class是类还是接口;十分定义为public;是否定义为abstract类型;如果是类,是否被定义为final类型等等。

3.4 类索引、父类索引、接口索引集合

  • 长度:2字节,2字节,2字节+
  • 功能:Class文件用这3项来确定这个类的继承关系。
  • 类索引:用于确定这个类的全限定名
  • 父类索引:用于确定这个类的父类的全限定名。
  • 接口索引集合:用于描述这个类实现了哪些类。排列顺序为:代码中implements后面的接口顺序。
  • 接口索引集合结构:2字节接口计数器+接口索引表。

3.5 字段表集合

  • 功能:用于表述接口或类中声明的变量。

  • 字段:包括类级变量、实例级变量,不包括局部变量。
    包含的信息有:字段的作用域、是实例变量还是类变量(static)、可变性(final)、并发可见性(volatile,是否强制从主内存读写)、可否被序列化(transient)、字段数据类型(基本类型、对象、数组)、字段名称。各个修饰符可用标志位表示,字段名字和数据类型是无法固定的,只能引用常量池中的常量来描述。

    • 字段的简单名称
    • 字段和方法的描述符

    注意:
    全限定名: 包名+类名(org/fenixsoft/clazz/TestClass),多个全限定名之间用;隔开
    简单名称 指没有类型和参数修饰的方法或者字段名称,如inc()方法和m字段的简单名称为“inc”和“m”。
    描述符: 用于描述字段的数据类型、方法的参数列表(数量、类型以及顺序)和返回值。基本数据类型(byte, char, double, float, int, long, short, boolean)和无返回值的void类型都用一个大写字符表示,而对象类型用字符L加对象的全限定名表示。对于数组类型:java.lang.String[ ][ ]:[[Ljava/lang/String;
    描述符描述方法时,先参数列表,后返回值的顺序描述:
    void inc():()Vl
    java.lang.String.toString():()Ljava.lang.String;
    int indexOf(char[ ] source):([C)I;

  • 格式:2字节容量计数器+各个字段

  • 是否有继承的字段:不会列出从超类或者父接口中继承而来的字段。但有可能列出java代码中不存在的字段。譬如内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。java中字段无法重载,在字节码中,2个字段的描述符不一样,那字段重名就是合法的。

3.6 方法表集合

  • 存储格式和字段表基本一样。方法里的java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为Code的属性里面。
  • 是否有父类方法描述:如果在子类中没有复写父类方法,则不会出现来自父类方法的信息。同样,可能会出现由编译器自动添加的方法。最典型的被添加的方法是:类构造器< clinit>方法;实例构造器< init>方法。
  • java中重载一个方法,除了要与原方法具有相同的简单名称之外,还必须有与原方法不同的特征签名(就是方法中各个参数在常量池中的字段符号引用的集合,返回值不包含在其中)。但class中,特征签名只要描述不是完全一致的2个方法可以共存(2个方法有相同的名称和特征签名,但返回值不同,那么这2个方法可以共存)。

3.7 属性表集合

  • 存在位置:字段表、方法表、Class文件中。
  • 功能:用于描述某些场景出现的专有的信息。
  • 属性表限制宽松:各个属性表顺序不要求严格有序;
  • 属性表中关键常用11个属性:

★ Code属性

  • 使用位置:方法表
  • 功能:java方法体内代码,在javac编译后,最终形成的字节码指令存储在Code属性内。
  • 地位:是Class文件中最重要的一个属性。如果把java程序中信息分为代码(java方法体内的代码)和元数据(包括类、字段、方法定义,其他信息)。则整个Class文件中,Code属性用于描述代码,所有其他数据项都用于描述元数据。
  • 异常表:是方法的显示异常处理表集合,对于Code属性来说不是必须存在的。它的实质是:java代码的一部分,编译器使用异常表而不是跳转指令来实现java异常及finally处理机制。

★Exceptions属性

  • 使用位置:方法表
  • 作用:列举出方法中可能抛出的受查异常。也就是throws后面列举的异常。
    注意:和Code属性中的异常表完全不是一回事。存在位置和功能都是不一样的。

★LineNumberTable属性

  • 使用位置:Code属性
  • 作用:用于描述java源码和字节码行号之间的对应关系。
  • 地位:非运行时必须的属性。但是会默认生成到Class文件中。

★ LocalVariableTable属性

  • 使用位置:Code属性
  • 作用:用于描述栈帧中,局部变量表中的变量与java源码中定义的变量的关系。
  • 地位:不是运行时必须的属性。但默认会生成到Class文件中。

★SourceFile属性

  • 使用位置:类文件
  • 作用:记录生成这个Class文件的源码文件名称。
  • 地位:非必需存在。

★ConstantValue属性

  • 使用位置:字段表
  • 作用:通知虚拟机自动为静态变量赋值。只有类变量可以使用此属性。
  • 初始化时间:实例变量:在实例构造器方法中进行;
    类变量:有2种方式:变量被final和static修饰,则生成ConstantValue属性进行初始化;如果没有被final修饰,或者并非基本类型及字符串,则会在方法中进行初始化。

★InnerClasses属性

  • 使用位置:类文件
  • 作用: 用于记录内部类和宿主类之间的关联。

★Deprecated及Synthetic属性

  • 使用位置:类、方法表、字段表
  • 作用:这两个属性都属于标志类型的布尔属性,只存在有没有的区别,没有属性值的概念。
  • @Deprecated注解表示:该类、字段、方法已经被程序作者定为不再推荐使用
  • Synthetic属性代表此字段、方法并不是由java源码直接产生的,而是由编译器自行添加的。

★StackMapTable属性

  • 使用位置:Code属性表的属性表中
  • 作用:这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器使用。目的是:替代以前比较消耗性能的基于数据流分析的类型推导验证器。
  • 加入JDK时间:1.6开始
  • 工作原理:在编译阶段将一系列的验证类型直接记录在Class文件中,通过检查这些验证类型替代了类型推导过程,从而大幅提升了字节码验证功能。而曾经原始的字节码验证过程是:在运行期通过数据流分析去确认字节码的行为逻辑合法性。

★Signature属性

  • 使用位置:类、方法表、字段表
  • 作用:记录“类、接口、方法、成员的泛型签名”的泛型签名信息。为什么要用属性记录泛型类型,因为java语言的泛型采用的是擦除法实现的伪泛型。Java 反射API能够获取泛型类型,最终的数据来源就是这个属性。
  • 地位:非必需存在

★BootstrapMethods属性

  • 使用位置:类文件的属性表中
  • 使用时间:JDK1.7开始
  • 作用:用于保存invokedynamic指令引用的引导方法限定符。

4 字节码指令

pc机有自己的机器指令集,虚拟机有自己的字节码指令集。

  1. 长度:1字节(指令集操作码不超过256条)
  2. 格式:操作码+操作数(参数)
  3. 指令特点:大多数指令只有操作码,没有操作数。因为java虚拟机采用面向操作数栈而不是寄存器的架构。
  4. 大多数指令都包含了其操作对应的数据类型信息。并非每种数据类型都有一种对应的指令。大多数对于boolean,byte,short,char类型的数据操作,实际上都是使用相应的int类型作为运算类型。
  5. 9类字节码的指令用法
  • 加载和存储指令
    功能:将数据在栈帧中的局部变量表和操作数栈直接来回传输。

    • 将一个局部变量加载到操作栈 iload,aload
    • 将数值从操作数栈存储到局部变量 istore,astore
    • 将常量加载到操作数栈
    • 扩充局部变量表的访问索引的指令
  • 运算指令
    功能:对于两个操作数栈上的值进行某种特定运算,并把结果重新存到操作数栈顶。

    • 整型数据的运算 iadd
    • 浮点型数据的运算
      注:除数为0出现异常;溢出不抛异常,使用有符号的无穷大表示
  • 类型转换指令
    功能:将两种不同的数值类型相互转换。这种操作一般用于用户代码中的显式转换,or 用来处理字节码指令集中数据类型相关指令无法和数据类型一一对应的问题。
    注:宽化使用隐式指令;窄化必须使用显示指令;窄化可能会上限溢出、下线溢出和精度丢失,但不会抛出运行时异常。
    窄化规则:(浮点数窄化为T类型的整数)

    1. 如果浮点值是NaN,转换结果是int或long的0
    2. 如果浮点向零舍入取整后为v,且v在目标类型范围内,那结果就是v
    3. 否则将根据v的符号,转换成T所能表示的最大或最小正数
  • 对象创建与访问指令
    这里是比较重要的。因为java虚拟机中对象创建和数组创建是不同的字节码指令。

  • 操作数栈管理指令
    功能:直接操作操作数栈上面的数

  • 控制转移指令
    功能:可以让java虚拟机有条件or无条件从指定的位置指令继续程序程序,而无需控制转移指令。实质就是:有条件or无条件的修改PC寄存器的值。

  • 方法调用和返回指令

    ★方法调用
    前4条固话在java虚拟机内部,最后条是由用户设定的引导方法决定。

    • invokevirtual:调用对象的实例方法
    • invokeinterface:调用接口方法,会在运行时搜索实现零这个接口方法的对象
    • invokespecial:调用特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法
    • invokestatic:用于调用类方法(static方法)
    • invokedynamic:运行时动态解析出调用点限定符所引用的方法

    ★返回值指令
    根据返回值类型区分(ireturn, areturn)和为void方法、实例初始化方法以及类和接口的类初始化方法使用的return指令。

  • 异常处理指令
    注意区分:java程序中,显示抛出异常throw,最终由athrow指令完成;除了显示异常外,运行时异常会在虚拟机指令检测到异常时自动抛出。
    而在java虚拟机中,处理异常(catch)不是由字节码指令来实现的,而是用code属性中的异常表来完成的。

  • 同步指令(2类,都是用管程monitor来支持)
    包括:方法级同步、方法内部一段指令序列的同步

    • 方法级同步:隐式的,无需字节码控制,实现在方法调用和返回操作中。方法常量池的方法表结构中的acc_synchronized标志一个方法是否为同步方法。若是,执行线程就要求持有管程,然后才能执行方法,最后方法完成时(成功结束或异常退出)释放管程。
    • 方法内部一段指令序列的同步:通常有java语言中的synchronized语句块(用monitorenter和monitorexit指令来支持)来表示。 方法中每执行一条monitorenter指令,都必须执行对应的monitorexit指令(不管正常结束,还是异常结束)。

5 虚拟机的实现

2中方式:

  • 将输入的java虚拟机代码在加载或执行时翻译成另一种虚拟机的指令集
  • 将输入的java虚拟机代码在加载或执行时翻译成宿主机CPU的本地指令集(即JIT代码生成技术)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值