您的位置 首页 java

是时候了解一波:Java虚拟机了

前言

最近在写自己的个人app,所以写文章的时间渐渐少了。不过身边的小伙伴仍会有质量不俗的文章出现。今天是来自我学弟的一篇关于《深入理解Java虚拟机》的内容。

因为篇幅比较长,这篇文章的内容:类文件结构

正文

1.类文件结构

1.1 Class类文件结构

使用subline Test打开一个class文件,其十六进制代码为:

  • Class文件是一组以8字节为基础单位的二进制流,
  • 各个数据项目严格按照顺序紧凑排列在class文件中,
  • 中间没有任何分隔符,这使得class文件中存储的内容几乎是全部程序运行的程序。

这里需要明白1个字节是2位(16进制),比如上面图中开头的“cafa babe”是4个字节,也就是魔数。其它参考下图:

1.2 魔数与Class文件的版本

每个Class文件的头4个字节称为魔数,它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件储存标准中都使用魔数来进行身份识别,譬如图片格式,如 gif 或者 jpeg 等在文件头中都存有魔数。

Class 文件的魔数值为:0xCAFEBABE(咖啡宝贝)

紧接着魔数的4个字节储存的是Class文件的版本号:第5和第6个字节是次版本号,第7和第8个字节是主版本号。Java版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1(JDK 1.0 ~ 1.1 使用了 45.0 ~ 45.3 的版本号),

高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的 Class 文件。

1.3 常量池

紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一。

常量池主要存放两大类常量:字面量和符号引用。字面量比较接近Java语言层面的常量概念,如文本字符串,声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:

  • 1.类和接口的全限定名
  • 2.字段的名称和描述符
  • 3.方法的名称和描述符

Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法,字段的最终内存布局信息,因此这些字段,方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析,翻译到具体的内存地址中。

1.4 访问标志

在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstrack类型;如果是类的话,是否被声明为final等。

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

类索引(this_class)和父类索引(super_class)都是一个u2 类型的数据,而接口索引集合(interfaces)是一组 u2 类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的弗雷的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object 外,所有 Java 类的父类索引都不为 0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口就按implements语句(如果这个类本身是一个接口,则应当是 extends 语句)后的接口顺序从左到右排列在接口索引集合中。

1.6 字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。

字段中包括的信息:字段的作用域(public、private、protected修饰符)、是类级变量还是实例级变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符)、可否序列化(reansient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称等。在这些信息中,各个修饰符都是布尔值,要么有,要么没有。而字段叫什么名字、字段被定义成什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。

1.7 方法集合

Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构如同字段表一样依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项,方法里的 Java 代码,经过编译器编译成 字节码 指令后,存放在方法属性表集合中一个名为 “Code” 的属性里面。

与字段表集合相对应的,如果父类方法在子类中没有被重写,方法集合中就不会出现来自父类的方法信息。但有可能会出现由编译器自动添加的方法,最典型的便是类 构造器 “<clinit>”方法和实例构造器“<init>”方法。

1.8 属性表集

属性表(attribute_info)在前面的讲解之中已经出现过数次,在 Class 文件、字段表、方法表都可以携带子机的属性表集合,以用于描述某些场景专有的信息。

1.8.1 Code属性

Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合中,但并非所有的方法都存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性。

1.8.2 Exception属性

Exceptions 属性的作用是列举出方法中可能抛出的受检查异常(Checked Exceptions),也就是方法描述时在 throws 关键字后面列举的异常。

1.8.3 LineNumberTable属性

LineNumberTable 属性用于描述 Java 源码 行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必需的属性,但默认会生成到 Class 文件之中,可以在 javac 中分别使用 -g:none 或 -g:lines 选项来取消或要求生成这项信息。如果选择不生成LineNumberTable 属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。

1.8.4 LocalVariableTable属性

LocalVariableTable 属性用于描述栈帧中局部变量表中的变量与 Java 源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到 Class 文件之中,可以在 Javac 中分别使用 -g:none 或 -g:vars 选项来取消或要求生成这项信息。如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,IDE 将会使用诸如 arg0、arg1 之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不变,而且在调试期间无法根据参数名称从上下文获得参数值。

1.8.5 SourceFile属性

SourceFile 属性用于记录生成这个 Class 文件的源码文件名称。这个属性也是可选的,可以分别使用 javac 的 -g:none 或 -g:source 选项来关闭或要求生成这项信息。在 Java 中,对于大多数的类来说,类名和文件名是一致的,但是又一些特殊情况(如内部类)例外。如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。

1.8.6 ConstantValue属性

ConstantValue 属性的作用是通知虚拟机自动为静态变量赋值。只有被 static 关键字修饰的变量(类变量)才可以使用这项属性。类似 “int x = 123” 和 “static int x = 123” 这样的变量定义在 Java 程序中是非常常见的事情,但虚拟机对这两种变量赋值的方式和时刻都有所不同。对于非 static 类型的变量(也就是实例变量)的赋值是在实例构造器 <init> 方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器 <clinit> 方法中或者使用 ConstantValue 属性。目前 Sun Javac 编译器的选择是:如果同时使用 final 和 static 来修饰一个变量(按照习惯,这里称 “常量” 更贴切),并且这个变量的数据类型是基本类型或者 java.lang.String 的话,就生成 ConstantValue 属性来进行初始化,如果这个变量没有被 final 修饰,或者并非基本类型及字符串,则将会选择在 <clinit> 方法中进行初始化。

1.8.7 InnerClass属性

InnerClass 属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成 InnerClass 属性。

1.8.8 Deprecated及Synthetic属性

Deprecated 和 Synthetic 两个属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。

Deprecated 属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通过在代码中使用 @deprecated 注释进行设置。

Synthetic 属性代表此字段或者方法并不是由 Java 源码直接产生的,而是由编译器自行添加的,在 JDK 1.5 之后,标识一个类、字段或者方法是编译器自动产生的,也可以设置它们访问标志中的 ACC_SYNTHETIC 标志位,其中最典型的例子就是 Bridge Method。所有由非用户代码产生的类、方法及字段都应当至少设置 Synthetic 属性和 ACC_SYNTHETIC 标志位中的一项,唯一的例外是实例构造器 “<init>” 方法和类构造器 “<clinit>” 方法。

1.8.9 StackMapTable属性

StackMapTable 属性在 JDK 1.6 发布后增加到了 Class 文件规范中,它是一个复杂的变长属性,位于 Code 属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。

1.8.10 Signature属性

Signature 属性在 JDK 1.5 发布后增加到了 Class 文件规范之中,它是一个可选的定长属性,可以出现于类、属性表和方法表结构的属性表中。在 JDK 1.5 中大幅增强了 Java 语言的语法,在此之后,任何类、接口、初始化方法或成员的 泛型 签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则 Signature 属性会为它记录泛型签名信息。

之所以要专门使用这样一个属性去记录泛型类型,是因为 Java 语言的泛型采用的是擦除法实现的伪泛型,在字节码(Code 属性)中,泛型信息编译(类型变量、参数化类型)之后都通通被擦除掉。使用擦除法的好处是实现简单(主要修改 Javac 编译器,虚拟机内部只做了很少的改动)、非常容易实现 backport,运行期也能够节省一些类型所占的内存空间。但坏处是运行期就无法像 C# 等有真泛型支持的语言那样,将泛型类型与用户定义的普通类型同等对待,例如运行期做反射时无法获得泛型信息。Signature 属性就是为了弥补这个缺陷而增设的,现在 Java 的反射 API 能够获取泛型类型,最终的数据来源也就是这个属性。

1.8.11 BootstrapMethods属性

BootstrapMethods 属性在 JDK 1.7 发布后增加到了 Class 文件规范之中,它是一个复杂的变长属性,位于类文件的属性表中。这个属性用于保存 invokedynamic 指令引用的引导方法限定符。

尾声

今天的文章,比较偏理论化。但是不得不承认的是,JVM的内容,本身就是理论化较重的内容。但是!!一定要注意的一定,纯靠背这种理论化的内容,没有任何意义!!

要结合具体的业务,代码去理解。比如反射,动态代理等等…

关于JVM的后续内容,会在下篇文章娓娓道来。

文章来源:智云一二三科技

文章标题:是时候了解一波:Java虚拟机了

文章地址:https://www.zhihuclub.com/184597.shtml

关于作者: 智云科技

热门文章

网站地图