概述
一个类的生命周期如下图,箭头所指的是阶段的开始顺序,并非是一个阶段结束才执行下一个阶段。
类加载的过程指的是,加载、验证、准备、解析和初始化这五个阶段
类加载的时机
对于第一阶段加载(Loading)是由虚拟机实现的,是不确定的。
对于初始化的时机有且只有以下 6 种:
-
遇到 new、getstatic、putstatic 或 invokestatic 这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。
- 使用 new 关键字实例化对象时
- 读取或设置一个类型的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)时。
- 调用一个类型的静态方法时。
-
使用 java.lang.reflect 包的方法对类型进行反射调用的时候。
-
当初始化类的时候,如果父类还没有被初始化,则需要先初始化父类。
-
当虚拟机启动,用户需要指定一个需要执行的主类(Main 方法)
-
当使用 JDK7 新加入的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial 四种类型的方法句柄时
-
当接口定义了 JDK8 新加入的默认方法(被 default 修饰的接口方法),如果这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
类加载的过程
加载
在加载阶段虚拟机需要完成以下三件事情:
-
通过一个类的全限定名来获取此类的二进制字节流
- 虚拟机未对如何获取字节流做限制,可以从任意位置获取字节流,如:jar 包、war 包、网络、运行时生成、从数据库获取、从加密文件获取...
-
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 对于非数组类型的即可以使用虚拟机内置的引导类加载器,也可以自定义类加载器。
- 对于数组类型,数字类本身不通过类加载器创建,它是直接在内存中构造出来。
-
在堆内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
在加载阶段结束后,类就按照虚拟机所设定的格式存储在方法区,并会在 Java 堆内存实例化一个 java.lang.Class 类的对象,作为程序访问方法区外部接口。
正如开头所说,加载阶段的执行与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的。
验证
目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求。
验证阶段大致上会完成下面四个阶段的检验动作:
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
准备
准备阶段是为类中定义的静态变量(被 static 修饰的变量)分配内存并设置类变量初始值的阶段。
对于非静态常量(final static)变量,它的初始值都是“零值” 。比如:public static int value = 123; value 的初始值为 0而不是 123。因为这时尚未开始执行任何 Java 方法,而把 value 赋值为 123 的 putstatic 指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把 value 赋值为 123 的动作要到类的初始化阶段才会被执行。
对于常量,在准备阶段则会初始化为属性所制定的初始值。
解析
解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程。这一操作往往会在初始化完成之后再执行
- 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
- 直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。
初始化
初始化阶段就是执行类构造器<clinit>()方法的过程。
<clinit>()方法是在准备阶段由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。收集顺序是由语句在源文件中出现的顺序决定的,静态语句块可以向前访问,不能向后访问(但可以赋值)。
public class Test {
static {
i = 0; // 给变量复制可以正常编译通过
System.out.print(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
<clinit>()方法与加载阶段相同需要先将父类的<clinit>()方法加载完毕,所以第一个被执行<clinit>()方法的类型肯定是 java.lang.Object。所以就是说父类的赋值操作肯定会优先于子类的赋值操作优先执行。
如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
接口也会生成<clinit>()方法(有静态变量初始化的赋值操作)。但是接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当父接口中定义的变量被使用是,父接口才会被初始化。并且接口的实现类在初始化时也不会执行接口的<clinit>()方法。
虚拟机会对<clinit>()方法进行加锁同步处理,用来保证在多线程环境下只能有一个线程执行<clinit>()方法,其他线程都需要阻塞等待<clinit>()方法执行完毕。
当一个类被触发初始化动作后
- 如果类还未被加载,那么先执行加载、链接动作
- 如果类的父类还未被初始化,那么先触发父类的初始化
- 按照构造器方法原文中出现的顺序去顺序执行
JVM的类加载器
类加载器实现的是类加载阶段的“通过一个类的全限定名来获取此类的二进制字节流”这个动作。
在虚拟机中已经提供了三种类加载器,在JDK9之前Java引用都是由这三种类加载器相互配合来完成加载的。用户也可以加入自定义的类加载器来进行扩展
虚拟机提供的类加载器
Bootstrap 引导类加载器
引导类加载器也被称为启动类加载器或根类加载器,都是指的 BootstrapCalssLoader。使用C++实现,主要负责的是<JAVA_HOME>\lib 路径下的核心类库,或者-Xbootclasspath 参数指定路径下的jar包。
注意:Bootstrap引导类加载器是通过全限定名加载类库的,如rt .jar、tools.jar,所以不能加载我们自定义的类库,就算放入lib包下也不会被引导类加载器加载。
Extension 扩展类加载器
这个类加载器是在类sun.misc.Launcher$ExtClassLoader 中以Java代码的形式实现的。它负责加载<JAVA_HOM E>\lib\ext目录中,或者被-Djava.ext.dirs系统变量所指定的路径中所有的类库。
Application 程序类加载器
这个类加载器由sun.misc.Launcher$App ClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSy stem-ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径 (ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
User 自定义类加载器
通过继承实现ClassLoader实现,如下是一个非常简单的自定义类加载器
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1)+".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()]; is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("com.lester.part3.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof com.lester.part3.ClassLoaderTest);
}
}
双亲委派模型
双亲委派模型是在jdk1.2时被引入的,它要求除了顶层的引导类加载器外,其余的加载器都应有自己的父类加载器(父类并不是继承关系,子类一般通过组合的关系来复用父加载器的代码)。 双亲委派模型的工作机制是:当一个类收到了类加载的请求,它首先需要把这个请求委托给父类的加载器去完成,请求会一层一层向上委托,因此所有的加载请求最粽都会传送到顶层的引导类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时(不在加载器的搜索范围内),子加载器尝试自己去完成加载。 双亲委派模型保证了在一个程序中,同一个类始终是由同一个加载器完成加载的。注意:这个点非常重要,因为在java中若要判断两个类是否相等必须要由同一个加载器加载才会相等。例如:java.lang.Object这个是由引导类加载器加载的,若用户自定义了java.lang.Object那么程序中就会出现很多个Object类,程序的安全运行就不能得到保障。在双亲委派模型下,用户自定义java.lang.Object可以正常编译,但永远不会被加载(因为已经存在)。