【修炼内功】[JVM] 类文件结构

本文已收录【修炼内功】跃迁之路

类文件结构

学习C语言的时候,需要在不同的目标操作系统上(或者使用交叉编译环境),(使用正确的CPU指令集)编译成对应操作系统可运行的执行文件,才可以在相应的系统上运行,如果使用操作系统差异性的库或者接口,还需要针对不同的系统做不同的处理(宏)

Java的出现也正是为了解决"平台无关性","Write Once, Run Anywhere"的口号也充分表达了软件开发人员对冲破平台接线的渴求

"与平台无关"的最终实现还是要在操作系统的应用层上,这便是JVM的存在,不同的平台有不同的JVM,而所有的JVM都可以载入与平台无关的字节码,从而实现程序的"一次编写,到处运行"

JVM并非只为Java设计,而字节码也并非只有Java才可以编译得到,早在Java发展之初,设计者便将Java规范拆分为Java语言规范Java虚拟机规范,同时也承诺,对JVM做适当的扩展,以便更好地支持其他语言运行于JVM之上,而这一切的基础便是Class文件(字节码文件),Class文件中存放了JVM可以理解运行的字节码命令

In the future, we will consider bounded extensions to th Java virtual machine to provide better support for other languages

JVM并不关心Class的来源是何种语言,在JVM发展到1.7~1.8的时候,设计者通过JSR-292基本兑现了以上承诺

class_compile

本篇不会去详细地介绍如何去解析Class文件,目的是为了了解Class文件的结构,Class文件中都包含哪些内容

Class文件可以由JVM加载并执行,其中记录了类信息、变量信息、方法信息、字节码指令等等,虽然JVM加载Class之后(在JIT之前)进行的是解释执行,但Class文件并不是文本文件,而是被严格定义的二进制流文件

接下来,均会以这段代码为示例进行分析

import java.io.Serializable;

public class ClassStruct implements Serializable {
    private static final String HELLO = "hello";
    private String name;

    public ClassStruct(String name) {
        this.name = name;
    }

    public void print() {
        System.out.println(HELLO + ": " + name);
    }

    public static void main(String[] args) {
        ClassStruct classStruct = new ClassStruct("ManerFan");
        classStruct.print();
    }
}

使用$ javac ClassStruct.java进行编译,编译后的文件可以使用$ javap -p -v ClassStruct查看Class文件的内容(见文章末尾)

魔数

很多文件存储都会使用魔数来进行身份识别,比如图片文件,即使将图片文件改为不正确的后缀,绝大多数图片预览器也会正确解析

同样Class文件也不例外,使用二进制模式打开Class文件,会发现所有Class文件的前四个字节均为OxCAFEBABE,这个魔术在Java还被称为"Oak"语言的时候就已经确定下来了

magic_number

版本号

紧接着魔术的四个字节(接下来不再对照二进制进行查看,而是直接查看javap帮我们解析出来的结果,见文章末尾)存储的是Class文件的版本号,前两个字节为次版本号(minor version),后两个字节为主版本号(major version)

class_1

Java版本号从45开始,高版本的JDK可以向下兼容低版本的Class文件,但无法向上兼容高版本,即使文件格式并未发生变化

常量池

紧接着主次版本号之后的是常量池入口

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)

字面量:如文本字符串、被声明为final的常量值等
符号引用:类和接口的全限定名(Fully Qualified Name)、字段的名称和描述(Descriptor)、方法的名称和描述符

class_2

Java代码在进行编译时,并不像C或C++那样有"连接"这一步骤,而是在虚拟机加载Class文件时进行动态连接,Class文件中不会保存各方法和字段的内存布局,在虚拟机运行时,需要从常量池中获得对应的符号引用,再在类创建或运行时解析并翻译到具体的内存地址中,才能被虚拟机使用

访问标志

访问标志用于识别一些类或接口层次的访问信息

class_3

访问标志用于识别这个Class是类还是接口;是否定义为public;是否为abstract类型;是否声明为final;等等,具体标志含义如下

标志 名称
ACC_PUBLIC 是否为public类型
ACC_FINAL 是否被声明为final
ACC_SUPER 是否允许使用invokespecial字节码指令
ACC_INTERFACE 是否为接口
ACC_ABSTRACT 是否为abstract
ACC_SYNTHETIC 标识这个类并非由用户代码生成
ACC_ANNOTATION 标识这是一个注解
ACC_ENUM 标识这是一个枚举

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

Class文件中由类索引(this_class)、父类索引(super_class)及接口索引集合(interfaces)三项数据确定这个类的继承关系

父类索引只有一个(对应extends语句),而接口索引则是一个集合(对应implements语句)

class_4

字段表集合

字段表(field_info)用于描述类或者接口中声明的变量

字段(field)包括了类级变量(如static)及实例级变量,但不包括在方法内部声明的变量

字段包含的信息有:作用域(public、private、protected)、类级还是实例级(static)、可变性(final)、并发可见性(volatile)、可否序列化(transient)、数据类型、字段名等

class_5

描述符

这里简单解释一下描述符(descriptor)

描述符用来描述字段数据类型、方法参数列表和返回值,根据描述符规则,基本数据类型及代表无返回值的void类型都用一个大写字符表示,对象类型则用字符L加对象全限定名来表示

标识字符 含义
B byte
C char
D double
F float
I int
J long
S short
Z boolean
V void
L 对象类型,如 Ljava/lang/Object;

对于数组,每一个维度使用一个前置的[来描述,如java.lang.String[][]将被记录为[[java/lang/String;int[]将被记录为[I

描述方法时,按照先参数列表,后返回值的顺序描述,参数列表放在()内,如void inc()描述符为()V,方法java.lang.String toString()描述符为()Ljava/lang/String;,方法int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex的描述符为([CII[CIII)I

方法表集合

Class文件中对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes),但是方法内部的代码并不在方法表中,而是经过编译器编译成字节码指令后,存放在属性表集合中一个名为"Code"的属性中

class_6

属性表集合

在Class文件、字段表、方法表中都可以携带自己的属性表集合,用于描述某些场景专有的信息,Java虚拟机规范中预定义了9种虚拟机实现应当能识别的属性

属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 final关键自定义的常量值
Deprecated 类、方法表、字段表 被声明为deprecated的方法和字段
Exceptions 方法表 方法抛出的异常
InnerClasses 类文件 内部类列表
LineNumberTable Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
SourceFile 类文件 原文件名称
Synthetic 类、方法表、字段表 标识方法或字段为编译器自动生成的

关于属性表,会在之后的文章中穿插介绍

附:Class文件

Classfile ~/articles/【修炼内功】跃迁之路/JVM/[JVM] 类文件结构/src/ClassStruct.class
  Last modified 2019-6-2; size 829 bytes
  MD5 checksum 9f7454acd0455837a33ff8e03edffdb3
  Compiled from "ClassStruct.java"
public class ClassStruct implements java.io.Serializable
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #14.#31        // java/lang/Object."<init>":()V
   #2 = Fieldref           #6.#32         // ClassStruct.name:Ljava/lang/String;
   #3 = Fieldref           #33.#34        // java/lang/System.out:Ljava/io/PrintStream;
   #4 = Class              #35            // java/lang/StringBuilder
   #5 = Methodref          #4.#31         // java/lang/StringBuilder."<init>":()V
   #6 = Class              #36            // ClassStruct
   #7 = String             #37            // hello:
   #8 = Methodref          #4.#38         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #9 = Methodref          #4.#39         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #10 = Methodref          #40.#41        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #11 = String             #42            // ManerFan
  #12 = Methodref          #6.#43         // ClassStruct."<init>":(Ljava/lang/String;)V
  #13 = Methodref          #6.#44         // ClassStruct.print:()V
  #14 = Class              #45            // java/lang/Object
  #15 = Class              #46            // java/io/Serializable
  #16 = Utf8               HELLO
  #17 = Utf8               Ljava/lang/String;
  #18 = Utf8               ConstantValue
  #19 = String             #47            // hello
  #20 = Utf8               name
  #21 = Utf8               <init>
  #22 = Utf8               (Ljava/lang/String;)V
  #23 = Utf8               Code
  #24 = Utf8               LineNumberTable
  #25 = Utf8               print
  #26 = Utf8               ()V
  #27 = Utf8               main
  #28 = Utf8               ([Ljava/lang/String;)V
  #29 = Utf8               SourceFile
  #30 = Utf8               ClassStruct.java
  #31 = NameAndType        #21:#26        // "<init>":()V
  #32 = NameAndType        #20:#17        // name:Ljava/lang/String;
  #33 = Class              #48            // java/lang/System
  #34 = NameAndType        #49:#50        // out:Ljava/io/PrintStream;
  #35 = Utf8               java/lang/StringBuilder
  #36 = Utf8               ClassStruct
  #37 = Utf8               hello:
  #38 = NameAndType        #51:#52        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #39 = NameAndType        #53:#54        // toString:()Ljava/lang/String;
  #40 = Class              #55            // java/io/PrintStream
  #41 = NameAndType        #56:#22        // println:(Ljava/lang/String;)V
  #42 = Utf8               ManerFan
  #43 = NameAndType        #21:#22        // "<init>":(Ljava/lang/String;)V
  #44 = NameAndType        #25:#26        // print:()V
  #45 = Utf8               java/lang/Object
  #46 = Utf8               java/io/Serializable
  #47 = Utf8               hello
  #48 = Utf8               java/lang/System
  #49 = Utf8               out
  #50 = Utf8               Ljava/io/PrintStream;
  #51 = Utf8               append
  #52 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #53 = Utf8               toString
  #54 = Utf8               ()Ljava/lang/String;
  #55 = Utf8               java/io/PrintStream
  #56 = Utf8               println
{
  private static final java.lang.String HELLO;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
    ConstantValue: String hello

  private java.lang.String name;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE

  public ClassStruct(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: aload_1
         6: putfield      #2                  // Field name:Ljava/lang/String;
         9: return
      LineNumberTable:
        line 7: 0
        line 8: 4
        line 9: 9

  public void print();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: new           #4                  // class java/lang/StringBuilder
         6: dup
         7: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
        10: ldc           #7                  // String hello:
        12: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        15: aload_0
        16: getfield      #2                  // Field name:Ljava/lang/String;
        19: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        22: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        25: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        28: return
      LineNumberTable:
        line 12: 0
        line 13: 28

  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: new           #6                  // class ClassStruct
         3: dup
         4: ldc           #11                 // String ManerFan
         6: invokespecial #12                 // Method "<init>":(Ljava/lang/String;)V
         9: astore_1
        10: aload_1
        11: invokevirtual #13                 // Method print:()V
        14: return
      LineNumberTable:
        line 16: 0
        line 17: 10
        line 18: 14
}
SourceFile: "ClassStruct.java"

参考:
深入理解Java虚拟机

订阅号

阅读 3.4k

推荐阅读
林中小舍
用户专栏

工作中的坑点及经验

51 人关注
41 篇文章
专栏主页