JVM内部,第2部分-类文件结构

大家好! 本文的翻译是专门为Java Developer课程的学生准备的。




我们将继续讨论Java虚拟机如何在内部工作。 在上一篇文章英文原版 )中,我们研究了类加载子系统。 在本文中,我们将讨论类文件的结构。

众所周知,所有用Java编程语言编写的源代码都首先使用Java开发工具包中的javac编译器编译为字节码 。 字节码存储在特殊类文件中的二进制文件中。 然后,这些类文件由类加载器(ClassLoader)动态(如有必要)加载到内存中。


图-Java源代码编译

每个具有.java文件都.java编译成至少一个.class文件。 对于源代码中定义的每个类,接口和模块,都会创建一个.class文件。 这也适用于接口和嵌套类。

注意-为简单起见,扩展名为.class文件将称为“类文件”。

让我们编写一个简单的程序。

 public class ClassOne{ public static void main(String[] args){ System.out.println("Hello world"); } static class StaticNestedClass{ } } class ClassTwo{ } interface InterfaceOne{ } 

为此文件运行javac将产生以下文件。

 ClassOne$StaticNestedClass.class ClassOne.class ClassTwo.class InterfaceOne.class 

如您所见,将为每个类和接口创建一个单独的类文件。

类文件中包含什么?


该类文件为二进制格式。 其中的信息通常在连续的信息之间没有缩进的情况下写入,所有内容都与字节边界对齐。 使用两个或四个连续的8位字节写入所有16位和32位值。

该类文件包含以下信息。

幻数,签名 。 每个类文件的前四个字节始终为0xCAFEBABE 。 这四个字节标识Java类文件。

文件版本。 接下来的四个字节包含文件的主要版本和次要版本。 这些数字一起确定了类文件格式的版本。 如果类文件具有M的主要主要版本和次要的m,则我们将该版本指定为Mm

每个JVM在支持的类文件版本上都有限制。 例如,Java 11支持45至55的主要版本,Java 12支持45至56的主要版本。

常量池。 结构表,表示ClassFile结构及其子结构中的字符串常量,类名,接口,字段,方法和其他常量。 每个常量池元素都以一个定义常量类型的单字节标签开头。 根据常量的类型,以下字节可以是立即常量值,也可以是对池中另一个元素的引用。

访问标志。 指示类是公共接口还是私有接口的标志列表,是否为最终类。 Java虚拟机规范中描述了各种标志,例如ACC_INTERFACEACC_ENUMACC_INTERFACEACC_ENUM等。

这个课 链接到常量池中的条目。

超级班。 链接到常量池中的条目。

介面 类实现的接口数。

字段数。 类或接口中的字段数。

领域。 在字段数之后,是一个可变长度的结构表。 每个字段一个,带有字段类型和名称的描述(参考常量池)。

方法数量。 类或接口中方法的数量。 此数字仅包括在类中明确定义的方法,而没有从超类继承的方法。

方法 接下来是方法本身。 对于每个方法,包含以下信息:方法描述符(返回类型和参数列表),方法的局部变量所需的字数,方法操作数堆栈所需的最大堆栈字数,方法捕获的异常表,方法字节码和表行号。

属性数。 此类,接口或模块中的属性数。

属性 属性数量后跟描述每个属性的表或可变长度结构。 例如,始终有一个“ SourceFile”属性。 它包含从中编译类文件的源文件的名称。

尽管类文件不是直接可读的,但JDK中有一个名为javap的工具,它以方便的格式显示其内容。

让我们编写一个简单的Java程序,如下所示。

 package bytecode; import java.io.Serializable; public class HelloWorld implements Serializable, Cloneable { public static void main(String[] args) { System.out.println("Hello World"); } } 

让我们使用javac编译该程序,该程序将创建HelloWorld.class文件,并使用javap查看HelloWorld.class文件。 对HelloWorld.class使用-v (verbose)选项运行javap会得到以下结果:

 Classfile /Users/apersiankite/Documents/code_practice/java_practice/target/classes/bytecode/HelloWorld.class Last modified 02-Jul-2019; size 606 bytes MD5 checksum 6442d93b955c2e249619a1bade6d5b98 Compiled from "HelloWorld.java" public class bytecode.HelloWorld implements java.io.Serializable,java.lang.Cloneable minor version: 0 major version: 55 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #5 // bytecode/HelloWorld super_class: #6 // java/lang/Object interfaces: 2, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #6.#22 // java/lang/Object."<init>":()V #2 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #25 // Hello World #4 = Methodref #26.#27 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #28 // bytecode/HelloWorld #6 = Class #29 // java/lang/Object #7 = Class #30 // java/io/Serializable #8 = Class #31 // java/lang/Cloneable #9 = Utf8 <init> #10 = Utf8 ()V #11 = Utf8 Code #12 = Utf8 LineNumberTable #13 = Utf8 LocalVariableTable #14 = Utf8 this #15 = Utf8 Lbytecode/HelloWorld; #16 = Utf8 main #17 = Utf8 ([Ljava/lang/String;)V #18 = Utf8 args #19 = Utf8 [Ljava/lang/String; #20 = Utf8 SourceFile #21 = Utf8 HelloWorld.java #22 = NameAndType #9:#10 // "<init>":()V #23 = Class #32 // java/lang/System #24 = NameAndType #33:#34 // out:Ljava/io/PrintStream; #25 = Utf8 Hello World #26 = Class #35 // java/io/PrintStream #27 = NameAndType #36:#37 // println:(Ljava/lang/String;)V #28 = Utf8 bytecode/HelloWorld #29 = Utf8 java/lang/Object #30 = Utf8 java/io/Serializable #31 = Utf8 java/lang/Cloneable #32 = Utf8 java/lang/System #33 = Utf8 out #34 = Utf8 Ljava/io/PrintStream; #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Ljava/lang/String;)V { public bytecode.HelloWorld(); descriptor: ()V flags: (0x0001) 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 Lbytecode/HelloWorld; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) 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 7: 0 line 8: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String; } SourceFile: "HelloWorld.java" 

在这里,您可以看到该类是公共的,并且在常量池中有37个条目。 有一个属性(下面的SourceFile),该类实现了两个接口(Serializable,Cloneable),没有字段,并且有两个方法。

您可能已经注意到,源代码中只有一个静态main方法,但是类文件却说有两种方法。 记住默认构造函数-这是javac编译器添加的无参数构造函数,其字节码在输出中也可见。 构造函数被视为方法。

您可以在此处阅读有关javap的更多信息。

提示 :您还可以使用javap查看lambda与匿名内部类的区别。

Source: https://habr.com/ru/post/zh-CN478584/


All Articles