Java类加载机制
- 类加载器
- 类加载器的执行流程
- 类加载器的种类
- 加载器之间的关系
- ClassLoader 的主要方法
- Class.forName()与ClassLoader.loadClass()区别
- 双亲委派模型
- 双亲委派 类加载流程
- 优缺点
- 热部署简单示例
类加载器
类加载器的执行流程
类加载器的种类
AppClassLoader
应用类加载器,默认的系统类加载器,负责加载java应用种classpath中的类
设置 classpath java -cp D:\aaa Main.class 获取 classpath
System.getProperty(“java.class.path”)
ExtClasLoader
扩展类加载器,负责加载扩展目录中的java类。
设置扩展目录:java -Djava.ext.dirs=“D:\aa” Main.class
获取扩展目录:System.getProperty(“java.ext.dirs”)
从java9开始,扩展机制被移除,加载器被PlatFormClassLoader取代
BootStrapClassLoader
启动类加载器,负责加载JDK核心类库(/jre/lib)
由 C++/C 语言编写,无法通过java代码打印
用户自定义的ClassLoader
继承抽象类 ClassLoader 的自定义类加载器
加载器之间的关系
从JVM的角度来看,一共有两种类加载器,BootStrapClassLoader 和 java 自定义的类加载器
其中 BootStrapClassLoader 是使用C/C++语言编写的 最高级别的 类加载器
Java 自定义的类加载器 都继承于抽象类 ClassLoader ,并且内部有一个parent 属性,指向父加载器(是组合,不是继承)
ExtClassLoader 的 parent属性 是null,实际是通过native方法 关联 BootStrapClassLoader ,所以ExtClassLoader 的父类加载器 是 BootStrapClassLoader
JDK自带的类加载器有:BootStrapClassLoader,ExtClassLoader,AppClassLoader
三者并不是集成关系,而是组合
用户可以继承ClassLoader 来实现自己的类加载器,默认的父类加载器是 AppClassLoader
ClassLoader 的主要方法
//返回该类加载器的超类加载器
public final ClassLoader getParent()
//加载名称为name的类,返回结果为java.lang.Class类的实例。如果找不到类,
//则返回 ClassNotFoundException异常。该方法中的逻辑就是双亲委派模式的实现
public Class<?> loadClass(String name) throws ClassNotFoundException
//查找二进制名称为name的类,返回结果为java.lang.Class类的实例。
//这是一个受保护的方法,JVM鼓励我们重写此方法,需要自定义加载器遵循双亲委托机制,
//该方法会在检查完父类加载器之后被loadClass()方法调用。
protected Class<?> findClass(String name) throws ClassNotFoundException
//根据给定的字节数组b转换为Class的实例,off和len参数表示实际Class信息在byte数组中的位置和长度,
//其中byte数组b是ClassLoader从外部获取的。
//这是受保护的方法,只有在自定义ClassLoader子类中可以使用。
protected final Class<?> defineClass(String name, byte[] b,int off,int len)
//链接指定的一个Java类。使用该方法可以使用类的Class对象创建完成的同时也被解析。
//链接阶段主要是对字节码进行验证,
//为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。
protected final void resolveClass(Class<?> c)
//查找名称为name的已经被加载过的类,返回结果为java.lang.Class类的实例。这个方法是final方法,无法被修改。
protected final Class<?> findLoadedClass(String name)
//它也是一个ClassLoader的实例,这个字段所表示的ClassLoader也称为这个ClassLoader的双亲。
//在类加载的过程中,ClassLoader可能会将某些请求交予自己的双亲处理。
private final ClassLoader parent;
Class.forName()与ClassLoader.loadClass()区别
public static Class<?> forName(String className)
- 是类方法
- 默认使用系统类加载器进行加载
- 加载一个指定的类,会对类进行初始化,执行类中的静态代码块,以及对静态变量的赋值等操作, 一般用于加载驱动,例如jdbc驱动
public Class<?> loadClass(String name)
- 是成员方法
- 懒加载,只是加载,不会解析更不会初始化所反射的类
双亲委派模型
rents Delegation Model
注意:双亲并不是指存在父母两个类加载器,实际只有一个parent 父加载器 并且是作为加载器的属性,而不是继承,可以理解为 雌雄同株
双亲委派 类加载流程
- 当一个类开始进行加载时,会先从判断这个类是否已经被加载了,如果已经加载了,返回已加载的类Class对象
- 如果还没有被加载,通过parent属性 将加载请求传递给上层类加载器进行加载
- 一直调用到扩展类加载器(ExtClassLoader),如果都没有找到已经被加载的Class对象,此时parent == null ,通过 findBootstrapClassOrNull() 方法传递给 BootStrapClassLoader 进行类加载
- 如果BootStrapClassLoader 没有加载成功,其下层类加载器开始尝试进行加载
- 如果一直到最底层的类加载器(用户自定义的类加载器)都没有加载成功,则抛出ClassNotFoundException异常
一句话概括:子类加载器调用父类加载器去加载类
源码如下:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 如果有parent ,交给parent 类加载器进行加载
c = parent.loadClass(name, false);
} else {
//没有parent,通过native方法委派给BootStrapClassLoader 进行加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
//如果父类没有加载出Class对象,开始尝试自己加载
long t1 = System.nanoTime();
//根据类的全限定名,获取Class 对象,分为两步
//1.根据类的全限定名,获取字节码对象
//2. 根据字节码的二进制流,调用defineClass()方法,生成Class对象
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;
}
}
优缺点
优点:
- 防止重复加载,确保一个类的全局唯一性
当一个类加载器要加载一个类,首先交给父类加载器进行加载,并且加载过程由syncronized锁控制。避免重复加载同一个类
- 确保JVM的安全,防止用户用自己编写的类替换核心类库中的类
例如:用户自定义一个java.lang.String的类,通过双亲委派模型,加载的还是核心类库中的String类,不会被用户自己定义的String类替换
缺点:
父类加载器不能访问子类加载器加载的类,有时需要打破双亲委派模型,例如Tomcat
热部署简单示例
public class MyClassLoader extends ClassLoader{
/**
* 文件路径
*/
final private String path;
protected MyClassLoader(String path) {
this.path = path;
}
/**
* 重写findClass,实现自己的类加载器
* 1. 获取class文件的字节流
* 2. 调用defineClass 将字节流 转化为 CLass 对象
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 读取字节码的二进制流
byte[] b = loadClassFromFile(name);
// 调用 defineClass() 方法创建 Class 对象
Class<?> c = defineClass(name, b, 0, b.length);
return c;
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
/**
* 如果要遵循双亲委派模型,则不用重写loadClass方法
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
//为了测试方便,这里简单打破下双亲委派模型,先从自定义类加载器进行加载
//如果不想打破双亲委派模型,path路径可以设置非classpath下的路径,手动复制class文件到对应目录下
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
//先从自定义类加载器进行加载,这里打破了双亲委派模型
try {
c = findClass(name);
} catch (ClassNotFoundException ignored) {
}
//自定义类加载器加载失败,交给父 类加载器
if(c == null){
return super.loadClass(name);
}
}
return c;
}
}
private byte[] loadClassFromFile(String name) throws IOException {
String fileName = name.replace('.', File.separatorChar) + ".class";
String filePath = this.path + File.separatorChar + fileName;
try (InputStream inputStream = new FileInputStream(filePath);
ByteArrayOutputStream byteStream = new ByteArrayOutputStream()
) {
int nextValue;
while ((nextValue = inputStream.read()) != -1) {
byteStream.write(nextValue);
}
return byteStream.toByteArray();
}
}
}
public class ApplicationConfig {
public void getConfig(){
System.out.println("这是我的配置...");
}
}
public class HotDeploymentTest {
public static void main(String[] args) throws Exception{
Scanner sc = new Scanner(System.in);
while(true){
MyClassLoader loader = new MyClassLoader("E:\\Work\\classloader\\target\\classes");
Class<?> aClass = loader.loadClass("cn.rwto.sample.ApplicationConfig");
Object applicationConfig = aClass.newInstance();
Method method = aClass.getMethod("getConfig");
method.invoke(applicationConfig);
Thread.sleep(5000);
}
}
}
在不关闭进程的情况下,修改代码,重新编译,控制台发现 打印的内容跟着改变,热部署实现完毕!