jvm classloader

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/daguanjia11/article/details/80570923

本篇博客来(不深不浅地)讨论一下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上也写下了我的解决办法,地址是:

https://stackoverflow.com/questions/4749094/reloading-resources-loaded-by-getresourceasstream/50875165#50875165

那个kite就是我,欢迎点赞哦!

猜你喜欢

转载自blog.csdn.net/daguanjia11/article/details/80570923