Java类加载器浅析以及双亲委派模型

355 阅读6分钟

类加载器

寻找类加载器

  • example
public class ClassLoaderTest {
     public static void main(String[] args) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        System.out.println(loader);
        System.out.println(loader.getParent());
        System.out.println(loader.getParent().getParent());
    }
}

输出结果:

sun.misc.Launcher$AppClassLoader@64fef26a
sun.misc.Launcher$ExtClassLoader@1ddd40f3
null

分析:

从上面的结果可以看出,并没有获取到 ExtClassLoader 的父Loader,原因是 Bootstrap Loader(引导类加载器)是用C语言实现的,找不到一个确定的返回父Loader的方式,于是就返回null。

类加载器的层次关系

                    Bootstrap ClassLoader
                             ↑
                        ExtClassLoader 
                             ↑
                        AppClassLoader
                             ↑
                    Customer ClassLoader

注意:这里父类加载器并不是通过继承关系来实现的,而是采用组合实现的。

站在Java虚拟机的角度来讲

只存在两种不同的类加载器:启动类加载器和所有其他的类加载器

  1. 启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分;这也是上面例子中 ExtClassLoader 获取不到 父加载器的原因

  2. 所有其他的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。

站在Java开发人员的角度来看,

类加载器可以大致划分为以下三类:

  1. 启动类加载器:Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的

  2. 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。

  3. 应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader 来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器

应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:

  1. 在执行非置信代码之前,自动验证数字签名。

  2. 动态地创建符合用户特定需要的定制化构建类。

  3. 从特定的场所取得java class,例如数据库中和网络中。

JVM类加载机制

  1. 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入

  2. 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类

  3. 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

类的加载

类加载有三种方式:

  1. 命令行启动应用时候由JVM初始化加载

  2. 通过Class.forName()方法动态加载

  3. 通过ClassLoader.loadClass()方法动态加载

example:

public class loaderTest { 
        public static void main(String[] args) throws ClassNotFoundException { 
                ClassLoader loader = HelloWorld.class.getClassLoader(); 
                System.out.println(loader); 
                //使用ClassLoader.loadClass()来加载类,不会执行初始化块 
                loader.loadClass("Test2"); 
                //使用Class.forName()来加载类,默认会执行初始化块 
//                Class.forName("Test2"); 
                //使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块 
//                Class.forName("Test2", false, loader); 
        } 
}

public class Test2 { 
        static { 
                System.out.println("静态初始化块执行了!"); 
        } 
}

Class.forName()和ClassLoader.loadClass()区别

Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;

ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

注:Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

双亲委派模型

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

双亲委派机制:

  1. 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。

  2. 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。

  3. 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;

  4. 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

ClassLoader源码分析


public Class<?> loadClass(String name)throws ClassNotFoundException {
            return loadClass(name, false);
    }
    
    protected synchronized 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 {
                    //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
                        c = findBootstrapClass0(name);
                    }
                } catch (ClassNotFoundException e) {
                 // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }


双亲委派模型意义

  • 系统类防止内存中出现多份同样的字节码

  • 保证Java程序安全稳定运行

自定义类加载器

通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java 类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自 ClassLoader 类,从上面对 loadClass 方法来分析来看,我们只需要重写 findClass 方法(java.net.URLClassLoader#findClass)即可。下面我们通过一个示例来演示自定义类加载器的流程:

public class MyClassLoader extends ClassLoader {

    private String root;

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        String fileName = root + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
        try {
            InputStream ins = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length = 0;
            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getRoot() {
        return root;
    }

    public void setRoot(String root) {
        this.root = root;
    }

    public static void main(String[] args)  {

        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("E:\\temp");

        Class<?> testClass = null;
        try {
            testClass = classLoader.loadClass("com.neo.classloader.Test2");
            Object object = testClass.newInstance();
            System.out.println(object.getClass().getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

这里有几点需要注意:

  1. 这里传递的文件名需要是类的全限定性名称,即com.paddx.test.classloading.Test格式的,因为 defineClass 方法是按这种格式进行处理的。

  2. 最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。

  3. 这类Test 类本身可以被 AppClassLoader 类加载,因此我们不能把 com/paddx/test/classloading/Test.class 放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由 AppClassLoader 加载,而不会通过我们自定义类加载器来加载。

能不能自己写一个类叫java.lang.System/String

答案是不能

上测试代码:

public class MyClassLoader extends ClassLoader{
    
    public MyClassLoader() {
        super(null);
    }
    
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try{
            String className = null;
            if(name.startsWith("java.lang")){
                className = "/" + name.replace('.', '/') + ".class";
            }else{
                className = name.substring(name.lastIndexOf('.') + 1) + ".class";
            }
            System.out.println(className);
            InputStream is = getClass().getResourceAsStream(className);
            System.out.println(is);
            if(is == null)
                return super.loadClass(name);
            
            byte[] b = new byte[is.available()];
            is.read(b);
            return defineClass(name, b, 0, b.length);
        }catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }
}
public class ClassLoaderTest {
 
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        ClassLoader myLoader = new MyClassLoader();
        Object obj = myLoader.loadClass("java.lang.Math").newInstance();
        System.out.println(obj);
    }
    
}


public final class Math {
    
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}
public class MyMath {
    
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

上面的测试代码没用自定义java.lang.System类,因为测试代码用到了JDK自带的System类进行输出打印,会冲突,所以改用为自定义的java.lang.Math类。如果自定义的Math类能加载,那么自定义的System类同样能加载。 我们先直接运行下Math类,输出如下:

java.lang.NoSuchMethodError: mainException in thread "main" 

提示Math类没有main方法。首先大家要明白一个概念,当类首次主动使用时,必须进行类的加载,这部分工作是由类加载器来完成的。根据双亲委托原则,Math类首先由启动类加载器去尝试加载,很显然,它找到rt.jar中的java.lang.Math类并加载进内存(并不会加载我们自定义的Math类),然后执行main方法时,发现不存在该方法,所以报方法不存在错误。也就是说,默认情况下JVM不会加载我们自定义的Math类。

再直接运行MyMath类,输出如下:

java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:479)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:625)
at java.lang.ClassLoader.defineClass(ClassLoader.java:615)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:141)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:283)
at java.net.URLClassLoader.access$000(URLClassLoader.java:58)
at java.net.URLClassLoader$1.run(URLClassLoader.java:197)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
Exception in thread "main" 

由堆栈异常信息可知道,当应用程序类加载器类(AppClassLoader)尝试加载MyMath类时,ClassLoader.java的479行抛出了SecurityException

禁止使用包名:java.lang。

直接查看抽象类java.lang.ClassLoader的preDefineClass方法代码,摘抄如下:

    private ProtectionDomain preDefineClass(String name,
					    ProtectionDomain protectionDomain)
    {
	if (!checkName(name))
	    throw new NoClassDefFoundError("IllegalName: " + name);
 
	if ((name != null) && name.startsWith("java.")) {
	    throw new SecurityException("Prohibited package name: " +
					name.substring(0, name.lastIndexOf('.')));
	}
	if (protectionDomain == null) {
	    protectionDomain = getDefaultDomain();
	}
 
	if (name != null)
	    checkCerts(name, protectionDomain.getCodeSource());
 
	return protectionDomain;
    }

可以看到如果加载的类全名称以“java.”开头时,将会抛出SecurityException,这也是为什么直接执行MyMath类会出现SecurityException。

照这样,我们自定义的类加载器必须继承自ClassLoader,其loadClass()方法里调用了父类的defineClass()方法,并最终调到preDefineClass()方法,因此我们自定义的类加载器也是不能加载以“java.”开头的java类的。我们继续运行下ClassLoaderTest类,输出如下:

/java/lang/Math.class sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream@a981ca java.lang.SecurityException: Prohibited package name: java.lang at java.lang.ClassLoader.preDefineClass(ClassLoader.java:479) at java.lang.ClassLoader.defineClassCond(ClassLoader.java:625) at java.lang.ClassLoader.defineClass(ClassLoader.java:615) at java.lang.ClassLoader.defineClass(ClassLoader.java:465) at com.tq.MyClassLoader.loadClass(MyClassLoader.java:28) at com.tq.ClassLoaderTest.main(ClassLoaderTest.java:8) Exception in thread "main" java.lang.ClassNotFoundException at com.tq.MyClassLoader.loadClass(MyClassLoader.java:31) at com.tq.ClassLoaderTest.main(ClassLoaderTest.java:8)

红色部分清楚表明,也是在preDefineClass方法中抛出的SecurityException。

通过代码实例及源码分析可以看到,对于自定义的类加载器,强行用defineClass()方法去加载一个以"java."开头的类也是会抛出异常的。

总结

我们不能自己写以"java."开头的类,其要么不能加载进内存,要么即使你用自定义的类加载器去强行加载,也会收到一个SecurityException。