类加载的时机
一个类从加载到虚拟机中到卸载,它的生命周期要经历以下7个阶段:加载、验证、准备、解析、初始化、使用、卸载。,其中验证、准备、解析合称为链接。如下图所示
虚拟机规范中严格规定了有且只有以下六种情况,若类没进行初始化,虚拟机才会对类进行初始化(当然前面的步骤必须先执行,前面几个阶段并没有明确规定什么时候执行)。
- 遇到new 、getstatic、putstatic、invokestatic四个字节码指令时,。(即,new 关键字创建对象时、读取或者设置一个静态字段时,被final修饰、已在编译期把结果放入常量池的静态字段除外、调用一个类的静态方法时)
- 使用reflect包的方法对类型进行反射调用时
- 初始化类的时候,其父类没有初始化,需要先初始化其父类
- 虚拟机启动时,main()方法所在的类
- 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解 析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句 柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
- 一个接口中定义了JKD8新加入的默认方法(default修饰的方法),如果有这个接口的实现类发生了初始化,该接口需要在其之前初始化。
上述6种行为被称为对一个类型的主动引用。除此之外所有引用类型的方式都不会被初始化,称为被动引用。
几种被动引用的例子
- 子类引用父类的静态变量,不会导致子类初始化:
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
通过上述程序运行结果,对于静态字段的访问,只有直接定义这个字段的类会被初始化。
通过-XX:+TraceClassLoading 可以发现,子类被加载了,如下图所示。
- 通过数组定义来引用类,不会触发此类的初始化
public static void main(String[] args) {
//System.out.println(SubClass.value);
SuperClass[] scs = new SubClass[10];
}
-
对常量池中常量的引用,不回触发类的初始化
public class ConstClass { static { System.out.println("ConstClass init!"); } public static final String HELLO_WORLD = "hello world"; } public class NotInitialization { public static void main(String[] args) { System.out.println(ConstClass.HELLOWORLD); } }
常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的
类加载的过程
加载
加载的过程主要完成三件事
- 通过类的全限定名获取此类的二进制字节流
- 将这个字节流代表的静态结构转化为运行时方法区的结构。
- 在内存中生存一个该类的Class对象,作为这个类各种数据访问的入口
类的来源可以包括:从zip压缩包中获取、从网络中获取、运行时计算生成(动态代理)、从数据库、加密文件中读取等。
验证
这一步的目的是确保class文件的字节流信息全部符合java虚拟机规范的约束,保证这些信息被运行后不会对java 虚拟机的安全造成影响。
主要包括:文件格式验证、元数据验证、字节码验证、符号引用验证。
准备
准备阶段时正式为类中的定义的变量(即静态变量、被static修饰的变量。)分配内存并设置初始值的阶段。两个主意点:
- 这个时候分配内存的变量仅包括类变量(静态变量),不包括实例变量(类的成员变量)。
- 这里说的初始值,通常指对应类型的零值。
假设一个类变量定义为:
public static int value = 123;
value在准备阶段过后的值为0而不是123.这个阶段尚未执行任何java方法,把value赋值为123的putstatic指令时程序编译后放入类的构造器方法中。因此赋值为123需要在类的初始化阶段进行
如果定义为:
public static final int value = 123;
编译时将会为value生成ConstantValue属性,在准备阶段就会给value赋值123.
解析
解析是java虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是在Class文件中它以CONSTANT_Class_info、 CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现
初始化
初始化阶段,虚拟机才真正开始执行类中编写的java程序代码。初始化阶段就是类执行类构造器()并方法的过程 。()方法不是程序员写的构造方法,而是有编译器自动生成的。父类的()方法必须先于子类的方法执行。
类加载器
Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节 流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动 作的代码被称为“类加载器”(Class Loader)。
对于任意一个类,都必须有加载这个类的类加载器和他本身一起才能确定其在虚拟机中的唯一性。
双亲委派模型
-
启动类加载器
这个类加载器负责加载存放在 <JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够 识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类 库加载到虚拟机的内存中。无法被java程序直接引用
-
扩展类加载器
这个类加载器是在类sun.misc.Launcher$ExtClassLoader 中以Java代码的形式实现的。它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所 指定的路径中所有的类库。可以直接使用
-
应用程序类加载器
应用程序类加载器(Application Class Loader):这个类加载器由 sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystemClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径 (ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加 载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的 加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请 求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。