类加载机制(五)
类的生命周期
类的加载过程
CLass文件需要加载到虚拟机中才能运行和使用,那么虚拟机是如何加载这些Class文件呢?
系统加载Class类型的文件主要三步:加载、连接、初始化。连接过程有可分为验证、准备、解析。
加载(Loading)
类加载过程的第一步,主要完成一下三个事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证(Verification)
确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备(Preparation)
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
- 这时候进行内存分配的仅包括类变量(即静态变量,被static修饰的变量,只与类相关,因此被称为类变量),而不包括实例变量,实例变量会在对象实例化时随这对象一块分配在Java堆中。
- 类变量所使用的的内存都应当在方法区中进行分配在,jdk7之前,Hotspot使用永久代来实现方法区的时候,实现是完全符合这种逻辑概念的。而在jdk7之后,原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着Class对象一起存放在Java堆中。
- 这里设置的初始值“通常情况下”是数据类型的默认的零值,特殊情况,如果给变量加上了final关键字修饰,那么准备阶段value的值就被赋值为给定的值。
解析(Resolution)
将常量池的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。
符号引用:一组符号来描述目标,可以是任何字面量。
直接引用:就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持Java的动态绑定。
初始化(Initialization)
初始化阶段才真正开始执行类中定义的Java程序代码。初始化阶段是虚拟机执行类构造器方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序指定的主管计划区初始化类变量和其他资源。
类会初始化的情况:
- main方法所在的类,总会被首先初始化
- 首先访问这个类的静态变量或静态方法时
- 子类初始化如果父类没有初始化,会对父类进行初始化
- 子类访问父类的静态变量,只会触发父类的初始化、
- Class.forName()方法会初始化
- new会导致初始化
不会导致类初始化的情况:
- 访问类的static final静态变量变量不会触发初始化
- 类对象.class不会触发初始化
- 创建该类的数组不会触发初始化
- 类加载器的loadClass方法
类加载器
类加载器的分类
JVM中内置了三个重要的ClassLoader,除了BotstrapClassLoader其他类加载器均由Java实现且全部继承自java.lang.ClassLoader:
- BootstrapClassLoader(启动类加载器):最顶层的加载类,有C++实现,负责加载%JAVA_HOME%/lib目录下的jar包和类或者被-Xbootclasspath参数指定的路径中的所有类。
- ExtensionClassLoader(扩展类加载器):主要负责加载%JAVA_HOME%/lib/ext目录下的jar包和类,或被java.ext.dire系统变量所指定的路径下的jar包。
- APPClassLoader(应用程序类加载器):面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。
类加载器的优先级(从高到低):启动类加载器->扩展类加载器->应用程序类加载器->自定义类加载器。
双亲委派模型
图中展示了类加载器之间的层次关系,称为 双亲委派模型(Parents DeleGation Model)。该模型要求除了顶层的启动类加载器外,其他的类加载器都要有自己的父类加载器。这里的父子关系一般通过组合关系(Composition)来复用父加载器的代码,而不是继承关系(Inheritance)。
双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是会把这个请求委派给父类加载器区完成,每一个层次的类加载器都是如此,所以最终会传送到顶层的启动类加载器中,只有当父加载器反馈自己无法加载这个加载请求时(它的搜索范围中没有找到所需的类),子加载器才会尝试自己去加载。
双亲委派模型实现源码分析
双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在 java.lang.ClassLoader
的 loadClass()
中,相关代码如下所示。
private final ClassLoader parent;
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查请求的类是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {//父加载器不为空,调用父加载器loadClass()方法处理
c = parent.loadClass(name, false);
} else {//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//抛出异常说明父类加载器无法完成加载请求
}
if (c == null) {
long t1 = System.nanoTime();
//自己尝试加载
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
使用双亲委派模型的好处
双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载(JVM区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了Java的核心API不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为java.lang.Object类的话,那么程序运行的时候,系统就会出现多个不同的Object类。
自己定义加载器的话,需要继承ClassLoader。如果我们不想打破双亲委派模型,就重写CLassLoader类中的findClass()方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型,则需要重写loadClass()方法。
参考文章:
深入理解Java虚拟机第三版-周志明