JVM学习(七)类加载器
类加载
考点
- 类的加载的生命周期有哪些?
- 加载过程具体流程?加载触发的时机?
- 加载过程的Class对象是否唯一还是随创建方法数量而定?
- 连接过程具体流程?连接触发的时机?
- 类的静态变量赋值经历的过程?
- 类的初始化具体流程?初始化触发的时机(何为主动加载/被动加载)?
- <clinit>方法怎样才会导致静态变量值覆盖?
- 为什么单例安全模式会使用静态单例方法?
- 什么是双亲委派机制?
- SPI是怎么打破双亲委派机制的?
- 何时进行类的加载?
类加载的七个周期

针对类的加载时机,jvm没有强制的约束,jvm堆类的初始化由明确且严格的规定,当然执行类的初始化说明类的加载和连接需要在此前开始。
七个周期只是保证了开始时间的固定顺序,并没有保证结束时间,也没有保证各阶段是独立进行的。
加载
加载过程需要完成以下三件事情
- 通过一个类的全限定名来获取其定义的二进制字节流。
- 将这个字节流转化为方法区的运行时数据结构。(要通过验证阶段才可以)
- 在Java堆中生成一个代表这个类的java.lang.Class对象(唯一),作为对方法区中这些数据的访问入口。

它没有指明二进制字节流必须要从class文件中获取,因此可以从zip包,网络,运行时动态生成,数据库中读取都可以,这也是加载过程最大的特点
连接
连接过程具体流程
验证-》准备-》解析
验证
保证class字节流中包含的信息符合jvm规范要求,包括(平时点运行就报错就是验证通不过):
- 文件格式验证:保证能正常解析和储存方法区,只有通过该验证,才能进入方法区存储,后面的三个验证都是基于方法区上的结构进行。
- 元数据验证:类的元数据信息(语法级别)验证,如父类是否可以继承,方法重载是否满足
- 字节码验证:方法体(语法级别)验证,如操作数类型是否保持一致,跳转指令是否不会跳转到方法体以外的字节码指令上
- 符号引用验证:对类自身以外的信息进行匹配性校验,即检查符号引用的类或方法是否能找到,以及符号引用的类或方法可访问性是否可被当前类访问
验证阶段是一个非常重要,却不是必须执行的阶段。如果你能保证自己的代码不会被篡改且正确,可以在生产环境使用-Xverify:none来关闭检查。因为验证工作在类加载过程占了相当大的比重
准备
为类的静态变量(即被static修饰的变量)在方法区分配内存,并赋默认初值(0值或null值)。
如static int a = 100;(不会执行静态代码块,因为不会执行语句)

如果存在final的类变量,则会在此时赋值,不会需要类的初始化的时候再赋值
解析
它是将类的二进制数据中的符号引用换为直接引用。
在解析阶段,虚拟机会把类或接口,字段,类方法,接口方法,方法类型,方法句柄,调用点限定符这七类符号引用替换为具体的内存地址或偏移量,也就是直接引用(因为你写代码的时候并不清楚内存结构)
如果出现NoXXX找不到类找不到方法错误,就是这个阶段抛出的。
类的初始化
这时候,虚拟机才真正开始执行类中编写的java程序代码
- 类构造器<clinit>由所有类变量(静态变量)的赋值动作和静态代码块的语句合并产生,合并的顺序与代码的编写顺序一致。
- 静态变量的值先,静态语句块的值后,导致被覆盖
- 静态语句块值先,静态变量的值后,导致静态语句块非法前向引用
- jvm保证子类<clinit>执行前,父类<clinit>已执行(也就是主动引用中的一种)
- JVM保证该方法是线程安全,因此可以用来实现静态单例
初始化时机(也叫主动引用和被动引用)
- 遇到new,getstatic,putstatic,invokestati这四条字节码指令时
- 使用new关键字实例化对象时
- 读取或设置一个静态字段(非final)时
- 调用一个静态方法时
- 使用java.lang.reflect包的方法进行反射调用的时候
- 初始化类时,如果发现父类没有初始化则需要先触发父类的初始化
被动引用(不会触发<clinit>方法)
- final修饰的静态常量(连接过程中就会执行赋值并存入常量池中)。
- 通过数组定义来引用,Obj[] array = new Obj[10],不会触发Obj的初始化,数组类本身不通过类加载器创建,他是直接在内存中动态创建
- 当访问一个静态域时,只有真正声明这个域的类才会被初始化,如子类访问父类的静态变量,子类不会初始化,父类会初始化
使用
使用既是所需要的对象开始被调用。
卸载
对象被jvm回收,JVM中的class满足一下3个条件才能被GC回收
- 该类所有实例已经被GC
- 加载该类的ClassLoader实例被GC
- 该类的java.lang.class对象没有被引用
GC的时机是不可控的,同样对于class的卸载也是不可控的
类加载器ClassLoader
- 任务:根据一个类的全限定名来读取此类的二进制字节流到JVM中,是类加载的第一阶段。
- 特点:每个类加载器由自己特有的加载路径,因此不同位置的包需要不同的加载器
类架构体系:三层类加载器,双亲委派机制

三层类加载器时系统内置的,无法进行new,只能通过getClassLoader获取
ClassLoader cl = Object.class.getClassLoader();
父委托机制/双亲委托机制
某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。(先↑再↓的过程)
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) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出ClassNotFoundException
// 说明父类加载器无法完成加载请求
}
if (c == null) {
// 如果父类加载器无法加载时
// 再调用本身的findClass来进行类加载
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;
}
}
SPI打破双亲机制
首先要理解双亲机制为什么需要被打破?
双亲委派机制很好的维护了基础类型一致性问题,保证像String,Object这样的类不会被第三方恶意篡改。然而没有完美的事情,双亲委派模型,父加载器是拿不到通过子加载器加载的类的,也就意味着父类加载器加载的接口,没有办法拿到子类加载器加载的实现类。(当接口是Bootstrap ClassLoader,子类是Application ClassLoader)
举一个例子就是
Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。
因此java设计团队提供了一个线程上下文类加载器,该类加载器可以通过java.lang.Thread.setContextClassLoader()设置,默认是Application ClassLoader,父类加载器会先通过ContextClassLoader(),如果没有实现类,再执行自己的加载。该过程就叫做打破双亲委托机制。

委派给默认系统类加载器的时候系统类加载器会不会重新向上委托?
会,但是此时它需要加载的用户自定义类,父加载器加载不了的
代码层面理解
打破双亲委托机制在父类加载器使用下面函数进行类加载
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public final class ServiceLoader<S> implements Iterable<S>{
private static final String PREFIX = "META-INF/services/";
// 代表被加载的类或者接口
private final Class<S> service;
// 用于定位,加载和实例化providers的类加载器
private final ClassLoader loader;
// 创建ServiceLoader时采用的访问控制上下文
private final AccessControlContext acc;
// 缓存providers,按实例化的顺序排列
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 懒查找迭代器
private LazyIterator lookupIterator;
......
}
- 应用程序调用ServiceLoader.load方法 ServiceLoader.load方法内先创建一个新的ServiceLoader,并实例化该类中的成员变量,包括:
- loader(ClassLoader类型,类加载器)
- acc(AccessControlContext类型,访问控制器)
- providers(LinkedHashMap<String,S>类型,用于缓存加载成功的类)
- lookupIterator(实现迭代器功能)
- 应用程序通过迭代器接口获取对象实例 ServiceLoader先判断成员变量providers对象中(LinkedHashMap<String,S>类型)是否有缓存实例对象,如果有缓存,直接返回。 如果没有缓存,执行类的装载,实现如下:
- (1)读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称,值得注意的是,ServiceLoader可以跨越jar包获取META-INF下的配置文件,具体加载配置的实现代码如下:
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
- (2) 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化。
- (3) 把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型) 然后返回实例对象。
相当于IOC容器,通过配置进行加载;
相当于自定义了一个类加载器,在双亲委派机制之上通过特定的规则去加载;

浙公网安备 33010602011771号