本文主要介绍了对JVM类文件结构的深入理解,并结合实例详细分析了类文件结构的相关概念、原理、结构、常用方法和属性。有需要的可以参考一下。
本文阐述了如何深入理解JVM的类文件结构。分享给你,供你参考,如下:
概述
通常在DOS界面下,我们需要先运行命令来运行javac。这个命令的直接结果就是生成相应的类文件,然后我们就可以实际运行基于这个类文件的程序来得到结果。大自然。这要归功于Java虚拟机,那么Java虚拟机真的只能编译吗?java源文件?答案是否定的,今天Java虚拟机已经实现了语言独立。语言独立的基础是虚拟机和字节码的存储格式,Java虚拟机不绑定任何语言包括Java语言。它只与特定的二进制文件相关联,如“类”文件。类文件包含Java虚拟机指令集、符号表和一些辅助信息。很容易想到Java(本质上不是Java语言本身的平台无关性,而是其底层Java虚拟机的平台无关性)。),因为任何函数式语言都可以表示为Java虚拟机可以接受的有效类文件。比如除了Java虚拟机可以直接把Java源文件编译成类文件,使用JRuby等语言的编译器也可以把程序代码编译成类文件。因此,Java虚拟机不关心用什么语言编译类文件。
Class类文件结构
类文件是一组基于8个字节的二进制流,所有数据项在类文件中严格按照顺序排列,中间没有任何分隔符,这使得类文件中存储的内容几乎可以运行所有程序。根据Java虚拟机的规范,类文件格式使用类似C语言结构的伪结构存储数据,只有两种数据类型:
无符号数和表。
。无符号数属于基本数据类型,主要可以用来描述按照UTF-8编码的数字、索引符号、量值或字符串值。大小用u1、u2、u4、u8表示,分别为1字节、2字节、4字节、8字节。
表是一种复合数据类型,由多个无符号数字或其他表作为数据项组成。所有表格都习惯以“_info”结尾。那这块表是干什么用的?表主要用于描述具有分层复合结构的数据,如方法和字段。注意,类文件是
没有分隔符的
,所以每个的二进制数据类型都有严格的定义。具体顺序定义如下:类文件主要分为幻数、类文件版本号、常量池、访问标志、类索引(包括父类索引和接口索引集)、字段表集、方法表集和属性表集。
魔数与Class文件版本号
前四个字节是幻数,幻数的唯一作用是确定这个类文件是否是Java虚拟机接受的类文件。比如gif和jpeg,在文件头里有魔力。基于安全考虑的——分机可以用魔术代替分机随意更改。类文件的魔值是“0xCAFEBABE”(咖啡宝贝?)。
幻数的后四个字节是类文件的版本号:版本号分为次版本号和主版本号。前两个字节用于指示次版本号,后两个字节用于指示主版本号。这个版本号表示不同jdk版本的不同版本范围。如果类文件的版本号超过虚拟机版本,将被拒绝。
常量池
常量池可以简单理解为类文件的资源从库。该数据类型是类文件结构中与其他项目关联最多的数据类型,也是占用类文件空间最大的项目之一。
字面量
和符号引用
主要存放在常量池中。字面量接近Java语言中的常量概念,如文本字符串、声明为final的常量值等。(百度百科解释字面量是由双引用号引用的一系列字符)。符号主要包括三种类型的常数:和类接口的完全限定名。
的字段名和描述符
方法的名称和描述符。
符号引用与直接引用的关联
参考是用来描述参考目标的一组符号。符号是任何形式的文字量。对Java虚拟机的符号引用没有严格的限制。要求只有在需要使用时才能明确定位目标。常量池存在于类文件中,类文件首先要通过Java虚拟机的类加载机制(具体来说就是方法区的内存区)加载到内存中。回想一下,方法区主要存储对象的实例,这个类文件是虚拟机接收外部访问的接口)。符号引用属于常量池的内容,那么是否意味着符号引用的目标已经加载到内存中?答案是否定的,因为
符号引用与虚拟机的内存布局无关,符号引用的目标并不一定已经加载到内存中了。
直接引用可以是直接指向引用目标的指针、相对偏移量或可以间接定位到目标的句柄。直接引用与虚拟机的内存布局有关,同一符号引用在不同虚拟机上翻译出来的直接引用一般是不一样的。
如果有了直接引用,那么引用的目标必定是存在内存中的。
常量池中的每一个常量都是一个表,jdk1.7中有14个常量类型,所以常量池中的项对应14个表,而这14个表的每一个类型都是不同的。但是有一个共同的特点:表格开头的第一位是u1类型的标志位,表示这个常量属于哪种类型。
需要注意的是,在类文件中,方法和字段需要引用CONSTANT-Utf8_info类型的常量,所以这类常量的长度是有一定限制的,也就是Java中方法和字段的最大长度。
在CONSTANT-Utf8_info中,其length的值u2,说明Java虚拟机只能编译最大大约64KB的变量或者方法名。超过的话将不会进行编译。
访问标志
常量池后的数据结构是访问标志(access_flags),主要用来标识一些类或接口的访问信息,包括:这个类是否是类或接口,是否定义了public,是否定义了抽象类型;如果是类,是否声明为final等。具体的logo访问如下:
类索引、父类索引和接口索引集合
该数据项主要用于确定该类的
继承关系
。类索引和父索引都是u2类型的数据,接口索引集是一组u2类型的数据。因为Java不允许多重继承,父类索引是唯一的,但是一个类可以实现多个接口,所以得到的接口索引是一个集合,表示这个类实现了哪些接口。
字段表集合
字段用于描述接口或类中声明的变量。
包括字段类级变量和实例级变量,但不包括方法内部声明的局部变量(这些变量存储在Java虚拟机堆栈中的局部变量表中)。自然,描述一个字段的信息包括:字段的作用域(public,protected,private),实例变量是否是静态的,可变性(final),并发可见性(volatile),是否可以序列化(transient),字段数据类型(基本数据类型,对象,数组),字段名。的字段信息也存储在表中,其字段表包括三种类型:
键入u2访问标志(access_flags),其访问标志在access_flags中。
u2类型的name_index(字段的简单名称)
类型u2描述符(描述符_索引)
上面的简单名,上面的完全限定名,和这里的描述符有什么区别?完全限定名很好理解,就是类的完整路径信息。简单名称是指没有类型和参数修改的方法或字段的名称。例如,一种方法如下:
public void inc(int a,int b){
system . out . println(a b);
}
那么这个方法的简单名字就是Inc。
和上面两个相比,描述符相对复杂一些。描述符的主要作用是描述字段的数据类型、方法的参数列表和返回值。我们熟悉的Void,在类文件中用V表示。以下是完整描述符标记的含义:
对于数组类型,每个维度由前导“[”字符描述。如果是二维数组,那么有两个“[”符号。例如,“java.lang.String[][]”将被记录为“[ljava . lang . string;”
对于方法,在县参数列表之后按照返回值的顺序进行描述。例如,方法int Inc (int a,int [] b,char [] [] c,int d)的描述符是“(i [i [[ci] i”)。
方法表集合
JVM中堆方法表的描述与字段表一致,包括访问标志、名称索引、描述符索引和属性表集。表的结构与字段表相同,但区别在于访问标志不同。volatile和transient关键字不能在方法中使用,因此这两个标志不能在方法表中使用。不能在字段中使用的访问标志被添加到方法中。例如,方法可以用synchronized、native、strictfp和abstract关键字来修饰,因此相应的访问标志被添加到方法表中。
请注意,如果父类方法没有在子类中被重写,则父类的方法信息不会自动出现在方法中。同样,也可以添加由编译器自动添加的方法,比如方法。
属性表集合
前面的类文件、字段表、方法表都可以携带自己的属性信息,这些信息是用属性表来描述的,用来描述一些场景特有的信息。与属性表中的类文件相似,对数据项的类型和顺序没有严格的要求。只要新属性不与现有属性名称重复,任何人都可以将自己定义的属性信息写入属性表。
Code属性
Java方法体中的代码由javac编译,最终编译的字节码指令保存在Code属性中。然而,并不是所有的方法表都必须有这个属性。
Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code)和元数据(Metadata,包括类、字段、方法定义及其其他信息)两部分,那么在整个Class文件中,Code属性用于描述代码,所有其他的数据项目都用于描述元数据。
Exceptions属性
该属性的作用是列出方法中可能抛出的已检查异常,即描述抛出后列出的异常。
LineNumberTable属性
主要用来描述Java源代码行号和字节码行号的对应关系。这个属性也不是必需的。如果没有这个属性,对程序的直接影响就是抛出异常时无法显示对应的行号;而调试时设置断点的方式就是调试程序。
LocalVariableTable属性
用于描述堆栈框架中局部变量表中的变量与Java源代码中定义的变量之间的关系。它也不是必需的属性。如果没有这个属性,直接的影响就是当别人引用这个方法时,所有的参数名都会丢失,IDE会使用args0、args1等参数进行显示。自然,在调试程序时,显示的参数名是未知的。
SourceFile属性
用于记录该类文件的源文件的名称。如果不使用此属性,则在引发异常时,堆栈中不会显示错误代码的文件名。
ConstantValue属性
该功能是通知虚拟机自动为静态变量赋值。注意,这个属性(类变量)只能由static关键字修改的amount变量使用。对于非类变量,初始化在方法中完成;对于类变量,可以选择两种方式初始化变量:一种是在类构造函数方法中使用;第二个是ConstantValue属性。目前Sun Hotspot的选择原则是:如果一个变量同时用static和final关键字修饰,且该变量是基本数据类型或java.lang.String类型,则用ConstantValue属性初始化。如果它不是由final修饰的或者不是基本数据类型,则该方法将用于初始化。
InnerClass属性
该属性主要用于记录内部类和宿主类之间的关系。
Deprecated以及Synthetic属性
这两个属性都属于标志类型的布尔属性,两者只有区别。
Deprecated属性用于指示一个类、字段或方法,该类、字段或方法已被程序作者弃用,可通过注释@deprecated实现。
Synthetic属性表示该字段不是由Java源代码生成的,而是由编译器自己添加的。
StackMapTable属性
此属性的目的是取代以前的基于数据流分析的类型派生验证器,后者比较消费性能。
Signature属性
这个属性是专门用来记录泛型类型的,因为Java语言采用的是erasure实现的泛型,在字节码(Code属性)中,泛型信息会在编译后被擦除。擦除法的优点是可以节省泛型占用的内存空间,缺点是运行时无法通过反射获取泛型信息,而Signature属性弥补了这一缺陷。由于这个属性,现在Java反射API已经能够获得通用信息。
BootstrapMethods属性
此属性用于保存invokedynamic指令引用的引导方法限定符。此指令用于在运行时动态解析调用点限定符引用的方法,并执行该方法。
涉及
1.周志明,深入理解Java虚拟机:JVM的高级特性和最佳实践,机械工业出版社。
对更多java相关内容感兴趣的读者可以查看我们的专题:《Java面向对象程序设计入门与进阶教程》、《Java数据结构与算法教程》、《Java操作DOM节点技巧总结》、《Java文件与目录操作技巧汇总》和《Java缓存操作技巧汇总》。
希望这篇文章对大家的java编程有所帮助。