按照《java虚拟机规范SE7》章节顺序整理的笔记。
目录:
- 运行时常量池
- 虚拟机启动
- 创建和加载
- 链接
- 初始化
- 绑定本地方法实现
- Java虚拟机退出
第四章:加载,链接与初始化
java虚拟机加载的对象当然是针对class文件(字节码文件),这个文件的构成以及编译器的编译过程,在前面三章已经有了很全面的介绍。
加载的工作是将二进制的字节码文件加载如虚拟机中。
链接的作用当然是为了让这个将要被加载的类或者接口符合java虚拟机的执行的要求。它有验证,准备,解析这三个阶段。从名称上便能知道,这些词中都是一种“准备”工作。
初始化,“人如其名”,作用当然是对加载的类进行一些必要的初始化工作,这工作主要体现在调用类的初始化方法 < clinit > 上。
注:这里的< clinit >方法与实例初始化方法< init> 以及默认初始化方法之间的区别,这些区别在这里我就先不解释,如果感兴趣,可以去查看相关资料。
<1>. 运行时常量池
运行时常量池中存储了类或接口中所有的常量,这些常量包括了,类,方法,字段,静态变量的值,等等所有类中的信息。
这个常量池中有很多符号引用指向其他的常量池项,但最终都会指向一个终结项,这些有着确切具体的值。
运行时常量池与解析过程有着密切的关系,在解析过程会大量与常量池接触,因为这个过程就是将具体的操作数与常量池的值进行对照解析的过程。
<2>. 虚拟机启动
虚拟机的启动有如下几步:
- 虚拟机的引用类加载器创建一个初始化类。
- 调用初始化类的main()方法。
- 通过main()方法,执行其他的任务。
注:初始化类也可以通过虚拟机参数传入。
<3>. 创建和加载
这里先重点提醒一下,数组在加载过程中的特殊情况。因为数组类型没有外部二进制文件,它们都是在虚拟机内部加载的,而不是通过加载器。
什么情况会触发类加载:
虚拟机不会无缘无故加载一个类,既然要加载,那么说明程序需要它,也就说一定在某个地方触发了类的加载,这个触发可以有如下一些方式(注: 并不全):
- 由另外一个类引用了这个类而出发。
- 使用了java类库中的一些方式,如反射。
两种类加载器:
类加载器的作用就是获得待加载类的二进制文件流。
类加载器有两种:
- 引导类加载器。
- 自定义类加载器。
应该很好猜到,第一种加载器是虚拟机内部自带的。
每个用户自定义的类加载器,都必须是ClassLoader类的子类。它是为了扩展java虚拟机自带的引导类加载器的。
类加载器的类的来源不仅仅只能从class文件中获取,也可以有很多种方式,如网络传输过来的文件,jar包,动态生成等等。
哪个加载器加载了这个类,就可以说,这个加载器是这个类的定义加载器。如果是通过加载器委托给别的加载器,那么这个加载器是这个类的委托加载器。
类或接口的唯一性是通过类或接口的名称以及定义加载器共同决定,只有名称以及定义加载器均相同这个两个类或接口才相同。
类加载器会递归调用加载它的父类
<4>. 链接
链接的作用是对class文件的格式,结构,内容等进行验证,并做一下初始化准备工作。它是为了虚拟机正确执行类而做准备。
链接有三个步骤:
- 验证
- 准备
- 解析
验证: 验证是对class文件的结构进行校验,是为了保证结构上的正确性。他的顺序是有讲究的,在解析之前,必须保证验证与准备工作正确进行。
准备: 准备是对类或接口的静态字段分配空间。注意,这里只是分配空间,而赋值也只是赋默认值,除非这个静态字段是final修饰的。这个阶段不会执行字节码文件的字节码指令。在初始化阶段才会真正赋值。
解析: 解析是根据运行时常量池的符号引用来动态决定具体的值的过程。解析,有一种翻译的味道。
解析与字节码指令密切相关,而这些指令都方在方法的code属性中,当执行的指令的操作数是常量池的符号引用时,解析边开始将这些符号引用解析成具体的值。这些符号引用的对象可以是:
- 类或接口
- 字段
- 普通方法
- 接口方法
- 方法类型与方法句柄
- 调用点限定符
<5>. 初始化
初始化的作用就是对类或接口中的静态字段进行赋值,并调用类或接口中的静态代码块。
具体实现便是调用类或接口的类初始化方法 < clinit > ,这个方法手机了类或接口中的静态字段赋值,静态代码块。
什么时候类或接口会被初始化:
- 在执行下列需要引用类或接口的Java虚拟机指令时:new,getstatic,putstatic或invokestatic。这些指令通过字段或方法引用来直接或间接地引用其它类。执行上面所述的new指令,在类或接口没有被初始化过时就初始化它。执行上面的getstatic,putstatic或invokestatic指令时,那些解析好的字段或方法中的类或接口如果还没有被初始化那就初始化它。
- 在初次调用java.lang.invoke.MethodHandle实例时,它的执行结果为通过Java虚拟机解析出类型是2(REF_getStatic)、4(REF_putStatic)或者6(REF_invokeStatic)的方法句柄。
- 在调用JDK核心类库中的反射方法时,例如,Class类或java.lang.reflect包。
- 在对于类的某个子类的初始化时。
- 在它被选定为Java虚拟机启动时的初始类(§5.2)时。
注意:在类或接口被初始化之前,它必须被链接过,也就是经过验证、准备阶段,且有可能已经被解析完成了。
==初始化的多线程问题:==由于java虚拟机是支持多线程的,所以要确保一个类在初始化的时候,不会有另外一个线程初始化或使用这个类。这个问题的解决是由锁机制完成的。
<6>. 绑定本地方法实现
绑定是指将使用Java之外的语言编写的函数集成到Java虚拟机中的过程。此函数需要实现在代码中定义好的native方法,之后才可以在Java虚拟机中运行。这个过程在传统编译原理的表述中被称“链接”,所以规范里使用“绑定”这个词就,就是为了避免与Java虚拟机中链接类或接口的语义发生冲突。
<7>. Java虚拟机退出
Java虚拟机的退出条件一般是:某些线程调用Runtime类或System类的exit方法,或是Runtime类的halt方法,并且Java安全管理器也允许这些exit或halt操作。
除此之外,在JNI(Java Native Interface)规范中还描述了当使用JNI API来加载和卸载(Load & Unload)Java虚拟机时,Java虚拟机的退出过程。