本篇博客来(不深不浅地)讨论一下jvm中的classloader。顾名思义,classloader主要的功能就是load class,也就是加载一个类。简单来讲,加载一个类就是从将一个编译过的class文件的字节流加载到jvm中并解析程序员定义的字段、方法等内容。
classloader的父级委派模型
jvm在实例化一个对象之前(通过new
关键字或者反射),会先把这个类load到jvm中,其中,不同的类是由不同的classloader来载入的,jvm官方推荐的方式是父级委派模型(parent delegation model)。
有人翻译为双亲委派模型,由于我对双亲的第一反映是两个parent指针,所以,我个人将其称为父级委派模型。当然,这个不是重点。
不同的classloader
jvm中共包含两大类classloader,第一类是用c++实现的native级别的classloader,是jvm虚拟机的一部分。第二类是用java实现的虚拟机级别的classloader,这个级别的classloader都继承自java.lang.ClassLoader
,是可以由java程序员来控制和使用的。下面来详细说一下不同的classloader用途:
- bootstrap classLoader: 这个是用c++实现的,负责加载$JAVA_HOME/lib下的类
- extension classLoader:用来加载$JAVA_HOME/lib/ext目录下的类
- application classLoader:用来加载classpath上的类(运行java命令时,通过-cp或-classpath来指定当前应用程序的classpath)
- url classLoader:用来加载一个位于特定位置的类
委派过程
父级委派模型的委派过程定义在了java.lang.ClassLoader.loadClass()
方法中。先来看下代码
protected Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
synchronized(this.getClassLoadingLock(var1)) {
Class var4 = this.findLoadedClass(var1);
if (var4 == null) {
long var5 = System.nanoTime();
try {
if (this.parent != null) {
var4 = this.parent.loadClass(var1, false);
} else {
var4 = this.findBootstrapClassOrNull(var1);
}
} catch (ClassNotFoundException var10) {
;
}
if (var4 == null) {
long var7 = System.nanoTime();
var4 = this.findClass(var1);
PerfCounter.getParentDelegationTime().addTime(var7 - var5);
PerfCounter.getFindClassTime().addElapsedTimeFrom(var7);
PerfCounter.getFindClasses().increment();
}
}
if (var2) {
this.resolveClass(var4);
}
return var4;
}
}
大致的过程如下:
1. this.findLoadedClass(var1)
。先查看一下要载入的类是否已经载入了,如果载入了,就不再重复载入,否则,进入下面的流程
2. 如果存在parent classloader的话,就委托给parent classloader来,否则,就调用bootstrap Classloader来载入
3. 如果还没有载入的,就调用findClass()
方法来载入。这是方法在java.lang.ClassLoader
中会直接抛出异常,因为它是留给子类的来实现的。这是一个比较典型的模板方法模式的套路。
这就是jvm官方推荐的classloader的执行过程,父级委派模型主要体现在第2步。这个父级委派的过程是可以破坏的,因为子类可以直接覆盖掉这个方法并修改为自己的load过程,但这不再本篇的讨论范围内。
谈谈URLClassLoader
就我个人而言,我使用比较多的是URLClassLoader,它可以从一组jar包中寻找一个类并载入到JVM中,然后创建这个类的实例并运行。这可以让笨重的java变得很灵活。
来看一个使用URLClassLoader的demo
static void doReflect() throws Exception {
URL url = new URL("file:/usr/local/lib/jobcenter/1/demo-job-1.0-SNAPSHOT.jar");
URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
Class cls = classLoader.loadClass("cn.enn.portal.demo_job.DemoClass");
Object instance = cls.newInstance();
Method method = cls.getDeclaredMethod("hello", String.class);
method.invoke(instance, "kite");
}
我创建URLClassLoader
实例时,传递了一组jar包的url,用这个classLoader对象加载一个类时,它就会从这一组jar包中来找类,并加载到JVM中。当然,同一个classLoader,默认情况下不会重复加载同一个类。然后,创建这个类的对象,并通过反射来调用它的方法。还有一种更加常用的方法,就是让这个类实现一个接口,创建完对象后强制转换成这个接口类型,就可以直接调用这个接口中定义的方法了。
类的唯一性
在JVM中,类的唯一性是由className + classLoader共同决定的,同一个类(名字一样),如果由两个不同的classLoader实例加载,则是两个完全不同的类型,他们的对象是不能相互赋值的。
类的动态载入
类的动态载入指的是,一个类被加载到了JVM中之后,发生了修改,需要被再次加载到JVM中。
这个问题,可简单,可复杂。
简单来讲,可以这么实现:使用URLClassLoader 来加载一个类,当这个类发生变化后,使用另一个URLClassLoader 重新加载一次,新的类就会被加载到JVM。这是由类的唯一性来决定的。需要注意的是,如果重新加载了,需要把原来的线程停掉,因为他们用的不是最新的类。
复杂来讲,复杂的OSGi就是专门来做这种事情的,我没用过,可能它的方案更加稳定和成熟吧。
关于资源的动态载入
根据我目前遇到的情况来看,资源是无法动态载入的,因为没有ClassLoader。如果你通过ClassLoader来loadResourceAsStream的话,如果某个资源被载入之后,用户由修改了,并打个包放到了原来的地方,再次载入该资源时,拿到的确实缓存的Stream,而不是最新的。换一个ClassLoader实例也无效。我试过了设置各种属性,都没有解决我的问题。最终,我发现了一个比较low但确实可行的解决方案,就是每次修改资源文件后,修改jar包的名字,然后创建一个新的URLClassLoader来加载资源流,就解决了。
关于这个问题,我在Stack Overflow上也写下了我的解决办法,地址是:
那个kite就是我,欢迎点赞哦!