一.类加载时机
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking),这7个阶段的发生顺序如下图所示:
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(动态绑定)。
Java虚拟机规范没有对什么时候开始执行加载做强制约定,但是对初始化阶段严格规定了有且只有以下5种情况下必须对类进行初始化:
- 1.遇到:new、getstatic、putstatic、invokestatic指令时,如果类尚未初始化,那就要进行初始化。这四个指令对应的Java代码场景是:
通过new创建对象;
读取、设置一个类的静态成员变量(不包括final修饰的静态变量);
调用一个类的静态成员函数。
- 2.使用
java.lang.reflect
进行反射调用的时候,如果类没有初始化,那就需要初始化; - 3.当初始化一个类的时候,若其父类尚未初始化,那就先要让其父类初始化,然后再初始化本类;
- 4.当虚拟机启动时,虚拟机会首先初始化带有
main方法
的类,即主类; - 5.当使用 JDK.7 的动态语言支持时,如果一个
java.lang.invoke.MethodHandle
实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
注意:
对于静态字段,只有直接定义这个字段的类才会被初始化。
通过数组定义引用类,不会触发此类的初始化。
常量在编译阶段会存入调用类的常量池,因此不会触发定义类的初始化。
//父类
public class SuperClass{
static{
System.out.println("SuperClass init!");
}
public static int value = 1;
}
//子类
public class SubClass extends SuperClass{
static{
System.out.println("SubClass init!");
}
}
//只会输出SuperClass init!,也就是说子类没有被初始化。
public static void main(String[] args) {
SubClass.value;
}
//不会输出任何的内容,被动引用不会触发此类的初始化。
public static void main(String[] args) {
SuperClass scas = new SuperClass[8];
}
public class ConstClass{
static{
System.out.println("ConstClass init!");
}
public static final int VALUE = 1;
}
//不会输出内容,常量在编译阶段直接存入常量池中,因此不会触发定义类的初始化。
public static void main(String[] args) {
ConstClass.VALUE;
}
对于接口而言,只有类的第3种情况有区别:一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。
二.类加载过程
类加载过程就是加载、验证、准备、解析和初始化这5个阶段所执行的具体动作。
1.加载
- 通过一个类的全限定名来获取定义此类的二进制字节流。
JVM规范没有限制从本地读取.class文件,所以可以从jar包或者其他方式来读取;
最终会返回ClassFileStream对象的指针。(也可以运行时计算生成,比如动态代理技术。Proxy)
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
获取到指针后,会创建实例,并且解析ClassFileStream结构:
1.读取魔数并校验 校验jdk版本信息
2.读取常量池引用 包括常量和符号应用,符号引用指父类,接口,字段,放法等
3.读取访问标识并校验 类的类型class还是interface,访问类型public 还是抽象的
4.获取类的全限定名 读取当前类的索引,并在常量池中找到当前类的全限定名,读取常量池时,会获得常量池句柄,会标识全限定名的地址
5.获取父类或者接口信息,如果有继承或者实现,则需先加载父类和接口,如果已经加载则直接获取它们的句柄记录到本类中,并简单校验类名
6.读取字段信息和方法信息加载到本类的信息中,之后会评估类的大小,类加载完成之后,大小不会发生改变
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口。
1.计算虚拟函数表和接口函数表的大小
2.创建instanceKlass对象(.class文件对应的所有类信息)
3.创建Java镜像类并初始化静态域,通知JVM加载完成,方法区创建该类的元数据
注意:对于数组而言,数组本身不通过类加载器创建,它是由Java虚拟机直接创建的。只有数组的元素类型是最终是靠类加载去创建。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义,虚拟机规范未规定此区域的具体数据结构。然后在内存中实例化一个java.lang.Class
类的对象(并没有明确规定是在Java堆中,对于HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但是存放在方法区里面),这个对象将作为程序访问方法区中的这些类型数据的外部接口。
2.验证
这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求。
比如:将一个对象转型为它并未实现的类型、跳转到不存在的代码行之类的事情,如果这样做了,编译器将拒绝编译。但是Class文件并不一定要求用Java源码编译而来,可以使用任何途径产生,甚至包括用十六进制编辑器直接编写来产生Class文件。在字节码语言层面上,上述Java代码无法做到的事情都是可以实现的。虚拟机如果不检查输入的字节流,很可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。
加载和验证是交叉进行的,验证在各个阶段都是存在的,验证二进制字节流代表的字节码文件是否合格,主要从一下几方面判断:
文件格式:参看class文件格式详解,经过文件格式验证之后的字节流才能进入方法区分配内存来存储。
元数据验证:是否符合java语言规范。
字节码验证:数据流和控制流的分析,这一步最复杂。
符号引用验证:符号引用转化为直接引用时(解析阶段),检测对类自身以外的信息进行存在性、可访问性验证。
3.准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,public static int value = 8;
变量value在准备阶段过后初始值为0而不是8,因为此时还为开始执行任何的Java方法。
4.解析
解析阶段是虚拟机将常量池内的符号引用(存在class文件中)替换为直接引用的过程。包括类,接口,字段,类方法和接口方法的解析。
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那么引用的目标一定是已经存在于内存中。
5.初始化
类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。
初始化阶段是执行类构造器
类构造器()方法是由编译器自动收藏类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生。
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步。
三.类加载器
通过一个类的全限定名来获取描述此类的二进制字节流,实现这个动作的代码模块称为“类加载器”。
1.类与类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。每一个类加载器,都拥有一个独立的类名称空间。也就是说,比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
类加载器分类
- 启动类加载器
负责将存放在
<JAVA_HOME>/lib
目录中的,或者被-Xbootclasspath
参数所指定的路径中的,并且是虚拟机按照文件名识别的(如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。
- 扩展类加载器
由
sun.misc.Launcher$ExtClassLoader
实现。负责加载<JAVA_HOME>/lib/ext
目录中的,或者被java.ext.dirs
系统变量所指定的路径中的所有类库。开发者可以直接使用扩展类加载器。
- 应用程序类加载器
由
sun.misc.Launcher$AppClassLoader
实现。由于这个类加载器是ClassLoader.getSystemClassLoader()
方法的返回值,所以一般也称它为系统类加载器。
它负责加载用户类路径ClassPath
上所指定的类库,开发者可以直接使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
- 自定义的类加载器
JVM建议用户将应用程序类加载器作为自定义类加载器的父类加载器。
2.双亲委派机制
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。
“双亲”只是“parents”的直译,实际上并不表示汉语中的父母双亲,而是一代一代很多parent,即parents。
双亲委派模型的工作过程是:
- 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。
- 每一个层次的类加载器都是如此。因此,所有的加载请求最终都应该传送到顶层的启动类加载器中。
- 只有当父加载器反馈自己无法完成这个加载请求时(搜索范围中没有找到所需的类),子加载器才会尝试自己去加载。
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。
例如类`java.lang.Object`,它存放在`rt.jar`之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载;
因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了
一个称为`java.lang.Object`的类,并放在程序的`ClassPath`中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,
应用程序也将会变得一片混乱。
自己编写的重名的类可以正常编译,但是永远无法运行。