ClassLoader原理
源头
最近在学习SpringBoot源码的时候,发现SpringBoot使用ClassLoader加载文件,所以好奇的ClassLoader的实现原理是如何加载文件内容的。
Enumeration urls = classLoader.getResources("META-INF/spring.factories");
什么是ClassLoader
ClassLoader是java提供的类加载API,主要用于加载指定路径下的类。
常用的方法
方法名 | 作用 | 额外 |
---|---|---|
loadClass | 加载指定类路径名称来查找类 | 如果找不到就抛出ClassNotFoundException异常。即使在BootStrapClassLoader顶层的加载器范围内的类也可以加载。 |
getSystemClassLoader | 加载指定路径的资源 | 如果当前类加载器没找到就委托父类加载器查找。 |
getResource | 加载指定路径的资源。 | 如果当前类加载器没找到就委托父类加载器查找。 |
getSystemResourceAsStream | 加载类的搜索路径中指定名称的资源 | |
getSystemResources | 加载类的搜索路径中查找指定名称的资源 | |
getParent() | 获取上一级的类加载器 | |
findClass(String name) | 加载名称为name的类,返回的是一个Class实例。通常自定义类加载器时,会重写此方法。 | |
findLoadedClass(String name) | 检查名称为name的Class是否已经加载过,返回的是一个Class实例,如果Class为null,就表示未加载,否则就是已经加载 | |
resolveClass(Class c) | 解析并连接到指定的Class | |
defineClass(String name, byte[] b, int off, int len) | 将字节数组b的二进制数据转换成Java中的Class对象 |
例子:
public static void main(String[] args) throws Exception {
ClassLoader classLoader = getClassLoader(ClassLoaderTest.class);
// 加载指定路径下的类
Class<?> nowClazz = classLoader.loadClass("java.lang.Object");
Method method = nowClazz.getMethod("equals", Object.class);
System.out.println("loadClass方法 " + method.invoke(nowClazz.newInstance(), new Object()));
// 加载resources下的yml文件
InputStream is = classLoader.getResourceAsStream("sunpy.properties");
Properties props = new Properties();
props.load(is);
Enumeration<?> enumeration = props.propertyNames();
while (enumeration.hasMoreElements()) {
Object o = enumeration.nextElement();
System.out.println(" ==============> " + o);
}
}
结果:
jvm类加载器
类加载器作用
类加载器本身作用就是用于加载类的,将类转换成java.lang.Class的实例;类加载器是通过类的全限定名来获取类的二进制字节流进而实现加载类。至于类的初始化和链接等操作类加载并不关心,只是关注加载类。
jvm提供的类加载器
-
启动类加载器 BootstrapClassLoader
启动类加载器该加载器由jvm控制,没有父类加载器,也没有子类加载器。该类加载器使用c++语言实现,是虚拟机自身实现的一部分。加载类库范围:加载jdk1.8.0_65\jre\lib下或者-Xbootclasspath参数指定的路径下的类库,加载到jvm的内存中去。 -
扩展类加载器 ExtClassLoader
扩展类加载器由sun.misc.Launcher$ExtClassLoader类加载器实现。
专门加载jdk1.8.0_65\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext或者java.ext.dirs系统变量指定路径的类库; -
应用程序类加载器 AppClassLoader
类加载器由sun.misc.Launcher$AppClassLoader实现。
专门加载类路径Classpath上指定的类库。(就是项目bin目录下的class文件)。
类加载器用例
- 测试
public static void main(String[] args) throws Exception {
ClassLoader appClassLoader = ClassLoaderUtils.getClassLoader(null);
ClassLoader extClassLoader = appClassLoader.getParent();
// 查看系统变量java.ext.dirs
String str = System.getProperty("java.ext.dirs");
System.out.println(str);
// 加载ExtClassLoader下的类 - jdk1.8.0_65\jre\lib\ext
Class<?> clazz = extClassLoader.loadClass("sun.security.ec.CurveDB");
Method method = clazz.getDeclaredMethod("bi", String.class);
method.setAccessible(true);
Object o = method.invoke(clazz.newInstance(), "-2");
if (o instanceof BigInteger) {
BigInteger bigInteger = (BigInteger) o;
System.out.println("-------------> " + bigInteger.abs());
}
// 加载AppClassLoader下的类 - Classpath上指定的类库
Class<?> clazz1 = appClassLoader.loadClass("com.spyu.utils.DateUtils");
Method method1 = clazz1.getMethod("getCurrentDate");
Object o1 = method1.invoke(clazz.newInstance());
System.out.println("-------------> " + o1);
}
结果:
jvm双亲委派模型
什么是双亲委派模型
- 双亲委派模型除了顶层的启动类加载器之外,剩下的类加载器都必须要有自己的父类加载器(此处的父子关系不是继承而是组合)。
- 双亲委派模型加载类的方式是先委派给顶层类加载器去加载类,如果父类加载器在其范围内无法加载该类,那么子加载器就会去加载该类。
证明双亲委派模型向上委派
classpath,当前系统下:
public class ClassLoadTest {
public void printNowLoader() {
System.out.println("App Classloader");
System.out.println("当前的类 : " +this.getClass());
System.out.println("当前的类加载器 : " +this.getClass().getClassLoader());
}
public static void main(String[] args) {
new ClassLoadTest().printNowLoader();
}
}
运行结果:
自定义jar包:
public class ClassLoadTest {
public void printNowLoader() {
System.out.println("Bootstrap Classloader");
System.out.println("当前的类 : " +this.getClass());
System.out.println("当前的类加载器 : " +this.getClass().getClassLoader());
}
public static void main(String[] args) {
new ClassLoadTest().printNowLoader();
}
}
maven install 生成jar包:
-Xbootclasspath/a:F:\java_item\jvm-test-1.0-SNAPSHOT.jar;添加到vm options,让虚拟机加载:
-Xbootclasspath命令:使用BootstrapClassloader顶层加载器加载指定的jar包。
运行结果:
说明:
- 虽然我当前使用的类加载器为AppClassloader,但是依据双亲委派模型,会一直向上委派,直到使用第一层类加载器为BootstrapClassloader顶层加载器。而BootstrapClassloader类加载器在其范围内找到了我们指定的jar包,所以就直接加载成功了,轮不到AppClassloader类加载器来加载了。
- 至于打印的类加载器为空,是因为向上委派时,顶层加载器BootstrapClassloader,不是任何类加载器的父类,所以找不到为null。
双亲委派模型的源码
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) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
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;
}
}
过程:
- 首先先检查类是否已经被加载了。如果被加载了,那么解析加载的类。
- 如果没有被加载,那么使用父类加载器加载,如果父类加载器为空,那么直接使用启动类加载器来加载类。
- 如果最终父类加载失败,那么直接使用当前的findClass方法来加载。
双亲委派模型总结
1. 类加载器接收到类加载请求不会马上去加载,而是委派父类去加载。
(所以,虽然收到请求的是AppClassloader,但是加载顺序却是 BootstrapClassloader -> ExtClassloader -> AppClassloader)
2. 只有父类加载器无法加载(主要是在它的搜索范围内无法找到该类),子类加载器才会去加载。
3. 上层类加载器不会去询问下层类加载器。
4. 双亲委派模型上层加载器无法加载下层加载器的API。
5. 不同的类加载器加载同名的类属于不同的类型,不能互相转化和兼容,所以通过equals和instanceof 判断也是false。
自定义ClassLoader实现热替换
思路:就是在运行期间将class类换掉,但是替换的class类必须符合class类结构。重写findClass方法,在loadClass方法源码实现会自动委派父类查找,最后会落在我们自己编写类加载器身上。loadClass方法查找class的方法就是findClass方法实现,我们只需要重写findClass方法来实现自己的逻辑。
public class SunpyClassloader extends ClassLoader {
private String clazzFilePathPrefix;
public SunpyClassloader(String clazzFilePathPrefix) {
this.clazzFilePathPrefix = clazzFilePathPrefix;
}
private Class<?> writeClazz(String name) {
String clazzFilePath = clazzFilePathPrefix + name.replace('.', '/') + ".class";
File clazzFile = new File(clazzFilePath);
ByteBuffer buffer = ByteBuffer.allocate(1024);
FileInputStream fis = null;
FileChannel fileChannel = null;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try {
fis = new FileInputStream(clazzFile);
fileChannel = fis.getChannel();
while (fileChannel.read(buffer) > 0) {
buffer.flip();
byteArrayOutputStream.write(buffer.array(), 0 , buffer.limit());
buffer.clear();
}
return defineClass(name, byteArrayOutputStream.toByteArray() , 0 , byteArrayOutputStream.toByteArray().length) ;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fis != null) {
fis.close();
}
if (fileChannel != null) {
fileChannel.close();
}
if (byteArrayOutputStream != null) {
byteArrayOutputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
@Override
protected Class<?> findClass(String name) {
try {
return writeClazz(name);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
测试类:
public class SpyTest {
public static void main(String[] args) throws Exception {
while (true) {
SunpyClassloader sunpyClassloader = new SunpyClassloader("F:/");
Class<?> clazz = sunpyClassloader.loadClass("loaded.User");
Method method = clazz.getMethod("printUser");
method.invoke(clazz.newInstance());
Thread.sleep(1000);
}
}
}
生成的class:
随意编写User.java
public class User {
public void printUser() {
System.out.println("--> old user");
}
}
运行起来后修改User.java
结果:
ClassLoader工具类
点击查看代码
/**
* 类加载工具类
*
* @author spyu
* @date 2023-03-08
*/
public class ClassLoaderUtils {
/**
* 获取类加载器
* @param clazz
* @return
*/
public static ClassLoader getClassLoader(Class<?> clazz) {
if (Objects.isNull(clazz)) {
return ClassLoader.getSystemClassLoader();
}
return clazz.getClassLoader();
}
/**
* 获取指定路径下的Properties,并且转为Map结构
* @param path
* @return
*/
public static Map<String, String> getProperties(String path) {
ClassLoader classLoader = getClassLoader(ClassLoaderUtils.class);
InputStream is = classLoader.getResourceAsStream(path);
Properties props = new Properties();
try {
props.load(is);
} catch (IOException e) {
throw new RuntimeException(e);
}
Map<String, String> resultMap = new HashMap<String, String>((Map) props);
return resultMap;
}
/**
* 获取resource下的properties文件
* @param path
* @return
*/
public static Properties getProperty(String path) {
try {
ClassLoader classLoader = getClassLoader(ClassLoaderUtils.class);
InputStream is = classLoader.getResourceAsStream(path);
Properties props = new Properties();
props.load(is);
return props;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 加载java中包名+类名的类
* @param allClazzName 包名+类名
* @param <T>
* @return
*/
public static <T> T loadClass(String allClazzName) {
try {
ClassLoader classLoader = getClassLoader(ClassLoaderUtils.class);
Class<?> clazz = classLoader.loadClass(allClazzName);
return (T) clazz.newInstance();
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
throw new RuntimeException(e);
}
}
/**
* 获取对应classpath根路径下的文件
* @param path
* @return
*/
public static URL getResource(String path) {
ClassLoader classLoader = getClassLoader(ClassLoaderUtils.class);
return classLoader.getResource(path);
}
/**
* 加载指定路径下的存在相同的多个字段
* @param path
* @return
*/
public static Enumeration<URL> getResources(String path) {
ClassLoader classLoader = getClassLoader(ClassLoaderUtils.class);
try {
return classLoader.getResources(path);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}