深入理解Java虚拟机Day3(虚拟机执行子系统)

第6章 类文件结构

6.1 概述

6.2 无关性的基石

  我的理解是,不管什么语言,只要能翻译成.class文件(字节码文件),就可以在虚拟机中运行。

6.3 Class类文件的结构

  Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8个字节进行存储。
1.魔数与Class文件的版本
  每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本(Minor Version),第7和第8个字节是主版本号(Major Version)。Java的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1(JDK 1.0~1.1使用了45.0~45.3的版本号),高版本的JDK能
向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,因为《Java虚拟机规范》在Class文
件校验部分明确要求了即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。
2 常量池
  常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,主要包括下面几类常量:
·被模块导出或者开放的包(Package)
·类和接口的全限定名(Fully Qualified Name)
·字段的名称和描述符(Descriptor)
·方法的名称和描述符
·方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
·动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)
3 访问标志
  在常量池结束之后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final;等等。
4 类索引、父类索引与接口索引集合
  告诉类的继承和实现的关系。
5 字段表集合
  字段表(field_info)用于描述接口或者类中声明的变量。Java语言中的“字段”(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。字段可以包括的修饰符有字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫做什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。(这里管标识,能干什么不能干什么,而常量池里放描述,比如static int i = 1 和static final int j = 2,常量池告诉你有个i和j,并且告诉是int。字段表告诉你是1是否static,2是否static,是否final)
6 方法表集合
  上面字段表是对变量的标识,这个是对方法的标识,包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。因为volatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有了ACC_VOLATILE标志和ACC_TRANSIENT标志。与之相对,synchronized、native、strictfp和abstract关键字可以修饰方法,方法表的访问标志中也相应地增加了ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志。
7 属性表集合
  也就是方法里面的代码了。换句话说,这个就是类里面具体的属性。类中的方法,方法中的代码,等等。这里告诉你static int i = 1 这个i属性的值为1.

  • 1.Code属性  Java程序方法体里面的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性。  Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个Class文件里,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。
  • 2.Exceptions属性  这里的Exceptions属性是在方法表中与Code属性平级的一项属性,不要与前面刚刚提到的异常表产生混淆。Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Excepitons),也就是方法描述时在throws关键字后面列举的异常。
  • 3.LineNumberTable属性  LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必需的属性,但默认会生成到Class文件之中。
  • 4.LocalVariableTable及LocalVariableTypeTable属性  LocalVariableTable属性用于描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到Class文件之中。
  • 5.SourceFile及SourceDebugExtension属性  SourceFile属性用于记录生成这个Class文件的源码文件名称。这个属性也是可选的。
  • 6.ConstantValue属性  ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量(类变量)才可以使用这项属性。对非static类型的变量(也就是实例变量)的赋值是在实例构造器<init>()方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器<clinit>()方法中或者使用ConstantValue属性。目前Oracle公司实现的Javac编译器的选择是:如果同时使用final和static来修饰一个变量(按照习惯,这里称“常量”更贴切),并且这个变量的数据类型是基本类型或者java.lang.String的话,就将会生成ConstantValue属性来进行初始化;如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在<clinit>()方法中进行初始化。(有final和static就是ConstantValue,只有static就是<clinit>(),什么都没有,就是实例构造器<init>())</init></clinit></clinit></clinit></init>
  • 7.InnerClasses属性  InnerClasses属性用于记录内部类与宿主类之间的关联。
  • 8.Deprecated及Synthetic属性  Deprecated和Synthetic两个属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通过代码中使用“@deprecated”注解进行设置。Synthetic属性代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的。
  • 9.StackMapTable属性
  • 10.Signature属性:和泛型有关
  • 11.BootstrapMethods属性
  • 12.MethodParameters属性:MethodParameters是在JDK 8时新加入到Class文件格式中的,它是一个用在方法表中的变长属性。MethodParameters的作用是记录方法的各个形参名称和信息。
  • 13.模块化相关属性
  • 14.运行时注解相关属性

6.4 字节码指令简介

6.5 公有设计,私有实现

6.6 Class文件结构的发展


第7章 虚拟机类加载机制

7.1 概述

7.2 类加载的时机

图片说明
  加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)。请注意,这里是按部就班地“开始”,而不是按部就班地“进行”或按部就班地“完成”,强调这点是因为这些阶段通常都是互相交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段。
  什么时候加载,由虚拟机决定,但是,初始化则是有要求。有且仅有以下6种情况。

  • 1.遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。具体情况分以下3种:a.使用new关键字实例化对象的时候。b.读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。c.调用一个类型的静态方法的时候。
  • 2.使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
  • 3.当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 5.当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
  • 6.当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。(java8新增!!!!)

  这六种场景中的行为称为对一个类型进行主动引用。除此之外,所有引用类型的方式都不会触发初始化,称为被动引用。比如:引用某个类的final定义的常量,则不会触发该类的初始化。或者你访问一个静态字段,也不会导致某个类初始化。

7.3 类加载的过程

详细了解加载、验证、准备、解析和初始化这五个阶段所执行的具体动作。
1.加载
  “加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段。在加载阶段,Java虚拟机需要完成以下三件事情:

  • 1)通过一个类的全限定名来获取定义此类的二进制字节流。
  • 2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

说白了,我之前不知道这个类,你就得告诉我这个类,然后我就把这个类该翻译的翻译,该干嘛的干嘛,然后你就可以用了。
  《Java虚拟机规范》对这三点要求其实并不是特别具体,留给虚拟机实现与Java应用的灵活度都是相当大的。例如“通过一个类的全限定名来获取定义此类的二进制字节流”这条规则,它并没有指明二进制字节流必须得从某个Class文件中获取,确切地说是根本没有指明要从哪里获取、如何获取。所以造成了:

  • a.从ZIP压缩包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础。
  • b.从网络中获取,这种场景最典型的应用就是Web Applet。
  • c.由其他文件生成,典型场景是JSP应用,由JSP文件生成对应的Class文件。等等

  稍微值得注意的时,一般都是使用系统提供的类加载器完成加载动作,而数组类是java虚拟机直接创建的。并且,生成一个代表这个类的java.lang.Class对象是在堆里面的(第三版这么说的,而第二版说HotSpot是在方法区建立的class对象)
2 验证
  验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚
拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
  Java语言本身是相对安全的编程语言(起码对于C/C++来说是相对安全的),使用纯粹的Java代码无法做到诸如访问数组边界以外的数据、将一个对象转型为它并未实现的类型、跳转到不存在的代码行之类的事情,如果尝试这样去做了,编译器会毫不留情地抛出异常、拒绝编译。但前面也曾说过,Class文件并不一定只能由Java源码编译而来,它可以使用包括靠键盘0和1直接在二进制编辑器中敲出Class文件在内的任何途径产生。上述Java代码无法做到的事情在字节码层面上都是可以实现的,至少语义上是可以表达出来的。Java虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有错误或有恶意企图的字节码流而导致整个系统受攻击甚至崩溃,所以验证字节码是Java虚拟机保护自身的一项必要措施。
几种常见的验证方式

  • a.文件格式验证:第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。比如:主、次版本号是否在当前Java虚拟机接受范围之内。或者是否以魔数0xCAFEBABE开头。等等。(我只懂中文,你要和我说中文)
  • b.元数据验证:第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求。(也就是语法格式要正确。你不仅要和我说中文,你还要按照中文的语法和我说)比如:
  • c.字节码验证:第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型校验完毕以后,这阶段就要对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。(按照中文语法说话后,我就可以翻译你说的话是什么意思,有没有骂人)
  • d.符号引用验证:最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。(就和中文里,有些话要联系上下文,比如他很牛这句话,这个他就是引用,看看之前聊了什么,才知道他是什么,找到所有的上下文,看看是否缺了些东西,免得引起歧义)

3.准备
  准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段,从概念上讲,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区本身是一个逻辑上的区域,在JDK 7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;而在JDK 8及之后,类变量则会随着Class对象一起存放在Java堆中,这时候“类变量在方法区”就完全是一种对逻辑概念的表述了。
  这里的类变量主要是静态,而不是常量和实例变量。常量更早。

public static int value = 123;
public static final int value1 = 12;

  变量value在准备阶段过后的初始值为0而不是123,因为这时尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为123的动作要到类的初始化阶段才会被执行。这里只把内存划分出来
  编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据Con-stantValue的设置将value1赋值为12。
4 解析
  解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。(就好比,你用文字表示,a节点和b节点是相连的。而我真的就用用根线把两个节点连起来了)
主要有:1.类或接口的解析。2.字段解析。3.类方法解析。4.接口方法解析
5 初始化
  之前都是准备工作。直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码。初始化阶段就是执行类构造器<clinit>()方法的过程。<clinit>()并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物。
一些细节:
1.<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问.</clinit>

public class Test {
    static {
    i = 0; // 给变量复制可以正常编译通过
    System.out.print(i); // 这句编译器会提示“非法向前引用”
    }
    static int i = 1;
}

2.<clinit>()方法与类的构造函数(即在虚拟机视角中的实例构造器<init>()方法)不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。
3.由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作.

static class Parent {
public static int A = 1;
    static {
    A = 2;
    }
}
static class Sub extends Parent {
    public static int B = A;
}//B是等于2的。Parent先初始化了

4.一个类中如果没有静态语句,就不会调用这个类构造器方法。
5.接口中不能使用静态语句块,但有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化。此外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
6.clinit方法是加锁了的。</clinit>

全部评论

相关推荐

自来熟的放鸽子能手面...:这个不一定,找hr跟进一下
点赞 评论 收藏
分享
10-17 13:54
上海大学 运营
雾凇岛:这还说什么了,冲了兄弟们
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务