从一个例子看Java ClassLoader

·  阅读 146

ClassLoader中文翻译过来类加载器,顾名思义就是用来加载Java Class文件的。程序员写出来的java代码,必须首先编译成java class文件,然后通过ClassLoader将class文件加载到计算机内存中执行。其实本人也看了很多classloader的文章或书籍,对其中反复介绍的class字节码加载过程、双亲委派模型一直不是很了解,今天通过一个简单的classloader的demo来详细理解其中的原理。

ClassLoader Demo

public class MyClassLoader extends ClassLoader{

   private static int counter = 0;


   @Override
   protected Class<?> findClass(String name) throws ClassNotFoundException {
       try {
           System.out.println("===========counter==========" + counter);
           if(counter>0){
               throw new RuntimeException("已经初始化");
           }
           byte[] classBytes = IOUtils.toByteArray(new FileInputStream("/tmp/RouteMappingServiceImpl.class"));
           counter++;
           return defineClass(name,classBytes,0,classBytes.length);
       } catch (IOException e) {
           throw new RuntimeException(e);
       }
   }




   @SneakyThrows
   public static void main(String[] args) {
       MyClassLoader loader = new MyClassLoader();

       Class c = loader.loadClass("com.cmcc.coc.demo.service.RouteMappingServiceImpl");
       RouteMappingService rms = (RouteMappingService) c.newInstance();
       rms.print("测试classloader");

       Class c1 = loader.loadClass("com.cmcc.coc.demo.service.RouteMappingServiceImpl");
       RouteMappingService rms1 = (RouteMappingService) c1.newInstance();
       rms1.print("测试classloader111");
   }
}
复制代码

这段代码实现了一个自定义的classloader的类,重写了classloader的findClass方法。

  1. 这段代码在执行过程中,若在target目录下面删除RouteMappingServiceImpl.class文件,则执行结果如下:

image.png

  1. 删除了RouteMappingServiceImpl.class文件之后,将相同的文件拷贝到/tmp目录下,执行结果如下:

image.png

运行结果分析

为什么counter=0

main函数中创建了一个classloader,同时使用这个classloader加载了两次类,但是findClass只执行了一次。这个主要是Java类加载的机制决定的。

何时加载类

  1. 遇到new,getstatic,putstatic,invokestatic这4条指令,注意这里的new并发java关键字new,而是字节码指令new。
  2. 读取或者设置静态字段的时候
  3. 调用类型的静态方法的时候
  4. 使用java.lang.reflect包的方法对类型进行反射调用的时候
  5. 类初始化的时候,若父类未进行初始化,则会触发父类的初始化
  6. 包含main()方法的类。 注意classLoader的loadClass并不会触发类的加载。 在demo中调用newInstance方法触发了类的加载,并且在类已经加载内存中之后,再次调用newInstance并不会再次调用类加载器。因此findClass只会执行一次。

ClassLoader双亲委派模型

终于说到大名鼎鼎的类加载机制了。Java的类加载的模型是自己先不加载,先调用父类加载器,父类加载器无法加载在调用子类的加载。那么为什么要这样设计呢,这个好像与继承的方式好像是相反的,其实这样设计最大的好处就是保证Java基础类的稳定性。比如java.lang.Object类,由于采用了双亲委派机制,就只能使用BootstrapClassLoader来进行加载,其他类加载器是无法加载到这个类,这样就确保了基础类的稳定性。

类加载层次

  1. Bootstrap ClassLoader:启动类加载器,JVM的一部分,C++实现。
  2. Extension ClassLoader:扩展类加载器:负责加载<JAVA_HOME>/lib/ext目录下面的类。
  3. Application ClassLoader:应用类加载器,负责加载用户的类,也就是classpath中的类。
  4. User ClassLoader:自定义类加载器。 这三个类并非Java中的继承关系,而是使用组合的方式实现,具体实现的代码在ClassLoader中。
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;
    }
复制代码

这里面的parent的值如下:

image.png 是一个类的实例变量,在初始化中就会配置好,比如ApplicationClassLoader类,其parent就应该是Extension ClassLoader。这个与java中的继承是不一样的。

loadClass方法未被调用原因

当删除target目录中的RouteMappingServiceImpl文件之后,首先调用的是用户类加载器,用户类加载器调用AppClassLoader->Ext ClassLoader->BootstrapClassLoader,由于BootstrapClassLoader无法加载RouteMappingServiceImpl.class文件,因此BootstrapClassLoader->extClassLoader->appClassLoader,此时有两种情况

  1. classpath中存在RouteMappingServiceImpl.class文件,则appClassLoader完成类的加载,整个类加载过程结束,并不会调用我们写的findClass方法。
  2. classpath中不存在RouteMappingServiceImpl.class文件,则调用MyClassLoader.findClass方法完成类的加载,整个流程结束。 以上就是两种不同情况下打印不同语句的原因。

小结

Java的类加载器是一个很精妙的设计,开发人员可以设计不同的类加载器实现不同的加载方式,比如可以从网络或数据库中读取java class文件完成类的加载,也可以对java class文件进行加解密等等,通过这些可以极大方便的java的扩展,这一切的基础都需要Java程序员对类加载器基础有比较深刻的理解。

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改