《Java高并发编程详解:多线程与架构设计》笔记(二)_所有参与的类加载器,即便没有亲自加载过该类,也都会被标识为该类的初始类加载器。

系统类加载器的父加载器是扩展类加载器,它是一种常见的类加载器,负责加载classpath下的类库资源,比如开发过程中引入的第三方jar包。

自定义类加载器(Custom ClassLoader)

除上面三种类加载器以外,还有自定义类加载器,它的默认父加载器是系统类加载器。下面给一个自定义类加载器的代码,如下

package com.hust.zhang.classloader;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class MyClassLoader extends ClassLoader {
    private final static Path DEFAULT_CLASS_DIR = Paths.get("/Users/kaizhang/repository", "myClassLoader");
    private Path classDir;

    //默认类加载路径
    public MyClassLoader() {
        super();
        this.classDir = DEFAULT_CLASS_DIR;
    }

    //允许传入指定路径的class路径
    public MyClassLoader(String classDir) {
        super();
        this.classDir = Paths.get(classDir);
    }

    //指定class路径的同时,指定父类加载器
    public MyClassLoader(String classDir, ClassLoader parent) {
        super(parent);
        this.classDir = Paths.get(classDir);
    }

    //重写父类的findClass方法(importance)
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //读取class的二进制数据
        byte[] classBytes = this.readClassBytes(name);
        //如果数据为null,或者没有读到任何信息,则抛出异常
        if (null == classBytes || classBytes.length == 0) {
            throw new ClassNotFoundException("Can not load the class " + name);
        }
        //调用defineClass方法定义class
        return this.defineClass(name, classBytes, 0, classBytes.length);
    }

    //将class文件读入内存
    private byte[] readClassBytes(String name) throws ClassNotFoundException {
        //将包名分隔符转换为文件路径分隔符
        String classPath = name.replace(".", "/");
        Path classFullPath = classDir.resolve(Paths.get(classPath + ".class"));
        if (!classFullPath.toFile().exists()) {
            throw new ClassNotFoundException("The class " + name + " not found.");
        }
        //此处try()括号的代码会自动资源释放,前提是可关闭的接口实现java.lang.AutoCloseable接口,比如inputStream和outputStream。
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            Files.copy(classFullPath, baos);
            return baos.toByteArray();
        } catch (IOException e) {
            throw new ClassNotFoundException("load the class " + name + " occur error.", e);
        }
    }

    @Override
    public String toString() {
        return "My ClassLoader";
    }
}

双亲委托机制

当一个类加载器被调用了loadClass之后,它并不会直接将其加载,而是先交给当前类加载器的父加载器尝试加载到最顶层的父加载器,然后依次向下加载。

父委托机制的逻辑主要是由loadClass来控制,有时候我们需要打破JDK提供的这种双亲委托的机制,打破双亲委托机制的方法有以下两种:

  1. 绕过系统类加载器,直接将扩展类加载器作为MyClassLoader的父加载器。
  2. 构造MyClassLoader的时候指定其父类加载器为null。

这里引申出来一个问题:获取类加载器方法getClassLoader()和获取上下文类加载器方法getContextClassLoader()有啥区别?可以看下面自己写的小栗子:

  1. 可以看到CompareClassLoader对象类的类加载器和主线程的上下文类加载器是同一对象。
  2. 子线程的类加载器也属于系统类加载器,它的父类加载器是扩展类加载器。

什么时候该用上下文类加载器?大家下面自己思考一下。

参考:https://iowiki.com/java/lang/thread_getcontextclassloader.html

public class CompareClassLoader {
    public static void main(String[] args) {
        //getClassLoader方法是当前类的加载器,它是使用双亲委派模型来加载类的
        ClassLoader classLoader = CompareClassLoader.class.getClassLoader();
        //getContextClassLoader是当前线程的类加载器,它是为了避开双亲委派模型的加载方式
        ClassLoader threadClassLoader = Thread.currentThread().getContextClassLoader();
        System.out.println(classLoader);
        System.out.println(threadClassLoader);
        //下面比较子线程与主线程的类加载器
        new Thread(()->{
            // returns the context ClassLoader for this Thread
            ClassLoader c = Thread.currentThread().getContextClassLoader();
            // sets the context ClassLoader for this Thread
            Thread.currentThread().setContextClassLoader(c);
            System.out.println("Class = " + c.getClass());
            System.out.println("Parent = " + c.getParent());
        }).start();
    }
}
类加载器命名空间

每个类加载器中同一个class都是独一无二的。类加载器进行类加载时,首先会加载记录表也就是缓存中,查看该类是否已经被加载过,如果已经加载过就不会重复加载,否则将会认为其是首次加载,下面就是同一个class被不同类加载器加载后的内存情况,可以看到同一个类在不同类加载器都是首次加载。

运行时包

编写代码的时候通常会给一个类指定一个包名,包的作用是为了组织类,防止不同包下同样名称的class引起冲突,还能起到封装的作用,包名和类名构成了类的全限定名称。在JVM运行时class会有一个运行时包,运行时的包是由类加载器的命名空间和类的全限定名称共同组成的。这样做主要是处于安全和封装的考虑。

初始类加载器

JVM规范的规定,在类的加载过程中,所有参与的类加载器,即使没有亲自加载过该类,也都会被标识为该类的初始类加载器。

类的卸载

JVM的启动过程中,JVM会加载很多的类,JVM运行期间加载了多少class可以启动JVM时指定-verbose:class参数观察得到。我们知道某个对象在堆内存中如果没有其他地方引用则会在垃圾回收器线程进行GC的时候被回收掉,那么该对象在堆内存中的Class对象以及Class在方法区中的数据结构在满足以下三个条件时才会被GC回收:

  1. 该类所有的实例都已经被GC。
  2. 加载该类的ClassLoader实例被回收。
  3. 该类的class实例没有在其他地方被引用。

需要注意的一点:在加载JDK核心类库时,如果想要通过自定义类加载器绕过双亲委派机制去加载核心类库时,会抛出SecurityException异常,这是由于JVM源码ClassLoader在defineClass时做了安全性检查,源码如下,

    /* Determine protection domain, and check that:
        - not define java.* class,
        - signer of this class matches signers for the rest of the classes in
          package.
    */
    private ProtectionDomain preDefineClass(String name,
                                            ProtectionDomain pd)
    {
        if (!checkName(name))
            throw new NoClassDefFoundError("IllegalName: " + name);

        // Note:  Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
        // relies on the fact that spoofing is impossible if a class has a name
        // of the form "java.*"
        if ((name != null) && name.startsWith("java.")) {
            throw new SecurityException
                ("Prohibited package name: " +


![img](https://img-blog.csdnimg.cn/img_convert/f73f76aa71b6260ccc8933a3a8df196c.png)
![img](https://img-blog.csdnimg.cn/img_convert/7ff62d957a66d9928ec0db5b9190293f.png)
![img](https://img-blog.csdnimg.cn/img_convert/350ebcf9a938cf7b0a520ae7e6c27efb.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**


加入社区》https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0
31309462)]
[外链图片转存中...(img-1kDau1uC-1725731309462)]

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**


加入社区》https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0
  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值