面试干货4——你对Java类加载器(自定义类加载器)有了解吗?

Posted LuckyWangxs

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试干货4——你对Java类加载器(自定义类加载器)有了解吗?相关的知识,希望对你有一定的参考价值。

类加载器


推荐:在准备面试的同学可以看看这个系列

        面试干货1——请你说说Java类的加载过程

        面试干货2——你对Java GC垃圾回收有了解吗?

        面试干货3——基于JDK1.8的HashMap(与Hashtable的区别、数据结构、内存泄漏…)

        面试干货4——你对Java类加载器(自定义类加载器)有了解吗?

        面试干货5——请详细说说JVM内存结构(堆、栈、常量池)

        面试干货6——输入网址按下回车后发生了什么?三次握手与四次挥手原来如此简单!

        面试干货7——刁钻面试官:关于redis,你都了解什么?

        面试干货8——面试官:可以聊聊有关数据库的优化吗?

        面试干货9——synchronized的底层原理


一、类加载器的作用

        Java代码是不能直接运行的,需要通过编译器编译成JVM能够识别的二进制字节码文件,而类加载器的作用就是将这些二进制字节码文件即.class文件装载进虚拟机,这样虚拟机才能将程序运行起来。

二、Java虚拟机类加载器结构

1. 引导类(启动类)加载器

        引导(Bootstrap)类加载器:又称之为根类加载器或原始类加载器,引导类加载器是由本地代码(C语言)实现的类加载器,它负责将JAVA_HOME/jre/lib下面的核心类库如rt.jar、resource.jar或者将-Xbootclasspath参数指定目录下的jar包等JVM能够识别的类库加载到内存,该加载器是以文件名去lib目录下识别的,例如文件名不为rt.jar,即使放到lib目录下也不会被加载。由于启动类是由C语言实现的,开发人员无法直接获取到启动类加载器的引用。所以启动类加载器加载到的路径可以由System.getProperty(“sun.boot.class.path”)查看。
        拓展: 引导类和其他类加载器一样,但它不是一个Java类,在java中所有的类都要被加载到内存,扩展类和系统类加载器也一样,需要被加载,当JVM启动时,会运行特殊的机器代码,去加载系统类加载器和扩展类加载器,而这段特殊机器指令会启动整个类加载过程,它就是引导类加载器,加载一个纯java类加载器,是他的工作,一句话,引导类加载器是由c++编写,由JVM启动的。引导类加载器还负责加载支持基本Java运行时环境所需的所有代码,包括java.util和java.lang包下的类。

2. 扩展类加载器

        扩展(Extend)类加载器:扩展类加载器是由Sun的ExtClassLoad实现的,是sun.misc.Luncher的静态内部类。它负责将JAVA_HOME/jre/lib/ext下的jar加载到内存或者由-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器,具体加载路径可通过System.getProperty(“java.ext.dir”)。

3. 系统类加载器

        系统(System)加载器:系统类加载器是由Sun的AppClassLoad实现的,跟ExtClassLoad一样,也是sun.misc.Luncher的静态内部类。它负责将用户类路径(java -classpath)或-Djava.class.path参数指定的位置的jar加载到内存,即当前类所在路径及其引用的三方类库,开发者可以直接使用系统类加载器,可以用过System.getProperty(“java.class.path”)查看加载路径。

三、类加载器的加载机制

        类加载器负责将.class文件加载到内存,系统为所有被载入到内存的类生成Class对象,类一但被加载,便不会加载第二次,而判断一个类是否被加载的标识是:类全限定名+类加载器,例如pg包下的Person类被CL1类加载器加载到内存,唯一标识为(Person, pg, CL1),又被CL2加载到内存唯一标识为(Person, pg, CL2),则跟上一个是完全不同且互不兼容的两个类,不过显然两个加载器加载同一个类,实际上是冗余了,那么类加载器的加载机制会如何防止这种情况发生呢?
        类加载器加载机制有三个,这三种机制共同完成了类的加载,使得类的加载安全高效。

1. 全盘负责

        当一个类加载器负责加载某个类时,那这个类所引用的所有类都用这个加载器去加载,除非显示调用其他类加载器,这样可以避免一个类被重复加载。

2. 双亲委派

        双亲委派,说白了就是坑爹机制,就是什么事都交给爹去擦屁股,只有爹说这事儿我干不了,儿子才自己想办法去干。哈哈哈,只是一个比喻,下面圆规正转,啊不,是言归正传~
        双亲委派,又叫做父类委托,即在加载一个类时,先让父类加载器试图去加载该类,如果父类还有父类,则一直递归到顶级父类加载器(引导类加载器没有父类,它为始祖类加载器),如果始祖类加载器无法加载当前类,则反向委托给始祖类的子类加载器,以此类推,直到源类加载器,此处的父子关系并非继承,而是一种组合关系,是类加载器的复用。
        将.class文件载入内存的过程由双亲委派机制完成的。
优点:
        1. 避免一个类被多次加载
        2. 比较安全,防止篡改核心API。Java的核心API是由引导类加载器加载的,而双亲委派会一直请求父类加载器加载,一般都会找到引导类加载器先去加载类,那么,如果从网络上传输过来一个java.lang.Integer类需要加载,此时找到了引导类加载器,引导类加载器发现这个类已经被我加载了,所以不再加载直接返回
        3. 又或者,我随便定义了一个java.lang.MyInteger,又找到了引导类加载器,引导类加载器发现java核心类库java.lang下根本没这个类,就会反向委托给子类,但是java.lang需要访问权限,普通加载器无法访问加载,强制加载会报错。

3. 缓存机制

        当jvm加载完成一个类是会将类放入jvm缓存中,加载流程为先去缓存区查看当前类是否被加载,如果没有则读.class文件并加载,如果加载则直接返回

四、自定义类加载器

1. 编写自定义类加载器
        实现一个用户自定义类加载器需要去继承ClassLoader类并重写findClass方法,代码如下:

public class MyClassLoader extends ClassLoader 
	// 默认读取的class路径, 你可以任意定义
    private String path = "D:\\\\workspace-IDEA\\\\cloud2020\\\\spring-cloud-test\\\\target\\\\classes\\\\";
    private final String fileType = ".class"; // 文件类型
    private String name; // 文件全限定名, 例如 com.entity.Person
    // name为类加载器的名称, 在此构造函数调用父类无参构造函数, 会初始化类加载器, 默认将系统类加载器作为父类加载器
    // 也可以用下面那个构造函数指定父类加载器. 大家可以去看下ClassLoader类的无参构造函数源码
    public MyClassLoader(String name) 
        super();
        this.name = name;
    
    // 可以指定父类加载器的构造方法
    public MyClassLoader(ClassLoader parent, String name) 
        super(parent);
        this.name = name;
    
    // 找到自己需要加载的class文件, 并按自己的方式读取到字节数组, 最后复用java的api得到一个Class类型的对象并返回
    @Override
    protected Class<?> findClass(String name) 
        byte[] b = loadClassData(name);
        return defineClass(name, b, 0, b.length);
    
    // 将class字节码文件读到字节数组
    private byte[] loadClassData(String name) 
        byte[] data=null;
        InputStream in=null;
        name=name.replace('.', '/');
        ByteArrayOutputStream out=new ByteArrayOutputStream();
        try 
            in=new FileInputStream(new File(path+name+fileType));
            int len=0;
            while(-1!=(len=in.read()))
                out.write(len);
            
            data=out.toByteArray();
         catch (Exception e) 
            e.printStackTrace();
         finally 
            try 
                in.close();
                out.close();
             catch (IOException e) 
                e.printStackTrace();
            
        
        return data;
    
	// 重写toString方法, 以便后续this.getClass().getClassLoader()
    @Override
    public String toString() 
        // TODO Auto-generated method stub
        return this.name;
    

        如果你想要打破双亲委派模型,那么你需要重写loadClass方法,ClassLoader的loadClass方法,大家可以去看下源码。在这里我就不打破双亲委派模型了。
2. 测试
被加载的类:

public class One
   public One() 
       // 在控制台输出加载当前类的类加载器
       System.out.println("One: i am loaded by " + this.getClass().getClassLoader());
   

------------------
public class Two
   public One() 
       // 在控制台输出加载当前类的类加载器
       System.out.println("Two: i am loaded by " + this.getClass().getClassLoader());
   

Main方法

 public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException 
    // 创建自己的根类加载器, 默认父类加载器是java的系统类加载器, 并设置加载路径
    MyClassLoader myClassLoader0 = new MyClassLoader("myRootClassLoader");
    myClassLoader0.setPath("/app/zero/");
    // 创建自己的拓展类加载器, 父类为自定义根类加载器, 并设置加载路径
    MyClassLoader myClassLoader1 = new MyClassLoader(myClassLoader0, "myExtendClassLoader");
    myClassLoader1.setPath("/app/one/");
	// 调用自定义拓展类加载器加载程序中不存在但存在于/app/zero/中的class文件
    Class<?> aClass1 = myClassLoader1.loadClass("com.wangxs.springcloud.classloader.One");
    aClass1.newInstance(); // 输出结果应该是 myRootClassLoader

        上述代码,我们自定义的根类加载器myClassLoader0,父类为java系统类加载器,再定义一个拓展类加载器myClassLoader2,父类为myClassLoader1,现在One.class文件存在于/app/zero/目录下,而不存在于程序类路径下,那我们用自定义拓展类加载器myClassLoader2去加载One.class,由双亲委派机制,我们可以推断出,会向上委托至java系统类加载器去加载One.class,但是系统类加载器发现自己不能加载该类,便反向委托给自定义根类加载器myClassLoader0去加载,输出结果应为:myRootClassLoader

public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException 
	// 创建自己的系统类加载器, 无父类加载器, 设置加载路径
    MyClassLoader myClassLoader2 = new MyClassLoader(null, "myAppClassLoader");
    myClassLoader2.setPath("/app/two/");
	// 加载程序中不存在, 但在/app/two/中存在的class文件
    Class<?> aClass2 = myClassLoader2.loadClass("com.wangxs.springcloud.classloader.Two");
    aClass2.newInstance(); // 输出结果应该是 myAppClassLoader

上述代码创建了自定义系统类加载器myClassLoader2,无父类加载器,那么直接由该加载器加载类,结果应为myAppClassLoader

以上结果如下图:

五、应用场景

1. 加密保护
        公司的有些核心类库的字节码是经过加密的,这样的话,就需要实现自己的加载器,在加载这些类库的时候进行解密,然后再载入到内存
2. 其他来源
        字节码是放在数据库,硬盘其他路径,甚至有可能放在云上。

以上便为自己学习JVM类加载器相关知识的总结,之前的文章也介绍了类加载过程以及垃圾回收机制,大家可以去看下。

以上是关于面试干货4——你对Java类加载器(自定义类加载器)有了解吗?的主要内容,如果未能解决你的问题,请参考以下文章

深入理解Java虚拟机——类加载器

深入理解Java虚拟机——类加载器

java类加载器有哪些

java类加载器有哪些

Java自定义类加载和ClassPath类加载器

java 自定义类加载器