《Java高并发编程详解-多线程架构与设计》JVM类加载器

摘自《Java高并发编程详解-多线程架构与设计》第九章 p158-p176

文章目录

    • 总结
    • 1.内置三大类加载器
      • 1.1 根加载器 Boostrap ClassLoader
      • 1.2 扩展类加载器 Ext ClassLoader
      • 1.3 系统类加载器 App ClassLoader
    • 2.自定义类加载器
      • 要点
        • 案例
      • 2.2 双亲委托机制
    • ★3.绕过双亲委托
    • 4.类加载命名空间、运行时包、类的卸载
      • ★类加载器命名空间
      • 运行时包
      • 初始类加载器
    • ★测试类加载器隔离同名类-不同的类加载器实现隔离
    • 类加载器的实例隔离Class对象
      • 编写代码证明

总结

  1. 内置类加载器 bootstrap ClassLoader,Ext ClassLoader ,App ClassLoader。分别加载jre\lib,jre\lib\ext,-cp或者-classpath对应的classpath

  2. 通过继承ClassLoader重写findClass特殊目录来实现自定义类加载器。特殊目录或者设置父加载器为空,让loadClass时跳过父类加载器(ps,如父类已经加载同名类,父加载器非空则会返回cache)。重写loadClass可以完全绕过双亲委托。

  3. class的实例是被类加载器的【实例】隔离。(当然不同的类加载器类型也会隔离) 因此代码中尽量得到同一个classLoader的实例,避免拿不到缓存,反复 findClass+defineClass。可以设置线程上下文类加载器。

  4. 在不指定parent的情况下,自定义ClassLoader的parent AppClassLoader是指的应用中唯一的AppClassLoader。因此加载classpath下已被加载过的类时,会因调用ClassLoader#getSystemClassLoader()对应AppClassLoader实例去调用findLoadedClass去获取Class缓存
    《Java高并发编程详解-多线程架构与设计》JVM类加载器_第1张图片

  5. 注意URLClassLoader#newInstance会调用到父类的ClassLoader()方法,导致传入系统的类加载器作为parent。而new URLClassLoader(url,null),会使得无parent类加载器。

ps:loadClass来观察,而不是使用Class.forName来观察。Class.forName在loadClass之前应该还有一次获取缓存的机会。

1.内置三大类加载器

《Java高并发编程详解-多线程架构与设计》JVM类加载器_第2张图片

《Java高并发编程详解-多线程架构与设计》JVM类加载器_第3张图片

1.1 根加载器 Boostrap ClassLoader

C++编写
-Xbootclasspath指定根加载器的路径。

sun.boot.class.path获得跟加载器加载的资源
jie\lib

《Java高并发编程详解-多线程架构与设计》JVM类加载器_第4张图片

1.2 扩展类加载器 Ext ClassLoader

Java编写,URLClassLoader的子类
用于加载 JAVA_HOME下的jre\lib\ext 里面的类库

java.ext.dirs可以获得扩展类加载器加载的类
jre\lib\ext

《Java高并发编程详解-多线程架构与设计》JVM类加载器_第5张图片

也可以将自己的类放到扩展类加载器的位置

《Java高并发编程详解-多线程架构与设计》JVM类加载器_第6张图片

1.3 系统类加载器 App ClassLoader

负责加载 -cp/-classpath 指定的类库资源

《Java高并发编程详解-多线程架构与设计》JVM类加载器_第7张图片

2.自定义类加载器

要点

  1. 自定义的类加载器都是ClassLoader的直接或间接在子类

  2. 需要重写抽象ClassLoader的 findClass方法。

  3. 需要指定一个父类类加载器。若不指定则绕过了双亲委派

  4. 需要自定义一个路径加载特殊的class,该目录不能为已经有类加载器使用过的目录。
    可以使用loadClass打破双亲委派后,再使用任意路径。(相同的目录导致被委托给了父类类加载器加载)

  5. 得到类的二进制数据(无论网络/本地读取或动态代理/cglib生存),使用defineClass将其变成Class

在这里插入图片描述

案例

/**
 * @auth thewindkee
 * @date 2018/12/15 0015 21:58
 */
public class MyClassLoader extends ClassLoader {
    //public static final String LIB_PATH = "C:\\Users\\gkwind\\Desktop";
    // 默认加载的位置
    public static final String LIB_PATH = "E:\\gkrep\\gitee\\test\\other\\src\\main\\java\\com\\gkwind\\ClassLoader\\demo\\";
    /*
     * !不要定义在bootstrap,ext,app类加载器能加载的位置。
     * 因为双亲委派的关系,会导致该类加载器失效。
     * 如下定义必须修改loadClass打破双亲委派
     */
    //public static final String LIB_PATH = MyClassLoader.class.getResource("/").getPath();
    public MyClassLoader() {
        //设置为null可以绕过双亲委派
        //super(null);
    }

    public MyClassLoader(ClassLoader parent) {
        super(parent);
    }


    protected Class findClass(String name) throws ClassNotFoundException {
        try {
            //这里查找多层
            byte[] bytes = getClassBytes(name);
            //defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class
            Class c = this.defineClass(name, bytes, 0, bytes.length);
            System.out.println(c);
            return c;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return super.findClass(name);
    }

    private byte[] getClassBytes(String name) throws Exception {
        // 这里要读入.class的字节,因此要使用字节流
        String resolvedPath = name.replace(".", "/");
        File file = new File(LIB_PATH + resolvedPath + ".class");
        System.out.println(MyClassLoader.class.getName() + "findClass:" + file);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        //FileInputStream fis = new FileInputStream(file);
        //FileChannel fc = fis.getChannel();
        //WritableByteChannel wbc = Channels.newChannel(baos);
        //ByteBuffer by = ByteBuffer.allocate(1024);
        //while (true) {
        //    int i = fc.read(by);
        //    if (i == 0 || i == -1)
        //        break;
        //    by.flip();
        //    wbc.write(by);
        //    by.clear();
        //}
        //fis.close();
        //wbc.close();
        java.nio.file.Files.copy(file.toPath(), baos);
        return baos.toByteArray();
    }
} 

《Java高并发编程详解-多线程架构与设计》JVM类加载器_第8张图片

自定义的类加载器指定特殊目录或者指定父类加载器为空,避免双亲委派导致该类加载器失效

classpath 不含有Demo.class
《Java高并发编程详解-多线程架构与设计》JVM类加载器_第9张图片

选择父类ClassLoader未加载的目录

《Java高并发编程详解-多线程架构与设计》JVM类加载器_第10张图片

2.2 双亲委托机制

为保证基础类不被破坏。加载类的时候优先祖师爷(递归父类)过目(加载)。 ----by 极客时间 jvm

loadClass 规定了双亲委托,所以可以直接重写loadClass打破双亲委托。
该方法是同步方法 – synchronized (getClassLoadingLock(name))
优先返回已经加载过的同名class --findLoadedClass(name)
loadClass(name,false)
false 指的是不做【连接(linked)阶段】的操作。这就是为什么加载类,导致类的初始化(【准备阶段】)。

《Java高并发编程详解-多线程架构与设计》JVM类加载器_第11张图片
《Java高并发编程详解-多线程架构与设计》JVM类加载器_第12张图片

★3.绕过双亲委托

不需要删除class.
1,2都是需要父类未加载过同名类,否则返回cache。3可以重复加载同名类

  1. 绕过系统类加载器,直接使用ext类加载器作为父类,并传入特殊的目录的class
    无cache且父类加载不到特殊目录,自动到子类去加载。

  2. 设置父类类加载器为null
    无cache且没有父类,自动到子类去加载

  3. 重写loadClass
    loadClass中不再请求父类去加载
    《Java高并发编程详解-多线程架构与设计》JVM类加载器_第13张图片

《Java高并发编程详解-多线程架构与设计》JVM类加载器_第14张图片

4.类加载命名空间、运行时包、类的卸载

★类加载器命名空间

类加载器作为一个命名空间,可以隔离class
使用不同类加载器/或者同一加载器的不同实例去加载同一个类,会产生多个实例。
简单来说,class实例被不同的classLoader实例隔离。

如:★测试类加载器隔离同名类-不同的类加载器实现隔离;《Java高并发编程详解-多线程架构与设计》JVM类加载器_第15张图片

使用loadClass来观察 获取缓存
《Java高并发编程详解-多线程架构与设计》JVM类加载器_第16张图片

《Java高并发编程详解-多线程架构与设计》JVM类加载器_第17张图片

运行时包

classloader名+全路径列明

初始类加载器

一个类的初始类加载器,包含尝试过加载的所有父类

《Java高并发编程详解-多线程架构与设计》JVM类加载器_第18张图片

★测试类加载器隔离同名类-不同的类加载器实现隔离

此处使用了不同的类加载器 去隔离。
Demo.java

不同的位置的Demo.java输出的内容不同

MyClassLoader对应的java文件
《Java高并发编程详解-多线程架构与设计》JVM类加载器_第19张图片

MyClassLoader2对应的另一个class
《Java高并发编程详解-多线程架构与设计》JVM类加载器_第20张图片

demo1与demo2中的class 放置的位置
《Java高并发编程详解-多线程架构与设计》JVM类加载器_第21张图片

MyClassLoader.java
注意加载class的位置不同!

MyClassLoader2.java

测试类
两个com.gkwind.Demo都被加载成功

类加载器的实例隔离Class对象

验证:【同一类加载器的不同实例 产生不同Class 实例】
《Java高并发编程详解-多线程架构与设计》JVM类加载器_第22张图片

编写代码证明

MyClassLoader 继承ClassLoader
《Java高并发编程详解-多线程架构与设计》JVM类加载器_第23张图片
已知
1.Demo.class是自己编译的Class,不存在于
lib,lib/ext,classpath.在目录X下
2.A类对应A.class编译在classpath下。
3.MyClassLoader继承了ClassLoader,自定义了 findClass的目录X(App,Ext类加载器无法加载X目录),没有重写loadClass,更没有打破双亲委派。

问题:为何MyClassLoader加载Demo的时候,无法通过findLoadedClass获取Class的Cache?
《Java高并发编程详解-多线程架构与设计》JVM类加载器_第24张图片

加载A类,注意看两次new MyClassLoader的parent都是同一个实例。

演示时,造成A类与Demo类不一样的原因是:classpath对应的AppClassLoader 全局唯一

  • new MyClassLoader().loadClass(“A”)多次,
    委托parent->唯一的AppClassLoader去加载,第一次findClass,第二次findLoadedClass

  • new MyClassLoader().loadClass(“Demo”)多次,由于Demo.class不存在classpath对应的目录,导致由new MyClassLoader()实例去加载,每次都是新的ClassLoader实例,因此无法从findLoadedClass中获得缓存的Demo.class

结论:

  1. 尽量保持ClassLoader的唯一,避免不同ClassLoader实例重复加载Class。
  2. 可以通过线程去传递ClassLoader。
  3. 【同一类加载器的不同实例 产生不同Class 实例】 正确。—由加载Demo类可以看出。
  4. 继承ClassLoader的自定义类加载器默认会调用super()传入默认的AppClassLoader作为parent。super()传入唯一的AppClassLoader

你可能感兴趣的:(Java,读书笔记)