Java类加载器(双亲委派模型)(综述)
阅读原文时间:2021年04月20日阅读:1

文章目录

类加载器

一、预定义类型类加载器

1、启动(Bootstrap)类加载器,负责将 /lib 下面的类库加载到内存中。
2、扩展(Extension)类加载器ExtClassLoader:负责将 < Java_Runtime_Home >/lib/ext 或者由系统变量 java.ext.dir 指定位置中的类库加载到内存中
3、系统(System)类加载器AppClassLoader:负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。
4、线程上下文类加载器ThreadContextClassLoader(TCCL):用于解决双亲委托模型的缺陷,可以实现核心库接口加载系统类(这一条先忽略

二、类加载器结构

1、jvm加载的顺序:BoopStrap ClassLoder–>ExtClassLoader–>AppClassLoder
2、类加载器之间的关系:AppClassLoader的父加载器为ExtClassLoader,ExtClassLoader的父加载器为null,BoopStrap ClassLoader为顶级加载器。

package test;
//展示了类加载器的继承关系
public class TestGC {

    public static void main(String []args){

        System.out.println(Test.class.getClassLoader().toString());

        System.out.println(Test.class.getClassLoader().getParent().toString());

        System.out.println(Test.class.getClassLoader().getParent().getParent().toString());
    }
}

考虑一下类加载器的结构为什么会选择首先通过父类加载器去加载,只有父类无法加载了才会使用子类加载器去加载?(类加载器代理模式
首先需要去明确一点,如果是相同的Java类,当使用不同的类加载器加载,并通过加载器获得的java.lang.Class实例化的对象,这两个实例化对象实不相等的。有了这个前提之后,在java中会具有一些核心类,如果去使用自身类加载器,实例化后就会造成这些类之间不兼容。因此会选择代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。

双亲委派模型

一、双亲委派模型流程

根据类加载器流程图,当需要查找一个class对象时候,由于类加载机制只要加载过该类,就不需要去重新加载,只需要查找缓存
1、缓存路:查找自身加载器是否有缓存,没有则委托父类AppClassLoader加载器---->查找AppClassLoader加载器是否有缓存,没有则委托父类ExtClassLoader---->查找ExtClassLoader加载器是否有缓存,没有则委托BoopStrap加载器–>查找BoopStrap加载器是否有缓存,没有则开始加载(在任何一个加载器中该类已经加载,则直接返回)
2、加载路:BoopStrap在核心库中加载,如果未加载成果---->ExtClassLoader在lib/ext中加载,如果未加载成果----->AppClassLoader在当前classpath中加载,如果未加载成果---->自定义加载器加载,如果未加载成果---->抛出异常ClassNotFoundException

二、双亲委派模型源码

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查是否已经加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        //父加载器不为空,调用父加载器的loadClass
                        c = parent.loadClass(name, false);
                    } else {
                        //父加载器为空则,调用Bootstrap Classloader
                        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();
                    //父加载器没有找到,则调用findclass
                    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()
                resolveClass(c);
            }
            return c;
        }
    }

自定义类加载器

一、类加载器继承关系

ExtClassLoader和AppClassLoader差异
1、Launcher E x t C l a s s L o a d e r 和 L a u n c h e r ExtClassLoader和Launcher ExtClassLoader和LauncherAppClassLoader都是URLClassLoader的子类,但是他们的实现又是有一点不同的,Launcher E x t C l a s s L o a d e r 的 实 现 是 遵 循 的 双 亲 委 派 模 型 , 它 重 写 的 是 f i n d C l a s s 方 法 , 而 L a u n c h e r ExtClassLoader的实现是遵循的双亲委派模型, 它重写的是findClass方法,而Launcher ExtClassLoader的实现是遵循的双亲委派模型,它重写的是findClass方法,而LauncherAppClassLoader的实现是没有遵循双亲委派模型的,它重的是loadClass方法

Launcher$AppClassLoader重写loadClass目的:

Launcher$AppClassLoader是用于加载各个不同应用下面的类,同一个JVM中可以同时存在多个应用,如同一个Tomcat中可以同时存在多个Web应用,而这些应用可能是来自不同的开发方,他们之间彼此可能都不知道是谁,但是他们写的类却可能具有相同的全限定名,所以为了确保这些应用之间互不干扰,就需要由各应用的类加载器去加载所属应用的类,这样就不会发生类冲突了。

二、ClassLoader

1、构造函数

1、默认类加载器是系统类加载器

 protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }

2、可以指定类加载器

protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
    }

2、核心方法

1、loadClass(String name):加载指定名称(包括包名)的二进制类型,供用户调用的接口
2、 loadClass(String name, boolean resolve):加载指定名称(包括包名)的二进制类型,同时指定是否解析(
3、findClass(String name):一般被loadClass方法调用去加载指定名称类,供继承用
4、defineClass(String name, byte[] b, int off, int len):解析对应的字节码,产生对应的内部数据结构放置到方法区,所以无需覆写,直接调用就可以了

loadClass与findClass区别:
loadClass:想在JVM的不同类加载器中保留具有相同全限定名的类,那就要通过重写loadClass来实现,此时首先是通过用户自定义的类加载器来判断该类是否可加载,如果可以加载就由自定义的类加载器进行加载,如果不能够加载才交给父类加载器去加载。
findClass方法的自定义类,首先会通过父类加载器进行加载,如果所有父类加载器都无法加载,再通过用户自定义的findClass方法进行加载。如果父类加载器可以加载这个类或者当前类已经存在于某个父类的容器中了,这个类是不会再次被加载的,此时用户自定义的findClass方法就不会被执行了。
一般在自定义类加载器的时候只重写findclass方法,不重写loadclass方法

三、自定义类加载器实例

一、设计java类,将其放置到G://盘下,通过javac 编译为robin.class

注意:由于本人使用eclipse编写,会在代码头部出现包名,为了避免Exception in thread "main" java.lang.NoClassDefFoundError: robin (wrong name: chapter7/robin),是因为我在文件中添加了包名,删除包名即可

public class robin {
    //之后将Test打包成jar,放置到运行环境F:\eclipse\jdk1.8\jre\lib\ext,
    //加载方式由AppClassLoader变为ExtClassLoader,可以用来验证不同类加载器加载的文件类型
    public robin() {
        // TODO Auto-generated constructor stub
    }
    public void say() {
        System.out.println("robin CLassLoader");
    }

}

二、设计ClassLoader类

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

public class MyClassLoader extends ClassLoader{
    String classpath;
    public MyClassLoader(String classpath) {
        this.classpath = classpath;
    }
    //重写findclass的目的在于父类无法加载的时候自定义加载器加载
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = getData(name);
            if(data!=null) {
                return defineClass(name, data, 0, data.length);//写入到运行时数据区
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        return super.findClass(name);
    }
    //将class文件转化为byte数组
    public byte[] getData(String name) throws IOException {
        InputStream in = null;
        ByteArrayOutputStream out = null;
        String path = classpath+name.replace(".", File.separator)+".class";
        try {
            in = new FileInputStream(path);
            out = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int len = 0;
            while((len = in.read(buffer))!=-1) {
                out.write(buffer, 0, len);
            }
            return out.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            in.close();
            out.close();
        }
        return null;
    }

}

三、测试类

package chapter7copy;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class TestMyClassLoader {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException {
        System.out.println(System.getProperty("sun.boot.class.path"));//bootstrap加载器加载的jar包
        System.out.println(System.getProperty("sun.class.path"));
        ClassLoader intTest = int.class.getClassLoader();
//        System.out.println("int ClassLoader:"+intTest.toString());//空指针,有bootstrap加载
        MyClassLoader classLoader = new MyClassLoader("G://");
        Class<?> c1 = classLoader.loadClass("robin");
        if(c1!=null) {
            Object obj = c1.newInstance();
            Method method = c1.getMethod("say", null);//获得方法
            method.invoke(obj, null);//方法使用
            System.out.println(c1.getClassLoader().toString());//输出类加载器
        }
    }
}

四、结果
robin类由MyClassLoader加载器加载

robin CLassLoader
chapter7copy.MyClassLoader@6d06d69c  

四、双亲委托模型的破坏

1、第一次破坏

第一次模型的破坏就是指在jdk1.2之前,因为此时还没有引入双亲委托模型,用户在自定义类加载器的时候需要去重写loadclass(),因为虚拟在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。jdk1.2之后就不提倡用户去重写loadclass(),类加载逻辑写到findclass()中,保证新写出的类符合双亲委托模型。

2、第二次破坏

第二次的破坏是由于双清委托模型自身的缺陷导致,Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。
这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(System ClassLoader)来加载的。引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。

关于SPI(Service Provider Interface)请参考:https://www.jianshu.com/p/46b42f7f593c

上边的问题往简单里说就是服务提供者接口属于核心库(Bootstrap Classloader加载),而接口实现类属于实现类(System ClassLoader加载),启动类加载器五法委派系统类加载器,自然而产产生解决方案:线程上下文类加载器(TCCL)

  • 线程上下文类加载器(TCCL)
    类是通过java.util.ServiceLoader类来加载,而ServiceLoader.class又加载在BootrapLoader中,此时肯定不能通过BootrapLoader来加载类,只能使用TCCL了,也就是说把自己加载不了的类加载到TCCL中

    public static ServiceLoader load(Class service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();//当前线程获取上下文加载器
    return ServiceLoader.load(service, cl);
    }

ContextClassLoader默认存放了AppClassLoader的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作

1、当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。
2、当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。

3、第三次破坏

由于用户对程序的动态性的追求导致的,动态性指:代码热替换、模块热部署等,通俗来讲类似于计算机外设,接上鼠标就可以使用,不需要重启计算机。其中OSGitHub实现的模块化热部署关键在于自定义的类加载器机制的实现,每一个模块都有一个自己的类加载器,当需要更换模块的时候,就把模块连同类加载器一起更换。

在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。

引用

【1】双亲委派模型流程图:https://www.cnblogs.com/gdpuzxs/p/7044963.html
【2】ExtClassLoader和AppClassLoader继承关系图:http://www.360doc.com/content/16/0614/10/7510008_567634519.shtml
【3】ExtClassLoader和AppClassLoader实现差异性:
loaderClass与findClass区别:https://blog.csdn.net/caomiao2006/article/details/47735245
【4】比较好:http://www.blogjava.net/zhuxing/archive/2008/08/08/220841.html
【5】第二次破坏,线程上下文类加载器https://blog.csdn.net/yangcheng33/article/details/52631940

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器