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

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

总结

  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缓存
    在这里插入图片描述

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

1.内置三大类加载器

在这里插入图片描述

在这里插入图片描述

1.1 根加载器 Boostrap ClassLoader

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

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

在这里插入图片描述

1.2 扩展类加载器 Ext ClassLoader

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

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

在这里插入图片描述

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

在这里插入图片描述

1.3 系统类加载器 App ClassLoader

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

在这里插入图片描述

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

在这里插入图片描述

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

classpath 不含有Demo.class
在这里插入图片描述

选择父类ClassLoader未加载的目录

在这里插入图片描述

在这里插入图片描述

2.2 双亲委托机制

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

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

在这里插入图片描述
在这里插入图片描述

★3.绕过双亲委托

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

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

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

  3. 重写loadClass
    loadClass中不再请求父类去加载
    在这里插入图片描述

在这里插入图片描述

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

★类加载器命名空间

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

如:★测试类加载器隔离同名类-不同的类加载器实现隔离;在这里插入图片描述

使用loadClass来观察 获取缓存
在这里插入图片描述

在这里插入图片描述

运行时包

classloader名+全路径列明

初始类加载器

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

在这里插入图片描述

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

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

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

MyClassLoader对应的java文件
在这里插入图片描述

MyClassLoader2对应的另一个class
在这里插入图片描述

demo1与demo2中的class 放置的位置
在这里插入图片描述

MyClassLoader.java
注意加载class的位置不同!在这里插入图片描述

MyClassLoader2.java
在这里插入图片描述

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

在这里插入图片描述

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

验证:【同一类加载器的不同实例 产生不同Class 实例】
在这里插入图片描述

编写代码证明

MyClassLoader 继承ClassLoader
在这里插入图片描述
已知
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?
在这里插入图片描述在这里插入图片描述

加载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
    在这里插入图片描述
    在这里插入图片描述在这里插入图片描述
发布了116 篇原创文章 · 获赞 44 · 访问量 31万+

猜你喜欢

转载自blog.csdn.net/thewindkee/article/details/96646275
今日推荐