深入JVM:(十)类加载器

作者: 小村医 | 来源:发表于2019-03-18 00:05 被阅读6次

类加载器是 Java 语言的一个创新,也是 Java 语言流行的重要原因之一。它使得 Java 类可以被动态加载到 Java 虚拟机中并执行。

类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。

双亲委派模型

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

类加载器双亲委派模型.png

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。

使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。

双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中,如代码清单7-10所示,逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException {

            // 先检查类是否已经加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                     //如果父类加载器抛出ClassNotFoundException
                     //说明父类加载器无法完成加载请求
                }

                if (c == null) {
                    //在父类加载器无法加载的时候
                    //再调用本身的findClass方法来进行类加载
                    c = findClass(name);
                }
            }
            return c;
    }

Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:

启动类加载器(Bootstrap ClassLoader)

这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。
String类就是rt.jar里面提供的,这个类我们经常用,下面我们看下String类的类加载器是什么。

public static void main(String[] args) {    
    ClassLoader cl = String.class.getClassLoader();
    System.out.println(cl);
}

执行结果如下。
null
可知由于BootstrapClassloader对Java不可见,所以返回了null,我们也可以通过看某一个类的加载器是否为null来作为判断该类是不是使用BootstrapClassloader进行加载的依据。另外ExtClassLoader的父加载器返回的null,那是否说明ExtClassLoader的父加载器是BootstrapClassloader呢?

扩展类加载器(Extension ClassLoader)

这个加载器由sun.misc.Launcher $ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

应用程序类加载器(Application ClassLoader)

这个类加载器由sun.misc.Launcher $App-ClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

调用ClassLoader.getSystemClassLoader()可以获取该类加载器。如果没有特别指定,则用户自定义的任何类加载器都将该类加载器作为它的父加载器,这点通过java.lang.ClassLoader的无参构造函数可以证明,代码如下

public abstract class ClassLoader {
    protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }
    private static Void checkCreateClassLoader() {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        return null;
    }
    private static synchronized void initSystemClassLoader() {
        if (!sclSet) {
            if (scl != null)
                throw new IllegalStateException("recursive invocation");
            sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
            if (l != null) {
                Throwable oops = null;
                //从launcher中获取classloader
                scl = l.getClassLoader();
                try {
                    scl = AccessController.doPrivileged(
                        new SystemClassLoaderAction(scl));
                } catch (PrivilegedActionException pae) {
                    oops = pae.getCause();
                    if (oops instanceof InvocationTargetException) {
                        oops = oops.getCause();
                    }
                }
                if (oops != null) {
                    if (oops instanceof Error) {
                        throw (Error) oops;
                    } else {
                        // wrap the exception
                        throw new Error(oops);
                    }
                }
            }
            sclSet = true;
        }
    }

下面看一下Launcher中的实现:

public class Launcher {
    private ClassLoader loader;
    public Launcher() {
        ...
        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
        ...
    }
    static class AppClassLoader extends URLClassLoader {
        final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

        public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
            final String var1 = System.getProperty("java.class.path");
            final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
            return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
                public Launcher.AppClassLoader run() {
                    URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                    return new Launcher.AppClassLoader(var1x, var0);
                }
            });
        }
    }
}

从AppClassLoader的构造函数中可以看到,通过获得classpath加载路径来创建应用程序加载器(看到这是不是终于知道环境变量里的java.class.path干嘛用的了)
另外我们写的含有main函数的类的加载就是使用该类加载器进行加载的,证明如下:

    public static void main(String[] args) {
        ClassLoader cl = App2.class.getClassLoader();
        System.out.println(cl);
        System.out.println(cl.getParent());
    }

运行结果如下。

sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
并且可以看出AppClassLoader的父加载器是ExtClassLoader

除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。

自定义类加载器

其中有如下三个比较重要的方法

方法 说明
defineClass(String name, byte[] b, int off, int len) 把字节数组 b中的内容转换成 Java 类,该字节数组可以看成是二进制流字节组成的文件,返回的结果是java.lang.Class类的实例。这个方法被声明为 final的。
loadClass(String name) 上文中已贴出源码,实现了双亲委派模型,调用findClass()执行类加载动作,返回的是java.lang.Class类的实例。
findClass(String name) 通过传入的类全限定名name来获取对应的类,返回的是java.lang.Class类的实例,该类没有提供具体的实现,开发者在自定义类加载器时需重用此方法,在实现此方法时需调用defineClass(String name, byte[] b, int off, int len)方法。

我们可以容易地意识到自定义类加载器有以下两种方式:

  • 采用双亲委派模型:继承ClassLoader类,只需重写其的findClass(String name)方法,而不需重写loadClass(String name)方法。
  • 破坏双亲委派模型:继承ClassLoader类,需要整个重写实现了双亲委派模型逻辑的loadClass(String name)方法。

下面我们来实现一个自定义类加载器,用来加载存储在文件系统上的 Java 字节代码。

public class FileSystemClassLoader extends ClassLoader { 
 
   private String rootDir; 
 
   public FileSystemClassLoader(String rootDir) { 
       this.rootDir = rootDir; 
   } 
 
   @Override
   protected Class<?> findClass(String name) throws ClassNotFoundException { 
       byte[] classData = getClassData(name); 
       if (classData == null) { 
           throw new ClassNotFoundException(); 
       } 
       else { 
           return defineClass(name, classData, 0, classData.length); 
       } 
   } 
 
   private byte[] getClassData(String className) { 
       String path = classNameToPath(className); 
       try { 
           InputStream ins = new FileInputStream(path); 
           ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
           int bufferSize = 4096; 
           byte[] buffer = new byte[bufferSize]; 
           int bytesNumRead = 0; 
           while ((bytesNumRead = ins.read(buffer)) != -1) { 
               baos.write(buffer, 0, bytesNumRead); 
           } 
           return baos.toByteArray(); 
       } catch (IOException e) { 
           e.printStackTrace(); 
       } 
       return null; 
   } 
 
   private String classNameToPath(String className) { 
       return rootDir + File.separatorChar 
               + className.replace('.', File.separatorChar) + ".class"; 
   } 
}

类 FileSystemClassLoader的 findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例。

相关文章

网友评论

    本文标题:深入JVM:(十)类加载器

    本文链接:https://www.haomeiwen.com/subject/ttqfmqtx.html