JVM-Class文件结构

  |   0 评论   |   0 浏览

用hex editor可以打开.class文件,

或者vim -b xxx.class 然后进入末行模式, :%!xxd 则可以看到 cafe babe......

Jdk8 00 00 00 34 相当于十进制52, 看下面 major version: 52 代表使用的jdk8编译的。

mac下面可以下载010 editor 工具查看。很好用。

警告: 二进制文件Test包含club.wujingjian.stack.Test
Classfile /Users/apple/coding/uaa/datastructure/target/classes/club/wujingjian/stack/Test.class
  Last modified 2021-3-11; size 2148 bytes
  MD5 checksum 5552637dd3404991c0d739a0a71a4f73
  Compiled from "Test.java"
public class club.wujingjian.stack.Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
    #1 = Methodref          #31.#69       // java/lang/Object."<init>":()V
    #2 = Class              #70           // java/util/ArrayList
    #3 = Methodref          #2.#69        // java/util/ArrayList."<init>":()V
    #4 = String             #71           // +
    #5 = InterfaceMethodref #72.#73       // java/util/List.add:(Ljava/lang/Object;)Z
    #6 = String             #74           // -
    #7 = String             #75           // *
    #8 = String             #76           // /
    #9 = String             #77           // ,
   #10 = Methodref          #78.#79       // java/lang/String.split:(Ljava/lang/String;)[Ljava/lang/String;

在class文件中会包含:

魔数 副版本号 主版本号 常量池计数器 常量池数据区(cp_info) 访问标志 类索引 父类索引 接口计数器 等等

常量池计数器是从1开始计数的,而不是从0开始(第0项空出来有特殊考虑,为了满足某些指向常量池的索引值的数据在特定的情况下表达"不引用任何一个常量池"的意思,这种情况下可以将索引值设置为0来表示),如果常量池计数器 constant_pool_count = 22, 则后面的常量池项(cp_info) 的个数就位21,

image.png

class常量池

编译期间生成的,存储于class文件中。

运行时常量池和字符串常量池 是 存储在JVM内存中的, 他们的数据来源于 class常量池。

常量池位置:

class文件中前四个字节是 ca fe ba be

接下来4个字节是 jdk 版本号

接下来两个字节是 常量池计数器

接下来是就是 常量池数据区了。

常量池内部是如何组织的

cp_info 常量池项

constant_pool_count: 常量池计数器

常量池项的结构

常量池项: 包含字面量 (literal) 和 符号引用 (symbolic reference)

字面量又包含: 文本字符串、被声明为final的常量值、基本数据类型的值、其他

符号引用包含: 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符

cp_info {
 u1 tag;
 u1 info [];
}

tag的值来确定某个常量池项标识什么类型的字面量

Tag值标识的字面量更细化的结构
1用于表示字符串常量的值CONSTANT_Utf8_info
3表示4字节(int)的数值常量CONSTANT_Integer_info
4表示4字节(Float)的数值常量CONSTANT_Float_info
5表示8字节的(Long)的数值常量CONSTANT_Long_info
6表示8字节(Double)的数值常量CONSTANT_Double_info
7表示类或接口的完全限定名CONSTANT_Class_info
8用于表示java.lang.String类型的常量对象CONSTANT_String_info
9表示类中的字段CONSTANT_Fieldref_info
10表示类中的方法CONSTANT_Methodref_info
11表示类所实现的接口的方法CONSTANT_InterfaceMethodref_info
12表示字段或方法的名称和类型CONSTANT_NameAndType_info
15表示方法句柄CONSTANT_MethodHandle_info
16表示方法类型CONSTANT_MethodType_info
18用于表示invokedynamic 指令所使用到的引导方法(Bootstrap Method)、引导方法使用到动态调用名称(Dynamic Invocation Name) 、参数和请求返回类型、以及可以选择性的附加被称为静态参数(Static Arguments)的常量序列CONSTANT_InvokeDynamic_info

Eg: int float类型在常量池中的定义

public class IntAndFloatTest { 
  private final int a = 10; 
  private final int b = 10; 
  private float c = 11f; 
  private float d = 11f; 
  private float e = 11f; 
}

使用 javap -v IntAndFloatTest 查看,虽然定义了5个变量,但是常量值 Constant Pool中只有一个常量10 和一个常量11f.

Constant pool: 
Xxxxxx
constant #8=int 10;
constant #23 = float 11.0f;

代码中用到int 类型10的地方,会使用常量池的指针值#8定位到第#8 个常量池项(cp_info),即值为10的结构体CONSTANT_Integer_info; 11f也同理。

String类型字符串常量在常量池中表示和存储?

CONSTANT_String_info{
  u1 tag =8;
  u2 string_index;
}

结构体中占用2个字节的string_index值指向了某个CONSTANT_Utf8_info结构体,即:string_index 的值是某个CONSTANT_Utf8_info结构体在常量池中的索引

Eg:

public class StringTest {
	private String s1 = "JVM原理"; 
	private String s2 = "JVM原理"; 
	private String s3 = "JVM原理"; 
	private String s4 = "JVM原理"; 
}

使用javap -v StringTest查看常量池信息

$ javap -v StringTest
警告: 二进制文件StringTest包含club.StringTest
Classfile /Users/apple/coding/uaa/datastructure/target/classes/club/StringTest.class
  Last modified 2021-4-20; size 433 bytes
  MD5 checksum dfb662b4ca52463103405204a6f35449
  Compiled from "StringTest.java"
public class club.StringTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #8.#23         // java/lang/Object."<init>":()V
   #2 = String             #24            // JVM原理
   #3 = Fieldref           #7.#25         // club/StringTest.s1:Ljava/lang/String;
   #4 = Fieldref           #7.#26         // club/StringTest.s2:Ljava/lang/String;
   #5 = Fieldref           #7.#27         // club/StringTest.s3:Ljava/lang/String;
   #6 = Fieldref           #7.#28         // club/StringTest.s4:Ljava/lang/String;
   #7 = Class              #29            // club/StringTest
   #8 = Class              #30            // java/lang/Object
   #9 = Utf8               s1
  #10 = Utf8               Ljava/lang/String;
  #11 = Utf8               s2
  #12 = Utf8               s3
  #13 = Utf8               s4
  #14 = Utf8               <init>
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               LocalVariableTable
  #19 = Utf8               this
  #20 = Utf8               Lclub/StringTest;
  #21 = Utf8               SourceFile
  #22 = Utf8               StringTest.java
  #23 = NameAndType        #14:#15        // "<init>":()V
  #24 = Utf8               JVM原理
  #25 = NameAndType        #9:#10         // s1:Ljava/lang/String;
  #26 = NameAndType        #11:#10        // s2:Ljava/lang/String;
  #27 = NameAndType        #12:#10        // s3:Ljava/lang/String;
  #28 = NameAndType        #13:#10        // s4:Ljava/lang/String;
  #29 = Utf8               club/StringTest
  #30 = Utf8               java/lang/Object
{
  public club.StringTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: ldc           #2                  // String JVM原理
         7: putfield      #3                  // Field s1:Ljava/lang/String;
        10: aload_0
        11: ldc           #2                  // String JVM原理
        13: putfield      #4                  // Field s2:Ljava/lang/String;
        16: aload_0
        17: ldc           #2                  // String JVM原理
        19: putfield      #5                  // Field s3:Ljava/lang/String;
        22: aload_0
        23: ldc           #2                  // String JVM原理
        25: putfield      #6                  // Field s4:Ljava/lang/String;
        28: return
      LineNumberTable:
        line 3: 0
        line 4: 4
        line 5: 10
        line 6: 16
        line 7: 22
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      29     0  this   Lclub/StringTest;
}
SourceFile: "StringTest.java"

#2 = String #24 // JVM原理 这个是CONSTANT_String_info

#24 = Utf8 JVM原理 这个是CONSTANT_UTF8_INFO

类文件中定义的类名和各种使用到的类在常量池中是怎样被组织和存储的?

JVM会将某个java类中所有使用到了的类的完全限定名 以 二进制形式的完全限定名 封装成constant_class_info结构体中,然后将其放置到常量池中。

CONSTANT_CLASS_info {
	u1 tag=7;
	u2 name_index;
}

例如:

package club;

import java.util.Date;

public class ClassTest {
    private Date date = new Date();
}

javap -v ClassTest

$ javap -v ClassTest
警告: 二进制文件ClassTest包含club.ClassTest
Classfile /Users/apple/coding/uaa/datastructure/target/classes/club/ClassTest.class
  Last modified 2021-4-20; size 346 bytes
  MD5 checksum 541dacbfcc6b1b077533700a290f3ed8
  Compiled from "ClassTest.java"
public class club.ClassTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#18         // java/lang/Object."<init>":()V
   #2 = Class              #19            // java/util/Date
   #3 = Methodref          #2.#18         // java/util/Date."<init>":()V
   #4 = Fieldref           #5.#20         // club/ClassTest.date:Ljava/util/Date;
   #5 = Class              #21            // club/ClassTest
   #6 = Class              #22            // java/lang/Object
   #7 = Utf8               date
   #8 = Utf8               Ljava/util/Date;
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               Lclub/ClassTest;
  #16 = Utf8               SourceFile
  #17 = Utf8               ClassTest.java
  #18 = NameAndType        #9:#10         // "<init>":()V
  #19 = Utf8               java/util/Date
  #20 = NameAndType        #7:#8          // date:Ljava/util/Date;
  #21 = Utf8               club/ClassTest
  #22 = Utf8               java/lang/Object
{
  public club.ClassTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: new           #2                  // class java/util/Date
         8: dup
         9: invokespecial #3                  // Method java/util/Date."<init>":()V
        12: putfield      #4                  // Field date:Ljava/util/Date;
        15: return
      LineNumberTable:
        line 5: 0
        line 6: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  this   Lclub/ClassTest;
}
SourceFile: "ClassTest.java"

上述 #2 #5 #6 这三行都是 =Class 分别是 Date类,ClassTest本身 ,以及Object类(所有类都继承自Object),

其中#5这一行代表ClassTest类 它的结构体CONSTANT_CLASS_INFO中 ,它在常量池中年的位置是#5,它的name_index值是#21,它(#21)指向了常量池的第21个常量池项,这第21项里面存储了以utf8编码的"club/ClassTest"字符串。

注:

任何Class文件中至少有两个CONSTANT_CLASS_info常量池项(因为有它本身,以及直接父类,如果没有声明直接父类那就是Object类,) 如果类声明实现了某些接口,那么接口的信息也会生产对应的CONSTANT_CLASS_INFO常量池项。

只有真正使用到的类才会编译到class的常量池中,如果只定义而未使用则不会编译到class文件中,比如:

package club.jvm.structure;

import java.util.Date;
public class Person extends Animal {
    private Date date;
    public Person() {
        Date date1;
    }
}

上述代码中虽然定义时候用到了Date类,但是并未使用,所以javap -v Person时候看常量池中并没有Date类的Constant_class_info信息。依然只有两个CONSTANT_CLASS_INFO(Person以及Animal)

哪些字面量能够进入常量池中?

  1. final类型的8种基本类型的值都会进入常量池
  2. 非final类型的(包括static的)8种基本类型的值,只有double、float、long的值会进入常量池
  3. 常量池中包含的字符串类型字面量(双引号引起来的字符串值)

Class文件中的引用和特殊字符串

符号引用

符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要能够无歧义定位到目标即可。

例如,在class文件中以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。

符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存。在编译时,java类并不知道引用的实际地址,因此只能使用符号引用来代替。

直接引用

直接引用可以是:

  • 直接指向目标的指针(比如,指向"类型"【class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
  • 相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
  • 一个能间接定位到目标的句柄

直接引用和虚拟机的布局相关,同一个符号的引用在不同的虚拟机实例上翻译出来的=直接引用=一般不会相同,如果有了直接引用,那引用的目标必定已经被加载入内存中了。

引用替换的时机

符号引用替换为直接引用的操作发生在类加载过程(加载 - > 连接(验证、准备、解析) -> 初始化)中的=解析阶段=,会将符号引用替换为对应的直接引用,放入=运行时常量池=中。

特殊字符串字面量

特殊字符串包括三种:类的全限定名,字段和方法的描述符,特殊方法的方法名。

类的全限定名

Object类,在源文件中的全限定名是java.lang.Object, class文件中的全限定名将"."替换为"/",变为java/lang/Object .(源文件中的一个类的名字,在class文件中是用全限定名表述的)

描述符
各类型的描述符

对于字段的数据类型,描述符主要以下几种

  • 基本数据类型(byte, char, double, float, int, long, short, boolean): 除long 和boolean ,其他基本数据类型的描述符用对应单词的大写首字母标识。long 用J 表示, boolean 用 Z 表示。
  • Void 描述符是 V.
  • 对象类型: 描述符用字符 L 加上对象的全限定名表示,如 String 类型的描述符为 Ljava/lang/String;
  • 数组类型: 每增加一个维度则在对应的字符描述符前增加一个[, 如一位数组 int []的描述符为 [I,二维数组 String [][]的描述符为 [[Ljava/lang/String;.
数据类型描述符
byteB
charC
doubleD
floatF
intI
longJ
shortS
booleanZ
特殊类型voidV
对象类型"L" + 类型的全限定名 + ";"。 如 Ljava/lang/String; 表示 String类型
数组类型若干个 "["+ 数组中元素类型的对应字符串,如一维数组 int[] 的描述符为 [I , 二维数组 String[][] 的描述符为 [[Ljava/lang/String;
字段描述符

字段的描述符就是字段的类型所对应的字符或字符串

eg: int i 中,字段i的描述符是 I

Object o中,字段o的描述符是 Ljava/lang/Object;

double [][]d 中, 字段d的描述符就是 [[D

方法描述符

方法描述符包含所有的参数类型列表和方法返回值。格式为:
(参数1类型 参数2类型 参数3类型...)返回值类型

不管参数类型还是返回值类型,都使用对应字符和对应字符串来表示,并且参数类别用小括号括起来,各参数直接没有空格,参数列表和返回值类型之间也没有空格

方法描述符示例

方法描述符方法声明
()Iint getSize()
()Ijava/lang/StringString toString()
([Ljava/lang/String)Vvoid main(String[] args)
()Vvoid wait()
(JI)Vvoid wait(long timeout, int nanos)
(ZILjava/lang/String;II)Zboolean regionMatches(boolean ignoreCase, int toOffset, String ther, int offset, int len)
([BII)Iint read(byte[] b, int off, int len)
()[[Ljava/lang/ObjectObject [][] getObjectArray()
特殊方法的方法名

特殊方法指: 类的构造方法 和 类型初始化方法(==静态初始化块==)

静态初始化块,在class文件中是以一个方法标识的,这个方法同样有方法描述符和方法名,具体如下:

  • 类的构造方法的方法名使用字符串<init> 表示
  • 静态初始化方法的方法名使用字符串<clinit>表示
  • 除了这两种特殊的方法外,其他普通方法的方法名,和源文件中的方法名相同。
总结:
  1. 方法和字段的描述符中,不包括字段名和方法名,字段描述符中只包括字段类型,方法描述符中只包括参数列表和返回值类型
  2. 无论method()是静态方法还是实例方法,它的方法描述符都是相同的。尽管实例方法除了传递自身定义的参数,还需要额外传递参数this,但是这一点不是由方法描述符来表达的。参数this的传递,是由java虚拟机实现在调用实例方法所使用的指令中实现的隐式传递。

javap命令分析java指令

javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区

(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。

当然这些信息中,有些信息(如本地变量表、指令和代码行偏移量映射表、常量池中方法的参数名称等

等)需要在使用javac编译成class文件时,指定参数才能输出,比如,你直接javac xx.java,就不

会在生成对应的局部变量表等信息,如果你使用javac -g xx.java就可以生成所有相关信息了。如果

你使用的eclipse,则默认情况下,eclipse在编译时会帮你生成局部变量表、指令和代码行偏移量映

射表等信息的。

通过反编译生成的汇编代码,我们可以深入的了解java代码的工作机制。比如我们可以查看i++;这行

代码实际运行时是先获取变量i的值,然后将这个值加1,最后再将加1后的值赋值给变量i。

通过局部变量表,我们可以查看局部变量的作用域范围、所在槽位等信息,甚至可以看到槽位复用等信

息。

$ javap -help
用法: javap <options> <classes>
其中, 可能的选项包括:
  -help  --help  -?        输出此用法消息
  -version                 版本信息
  -v  -verbose             输出附加信息
  -l                       输出行号和本地变量表
  -public                  仅显示公共类和成员
  -protected               显示受保护的/公共类和成员
  -package                 显示程序包/受保护的/公共类
                           和成员 (默认)
  -p  -private             显示所有类和成员
  -c                       对代码进行反汇编
  -s                       输出内部类型签名
  -sysinfo                 显示正在处理的类的
                           系统信息 (路径, 大小, 日期, MD5 散列)
  -constants               显示最终常量
  -classpath <path>        指定查找用户类文件的位置
  -cp <path>               指定查找用户类文件的位置
  -bootclasspath <path>    覆盖引导类文件的位置

一般常用的是-v -l -c三个选项。

javap -v classxx,不仅会输出行号、本地变量表信息、反编译汇编代码,还会输出当前类用

到的常量池等信息。

javap -l 会输出行号和本地变量表信息。

javap -c 会对当前class字节码进行反编译生成汇编代码

查看汇编代码时,需要知道里面的jvm指令,可以参考官方文档:

https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html

另外通过jclasslib工具也可以看到上面这些信息,而且是可视化的,效果更好一些

总结

  1. 通过javap命令可以查看一个java类反汇编、常量池、变量表、指令代码行号表等等信息。
  2. 平常,我们比较关注的是java类中每个方法的反汇编中的指令操作过程,这些指令都是顺序执行

的,可以参考官方文档查看每个指令的含义,很简单:

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

  1. 通过对前面两个例子代码反汇编中各个指令操作的分析,可以发现,一个方法的执行通常会涉及下

面几块内存的操作:

(1)java栈:局部变量表、操作数栈。这些操作基本上都值操作。

(2)java堆:通过对象的地址引用去操作。

(3)常量池。

(4)其他如帧数据区、方法区(jdk1.8之前,常量池也在方法区)等部分,测试中没有显示出来,这

里说明一下。

在做值相关操作时:

一个指令,可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(可

能是指,可能是对象的引用)被压入操作数栈。

一个指令,也可以从操作数数栈中取出一到多个值(pop多次),完成赋值、加减乘除、方法传参、系

统调用等等操作。

十六进制转字符串:http://www.bejson.com/convert/ox2str/

进制转换网址(十六进制转十进制):http://tool.oschina.net/hexconvert/


标题:JVM-Class文件结构
作者:码农路上
地址:http://wujingjian.club/articles/2021/04/22/1619071234008.html