JVM学习笔记P2—Java中的类加载器

275 阅读5分钟

  本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

类加载器

  类加载器只用于实现类的加载动作,但它在Java程序中起到的作用却远不限于类加载阶段。先看看Java中共有哪些类加载器吧。

public class ClassLoaderTest {
    public static void main(String[] args) {
        ClassLoader loader=ClassLoaderTest.class.getClassLoader();
        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

  这几种类加载器的层次关系如下图所示:

image.png

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

  站在Java虚拟机的角度来讲,只存在两种不同的类加载器:

  • 启动类加载器:由C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分其它的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类

  站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:

  • 启动类加载器Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的
  • 扩展类加载器Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
  • 应用程序类加载器Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器一般情况下这个就是程序中默认的类加载器

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

  • 1、在执行非置信代码之前,自动验证数字签名。
  • 2、动态地创建符合用户特定需要的定制化构建类。
  • 3、从特定的场所取得class文件,例如数据库中和网络中。

JVM类加载机制

  • 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入;
  • 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类;
  • 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效;

类的加载

  类加载有三种方式:

  • 1、命令行启动应用时候由JVM初始化加载
  • 2、通过Class.forName()方法动态加载
  • 3、通过ClassLoader.loadClass()方法动态加载

  如下所示:

public class LoaderTest {
​
    public static void main(String[] args) throws ClassNotFoundException {
       ClassLoader loader= Test.class.getClassLoader();
        //使用ClassLoader.loadClass()来加载类,不会执行初始化块
        loader.loadClass("com.jvm.loader.Test");
        System.out.println(loader);
        System.out.println("****************");
       
        //使用Class.forName()来加载类,默认执行初始化块
      ClassLoader loader1=  Class.forName("com.jvm.loader.Test").getClassLoader();
        System.out.println(loader1);
        System.out.println("================");
​
//Class.forName()來加载类,并指定ClassLoader,初始化时不执行静态块
        ClassLoader loader2=Class.forName("com.jvm.loader.Test",false,loader).getClassLoader();
        System.out.println(loader2);
        System.out.println("----------------");
   }
}
public class Test { 
        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()方法采用调用构造函数,创建类的对象 。

双亲委派模型

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

image.png

  举个例子:

  • 1、当AppClassLoader想要加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  • 2、当ExtClassLoader收到来自AppClassLoader的请求,要去加载某个class时,它也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader```去完成。
  • 3、紧接着,BootStrapClassLoader也收到了来自ExtClassLoader的请求,去加载某个class,加载成功,则结束;若是加载失败(例如在$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方法即可。下面我们通过一个示例来演示自定义类加载器的流程:

public class MyClassLoader extends ClassLoader {
​
    private String root;
​
    @Override
    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 in=new FileInputStream(fileName);
            ByteArrayOutputStream baos=new ByteArrayOutputStream();
            int bufferSize=1024;
            byte[] buffer=new byte[bufferSize];
            int length=0;
            while ((length=in.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("F:\Work_Space\jvm\src");
       try {
           Class<?> testClass=null;
           testClass=classLoader.loadClass("com.jvm.loader.Test");
           Object object=testClass.newInstance();
           System.out.println(object.getClass().getClassLoader());
       }catch (ClassNotFoundException e){
           e.printStackTrace();
       }catch (IllegalAccessException e){
           e.printStackTrace();
       }catch (InstantiationException e){
           e.printStackTrace();
       }
   }
}

  自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。由于这里只是演示,我并未对class文件进行加密,因此没有解密的过程。这里有几点需要注意:

  • 1、这里传递的文件名需要是类的全限定性名称,即com.jvm.loader.Test格式的,因为 defineClass方法是按这种格式进行处理的。
  • 2、最好不要重写loadClass方法,因为这样容易破坏双亲委托模式
  • 3、这类Test 类本身可以被 AppClassLoader类加载,因此我们不能把com/paddx/test/classloading/Test.class放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由AppClassLoader加载,而不会通过我们自定义类加载器来加载。

如何破坏双亲委派模式?

继承ClassLoader类并重写loadClass()方法,重新定义新的加载方式,则双亲委派模式就会被打破。

如下所示:

public class MyClassLoaderTest2 extends ClassLoader {
​
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
​
        ClassLoader loader=new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) {
               String fileName=name.substring(name.lastIndexOf(".")+1)+".class";
                InputStream in;
                try {
                    in=getClass().getResourceAsStream(fileName);
                    if (in==null){
                        return super.loadClass(fileName);
                   }
                    byte[] bytes=new byte[in.available()];
                    in.read(bytes);
                    in.close();
               } catch (Exception e) {
                    e.printStackTrace();
               }
                return null;
           }
       };
​
​
​
        System.out.println(loader);
        loader.loadClass("com.jvm.loader.Test").newInstance();
​
   }
}

  控制台输出:

com.jvm.loader.MyClassLoaderTest2$1@4554617c
Exception in thread "main" java.lang.NullPointerException
 at com.jvm.loader.MyClassLoaderTest2.main(MyClassLoaderTest2.java:36)

  出现报错,这是由于测试类Test,继承自java.lang.Object,因为破坏了双亲加载模型,Object类也会使用这个加载器加载,从Classpath下找这个类肯定是找不到的,因为Object位于java.lang包下。