类加载器原理

3 篇文章 0 订阅

类加载器原理

类的加载概述

我们编写的".java"扩展名的源代码中存储着需要执行的程序逻辑,这些文件需要经过java编译器编译成.class文件,".class"文件中存放着编译后虚拟机指令的二进制信息,当某个类需要被用到时,虚拟机会加载它,并在内存中新建对应的class对象,这个过程被称之为类加载。一个类的生命周期从类加载,连接和初始化开始,只有在虚拟机内存中,我们的java程序才能使用它。

类的加载、连接和初始化

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jBWK5kYn-1598495081371)(6589CF6D7D8E400CB112DE2EE85DF892)]

类加载器

类的加载是由类加载器完成的,类加载器可以分为两种:第一种是Java虚拟机自带的类加载器,分别为启动类加载器、扩展类加载器和系统类加载器,第二种是用户自定义的类加载器,是java.lang.ClassLoader的子类实例。

虚拟机内置类加载器

根类加载器(Bootstrap)

根类加载器是最底层的类加载器,是虚拟机的一部分,它是由C++语言实现的,且没有附加载器,也没有继承ClassLoader类。它主要负责加载系统属性“sun.boot.calss.path”指定路径下的核心类库(即<JAVA_HOME>\jre\lib),出于安全考虑,根类加载器只能加载java,javax,sun开头的类。

    public static void main(String[] args) {
        ClassLoader c1 = Object.class.getClassLoader();
        System.out.println(c1);
        // 输出为null
    }
扩展类加载器

扩展类加载器是指由原SUN公司实现的sun.misc.Laucher$ExtClassLoader类,它是由java语言编写的,父加载器是根类加载器,主要负责加载<JAVA_HOME>\jre\lib\ext目录下的类型或者系统变量"java.ext.dirs"指定的目录下的类库。

    public static void main(String[] args) {
        ClassLoader c1 = DNSNameService.class.getClassLoader();
        System.out.println(c1);
        // 输出 sun.misc.Launcher$ExtClassLoader@160bc7c0
    }
系统类加载器

系统类加载器也被称之为应用类加载器,它是原SUN公司实现的sun.misc.Launcher$AppClassLoad类,它的父类是扩展类加载器。它负责从 classpath环境变量或者系统属性java.class.path所指定的目录中加载类,它是用户自定义的类加载器的父加载器。一般情况下,该类加载器是程序中的默认加载器。可以通过ClassLoader.getSystemClassLoader()直接获得。

     ClassLoader c1 =   ClassLoader.getSystemClassLoader();
     System.out.println(c1);
    ClassLoader c2 = testDemo.class.getClassLoader();
    System.out.println(c2);
    // 输出
    //sun.misc.Launcher$AppClassLoader@7ef72e77
    //sun.misc.Launcher$AppClassLoader@7ef72e77
小结

在程序开发中,类的加载几乎是由上述3中类加载器配合执行的,同时我们还可以自定义类加载器,需要注意的是:Java虚拟机对class文件采用的是按需加载的方式(当该类需要使用时才会将他的class类加载到方法区生成class对象),而且加载某个类的class文件时,java虚拟机是采用双亲委派模式,即把加载类的请求交由父加载器处理,他是一种任务委派模式。

类加载器的双亲委派机制

除了根加载器以外,所有的类加载器都要有自己的父加载器。双亲委派机制能够很好的保护java程序安全,除了虚拟机自带的加载器之外,其余的类加载器都有唯一的父加载器。
比如:ClassLoader需要加载一个类时,该classLoader先委托自己的父加载器去先加载这个类,若父加载器能加载,则父加载器加载;否则这个ClassLoader才会自己去加载这个类。注意:双亲委派机制并非费面向对象程序中的继承关系,而是通过组合模式来复用类加载器代码

在这里插入图片描述

双亲委派模式的好处:

  1. 可以避免类的重复加载,当父类加载器已经加载该类时,就没必要子加载器再加载一次。
  2. 考虑安全因素,java核心api中定义类型不会被随意更换,假设通过网络传递一个名为java.lang.Object的类,通过双亲委托模式自动传递到类加载,而启动类加载器再核心java API发现这个名字的类,发现这个类已经被加载,并不会重新加载网络传递过来的lang.Object,而直接返回已经加载过的Object.class,这样可以防止核心API库被篡改。

ClassLoader

所有类加载(除了根类加载器)都必须要继承java.lang.ClassLoader。它是一个抽象类,主要方法如下:

  • loadClass
  • findClass
  • defineClass
  • resolveClass
loadClass

在ClassLoader的源码中,有一个方法loadClass(String, name, boolean resolve),这里就是双亲委托模式的代码实现。从源码中我们可以观察到它的执行顺序。需要注意的是:只有父类加载器加载不到类时,会调用findClass方法进行查找,所以,在定义自己的类加载器时,不要覆盖掉loadClass方法,而应该覆盖掉findClass方法。

 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;
        }
    }

使用指定的二进制名称来加载类,此方法的默认实现按一下顺序进行:

  1. 调用findLocalClss()来检查是否已经加载。
  2. 在父类加载器上调用loadClass方法,如果父类加载器为null,则使用虚拟机的内置类加载器。
  3. 调用findClass(String)方法找类。

如果上述步骤找到类,并且resolve=true,则此方法将在得到的Class上调用resolveClass()方法。

findClass

在自定义类加载器时,一般我们需要覆盖整个方法,且ClassLoader中给出了一个默认的错误实现。

 protected Class<?> findClass(String name) throws ClassNotFoundException 
defineClass

用来将byte字节解析成虚拟机能够识别的Class对象,defineClass()方法通常与findClass()共同使用。在自定义类加载器时,会直接覆盖 ClassLoader的findClass()方法获得要加载类的字节码,然后调用defineClass()方法生产的calss对象。

    protected final Class<?> defineClass(byte[] b, int off, int len)
        throws ClassFormatError
resolveClass

连接到指定的类。类加载器可以使用此方法来连接类。

URLClassLoader

在java.net包中,JDK提供了一个更加易用的类加载器URLClassLoader,它扩展了ClassLoader,能够从本地 或者网络上指定的位置加载类,我们可以使用该类作为自定义的类加载器使用。

构造方法:

// 指定要加载的类所在的URL地址,弗雷加载器默认为系统类加载器
public URLClassLoader(URL[] urls);
URLClassLoader(URL[] urls, ClassLoader parent);

自定义类加载器

如何自定义类加载呢?只需要将自己新建类的类加载器继承ClassLoader,并重写findClass方法即可。

自定义文件类加载器

public class MyCalssLoader extends ClassLoader {
   // 文件所在目录
    private String dir;

    public MyCalssLoader(String dir) {
        this.dir = dir;
    }

    public MyCalssLoader(String dir, ClassLoader parent) {
        super(parent);
        this.dir = dir;
    }

@Override
    protected Class<?> findClass(String name) {
    // 将文件名替换为目录
    String file = dir + File.separator + name + name.replace(".", File.separator) + ".class";
    InputStream in = null;
    try {
        in = new FileInputStream(file);

        // 构建字节输出流
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte buf[] = new byte[1024];
        int len = -1;
        while ((len = in.read(buf)) != -1) {
            baos.write(buf, 0, len);
        }
        byte data[] = baos.toByteArray();
        in.close();
        baos.close();

    return defineClass(name, data, 0, data.length);
    }catch (IOException e) {
        throw new RuntimeException(e);
    }
    }

    public static void main(String[] args) throws Exception {
        MyCalssLoader myCalssLoader = new MyCalssLoader("d:/");
        // 加载class
        Class clazz = myCalssLoader.loadClass("demo"); 
        // 实例化demo类
        Object o = clazz.newInstance();
    }
}

自定义网络类加载器
public class MyURLClassLoader extends ClassLoader {

    private String url;


    public MyURLClassLoader(String url) {
        this.url = url;
    }

    @Override
    protected Class<?> findClass(String name) throws Exception {
        String path = url + "/" + name.replace(".", "/") +".class";
        URL  url = new URL(path);
        InputStream in = url.openStream();
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            byte b[] = new byte[1024];
            int len = -1;
            while ((len = in.read(b)) != -1) {
                bos.write(len);
            }
            
            byte[] data = bos.toByteArray();
            in.close();
            bos.close();;
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            throw  new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws Exception {
        MyURLClassLoader urlClassLoader = new MyURLClassLoader("http://loaclhost:8080/example");
       Class c =  urlClassLoader.loadClass("demo;");
       Object o = c.newInstance();
    }
}
热部署类加载器

当我们调用老大Class方法加载类时,会采用双亲委派模式,即如果类已经被加载,就从缓存中获取,不会重新加载。如果同一个class被同一个类加载器多次加载,则会报错。因此,我们要实现热部署,让同一个class文件被不同的类加载器重复加载。但是不能调用loadClass方法,而应该调用findClass方法,避开双亲委托模式,从而实现同一个类被多次加载,实现热部署。

这里提出一个问题:一个class被同一个类加载器多次加载,为什么会报错呢?

类加载器与隐式加载

类的加载方式是指虚拟机将class文件加载到内存的方式。

显式加载是指,在java代码中通过调用ClassLoader加载class对象,比如Class.forName(String name);this.getClass().getClassLoader().loadClass()加载类。

隐式加载指不需要在java代码中明确调用加载的代码,而是通过虚拟机自动加载到内存中。比如在加载某个class时,改class引用了另外一个类的对象,那么这个对象的字节码就回被虚拟机自动加载到内存中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值