0.前言
我们都知道类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括以下7个阶段:
- 加载(Loading)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸载(Unloading)7个阶段
而其中加载 要做什么呢?
在加载阶段,虚拟机需要完成以下3件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。(这个二进制字节流可以从Class文件、JAR、ZIP、网络、数据库中、JSP中 等中获取;或者是在运行过程中计算生成的–也就是动态代理技术)
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口。(这里包含了Java类的常量池、类字段、类方法等信息;)-----后期JVM在运行时能从这个入口来获取Java类中的任意信息,我们常用的反射、字节码相关工具就是基于此做的。
加载.class文件的方式
- 从本地系统中直接加载
- 通过网络下载.class文件
- 从zip,jar等归档文件中加载.class文件
- 从专有数据库中提取.class文件
- 将Java源文件动态编译为.class文件
相对于类生命周期的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。
晚上上面的这些事情的就是【类加载器】—也就是我们这篇文章的主角~
1.啥是类加载器
我们经常听到类加载器这个词,但到底什么是类加载器呢?它是做啥的呢?各位看官们可知道?
负责完成JVM的类的加载过程的模块就叫做 【类加载器】
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。也就是说即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
例子:
package jvm.classLib.classLoader;
import java.io.IOException;
import java.io.InputStream;
/**
* @Description
* @Author 大龄波妞
* @Date 2021/4/12 4:28 下午
*/
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
ClassLoader classLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream inputStream = getClass().getResourceAsStream(fileName);
if(inputStream == null){
return super.loadClass(name);
}
byte[] bytes = new byte[inputStream.available()];
inputStream.read(bytes);
return defineClass(name,bytes,0,bytes.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object object = classLoader.loadClass("jvm.classLib.classLoader.ClassLoaderTest").newInstance();
System.out.println(object.getClass());
System.out.println(object instanceof jvm.classLib.classLoader.ClassLoaderTest);
}
}
结果:
从第一句可以看出,这个对象确实是类jvm.classLib.classLoader.ClassLoaderTest实例化出来的对象,但从第二句可以发现,这个对象与类jvm.classLib.classLoader.ClassLoaderTest做所属类型检查的时候却返回了false,这是因为虚拟机中存在了两个ClassLoaderTest类,一个是由系统应用程序类加载器加载的,另外一个是由我们自定义的类加载器加载的,虽然都来自同一个Class文件,但依然是两个独立的类,做对象所属类型检查时结果自然为false。
2.类加载器种类
那虚拟机都有哪些类加载器呢?
- 启动类加载器(Bootstrap ClassLoader),是虚拟机自带的类加载器。
- 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
- 应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher $AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
除了这三种之外,JVM还允许用户自定义类加载器UserClassLoader。 用户自定义的ClassLoader需要继承java.lang.ClassLoader,
3.双亲委派模型
上面的这些类加载器的关系是啥样的呢?请看下图:
也就是我们常说的 【双亲委派模型】
虽然这几种类加载器没有直接的继承关系,但扩展类加载器、应用程序加载器以及用户自定义加载器都需要继承 java.lang.ClassLoader
。也就是说当JVM加载Java类的时候,最终也是通过java.lang.ClassLoader.loadClass(String)
这个接口完成的。我们等下来看看看这个方法是如何实现的。图中的委派关系则是通过ClassLoader
类的parent
字段来表示【父加载器】;parent
字段是final private
修饰的,所以子类只能通过初始化ClassLoader
类才能初始化parent
字段。
4.双亲委派模型工作过程
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,如果父类加载器还存在父类加载器,则进一步向上委托,依次递归,请求最终到达顶层的启动类加载器Bootstrap ClassLoader,如果次数能通过这个加载器完成加载任务,则成功返回,否则就向下查找子加载器去尝试自己去加载。
双亲委派的优点就是:
- 不会重复加载,类加载器具备了带优先级的层级关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。
- 安全。因为JVM是通过类加载器去加载类的,Bootstrap ClassLoader在加载的时候,只会加载JAVA_HOME中的jar包里面的类,如java.lang.Integer,那么这个类是不会被随意替换的,可以保证核心的类的加载不由外界控制,
5.双亲委派模型的破坏
有些特殊的场景下是没有使用双亲委派模型来进行类加载的。
- 线程上下文类加载器(Thread Context ClassLoader)。在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载,而 SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在classpath路径下,由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由引导类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器就是很好的选择。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。SPI接口的加载过程就演变成AppClassLoader—>ExtClassLoader—>BootstrapClassLoader—>ContextClassLoader–>第三方jar包的具体实现类(此类可以通过AppClassLoader来进行加载)。
- 插件式 应用程序,例如“鼠标”,若鼠标坏了,则在不重启机器的前提下更换一个鼠标。
Thread Context ClassLoader
出现缘由:有一些第三方提供接口,实现也在第三方,接口由JVM的Bootstrap类加载器加载,但具体的实现类Bootstrap加载器无法加载,在双亲委派模型下,BootStrap类也无法反向委托第三方类的AppClassLoader来加载接口的实现类,从而引入了 Thread Context ClassLoader来解决这个问题。通过java.lang.Thread类的setContextClassLoader()设置当前线程的上下文类加载器(如果没有设置,默认会从父线程中继承,如果程序没有设置过,则默认是AppClassLoader)。有了线程上下文类加载器,应用程序就可以通过java.lang.Thread.setContextClassLoader()将应用程序使用的类加载器传递给使用更顶层类加载器的代码。比如JNDI服务,就可以利用这种方式获取到可以加载SPI实现的类加载器,获取需要的SPI实现类。
Thread Context ClassLoader
是java.lang.Thread
中的 一个ClassLoader类型的属性,
ContextClassLoader默认存放了AppClassLoader的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作。
应用场景
- 为了隔离各类调用环境下的Class Loader;如果我们对业务进行划分,不同的业务使用不同的线程池,线程池内部共享同一个 contextClassLoader,线程池之间使用不同的 contextClassLoader,就可以很好的起到隔离保护的作用,避免类版本冲突。
- 有调用SPi等接口需要的场景下可用
注意点
- Thread Context Class Loader 默认存放的是AppClassLoader
- 子线程默认使用父线程的context Class Loader,可单独设置
6.ClassLoader源码分析
ClassLoader的作用已经在上面有说啦,我们来看看ClassLoader有啥,以及是如何实现类加载的
6.1.ClassLoader UML图
6.2.ClassLoader有啥
属性
private final ClassLoader parent
------用来表示父类加载器private final ConcurrentHashMap<String, Object> parallelLockMap;
—用于loadClass方法的锁同步private static class ParallelLoaders
—支持并行加载能力的内部静态类
构造函数
从官网可知ClassLoader提供了参数为父类加载器的类加载器
关键方法
6.3.ClassLoader源码分析—loadClass
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 {//父类加载器为null,则委托给BootStrap加载器加载
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.
// 还是没有找到父类则调用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);//内部调用的是native方法,是当字节码加载到内存后进行链接操作,对文件格式和字节码验证,并为 static 字段分配空间并初始化,符号引用转为直接引用,访问控制,方法覆盖等
}
return c;
}
}
从源码上我看看到 loadClass逻辑其实是比较简单的:
先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。加载完成后,再进行验证、解析和初始化操作。
注意到在方法的顶层加了一个同步锁 synchronized (getClassLoadingLock(name)
,这里加锁是为了让只能有一个活动线程在加载这个类
我们来看下getClassLoadingLock 这个方法里是做什么的?
protected Object getClassLoadingLock(String className) {
Object lock = this;
if (parallelLockMap != null) {
Object newLock = new Object();
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
这个方法的逻辑是:如果parallelLockMap是NULL,则直接返回当前Class;否则若parallelLockMap中没有key=className的value则直接new Object放入Map中并返回;若parallelLockMap中存在key=className的value则将map中的value直接返回。----parallelLockMap 在内部静态类ParallelLoaders 中进行初始化的。这个方法其实就是看当前ClassLoader是否支持并行注册,若支持则返回与指定的名字className相关联的特定对象,否则,直接返回当前的ClassLoader对象。
6.4.ClassLoader源码分析—findClass
findClass是抽象方法,这个方法在ClassLoader中并未实现,由其子类负责实现。findClass()的功能是找到class文件并把字节码加载到内存中。自定义的ClassLoader一般覆盖这个方法。——以便使用不同的加载路径。
我们来看下URLClassLoader的findClass方法
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
//用/代替.然后追加上.class
String path = name.replace('.', '/').concat(".class");
//根据构建的path获取到字节流
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
//根据字节流解析出Class对象
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
这个方法用于根据Class的路径来查找到Class。若类没找到则抛出ClassNotFoundException
6.5.ClassLoader源码分析—findLoadedClass
findLoadedClass
这个方法用来检查该类是否已经被加载过,findLoadedClass会返回一个Class
类型的对象,如果该类已经被加载过,那么就可以直接返回该对象, 如果该类没有被加载过,那么执行加载过程
/**
* Returns the class with the given <a href="#name">binary name</a> if this
* loader has been recorded by the Java virtual machine as an initiating
* loader of a class with that <a href="#name">binary name</a>. Otherwise
* <tt>null</tt> is returned.
*
* @param name
* The <a href="#name">binary name</a> of the class
*
* @return The <tt>Class</tt> object, or <tt>null</tt> if the class has
* not been loaded
*
* @since 1.1
*/
protected final Class<?> findLoadedClass(String name) {
if (!checkName(name))
return null;
return findLoadedClass0(name);
}
private native final Class<?> findLoadedClass0(String name);
findLoadedClass0
这个是个native方法,判断此类是否被加载过的依据有两个条件:
- 完整类名是否一样----类的全限定名是否一样
- ClassLoader是否是同一个
也就是说即使全限定名一样,但ClassLoader不是同一个,JVM也认为这不是同一个类,认为此类没有没加载过。(具体case见上面的案例ClassLoaderTest)
7.AppClassLoader源码分析
来上源码~源码从哪看呢;可以从 Launcher源码看到AppClassLoader、ExtClassLoader的源码;也可以选择下载下来
static class AppClassLoader extends URLClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException
{
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);
// Note: on bugid 4256530
// Prior implementations of this doPrivileged() block supplied
// a rather restrictive ACC via a call to the private method
// AppClassLoader.getContext(). This proved overly restrictive
// when loading classes. Specifically it prevent
// accessClassInPackage.sun.* grants from being honored.
//
return AccessController.doPrivileged(
new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}
final URLClassPath ucp;
/*
* Creates a new AppClassLoader
*/
AppClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent, factory);
ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
ucp.initLookupCache(this);
}
/**
* Override loadClass so we can checkPackageAccess.
*/
public Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
int i = name.lastIndexOf('.');
if (i != -1) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPackageAccess(name.substring(0, i));
}
}
if (ucp.knownToNotExist(name)) {
// The class of the given name is not found in the parent
// class loader as well as its local URLClassPath.
// Check if this class has already been defined dynamically;
// if so, return the loaded class; otherwise, skip the parent
// delegation and findClass.
Class<?> c = findLoadedClass(name);
if (c != null) {
if (resolve) {
resolveClass(c);
}
return c;
}
throw new ClassNotFoundException(name);
}
return (super.loadClass(name, resolve));
}
/**
* allow any classes loaded from classpath to exit the VM.
*/
protected PermissionCollection getPermissions(CodeSource codesource)
{
PermissionCollection perms = super.getPermissions(codesource);
perms.add(new RuntimePermission("exitVM"));
return perms;
}
/**
* This class loader supports dynamic additions to the class path
* at runtime.
*
* @see java.lang.instrument.Instrumentation#appendToSystemClassPathSearch
*/
private void appendToClassPathForInstrumentation(String path) {
assert(Thread.holdsLock(this));
// addURL is a no-op if path already contains the URL
super.addURL( getFileURL(new File(path)) );
}
/**
* create a context that can read any directories (recursively)
* mentioned in the class path. In the case of a jar, it has to
* be the directory containing the jar, not just the jar, as jar
* files might refer to other jar files.
*/
private static AccessControlContext getContext(File[] cp)
throws java.net.MalformedURLException
{
PathPermissions perms =
new PathPermissions(cp);
ProtectionDomain domain =
new ProtectionDomain(new CodeSource(perms.getCodeBase(),
(java.security.cert.Certificate[]) null),
perms);
AccessControlContext acc =
new AccessControlContext(new ProtectionDomain[] { domain });
return acc;
}
}
从源码里能知道
- AppClassLoader是通过
System.getProperty("java.class.path");
来获取到;而这个class.path又是什么呢?
这里会加载jre/lib下的jar和项目中的classes目录的Class文件,以及用到的仓库中的jar包 - ExtClassLoader是通过【java.ext.dirs】来获取到要加载的ext的地址的类文件,【java.ext.dirs】
从图中可以得知extClassLoader是加载jre/lib/ext目录下的文件。
3. BootstrapClassLoader是通过【sun.boot.class.path】来获取到要加载的地址,
看到BootstrapClassLoader要加载的都是jre下的classes和一些基础的jar包
8.ClassLoader应用
8.1.自定义ClassLoader—安全
可以自定义ClassLoader来加密处理字节码文件,而后在JVM加载字节码文件前,同样通过自定义ClassLoader来将字节码文件进行解密,而后再由JVM处理;这样来避免外部知道字节码的内容,而后通过反编译获取到源码。—此举常用于客户端防破解上和从网络中加载到字节码流再进行内部解析。
8.2.热加载
在应用启动时,JVM会将所有应用程序都装载到虚拟机上,而我们一个业务应用经常会包含比较多的第三方jar;若每次修改微量业务代码都进行一次全量加载,则这个效率是可想而知的低;我们是否可以 做到当我们修改某些业务代码后,只是触发我们修改的类的重新加载呢?而不是进行全量加载。spring-boot-devtools
就是基于这个想法进行处理的,其底层仍然是基于自定义ClassLoader来实现这一点。
spring-boot-devtools
这个底层是定义了两个ClassLoader,一个Classloader加载那些不会改变的类(第三方Jar包),另一个ClassLoader加载会更改的类,称为restart ClassLoader,这样在有代码更改的时候,原来的restart ClassLoader 被丢弃,重新创建一个restart ClassLoader,由于需要加载的类相比较少,所以实现了较快的重启时间。
- 若有业务代码(例如静态资源文件)不变更,不需要进行每次重新加载时,可以通过设置
spring.devtools.restart.exclude
进行重启加载文件的排除。 - 若不想每次修改都触发自动重启,可以设置
spring.devtools.restart.trigger-file
指向某个文件,只有更改这个文件时才触发自动重启。
8.3.加特定代码
自定义ClassLoader,那么就可以在执行defineClass之前,在原class文件中添加一些代码;当然是要满足JVM虚拟机规范;我们常用的ASM工具就是基于此完成的。
8.4.OSGI类加载
在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:
- 将以java.*开头的类委派给父类加载器加载。
- 否则,将委派列表名单内的类委派给父类加载器加载。
- 否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
- 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
- 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
- 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
- 否则,类查找失败。
上面的查找顺序中只有开头两点仍然符合双亲委派规则,其余的类查找都是在平级的类加载器中进行的。
8.4.Tomcat中的类加载器
此处我们先留空,等我们后面学习tomcat的时候再补充进来~
9.自定义ClassLoader
我们在上面讲了JVM中类加载器以及其实现原理,那我们如何自己自定义一个加载器呢?本小节就带大家一起实验实验如何自定义ClassLoader;自定义ClassLoader有以下几个步骤:
- 定义一个ClassLoader,继承
java.lang.ClassLoader
- 重写
findClass
方法 - 在
findClass
方法中调用defineClass
方法
9.1.自定义File路径的ClassLoader
首先,我们编写一个ClassLoaderExample
类,将编译后的class文件复制出后放到目录【~/Downloads/tmp/project/ClassLoaderExample.class】下;然后我们通过我们自定义的FileClassLoader
来解析道这个Class文件,从而完成对类 ClassLoaderExample
的方法调用,以此来验证程序中已正常解析加载到目录【~/Downloads/tmp/project/ClassLoaderExample.class】下的这个Class文件。
来,上代码~
package jvm.classLib.classLoader;
/**
* @Description
* @Author 大龄波妞
* @Date 2021/4/19 2:28 下午
*/
public class ClassLoaderExample {
public void test(){
System.out.println("hello ClassLoaderExample");
}
}
再来看我们的ClassLoader
package jvm.classLib.classLoader;
import org.apache.commons.lang.StringUtils;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
/**
* @Description
* @Author 大龄波妞
* @Date 2021/4/19 2:23 下午
*/
public class FileClassLoader extends ClassLoader {
private String classDir = "";
public FileClassLoader(String classDirValue) {
classDir = classDirValue;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
//name为类的全限定名,而非文件名,所以我们需要把类限定名变成文件名
String fileName = getFileName(name);
if (StringUtils.isBlank(fileName)) {
return null;
}
File file = new File(fileName);
if (!file.exists()) {
System.out.println("未查找到文件;name:" + name);
return null;
}
FileInputStream fileInputStream = new FileInputStream(file);
//文件to字节流
ByteArrayOutputStream byteArrayInputStream = new ByteArrayOutputStream();
int len = -1;
while ((len = fileInputStream.read()) != -1) {
byteArrayInputStream.write(len);
}
byte[] buf = byteArrayInputStream.toByteArray();
byteArrayInputStream.close();
fileInputStream.close();
defineClass(name,buf,0,buf.length);
} catch (IOException e) {
e.printStackTrace();
}
return super.findClass(name);
}
public String getFileName(String className) {
if (StringUtils.isBlank(className)) {
return "";
}
int index = className.lastIndexOf(".");
if (index == -1) {
return classDir + className + ".class";
} else {
return classDir + className.substring(index + 1) + ".class";
}
}
}
再来看下我们的test
package jvm.classLib.classLoader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* @Description
* @Author 大龄波妞
* @Date 2021/4/19 2:32 下午
*/
public class FileClassLoaderTest {
public static void main(String[] args) {
FileClassLoader fileClassLoader = new FileClassLoader("~/Downloads/tmp/project/");
try {
Class fileClassExample = fileClassLoader.loadClass("jvm.classLib.classLoader.ClassLoaderExample");
if(fileClassExample != null){
System.out.println(fileClassExample.getName());
System.out.println("==========");
Object object = fileClassExample.newInstance();
Method method = fileClassExample.getDeclaredMethod("test",null);
method.invoke(object,null);
}
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
我们再来看下输出:
从输出结果上可知我们的这个简单自定义ClassLoader已ok;这样看是不是so easy~
所以ClassLoader也不是那么难,是不是~
998.总结
好啦,到这,ClassLoader就分享完啦
总结下:
- JVM类加载器主要负责JVM类加载整个阶段的加载模块
- JVM类加载器有BootstrapClassLoader、ExtClassLoader、AppClassLoader和用户自定义加载器;他们满足双亲委派模型,
- BootstrapClassLoader负责加载启动相关的以及一些基础&核心类
- ExtClassLoader负责加载ext目录下的jar包
- AppClassLoader负责加载classes类包
- 用户自定义的类加载器需要extends ClassLoader,重写findClass方法来做到自定义加载过程
- 为了满足SPI等第三方接口实现的类的加载,就有了Thread Context ClassLoader;默认为AppClassLoader,允许各个线程自行定义,来做到应用隔离。
- tomcat类加载器
- Spring类加载器