深入理解JVM之Class类文件结构详解

本文实例讲述了深入理解JVM之Class类文件结构。分享给大家供大家参考,具体如下:

概述

我们平时在DOS界面中往往需要运行先运行javac命令,这个命令的直接结果就是产生相应的class文件,然后基于这个class文件才可以真正运行程序得到结果。自然。这是Java虚拟机的功劳,那么是不是Java虚拟机只能编译.java的源文件呢?答案是否定的。时至今日,Java虚拟机已经实现了语言无关性的特点。而实现语言无关性的基础是虚拟机和字节码的存储格式,Java虚拟机已经不和包括Java语言在内的任何语言绑定。它只与“class”文件这种特定的二进制文件相关联。在class文件中包含了Java虚拟机指令集和符号表以及若干辅助信息。可以很容易想到Java(本质上不是Java语言本身的平台无关性,而是其底层的Java虚拟机的平台无关性使然。)的跨平台,因为任何一门功能性语言都可以表示为能被Java虚拟机接受的有效的class文件。比如,除了Java虚拟机可以将Java源文件直接编译为class文件外,使用JRuby等其他语言的编译器一样可以把程序代码编译成class文件,由此可见,Java虚拟机并不关心class文件是由何种语言编译来的。

Class类文件结构

Class文件是一组以8字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在class文件中,中间没有任何分隔符,这使得class文件中存储的内容几乎是全部程序运行的程序。Java虚拟机规范规定,Class文件格式采用类似C语言结构体的伪结构来存储数据,这种结构只有两种数据类型:无符号数和表。

无符号数属于基本数据类型,主要可以用来描述数字、索引符号、数量值或者按照UTF-8编码构成的字符串值,大小使用u1、u2、u4、u8分别表示1字节、2字节、4字节和8字节。

表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有的表都习惯以“_info”结尾。那么表是干嘛的呢?表主要用于描述有层次关系的复合结构的数据,比如方法、字段。需要注意的是class文件是没有分隔符的,所以每个的二进制数据类型都是严格定义的。具体的顺序定义如下:

在class文件中,主要分为魔数、Class文件的版本号、常量池、访问标志、类索引(还包括父类索引和接口索引集合)、字段表集合、方法表集合、属性表集合。

魔数与Class文件版本号

头4个字节是魔数,魔数的唯一作用在于确定这个Class文件是否是Java虚拟机接受的Class文件。如gif和jpeg等在文件头中都存在魔术,使用魔术而不是使用扩展名是基于安全性考虑的——扩展名可以随意被改变。Class文件的魔术值为“0xCAFEBABE”(咖啡宝贝?)。

紧接着魔数的4个字节是Class文件版本号:版本号又分为次版本号和主版本号。其中前两个字节用于表示次版本号,后两个字节用于表示主版本号。这个的版本号是随着jdk版本的不同而表示不同的版本范围的。如果Class文件的版本号超过虚拟机版本,将被拒绝执行。

常量池

常量池可以简单理解为class文件的资源从库,这种数据类型是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的项目之一。在常量池中主要存放字面量符号引用。字面量比较接近Java语言层面的常量概念,比如文本字符串、声明为final的常量值等(百度百科的解释是字面量是用双引用号引住的一系列字符)。符号引用则主要包括三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符。

符号引用与直接引用的关联

  • 符号引用是一组符号,用来描述所引用的目标,符号是以任何形式存在的字面量。对于符号引用Java虚拟机并没有严格的限制。规定只需要使用的时候能够无歧义定位到目标就可以。常量池存在于Class文件中,而Class文件是必须首先通过Java虚拟机的类加载机制加载到内存中(确切的说是方法区这个内存区域,回顾一下,方法区存放的主要是对象的实例,这个Class文件是虚拟机对外接受访问的接口)。符号引用属于常量池中的内容,那么是不是说符号引用的目标已经加载到内存中了呢?答案是否定的,因为符号引用与虚拟机的内存布局无关,符号引用的目标并不一定已经加载到内存中了。
  • 直接引用可以是直接指向引用目标的指针、相对偏移量或者是一个能够间接定位到目标的句柄。直接引用是和虚拟机的内存布局有关的,同一个符号引用在不同的虚拟机上翻译的直接引用一般是不同的。如果有了直接引用,那么引用的目标必定是存在内存中的。

在常量池中每一项常量都是一个表,在jdk1.7中共有14中常量类型,所以常量池的项目就对应14张表,这14张表的每种类型都不一样。但是有一个共同特点:表开始的第一位都是一个u1类型的标志位,代表这个常量属于哪种类型。

需要注意的是,在Class文件中,方法、字段都需要引用CONSTANT-Utf8_info类型的常量,所以这种类型的常量的长度有一定的限制,也就是Java中方法、字段的最大长度。在CONSTANT-Utf8_info中,其length的值u2,说明Java虚拟机只能编译最大大约64KB的变量或者方法名。超过的话将不会进行编译。

访问标志

常量池之后的数据结构是访问标志(access_flags),这个标志主要用于识别一些类或者接口层次的访问信息,主要包括:这个Class是类还是接口、是否定义public、是否定义abstract类型;如果是类的话是否被声明为final等。具体的标志访问如下:

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

这个数据项主要用于确定这个类的继承关系

其中类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据。在Java中由于不允许多继承,所以父类索引是唯一的,但是一个类可以实现多个接口,所以得到的接口索引是一个集合,表示这个类实现了哪些接口。

字段表集合

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

字段包括类级变量和实例级变量,但是不包括方法内部声明的局部变量(这些变量是存储在Java虚拟机栈中的局部变量表中的)。自然,描述一个字段的信息包括:字段的作用域(public、protected、private)、实例变量与否(static)、可变性(final)、并发可见性(volatile)、可否被序列化(transient)、字段数据类型(基本数据类型、对象、数组)、字段名称。字段的信息也被存放在一张表中,其字段表包括三种类型:

  • u2类型访问标志(access_flags),其访问标志在access_flags中
  • u2类型的name_index(字段的简单名称)
  • u2类型的描述符(descriptor_index)

上面出现了简单名称,上文中出现了全限定名,以及这里出现的描述符,三者有什么区别呢?其中全限定名称比较好理解,就是类的完整路径信息。而简单名称则是指没有类型和参数修饰的方法或者字段名称,比如一个方法如下:

public void inc(int a,int b){
  System.out.println(a+b);
}

那么这个方法的简单名称就是inc。

相对于以上两者,描述符相对复杂一些。描述符的主要的作用是描述字段的数据类型、方法的参数列表和返回值。其中我们熟悉的void,在Class文件中用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关键字修饰,所以在方法表中就增加了相应的访问标志。

要注意的是,如果父类方法没有在子类中重写,那么在方法中不会自动出现来自父类的方法信息。同样的,有可能添加编译器自动增加的方法,比如方法。

属性表集合

前面的Class文件、字段表和方法表都可以携带自己的属性信息,这个信息用属性表进行描述,用于描述某些场景专有的信息。在属性表中没有类似Class文件的数据项目类型和顺序的严格要求,只要新的属性不与现有的属性名重复,任何人都可以向属性表中写入自己定义的属性信息。

Code属性

Java程序方法体中的代码经过javac编译最终编译成的字节码指令就保存在Code属性中。但是并非所有的方法表都必须存在这个属性。Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code)和元数据(Metadata,包括类、字段、方法定义及其其他信息)两部分,那么在整个Class文件中,Code属性用于描述代码,所有其他的数据项目都用于描述元数据。

Exceptions属性

这个属性的作用是列举出方法中可能抛出的受查异常(Checked Exception),也就是描述throws 后的列举的异常

LineNumberTable属性

主要用于描述Java源代码行号与字节码行号之间的对应关系。这个属性也不是必须的。如果没有这个属性,对程序的直接影响就是当抛出异常的时候无法显示对应的行号;并且在调试的时候无法通过设置断点的方法是调试程序。

LocalVariableTable属性

用于描述栈帧中局部变量表中的变量与Java源码中定义的变量的之间的关系。也不属于必须的属性。如果没有这个属性,产生的直接影响就是当别人引用这个方法的时候,所有的参数名称都会丢失,IDE将会使用诸如args0、args1之类的参数进行显示。自然,当调试程序的时候,显示的参数名称是不可知的。

SourceFile属性

用于记录这个Class文件的源码文件名称。如果不使用这个属性,那么当抛出异常的时候,堆栈中将不会显示出错代码所属的文件名。

ConstantValue属性

作用是通知虚拟机自动为静态变量赋值。要注意的是,只有被static关键字修饰的额变量才可以使用这个属性(类变量)。对于非类变量,初始化是在方法中进行的;对于类变量可以选择两种方式进行变量的初始化:一是在类构造器方法中使用;二是是ConstantValue属性。目前Sun Hotspot的选择原则是:如果一个变量同时使用static和final关键字修饰,并且这个变量是基本数据类型或者java.lang.String类型的话,就使用ConstantValue属性进行初始化。如果没有被final修饰或者并非是基本数据类型,那么将会选择使用方法进行初始化。

InnerClass属性

这个属性主要用于记录内部类与宿主类之间的关联关系。

Deprecated以及Synthetic属性

这两个属性都属于标志类型的布尔属性,只存在有没有的区别。

Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,可以通过注解@deprecated实现

Synthetic属性代表此字段并不是由Java源码产生的,而是通过编译器自行添加的。

StackMapTable属性

该属性的目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。

Signature属性

这个属性是专门用来记录泛型类型的,因为在Java语言采用的是擦除法实现的泛型,在字节码(Code属性)中,泛型信息编译之后会被擦除。擦除法的优点是能够节省泛型所占的内存空间,缺点是在运行期间无法通过反射得到泛型信息,而Signature属性则弥补了这一缺陷。现在的Java反射API已经能够得到泛型信息,功劳就在于这个属性。

BootstrapMethods属性

这个属性用于保存invokedynamic指令引用的引导方法限定符。该指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。

参考
1、周志明,深入理解Java虚拟机:JVM高级特性与最佳实践,机械工业出版社

更多java相关内容感兴趣的读者可查看本站专题:《Java面向对象程序设计入门与进阶教程》、《Java数据结构与算法教程》、《Java操作DOM节点技巧总结》、《Java文件与目录操作技巧汇总》和《Java缓存操作技巧汇总》

希望本文所述对大家java程序设计有所帮助。

(0)

相关推荐

  • jvm类加载器基础解析

    [类加载器简介] 类加载器(classloader)用于将类的class文件加载到JVM虚拟机.JVM有三种加载器,引导类加载器器(bootstrapclassloader).扩展类加载器(extensionsclassloader)和应用类加载器(applicationclassloader),另外还可以继承java.lang.ClassLoader类创建自定义加载器. [类加载器种类] 1.引导类加载器(BootStrap):并不是一个Java类,采用C++语言编写.内嵌在JVM内核里面,使

  • 从JVM分析Java的类的加载和卸载机制

    类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构. 加载.class文件的方式: 1.从本地系统中直接加载 2.通过网络下载.class文件 3.从zip,jar等归档文件中加载.class文件 4.从专有数据库中提取.class文件 5.将Java源文件动态编译为.class文件 类的加载的最终产品是位于堆区中的Class对象. Class对象封装了类在

  • 解析Java的JVM以及类与对象的概念

    Java虚拟机(JVM)以及跨平台原理 相信大家已经了解到Java具有跨平台的特性,可以"一次编译,到处运行",在Windows下编写的程序,无需任何修改就可以在Linux下运行,这是C和C++很难做到的. 那么,跨平台是怎样实现的呢?这就要谈及Java虚拟机(Java Virtual Machine,简称 JVM). JVM也是一个软件,不同的平台有不同的版本.我们编写的Java源码,编译后会生成一种 .class 文件,称为字节码文件.Java虚拟机就是负责将字节码文件翻译成特定平

  • JVM加载一个类的过程

    类的加载过程 Java源代码被编译成class字节码,JVM把描述类数据的字节码.Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制. 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括了:加载(Loading).验证(Verification).准备(Preparation).解析(Resolution).初始化(Initialization).使用(Using).卸载(Unloading)七个阶段,

  • 一文读懂Jvm类加载机制

    前言 一个月没更新了,这个月发生了太多的事情,导致更新的频率大大降低,不管怎样收拾心情,技术的研究不能落下! jvm作为每个java程序猿必须了解的知识,博主推荐一本书<深入理解Java虚拟机>,以前博主在学校的时候看过几遍,每一次看都有新的理解.加上工作了也有一年多的时间了,有必要好好总结一番~ 什么是jvm 平常我们编写代码都是编写的.java文件,怎么部署到机器上运行呢?通过打jar包或者war包,然后部署运行. 如果看过jar包的内容那么就能知道,我们写的.java文件全部被编译成了.

  • JVM核心教程之JVM运行与类加载全过程详解

    为什么要使用类加载器? Java语言里,类加载都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会给java应用程序提供高度的灵活性.例如: 1.编写一个面向接口的应用程序,可能等到运行时再指定其实现的子类: 2.用户可以自定义一个类加载器,让程序在运行时从网络或其他地方加载一个二进制流作为程序代码的一部分:(这个是Android插件化,动态安装更新apk的基础) 为什么研究类加载全过程? 有助于连接JVM运行过程 更深入了解java动态性(解热部署,动态加载),提高程

  • 深入理解JVM之类加载机制详解

    本文实例讲述了深入理解JVM之类加载机制.分享给大家供大家参考,具体如下: 概述 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 与那些在编译时需要进行链接工作的语言不同,在Java语言里,类型的加载.连接和初始化过程都是在程序运行期间完成的,例如import java.util.*下面包含很多类,但是,在程序运行的时候,虚拟机只会加载哪些我们程序需要的类.这种策略虽然会令类加载时稍微增加

  • JVM虚拟机查找类文件的顺序方法

    JVM查找类文件的顺序: 在doc下使用set classpath=xxx, 如果没有配置classpath环境变量,JVM只在当前目录下查找要运行的类文件. 如果配置了classpath环境,JVM会先在classpath环境变量值的目录中查找要运行的类文件. 值的结尾处如果加上分号,那么JVM在classpath目录下没有找到要指定的类文件,会在当前目录下在查找一次. 值的结尾出如果没有分号,那么JVM在classpath目录下没有找到要指定的类文件,不会在当前目录下查找,即使当前目 录下有

  • JVM类加载机制详解

    一.先看看编写出的代码的执行过程: 二.研究类加载机制的意义 从上图可以看出,类加载是Java程序运行的第一步,研究类的加载有助于了解JVM执行过程,并指导开发者采取更有效的措施配合程序执行. 研究类加载机制的第二个目的是让程序能动态的控制类加载,比如热部署等,提高程序的灵活性和适应性. 三.类加载的一般过程 原理:双亲委托模式 1.寻找jre目录,寻找jvm.dll,并初始化JVM: 2.产生一个Bootstrap Loader(启动类加载器): 3.Bootstrap Loader自动加载E

  • 详解JVM类加载机制及类缓存问题的处理方法

    前言 大家应该都知道,当一个Java项目启动的时候,JVM会找到main方法,根据对象之间的调用来对class文件和所引用的jar包中的class文件进行加载(其步骤分为加载.验证.准备.解析.初始化.使用和卸载),方法区中开辟内存来存储类的运行时数据结构(包括静态变量.静态方法.常量池.类结构等),同时在堆中生成相应的Class对象指向方法区中对应的类运行时数据结构. 用最简单的一句话来概括,类加载的过程就是JVM根据所需的class文件的路径,通过IO流的方式来读取class字节码文件,并通

随机推荐