JVM类加载器之自定义类加载器

  • jvm目录

  • 废话不多说,自定义的类加载器都要继承于java.lang.ClassLoader类,它定义了默认的加载的规范,并提供了一些方法由我们重写而实现自己的加载逻辑。所以我们要先了解一下ClassLoader类。

java.lang.ClassLoader


  • ClassLoader为我们提供了几个方法,其中我们可能比较会需要重写的是findClass()与loadClass()方法。现在我们来看一下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 {
                        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;
        }
    }

  • 如上文代码,loadClass()是类加载器加载类的入口,也就是说loadClass是加载类的第一步。loadClass方法也是双亲委派模型的实现过程。如果你需要打破双亲委派模型,就去重写loadClass方法,如果不需要就直接使用ClassLoader的loadClass方法即可。
  • name就是需要加载的类的“标识”,它的作用是能让加载器找到需要加载的二进制文件。比如name值为"java.lang.Object"之类的。然后我们一步一步的看一下代码,是如何实现双亲委派模型的。
  • 第一步是加锁,一个类只允许被同一个加载器加载一次,避免多线程多次加载问题。
  • 第二步根据name查找一下是否已经加载过了,如果加载过了直接放回该类的Class对象。然后根据判断是否需要resolveClass方法。
    • resolveClass()是执行加载过程中的“连接”(验证,准备,解析)过程。通过源代码,我们可以得知这个方法不可以被重写,实际上连接的过程也是完全由虚拟机处理的,我们不能也不可能进行干预。
  • 如果name对应的类加载器不曾加载过,则现在需要加载。首先是查找当前类是否有父类,有父类应该交由父类处理,并层层传递上去,直到没有父类则交给启动类加载器类加载器(Bootstrap ClassLoader)进行加载。如上代码,如果类加载器无法加载则抛出ClassNotFoundException 异常,返回给子类加载器执行,如此层层传递回来。这样就能保证同一个类一定是由同一个类加载器加载的。这就是双亲委派模型。
  • 当传递回本类加载的时候,说明“父类”的加载器们都无法执行这个类。然后就是通过findClass根据name去寻找到对应的二进制字节流信息加载到方法区中,并返回一个java.lang.Class对象,来代表的这个类。这也就是我们需要重写的方法findClass(),findClass对应的就是类加载过程中的第一步(加载过程)。而我们自定义类加载器主要的也是实现这个过程,来控制二进制字节流的加载方式。
  • 如果都找不到,则将ClassNotFoundException 异常抛出到外面去了。

所以重写findClass方法是为了来控制二进制字节流的加载方式,比如从数据库中,或从其它文件,或者线上获取,再或者是为了实现对重要字节流的加密解密。

而重写loadClass方法是为了破坏双亲委派机制,实现自己加载机制。


  • 接下来提供一个自定义加载的示例代码
public class MyClassLoader extends ClassLoader {

    protected Class findClass(String name) throws ClassNotFoundException
    {

        File file = new File("D:/"+name.replaceAll(".","/")+".class");
        try{
       
            byte[] bytes = getClassBytes(file);
            Class c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

        return super.findClass(name);
    }

    private byte[] getClassBytes(File file) throws Exception
    {
        // 这里要读入.class的字节,因此要使用字节流
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);

        while (true){
            int i = fc.read(by);
            if (i == 0 || i == -1)
                break;
            by.flip();
            wbc.write(by);
            by.clear();
        }
        fis.close();
        return baos.toByteArray();
    }

}

  • 主程序测试
MyClassLoader myClassLoader = new MyClassLoader(); 
Class testInfoClass= Class.forName("spongebob.demo.TestInfo", true, myClassLoader); 
Object  testInfo= testInfoClass.newInstance();
       
System.out.println(testInfo);
System.out.println(testInfo.getClass().getClassLoader());
  • 这样我们就实现了自定义类加载器,并成功加载了在D://spongebob/demo/路径下预先编译好的TestInfo.class文件进入虚拟机中。
  • findClass之中主要有两行代码,getClassBytes是为了获取数据的二进制字节流。
  • defineClass是由系统提供且是必须的调用的,它是将二进制流字节转化为方法区中类的数据存储格式,并返回这个类的java.lang.Class对象。

你可能感兴趣的:(jvm虚拟机)