我爱编程

ClassLoader原理

2018-06-03  本文已影响55人  azmohan

什么是ClassLoader

java的源代码通过javac编译后,生成字节码(.class文件),通过jvm解析执行。但是在解析之前,需要加载到内存中。这就要用到ClassLoader。ClassLoader字面的意思,就是类加载器。主要的功能就是加载.class文件到内存。

ClassLoader是如何加载的

.class文件说白了,还是一个文件,如果像C和C++,把所有源代码都编译成一个可执行文件,文件很大时,加载就需要很大的内存,并且加载比较慢。如果java把所有的源代码都编译成一个文件,也会有这个问题。java为了解决这个问题,javac会把一个class编译成一个文件。例如如下代码:

class Outer {
      public class Inner {
       }
   }

经过javac编译后,会生成Outer.class 、Outer$Inner.class这两个class文件,尽管你把上面的代码写在一个文件中,所以也可以理解ClassLoader就是class加载器。并且支持动态加载,只要在使用之前加载过,就可以了。

PathClassLoader.java

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.File;
import java.io.FileInputStream;


public class PathClassLoader extends ClassLoader  {
    private String pathUrl;

    public PathClassLoader(String path)  {
        super(null);
        pathUrl = path;
        System.out.println("pathUrl = " + pathUrl);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException  {
        Class clazz = null;
        byte[] classData = getClassData(name);
        if  (classData == null)  {
            throw new ClassNotFoundException();
        }
        clazz = defineClass(name,classData,0,classData.length);
        System.out.println("findClass class = " + clazz.toString());
        return clazz;
    }

    private byte[] getClassData(String name)  {
        String classPath = classNameToPath(name);
        File file = new File(classPath);
        byte[] buff = new byte[1024*4];
        int len = -1;
        InputStream is = null;
        System.out.println("getClasData classPath:" + classPath);
        try  {
            is = new FileInputStream(file);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            while ((len = is.read(buff)) != -1)  {
                baos.write(buff,0,len);
            }
            return baos.toByteArray();
        }  catch (Exception e)  {
            System.out.println("Exception e : " + e.toString());
        }  finally  {
            if  (is != null)  {
                try {
                    is.close();
                }  catch (IOException e)  {
                    System.out.println("Exception e : " + e.toString());
                }
            }
        }
        return null;
    }

    private String classNameToPath(String name)  {
        return pathUrl + "/" + name.replace(".","/") + ".class";
    }
}
public class ClassLoaderSimple  {
    private ClassLoaderSimple instance;
    public void setClassLoaderSimple(Object obj)  {
        try  {
            instance = (ClassLoaderSimple) obj;
        }  catch(Exception e)  {
            System.out.println("Exception e = " + e.toString());
        }
        System.out.println("invoke setClassLoaderSimple");
    }
}
public class ClassLoaderTest  {
    public static void main(String [] args)  {
        try  {
            String path = "/home/azmohan/java/classloader/test";
            String className = "ClassLoaderSimple";
            PathClassLoader load1 = new PathClassLoader(path);
            Class<?> clazz1 = load1.loadClass(className);
            Object obj1 = clazz1.newInstance();
            System.out.println("object 1 classloader = " + clazz1.getClassLoader());
        }  catch (Exception e)  {
            System.out.println("Exception e:" + e.toString());
        }
    }
}

上面的用于测试ClassLoaderTest代码,表示了ClassLoader的加载和使用的过程,首先你要new一个ClassLoader,把.class文件的路径传进去,然后loadClass,获取Class的句柄,最后可以实例化对象,并且通过反射调用对象的方法。

jvm如何认别一个类

上面我们知道了class是ClassLoader加载到内存中的,那么问题来了,如果我们用两个不同ClassLoader去加载同一个.class文件。那会是什么表现呢?如果把上面的测试代码修改成

public class ClassLoaderTest  {
    public static void main(String [] args)  {
        try  {
            String path = "/home/azmohan/java/classloader/test";
            String className = "ClassLoaderSimple";
            PathClassLoader load1 = new PathClassLoader(path);
            PathClassLoader load2 = new PathClassLoader(path);
            Class<?> clazz1 = load1.loadClass(className);
            Class<?> clazz2 = load2.loadClass(className);
            Object obj1 = clazz1.newInstance();
            Object obj2 = clazz2.newInstance();
            System.out.println("object 1 classloader = " + clazz1.getClassLoader());
            System.out.println("object 2 Classloader = " + clazz2.getClassLoader());
            clazz1.getMethod("setClassLoaderSimple",Object.class).invoke(obj1,obj2);
        }  catch (Exception e)  {
            System.out.println("Exception e:" + e.toString());
        }
    }
}

运行结果如下:

pathUrl = /home/azmohan/java/classloader/test
pathUrl = /home/azmohan/java/classloader/test
getClasData classPath:/home/azmohan/java/classloader/test/ClassLoaderSimple.class
findClass class = class ClassLoaderSimple
getClasData classPath:/home/azmohan/java/classloader/test/ClassLoaderSimple.class
findClass class = class ClassLoaderSimple
object 1 classloader = PathClassLoader@6d06d69c
object 2 Classloader = PathClassLoader@4e25154f
Exception e = java.lang.ClassCastException: ClassLoaderSimple cannot be cast to ClassLoaderSimple
invoke setClassLoaderSimple

现在就是可回答刚才疑问了。JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。比如ClassLoaderSimple.java,javac编译之后生成字节码文件ClassLoaderSimple.class,loader1和loader2这两个类加载器并读取了ClassLoaderSimple.class文件,对于JVM来说,它们是两个不同类型的实例对象,但它们确实是同一份字节码文件,如果试图将这个Class实例生成具体的对象进行转换时,就会抛运行时异常java.lang.ClassCastException。

能过上面的分析,我们也能得出,一个类被加载后,它的ClassLoader是唯一确定的。测试如下:

class ClassLoaderTest1 {
    public static void main(String [] args) {
        ClassLoader loader = ClassLoaderSimple.class.getClassLoader();
        System.out.println("ClassLoader is : " + loader.toString());
        loader = String.class.getClassLoader();
        System.out.println("ClassLoader is : " + loader.toString());
    }
}

运行结果:

ClassLoader is : sun.misc.Launcher$AppClassLoader@73d16e93
Exception in thread "main" java.lang.NullPointerException
    at ClassLoaderTest1.main(ClassLoaderTest1.java:6)

可以看出ClassLoaderSimple.class是由AppClassLoader加载的,String的类加载器,居然是null.解释这个问题,我们要知道java系统的类加载流程。

java系统的类加载流程

java语言系统提供了三个主要的类加载器

  1. BootStrap ClassLoader.
  2. Extension ClassLoader.
  3. Application ClassLoader.

这三个加载器主要用途如下:

java系统运行时,这几个类加载器代表三个阶段的类加载流程。当你想在java系统中运行一个应用,是需要把jvm跑起来的,BootstrapClassLoader提供支持。Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个java类。也就是无法在java代码中获取它的引用,JVM启动时通过Bootstrap类加载器加载rt.jar等核心jar包中的class文件,int.class,String.class都是由它加载。

这样就能解释了上面测试代码的运行结果。

为了更好的理解,可以参考sun.misc.Launcher,它是一个java虚拟机的入口应用。

public class Launcher {
    private static Launcher launcher = new Launcher();
    private static String bootClassPath =
        System.getProperty("sun.boot.class.path");

    public static Launcher getLauncher() {
        return launcher;
    }

    private ClassLoader loader;

    public Launcher() {
        // Create the extension class loader
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                    "Could not create extension class loader", e);
        }

        // Now create the class loader to use to launch the application
        try {
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                    "Could not create application class loader", e);
        }

        Thread.currentThread().setContextClassLoader(loader);
    }

    /*
     * Returns the class loader used to launch the main application.
     */
    public ClassLoader getClassLoader() {
        return loader;
    }
    /*
     * The class loader used for loading installed extensions.
     */
    static class ExtClassLoader extends URLClassLoader {
        static {
            ClassLoader.registerAsParallelCapable();
        }

        /**
         * create an ExtClassLoader. The ExtClassLoader is created
         * within a context that limits which files it can read
         */
        public static ExtClassLoader getExtClassLoader() throws IOException
        {
            final File[] dirs = getExtDirs();

            try {
                // Prior implementations of this doPrivileged() block supplied
                // aa synthesized ACC via a call to the private method
                // ExtClassLoader.getContext().

                return AccessController.doPrivileged(
                        new PrivilegedExceptionAction<ExtClassLoader>() {
                        public ExtClassLoader run() throws IOException {
                        int len = dirs.length;
                        for (int i = 0; i < len; i++)  {
                        MetaIndex.registerDirectory(dirs[i]);
                        }
                        return new ExtClassLoader(dirs);
                        }
                        });
            } catch (java.security.PrivilegedActionException e) {
                throw (IOException) e.getException();
            }
        }

        private static File[] getExtDirs() {
            String s = System.getProperty("java.ext.dirs");
            File[] dirs;
            if  (s != null)  {
                StringTokenizer st =
                    new StringTokenizer(s, File.pathSeparator);
                int count = st.countTokens();
                dirs = new File[count];
                for (int i = 0; i < count; i++) {
                    dirs[i] = new File(st.nextToken());
                }
            } else {
                dirs = new File[0];
            }
            return dirs;
        }
  
        public ExtClassLoader(File[] dirs) throws IOException {
            super(getExtURLs(dirs), null, factory);   
        }
        ......
    }

    /**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     */
    static class AppClassLoader extends URLClassLoader {
        public static ClassLoader getAppClassLoader(final ClassLoader extcl)
            throws IOException
            {
                final String s = System.getProperty("java.class.path");
                final File[] path = (s == null) ? new File[0] : getClassPath(s);


                return AccessController.doPrivileged(
                        new PrivilegedAction<AppClassLoader>() {
                        public AppClassLoader run() {
                        URL[] urls =
                        (s == null) ? new URL[0] : pathToURLs(path);
                        return new AppClassLoader(urls, extcl);
                        }
                        });
            }

        ......
    }

我们可以得到相关的信息。

  1. Launcher初始化了ExtClassLoader和AppClassLoader。
  2. Launcher中并没有看见BootstrapClassLoader,但通过System.getProperty("sun.boot.class.path")得到了字符串bootClassPath,这个应该就是BootstrapClassLoader加载的jar包路径。
  3. AppClassLoader和ExtClassLoader有一种关系 new AppClassLoader(urls, extcl) 是给ExtClassLoader的实例传了进去,到底有什么关系,需要看ClassLoader的代码。因为顶层父类就是ClassLoader.
public abstract class ClassLoader {
    // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent;

    protected ClassLoader() {
        this(checkCreateClassLoader(), null, getSystemClassLoader());
    }
    protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), null, parent);
    }
    protected ClassLoader(String name, ClassLoader parent) {
        this(checkCreateClassLoader(name), name, parent);
    }
    private ClassLoader(Void unused, String name, ClassLoader parent) {
        this.name = name;
        this.parent = parent;
        this.unnamedModule = new Module(this);
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            assertionLock = this;
        }
    }
}

URLClassLoader的构造函数

 URLClassLoader(String name, URL[] urls, ClassLoader parent,
                   AccessControlContext acc) {
        super(name, parent);
        // this is to make the stack depth consistent with 1.1
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        this.acc = acc;
        this.ucp = new URLClassPath(urls, acc);
    }

从上面的代码知道,ExtClassLoader就是AppClassLoader的父加载器。ClassLoader构造函数表明:

  1. 构造ClassLoader时,可以直接指定一个ClassLoader作为它的parent.
  2. 如果没有指定就用getSystemClassLoader()方法生成,其实就是AppClassLoader.也就是说,一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。

ExtClassLoader、URLCLassLoader构造函数表明:

  1. ExtClassLoader的parent是null.

我们可以通过下面的代码测试上面的结论。

class ClassLoaderTest1 {
    public static void main(String [] args) {
        ClassLoader loader = ClassLoaderSimple.class.getClassLoader();
        System.out.println("ClassLoader is : " + loader.toString());
        System.out.println("Parent ClassLoader is : " + loader.getParent().toString());
        System.out.println("ExtClassLoader Parent ClassLoader is : " + loader.getParent().getParent().toString());
        loader = String.class.getClassLoader();
        System.out.println("ClassLoader is : " + loader.toString());
    }
}

输出结果:

ClassLoader is : sun.misc.Launcher$AppClassLoader@626287d3
Parent ClassLoader is : sun.misc.Launcher$ExtClassLoader@32c3601b
Exception in thread "main" java.lang.NullPointerException
    at ClassLoaderTest1.main(ClassLoaderTest1.java:6)

ExtClassLoader就是AppClassLoader的父加载器。ExtClassLoader的父加载器是null。

父加载器和父类没有关系

在帖Launcher的代码时,里面有ExtClassLoader和AppClassLoader的实现。

static class ExtClassLoader extends URLClassLoader {}
static class AppClassLoader extends URLClassLoader {}

可以看见ExtClassLoader和AppClassLoader同样继承自URLClassLoader。下图是各种ClassLoader的继承图:

20170211112754197.png

可以清楚的看到ExtClassLoader和AppClassLoader没有继承关系。

双亲委托

一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托。

整个流程可以如下图所示:

20170210192931505.png
  1. 一个AppClassLoader查找资源时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器。
  2. 递归,重复第1部的操作。
  3. 如果ExtClassLoader也没有加载过,则由Bootstrap ClassLoader出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下,也就是sun.mic.boot.class下面的路径。找到就返回,没有找到,让子加载器自己去找。
  4. Bootstrap ClassLoader如果没有查找成功,则ExtClassLoader自己在java.ext.dirs路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。
  5. ExtClassLoader查找不成功,AppClassLoader就自己查找,在java.class.path路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。

委托是从下向上,然后具体查找过程却是自上至下:

20170211135054825.png

上面已经详细介绍了加载过程,但具体为什么是这样加载,我们还需要看ClassLoader.java的源代码,了解几个个重要的方法loadClass()、findLoadedClass()、findClass()、defineClass()。

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
                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

为什么ClassLoader要设计成双亲委托模式

可以想一下,如果不是双亲委托模式,我们是不是可以自定义一个ClassLoader,让它去load jvm的核心库,那java系统的表现形式是不是也要改写了。例如自己写的ClassLoader不是双亲委托模式,本身一个程序是想调用系统定义的String.java,我就可以加载自定义的String.java,这样就是可修改String.java中接口的特性,那java宣称的compile once,run anywhere,就是不能保证正确了,只要被ClassLoader接管,程序运行的表现就不一样了,根本没有安全可言。

总结

  1. ClassLoader用来加载class文件的。
  2. 系统内置的ClassLoader通过双亲委托来加载指定路径下的class和资源。
  3. 可以自定义ClassLoader一般覆盖findClass()方法。
上一篇下一篇

猜你喜欢

热点阅读