java的ClassLoader知识点总结

前言

视频地址:https://www.bilibili.com/video/BV1qE411Y7FQ?p=1,只想要稍微了解一下可以看p34-p36
学了一段时间的ClassLoader,决定通过博客的形式复习一遍

注:本文所有的代码都是直接看笔记手动打出来的,所以难免有出错,如果编译错误就自己改一改吧

目录
基本信息
类的加载
自定义ClassLoader
注释相关
代码相关

基本信息

概念
在java代码中,类型的加载、连接与初始化过程都是在程序运行期间完成的。这种方式提供了更加强大的灵活性,增加了更多的可能性。

生命周期
在以下几种情况下,Java虚拟机将结束生命周期
  • 执行了System.exit()方法
  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或错误而异常终止
  • 由于操作系统出现错误而导致Java虚拟机进程终止

五个重要过程

  • 加载:查找并加载类的二进制数据
  • 连接
    • 验证:确保被加载的类的正确性,这里主要是检验class文件的正确性
    • 准备:为类变量分配内存,设置默认值,但是在到达初始化之前,类变量都没有初始化为真正的初始化值,代码
    • 解析:解析过程就是在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用的过程,符号引用和直接引用的概念查百度吧。
  • 初始化:为类的静态变量赋予正确的初始化值,代码
  • 使用:对类的使用
  • 卸载:将一个已经装载到jvm的class文件爱你从java卸载掉

类的初始化

  • Java程序对类的使用方式可以分为两种
    • 主动使用
    • 被动使用
  • 所有的Java虚拟机实现必须在每个类或接口被Java程序首次主动使用时才初始化他们
  • 主动使用(七种
    • 创建类的实例
    • 访问某个类或接口的静态变量,或者对该静态变量赋值,代码
    • 调用类的静态方法,代码
    • 反射,代码
    • 初始化一个类的子类,代码
    • Java虚拟机启动时被标明为启动类的类(即带有main方法的启动类)
    • JDK1.7开始提供的动态语言支持:(说实话,这个还真看不懂)
      java.lang.invoke.MethodHande实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化

除了以上七种情况,其他使用Java类的方式都被看作对类的被动使用,不会导致类的初始化

类的加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区中,然后在内存中创建一个java.lang.Class对象(Java规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中)用于封装类在方法区内的数据结构

加载.class文件的方式

  • 从本地系统中直接加载(在日常开发中用得最多的方式)
  • 通过网络下载.class文件
  • 从zip、jar等归档文件中加载.class文件
  • 从专有数据库中提取.class文件
  • 将Java源文件动态编译成.class文件(动态代理)

类的实例化

  1. 为新的对象分配内存
  2. 为实例变量赋默认值
  3. 为实例变量赋正确的初始值

注:Java编译器为它编译的每一个类都至少生成一个实例初始化方法,在Java的class文件中,这个实例初始化方法被成为“”。针对源代码中每一个类的构造方法,Java编译器都产生一个方法。

类的加载

类的加载的最终产品是位于内存中的Class对象。
Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

有两种类型的类加载器

  • Java虚拟机自带的加载器
    • 根类加载器(BootstrapClassLoader)
    • 扩展类加载器(ExtClassLoader)
    • 应用(系统)类加载器(注:在Java中,使用AppClassLoader这个类表示,但ClassLoader有一个静态方法ClassLoader.getSystemClassLoader(),实际上获取到的就是AppClassLoader,所以才有 应用/系统 两种说法)
  • 用户自定义的类加载器
    • java.lang.ClassLoader的子类
    • 用户可以定制类的加载方式

类加载器并不需要等到某个类被“首次主动使用”时再加载它
JVM规范允许类加载器在预料到某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError)
如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误

类被加载后,就进入连接阶段,连接就是将已经读入到内存中的类的二进制数据合并到虚拟机的运行时环境中去。

类的验证
注:这里只是将主要步骤列举出来,因为每个版本的验证过程都不太一样

  • 类文件的结构检查
  • 语义检查
  • 字节码验证
  • 二进制兼容性的验证

类的准备
在准备阶段,Java虚拟机为类的静态变量分配内存,并设置默认的初始值。例如对于以下Sample类,在准备阶段,将为int类型的静态变量a分配4个字节的内存空间,并且赋予默认值0.为long类型的静态变量b分配8个字节的内存空间,并赋予默认值0。

public class Smaple{
	private static int a;
	public static long b;
}

类的初始化
在初始化阶段,Java虚拟机执行类的初始化语句,为类的静态变量赋予初始值。在程序中,静态变量的初始化有两种途径:

  • 在静态变量的声明处进行初始化
  • 在静态代码块中进行初始化,例如在以下代码中,静态变量a和b都被显示初始化,而静态变量c没有被显示初始化,它将保持默认值0。
public class Sample{
	priavate static int a = 1;//在静态变量汇总的声明处进行初始化
	public static long b;
	public static long c;
	
	static{
			b = 2;//在纪姑娘太代码块中进行初始化
	}
}

静态变量的声明语句,以及静态代码块中都被看做类的初始化语句,Java虚拟机会按照初始化语句在类文件中的先后顺序来依次执行他们。例如当以下Sample类被初始化后,它的静态变量a的取值为4。

public class Sample{
	static int a = 1;
	static{
		a = 2;
	}
	static{
		a = 4;
	}
	public static void main(String[] args){
		System.out.println("a=" + a);//打印4
	}
}

类的初始化步骤

  • 假如这个类没有被加载和连接,那就先进行加载和连接。
  • 假如类存在直接父类,并且这个父类还没有被初始化,那就先初始化直接父类。
  • 假如类中存在初始化语句,那就依次执行这些初始化语句。

类的初始化时机
当Java虚拟机初始化一个类时,要求它的所有扶额累都已经被初始化,但这条规则并不试用于接口。

  • 在初始化一个类时,并不会先初始化它所实现的接口。
  • 在初始化一个接口时,并不会先初始化它的父接口

因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化,只有当程序首次主动使用特定接口的静态变量时,才会导致该接口的初始化。
只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用。

类加载器
类加载器用来把类加载到Java虚拟机中,从JDK1,2版本开始,类的加载过程采用双亲委托机制,这种机制能更好地保证Java平台的安全。在此委托机制中,除了Java虚拟机自带的根类加载器外,其余的类加载器都有且只有一个父加载器。当Java程序请求加载器loader1加载Sample类时,loader1首先委托自己的父加载器去加载Sample类,若父加载器能加载,则由父加载器完成加载任务,否则才由加载器loader1本身加载Sample类。

Java虚拟机自带了以下集中加载器:

  • 根(Bootstrap)类加载器:该加载器没有父加载器。它负责加载虚拟机的核心类库,如java.lang.*等,例如java.lang.Object就是由根类加载器加载的。根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。根类加载器的实现依赖于底层操作系统,属于虚拟机实现的一部分,它并没有继承java.lang.ClassLoader类
  • 扩展(Extension)类加载器:它的父加载器为根类加载器。它从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的俺炸U那个目录的jre\lib\ext子目录(扩展目录)下加载类库,如果把用户创建的JAR文件放在这个目录下,也会自动由扩展类加载器加载。扩展类加载器是纯Java类,是java.lang.ClassLoader类的子类。
  • 系统(System)类加载器:也称为应用类加载器,它的父加载器为扩展类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器,系统类加载器是纯Java类,是java.lang.ClassLoader类的子类。

除了以上虚拟机自带的加载器外,用户还可以定制自己的类加载器,java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader。

类加载器的双亲委托机制
在双亲委托机制中,各个加载器按照父子关系形成树形结构,除了根类加载器之外,其余的类加载器都有且只有一个父加载器。

假设有下面这样一个加载流程
java的ClassLoader知识点总结_第1张图片
loader1会先找loader1的父类加载器(即系统类加载器)加载Sample类,而系统类加载器又有父加载器,所以会继续往上找其父加载器,扩展类加载器再往上找他的父类加载器,最后会找到根类加载器,由于根类加载器是所有加载器的最顶层,所以它没有父加载器。这个时候根类加载器会尝试加载Sample类,发现加载不了,所以会将问题抛给扩展类加载器,这个时候扩展类加载器发现也加载不了,再将问题抛给系统类加载器。最后系统类加载器会加载成功,所以最终Sample类是由系统类加载器加载的,而不是由loader1加载出来。
java的ClassLoader知识点总结_第2张图片

  • Bootstrap ClassLoader/根类加载器:负责加载$JAVA_HOME$中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类。
  • Extension ClassLoader/扩展类加载器:负责加载Java平台中扩展功能的一些jar包,包括$JAVA_HOME$\中jre/lib/*.jar或者-Djava.ext.dirs指定目录下的jar包
  • App ClassLoader/系统类加载器:负责加载classpath中指定的jar包及目录中class

子class loader和父class loader实际上是一种包含关系,子class loader包含父class loader的引用、这种包含关系还能进一步的延生和泛化一些,通过包含关系,可以让同一层的class loader也可以成为父子class loader。

若有一个类加载器能够成功加载Test类,那么这个类加载器被成为定义类加载器,所有能成功返回Class对象引用的类加载器(包括定义类加载器)都被称为初始类加载器。

双亲委托机制的好处

  • 可以确保Java核心库的类型安全,所有Java应用都至少会引用java.lang.Object类,也就说在运行期,java.lang.Object这个类会被加载到Java虚拟机中;如果这个加载过程中是由Java应用自己的类加载器完成的,那么可能就会在Java虚拟机中存在多个版本的java.lang.Object类,而且这些类之间还是不兼容的(注:会出现两个全限定名相同的类在转换的时候出现转换异常,比如会提示Object没办法转换成Object,没记错的话后面会有相关的举例代码),相互不可见的(正是命名空间在发挥着作用)。借助双亲委托机制,Java核心类库中的类的加载工作都是由启动类加载器来统一完成,从而确保了Java应用所使用的都是同一个版本的Java核心类库,它们之间是相互兼容的。
  • 可以确保Java核心库所提供的类不会被自定义的类所代替。
  • 不同的类加载器可以相同名称(binary name)的类创建额外的命名空间。相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器来加载它们即可。不同加载器所加载的类之间是不兼容的,这就是相当于在Java虚拟机内部创建了一个又一个相互隔离的Java空间,这类技术在很多框架中得到了实际应用。

注:上面提到的binary name和命名空间后面会作解释。

当前类加载器(Current ClassLoader)
每个类都会使用自己的类加载器(即加载自身的类加载器)来去加载其他类(指的是所依赖的类)。
如果ClassX引用了ClassY,那么ClassX的类加载器就会去加载ClassY(前提是ClassY尚未被加载)。

线程上下文类加载器(Context ClassLoader)
线程上下文类加载器是从JDK1.2开始引入的,类Thread中的getContextClassLoader()与setContextClassLoader(ClassLoader cl)分别用来获取和设置上下文类加载器。如果没哟通过setContextClassLoader(ClassLoader cl)进行设置的话,线程将继承其父线程的上下文类加载器,Java应用运行时的初始线程的上下文类加载器是AppClassLoader,在线程中运行的代码可以通过该类加载器加载类与资源。

注:上面这两个概念大脑里留个印象,后面讲到ContextClassLoader在框架中的实际应用会用到。

默认的上下文类加载器
默认的上下文类加载器是AppClassLoader,在Laucher的构造方法里面,会将AppClassLoader设置到当前线程的上下文类加载器。

线程上下文类加载器的重要性
SPI(Service Provider Interface)
父类加载器可以使用当前线程Thread.currentThread().getContextClassLoader()所指定的ClassLoader加载类。
这就改变了父类加载器不能使用子ClassLoader或者其他没有直接父子关系的ClassLoader加载的类的情况,即改变了双亲委托模型(用来打破双亲委托机制)。
线程上下文类加载器就是当前线程的Current ClassLoader。
在双亲委托机制模型下,类加载是由下至上的,即下层的类加载器会委托上层进行加载。但是对于SPI来说,有些接口是Java核心库提供的,而Java核心库是由BootstrapClassLoader来加载的,而这些接口的实现却来自于不同的jar包(厂商提供),Java的BootstrapClassLoader是不会加载其他来源的jar包,这样传统的双亲委托机制模型就无法满足SPI的要求,而通过给当前线程设置上下文类加载器,就可以由设置的上下文类纪在前来实现对于接口实现类的加载。

注:上面这段话可以和自定义Claoder的上下文类加载器在框架中的实际应用配合食用。

线程上下文类加载的一般使用模式(获取-使用-还原)

//获取
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try{
	//使用
	Thead.currentThread().setContextClassLoader(myContextClassLoader);
	myMethod();
}finally{
	//还原
	Thread.currentThread().setContextClassLoader(classLoader);
}

当高层提供了统一的接口让低层去实现,同时又要在高层加载(或实例化)低层的类时,就必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。

获取三个ClassLoader的加载范围

//获取BootstrapClassLoader的加载范围
System.out.println(System.getProperty("sun.boot.class.path"));
//注:可以找到当前运行环境下的rt.jar这个jar包,可以看到很多Java的包都包含在这个jar里面
System.out.println();
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println();
System.out.println(System.getProperty("java.class.path"));

当使用ide运行上面的代码和手动运行上面的代码的时候,AppClassLoader会出现两个不容的结果。使用ide的时候会出现一个比较长的字符串,而手动运行时候会出现一个“.”。这是因为ide在运行的时候会给classpath进行配置各种可能需要的路径,而手动运行的时候会获取当前路径作为classpath。

如果将BootstrapClassLoader加载的路径修改成一个不存在启动Java程序所需的jar包及class文件的路径的话,就会在运行时抛出一下错误:

//这句是执行代码,%cd%为windows下当前目录的意思
java -Dsun.boot.class.path=%cd% xxxpackage.XXClass
//运行报如下错误
Error occurred during initialization of VM
java/lang/NoClassDefFoundError: java/lang/Object

这是我自己得出的结论:因为Object是任何类的父类,即使是ExtClassLoader和AppClassLoader,它们的最顶层的父类也是Object,所以Object是最先加载的,这个时候在当前目录下找不到Objecrt,所以报错。

这是老师的结论:内建于JVM中的BootstrapClassLoader会加载java.lang.ClassLoader以及其他的Java平台类,当jvm启动时,一块特殊的机器码会运行,它会加载ExtClassLoader和AppClassLoader,这亏啊特殊的机器码叫做BootstrapClassLoader。
BootstrapClassLoader并不是Java类,而其他的加载器都是Java类,BootstrapClassLoader是特定于平台的机器指令,它负责开启整个加载过程。
所有class loader(除了BootsrapClassLoader)都被实现为Java类,不过,总归要有一个组件来加载第一个ClassLoader,从而让整个加载过程能够顺序进行下去,加载第一个纯Java的ClassLoader就是BootstrapClassLoader的职责。
BootstrapClassLoader还会负责加载供jre正常运行所需要的基本组件,这包括java.util与java.lang包中的类等等。

获取当前类的ClassLoader

clazz.getClassLoader();

获取当前线程上下文的ClassLoader

Thread.currentThread().getCotnextClassLoader();

获取系统的ClassLoader

ClassLoader.getSystemClassLoader();

获取调用者的ClassLoader

DriverManager.getCallerClassLoader();

实际应用

Class clazz1 = Class.forName("java.lang.String");
System.out.println(clazz1.getClassLoader());

Class clazz2 = Class.forName("xxxpackage.XXXClass");//这里需要传入一个自己定义的class
System.out.println(clazz2.getClassLoader());

/*
结果:
null
sun.misc.Launcher$AppClassLoader@c387f44

结论:
String是由根类加载器(BootstrapClassLoader)加载出来的
自己定义的类是由应用类加载器(AppClassLoader)加载出来的
*/

数组的ClassLoader
数组的Class对象不是由ClassLoader加载的,而是由Java虚拟机在运行时动态创建出来的。然而对于数组的类加载器来说,返回过来的ClassLoader和数组的元素类型的ClassLoader一样的,如果数组的类型是原生类型,则数组是没有ClassLoader的。

String[] strings = new String[2];
//打印出null
//这是因为String是由BootstrapClassLoader加载的,所以是null
System.out.println(strings.getClass().getClassLoader());

class Test{}

Test[] tests = new Test[2];
//打印sun.misc.Launcher$AppClassLoader@18b4aac2
//这里同理,Test是由AppClassLoader负责加载的
System.out.println(tests.getClass().getClassLoader());

int[] ints = new int[2];
//打印出null
//注意:这里的null和上面String[]的null的含义是不一样的,这里是null是因为这个数组的类型是Java的原生类型
//表示的是没有ClassLoader,而不ClassLoader是BootstrapClassLoader
System.out.println(ints.getClass().getClassLoader());

证明三个ClassLoader的父子关系
java的ClassLoader知识点总结_第3张图片

自定义ClassLoader

想要自定义ClassLoader,需要继承ClassLoader。总共有3个构造方法,选两个比较常用的来将
protected ClassLoader() {
	this(checkCreateClassLoader(), getSystemClassLoader());
}
protected ClassLoader(ClassLoader parent) {
	this(checkCreateClassLoader(), parent);
}

第一个:不指定parent,如果不指定parent的话,就默认使用AppClassLoader作为parent。
第二个:指定parent,需要传入一个ClassLoader,也可以传入null,表示parent是BootstrapClassLoader,还可以指定同级的ClassLoader,如下代码。

public class MyClassLoader extends ClassLoader{
	public MyClassLoader(){
		super.();
	}
	public MyClassLoader(ClassLoader parent){
		super(parent);
	}
	....
}

MyClassLoader cl1 = new MyClassLoader();
MyClassLoader cl2 = new MyClassLoader(cl1);

所以这也说明了为什么,双亲委托机制是包含关系而不是继承关系。

上面列出的两个构造方法最终会调用这个私有的构造方法,可以看到最终会将parent赋给该类的parent字段。

// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;

private ClassLoader(Void unused, ClassLoader parent) {
    this.parent = parent;
    if (ParallelLoaders.isRegistered(this.getClass())) {
        parallelLockMap = new ConcurrentHashMap<>();
        package2certs = new ConcurrentHashMap<>();
        domains =
            Collections.synchronizedSet(new HashSet());
        assertionLock = new Object();
    } else {
        // no finer-grained lock; lock on the classloader instance
        parallelLockMap = null;
        package2certs = new Hashtable<>();
        domains = new HashSet<>();
        assertionLock = this;
    }
}

再说一下两个常见的重写的方法

  • findClass:一般继承ClassLoader之后会重写这个方法,重写这个方法之后就自己写加载class文件的代码,最终将加载完成的class文件转换成字节数组,再调用ClassLoader的defineClass方法返回Class对象。
  • loadClass:这个方法继承之后一般不重写,使用ClassLoader的实现。这个方法的主要的作用是寻找要加载的类是否存在当前的命名空间,如果不存在,再去加载。如果存在,则复用。关于命名空间,下面会给出相应的解释。

一个ClassLoader的案例

public class MyClassLoader extends ClassLoader {
    private final String FILE_EXTENSIONS = ".class";
    private String path;

    public MyClassLoader() {
        //不指定parent,表示使用AppClassLoader作为parent
        super();
    }

    public MyClassLoader(ClassLoader parent) {
        //自定义parent
        super(parent);
    }

    public void setPath(String path) {
        this.path = path;
    }

    //注意:这里拿到的是一个binary name,具体binary name的定义可以到注释相关查找
    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        byte[] data = loadClassData(name);
        return defineClass(name, data, 0, data.length);
    }

    private byte[] loadClassData(String binaryName) {
        //没有特别的含义,只是想确认是否执行了我们自己写的findClass方法
        System.out.println("loadClassData");
        String path = this.path + binaryName.replace(".", "\\") + FILE_EXTENSIONS;
        byte[] data = null;
        try (
                InputStream is = new FileInputStream(new File(path));
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ) {
            int ch;
            while ((ch = is.read()) != -1) {
                baos.write(ch);
            }
            data = baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }
}

测试代码

public class Test21 {
    public static void main(String[] args) throws Exception{
        MyClassLoader cl = new MyClassLoader();
        //这是一个常量,表示硬盘上某个目录,这个目录存放一些用来测试的class文件
        cl.setPath(ClassLoaderUtil.LOADER_TEST_DIR);
        Class clazz = cl.loadClass("xxxpackage.XXXClass");
        System.out.println(clazz);
    }
}

这里测试的方式我想到两种方式
1:用上面这种方式,但上面这种方式有一个地方需要注意,如果加载的class文件在当前项目目录下的build/classes/java/main(我用的开发工具是idea,eclipase不清楚是什么目录)有相同包名的class文件的话,需要先删除后测试。因为AppClassLoader在加载的时候会根据全限定名在classpath下面尝试寻找要加载的类,如果找到就自己完成加载操作。
2:直接将parent指定为null(BootstrapClassLoader),因为BootstrapClassLoader加载的是rt.jar下的class文件,所以只要将parent指定为BootstrapClassLoader,那parent自然会加载失败,这个时候就会使用我们自己的方式去加载。

上面在findClass方式还是做得相对简单,实际开发中,可以将加载方式延生到从网络进行加载,加载磁盘上某个位置的jar包等方式。

命名空间

  • 每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成。
  • 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。
  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。

关于命名空间对加载class的影响

根据上面的例子可以得出结论:

  • 子加载器所加载的类能够访问到父加载器所加载的类
  • 父加载器所加载的类无法访问到子加载器所加载的类

不同ClassLoader命名空间的关系

  • 同一个命名空间内的类是相互可见的。
  • 子加载器的命名空间包含所有父加载器的命名空间,因此由子加载器加载的类能看见父加载器所加载的类,例如AppClassLoader加载的类能看见BootstrapClassLoader加载的类。
  • 由父加载器加载的类不能看见子加载器加载的类。
  • 如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见。

不同命名空间引发的问题

结论:在运行期,一个Java类是由该类的全限定名(binary name,二进制名)和用户加载该类的定义类加载器(defining loader)所共同决定的。如果同样名字(即相同的全限定名)的类是由两个不同的加载器所加载,那么这些类就是不同的,即便.class文件的字节码完全一样,并且从相同的位置加载亦如此。

类的卸载

  • 当MySample类被加载、连接和初始化后,它的生命周期就开始了。当代表MySample类的Class对象不在被引用,即不可触及时,Class对象就会结束生命周期,MySample类在方法区的数据也会被加载,从而结束Sample类的生命周期
  • 一个类合适结束生命周期,取决于代表它的Class对象何时结束生命周期。
  • 由用户自定义的类加载器所加载的类是可以被卸载的。

由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。前面已经介绍过,Java虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器,Java虚拟机本身会使用引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。

MyClassLoader loader = new MyClassLoader();
loader.setPath(ClassLoaderUtil.LOADER_TEST_DIR);
Class clazz = loader.loadClass("xxxpackage.XXXClass");
Object object = clazz.newInstance();
loader = null;
clazz = null;
object = null;
System.gc();

运行以上程序时,XXXClass类由loader加载。在ClassLoader的内部实现中,用一个Java集合来存放所加载类的引用。另一方面,一个Class对象总是引用它的ClassLoader,调用Class对象的getClassLoader()方法,就能获得它的ClassLoader。由此可见,代表XXXClass类的Class实例与loader之间为双向关联关系。
一个类的实例总是引用代表这个类的Class对象。在Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用。此外,所有的Java类都有一个静态属性class,它引用代表这个类的Class对象。

ExtClassLoader的应用

public class Test3{
	public static void main(String[] args){
		System.out.println(Test1.class.getClassLoader());
		System.out.println(Test2.class.getClassLoader());
	}
}
//先切换到classes目录下的main目录,然后执行下面的命令运行class文件
//java -Djava.ext.dirs=%cd% xxxpackage.Test3
//上面的命令是将ExtClassLoader的加载范围修改成当前目录
//结果
sun.misc.Launcher$AppClassLoader@c387f44
sun.misc.Launcher$AppClassLoader@c387f44
//会发现这两个类的ClassLoader依然是AppClassLoader

//先执行命令将Test1和Test2打成jar包
//jar cvf test.jar com/mishaki/classloader/csdn/Test1.class
//jar cvf test2.jar com/mishaki/classloader/csdn/Test2.class
//注意:这里使用的是路径,不是包名
//这里的jar包的名字是随便去的,执行完上面这两条命令之后就存在两个jar包了
public class Test3 {
    public static void main(String[] args) {
        System.out.println(Test1.class.getClassLoader());
        System.out.println(Test2.class.getClassLoader());
        System.out.println(Test3.class.getClassLoader());
    }
}
//java -Djava.ext.dirs=%cd% xxxpackage.Test3
//结果
//sun.misc.Launcher$ExtClassLoader@3d4eac69
//sun.misc.Launcher$ExtClassLoader@3d4eac69
//sun.misc.Launcher$AppClassLoader@2a139a55

//可以发现,jar包的名字和数量并不会影响ExtClassLoader识别jar的功能
//只要将特定class文件打到jar包里面,ExtClassLoader就能在对应的环境找到class文件
//从而让该class文件被ExtClassLoader架子啊,而不是AppClassLoader加载

实际开发中,一般不用去关注ExtClassLoader,ExtClassLoader主要加载一些安全相关的。

上下文类加载器(ContextClassLoader)在框架中的应用

//这里先说明一下,之所以会找到Driver,是因为在项目中导入了MySql依赖
//compile "mysql:mysql-connector-java:5.1.34"
//这个时候idea会将MySql的jar加载到当前的环境下,所以需要AppClassLoader才能加载
public static void main(String[] args) {
    ServiceLoader loader = ServiceLoader.load(Driver.class);
    Iterator iterator = loader.iterator();
    while (iterator.hasNext()){
        Driver driver = iterator.next();
        System.out.println("driver:" + driver.getClass() + " loader:" + driver.getClass().getClassLoader());
    }
    System.out.println("当前上下文ClassLoader:" + Thread.currentThread().getContextClassLoader());
    System.out.println("ServiceLoader的ClassLoader" + ServiceLoader.class.getClassLoader());
}
//输出结果
/*
driver:class com.mysql.jdbc.Driver loader:sun.misc.Launcher$AppClassLoader@c387f44
driver:class com.mysql.fabric.jdbc.FabricMySQLDriver loader:sun.misc.Launcher$AppClassLoader@c387f44
当前上下文ClassLoader:sun.misc.Launcher$AppClassLoader@c387f44
ServiceLoader的ClassLoader:null
*/
//其中,ServiceLoader是java.util包的一个类,所以是由BootstrapClassLoader加载的
//看一下ServiceLoader.load的代码
public static  ServiceLoader load(Class service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}
//可以看到这里加载Driver的时候实际上是使用了当前上下文ClassLoader去加载的
//而不是使用当前的ClassLoader(BootstrapClassLoader)进行加载,如果使用这种方式
//进行加载,则会导致加载失败

//再举一个例子
public static void main(String[] args) {
    //设置成加载该class文件的父ClassLoader(ExtClassLoader)
    Thread.currentThread().setContextClassLoader(Test16.class.getClassLoader().getParent());
    ServiceLoader loader = ServiceLoader.load(Driver.class);
    Iterator iterator = loader.iterator();
    while (iterator.hasNext()){
        Driver driver = iterator.next();
        System.out.println("driver:" + driver.getClass() + " loader:" + driver.getClass().getClassLoader());
    }
    System.out.println("当前上下文ClassLoader:" + Thread.currentThread().getContextClassLoader());
    System.out.println("ServiceLoader的ClassLoader:" + ServiceLoader.class.getClassLoader());
}
//输出结果
/*
当前上下文ClassLoader:sun.misc.Launcher$ExtClassLoader@659e0bfd
ServiceLoader的ClassLoader:null
*/
//可以看到,没有打印出任何一个Driver出来,而且上下文ClassLoader也是ExtClassLoader
//注意:这里不能使用BootstrapClassLoader(null)进行测试,因为在ServiceLoader内部
//会对拿到的ContextClassLoader进行判断,如果是null就会使用SystemClassLoader(AppClassLoader)进行加载

这里的ServiceLoader是由BootstrapClassLoader加载的,根据当前类加载器的定义,接下来会使用BootstrapClassLoader去加载Driver类,这种情况下必定加载失败,所以这个时候ContextClassLoader的机制就发挥出作用,打破双亲委托机制的束缚。

注释相关

clazz.getClassLoader()的注释
java的ClassLoader知识点总结_第4张图片

findClass

1:首先会调用findLoadedClass方法检查这个类是否加载过了
2:调用父类的loadClass方法,如果父亲是null就会使用BootstrapClassLoader
3:会调用findClass寻找class
如果调用上述的方法找到类,并且参数resolve为true,就会调用resolveCLass方法。
鼓励ClassLoader的子类重写findClass方法,而不是重写loadClass方法。

参数含义
name:期待的class的binary name,如果不知道,可以为空。
b:构成class数据的字节。处于off到off+len-1这部分字节一定要符合java虚拟机规范所规定的字节码的基本要求。
off:参数b的偏移量。
len:参数b的长度。
return
根据指定的class的数据所创建的class对象。


findClass

1:首先会调用findLoadedClass方法检查这个类是否加载过了
2:调用父类的loadClass方法,如果父亲是null就会使用BootstrapClassLoader
3:会调用findClass寻找class
如果调用上述的方法找到类,并且参数resolve为true,就会调用resolveCLass方法。
鼓励ClassLoader的子类重写findClass方法,而不是重写loadClass方法。


binary name(二进制名称)
以下都表示二进制明名称

java.lang.String
javax.swing.JSpinner$DefaultEditor //这里的DefaultEditor是JSpinner的一个内部类
java.security.KeyStore$Builder$FileBuilder%1//这里的1表示的是KeyStore的Builder的内部类的FileBuilder内部类的第一个匿名内部类
java.net.URLClassLoader$3$1//表示的是URLClassLoader的第三个匿名内部类的第一个匿名内部类



代码相关

连接-准备的代码
public class Test{
	//在准备阶段,会将number这个变量加载到内存中
	//但初始化的值并不是1,而是默认值,即0
	//如果number是一个boolean类型,则为false,对象同理,为null
	public static int number = 1;
}

类加载初始化

public class Test{
	///这里的number只有在初始化阶段才会被赋值为1
	public static int number = 1;
}

类的初始化-调用类的静态变量

//案例一
public class Test1{
	public static void main(String[] args){
		//如果只执行下面两行代码的某一行代码,执行结果:
		
		//执行第一行
		//parent1 static block
		//parent1
		//结论:对于静态字段来说,只有直接定义了该字段的类才会被初始化

		//执行第二行
		//parent1 static block
		//child1 static block
		//child1
		//结论:当一个类被初始化是,要求其父类全部都已经完成初始化才会初始化该类

		//当两行都执行的时候
		//parent1 static block
		//parent1
		//child1 static block
		//child1
		//这是因为在执行第一行的时候已经对Parent1的class文件做了初始化操作
		//所以在执行第二行的时候不会再对父类进行初始化,所以不会再次执行父类的static代码块

		System.out.println(Child1.str1);
		System.out.println(Child1.str2);
	}
}
public class Parent1{
	public static String str1 = "parent1";
	static{
		System.out.println(“parent1 static block”);
	}
}
public class Child1 extends Parent1{
	public static String str2 = "child1"l;
	static{
		System.out.println("child1 static block");
	}
}

//案例二
public class Test2{
	public static void main(String[] args){
		//只会打印parent2,不会打印parent2 static block2
		/*
		原因:编译器常量在编译接U单会存入到调用这个常量的方法所在的类的常量池中
		本质上,调用类并没有直接引用到定义常量的类,因此并不会触发常量的类的初始化
		注意:这里指的是将常量存放到Test2的常量池中,之后Test2就和Parent2没有任何关系
		甚至,我们可以将Parent2的.class文件删除(指的是在本地删除已经编译完成的class文件,不是删除代码)
		*/
		System.out.println(Parent2.str1);
	}
}
public class Parent2{
	public static final String str1 = "parent2";
	staitc{
		System.out.println("parent2 static block2");
	}
}

//案例三
public class Test3{
	public static void main(String[] args){
		//结果:parent3 static block
		//随机数

		/*
		原因:str1的值在编译期并不能确定,只能在运行的时候确定
		因此,它是一个运行期常量
		一个常量的值在编译器不能确定的,那么它就不会被放入到调用类的常量池中
		这时在程序运行时,会导致主动使用该常量所在的类,显然会导致该类被初始化
		*/
		System.out.println(Parent3.str1);
	}
}
public class Parent3{
	public static final String str1 = UUID。randomUUID().toString();
	static{
		System.out.println("parent3 static block");
	}
}

//案例四
public class Test4{
	public static void main(String[] args){
		//没有打印parent4 static block
		
		/*
		原因
		对于数组来说,其类型是由JVM在运行期动态生成的,表示为[xxxpakcage.xxxClass
		数组是动态生成的类型,其父类就是Object
		对于数组来说,JavaDoc经常将构成数字的元素为Component,实际上就是将数组降低一个纬度后的类型

		其实这样的结果也可以在类的主动使用上得到答案,上面并没有写着创建数组会导致主动使用数组类
		*/
		Parent4[] parent1 = new Parent4[1];
	}
}
public class Parent4{
	static{
		System.out.println("parent4 static block");
	}
}

//案例五
public class Test5{
	static{
		System.out.println("test5 static block");
	}
	public static void main(String[] args){
		//执行结果
		/*
		test5 static block
		========================
		parent5 static block
		========================
		2
		========================
		child5 static block
		3
		*/

		//声明一个Parent5的引用,并不是对Parent5的主动使用
		Parent5 parent5;
		System.out.println("========================");
		//new显然是对一个类的主动使用,所以这里必然会执行arent5的静态代码块的代码
		parent5 = new Parent5();
		System.out.println("========================");
		//上面的new Parent5已经对Parent5进行了初始化,所以这里不会导致Parent5初始化
		System.out.println(Parent5.a);
		System.out.println("========================");
		//这里的话会导致父类被初始化,但上面的代码已经让Parent5初始化了,所以这里不会导致Parent5初始化
		System.out.println(Child5.b);
	}
}
public class Parent5{
	public static int a = 2;
	static{
		System.out;println("parent5 static block");
	}
}
public class Child5 extends Parent5{
	public static int b = 3;
	static{
		System.out.println("child5 static block");
	}
}

类的初始化-调用类的静态方法

//案例一
public class Test1{
	public static void main(String[] args){
		//结果
		//parent1 static block
		//2
		//parent1 doSomething
		//原因
		//如果通过子类直接调用父类的静态变量或静态方法,表示的是对父类的主动使用,而不是对子类的主动使用,所以并不会初始化子类
		System.out.prinlnt(Child1.a);
		Child6.doSomethiing();
	}
}
public class Parent1{
	public static int a = 2;
	static{
		System.out.println(“parent1 static block”);
	}
	public static void doSomething(){
		System.out.println("parent1 doSomething");
	}
}
public class Child1 extends Parent1{
	staitc{
		System.out.prinlnt("child1 static block");
	}
}

//案例二
public class Test2{
	public void main(String[] args){
		Parent2 parent2 = Parent2().getIntance();
		//1 1
		System.out.println(Parent2.a);
		System.out.println(Parent2.b);
		
		Child2 child2 = Child2.getInstance();
		//1 0
		System.out.println(Child2.a);
		System.out.println(Child2.b);

		//1 1,这里要注意一个问题,只有静态变量才会出现上面的一会1 1一会1 0的情况,下面会分析
		//非静态变量的执行过程和上面是不一样的,所以不能当作同一个问题进行分析
		ChildChild2 childChild2 = ChildChild2.getInstance();
		System.out.prinlnt(ChildChild2.a);
		System.out.prinlnt(ChildChild2.b);
		/*
		同样的代码,只是位置不同,执行出来的结果就不同,这里涉及到类的准备阶段和类的初始化阶段

		在类的准备阶段,会为类的静态变量初始化默认值,所以爱准备阶段的时候
		Parent2和Child2的a和b的值都是0

		类的初始化执行时机,这里涉及到类的主动使用,有一种情况,调用类的静态方法,上面使用了getIntance静态方式

		类的初始化:类在初始化的时候会顺序执行代码
	
		在Parent2中,由于a没有赋值语句,所以不执行赋值操作,还是0.而b,是将一个0的值赋给b,所以b也是0。
		然后执行Parent2的构造方法,次数a和b都是0,自增之后就变成1,所以此时在构造方法打印出来的值都是1.
		在main方法打印出来的值也都是1.
	
		在Child2中,由于a没有赋值语句,所以不执行赋值操作,还是0。由于b是在构造方法下面,所以构造方法的执行
		顺序优于b,此时a和b自增,由于a和b在准备阶段被赋予了一个默认值0,所以此时自增后他们的值都是1.
		在构造方法day8in出来的值也都是1,构造方法执行完成后,b被赋予一个0的值,所以最终在main方法打印出来的值就是1 0
		*/
	}
}
public class Parent2{
	public static int a;
	public static int b = 0;
	private static Parent2 instance = new Parent2();

	private Parent2(){
		a++;
		b++;
		
		System.out.println("parent2 a:" + a + ",b:" + b);
	}

	public static Parent2 getInstance(){
		return instance;
	}
}
public class Child2{
	public static int a;
	private static Child2 instance = new Child2();
	
	priavte Child2(){
		a++;
		b++;
 
 		System.out.println("child2 a:" + a + ",b:" +b);
	}

	public static int b = 0;

	public static Child2 getInstance(){
		return instance;
	}
}
public class ChildChild2{
	public int a;
	private static ChildChild2 instance = new ChildChild2();

	private ChildChild2(){
		a++;
		b++;

		System.out.println("childChild2 a:" + a + ",b:" + b);
	}

	public int b = 0;
	
	public static ChildChild2 getInstance(){
		return instance;
	}
}

//案例三
public class Test3{
	public static void main(String[] args){
		//结果
		//doSomething a:1,b:1
		//1
		//1
		//这里我的理解是,静态变量的赋值操作是在初始化阶段完成的
		//当调用Parent3.doSomething()的时候会触发Parent3初始化,此时b的值被赋予0
		//完成上面的操作之后才会执行doSomething
	
		Parent3.doSomething();
		System.out.printlnt(Parent3.a);
		System.out.printlnt(Parent3.b);
	}
}
public class Parent3{
	public static in a;
	public staitc void doSomethiing(){
		a++;
		b++;
		System.out.printlnt("doSomething a:" + a + ",b:" + b):
	}
	public static int b = 0;
}

类的初始化-反射

public class Test1{
	public static void main(String[] args) throws Exception{
		/*
		结果
		class Parent1
		==============
		parent1 static block

		结论:调用ClassLoader的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化
		*/

		//后面会对ClassLoader进行介绍,这里只要知道这是获取一个ClassLoader即可
		ClassLoader classLoader = ClassLoader.getSystemClassLoader();
		Class parent1 = classLoader.loadClass("xxxpackage.Parent1");
		System.out.prinln(parent1);
		System.out.println("==============");
		parent1 = Class.forName("xxxpackage.Parent1");
	}
}
public class Parent1{
	staitc{
		System.out.println("parent1 static block");
	}
}

类的初始化-初始化一个类的子类

public class Test1{
	public static void main(String[] args){
			new Child1();
			/*
			结果:
			parent1 static block
			parentParent1 static block
			child1 static block

			结论:当初始化一个类的时候,会对其所有父类进行初始化
			*/
	}
}
public class Parent1{
	static{
		System.out.println("parent1 static block"):
	}
}
public class ParentParent1 extends Parent1{
	static{
		System.out.println("parentParent1 static block"):
	}
}
public class Child1 extends ParentParent1{
	static{
		System.out.println("child1 static block"):
	}
}

命名空间对加载class的影响

//Sample类
public class Sample {
    public Sample() {
        System.out.println("Sample class loaded, class loader: " + getClass().getClassLoader());

		//注意:这里初始化了CLDemo类
        new CLDemo();
    }
}

//CLDemo类
public class CLDemo {
    public CLDemo(){
        System.out.println("CLDemo class loaded, class loader: " + getClass().getClassLoader());
    }
}

//测试类
public class Test {
    public static void main(String[] args) throws Exception{
    	//这里的ClassLoader沿用上面的实例代码即可
        MyClassLoader cl = new MyClassLoader();
        cl.setPath(ClassLoaderUtil.LOADER_TEST_DIR);
        Class clazz = cl.loadClass("com.mishaki.classloader.csdn.Sample");
        System.out.println("hashCode:" + clazz.hashCode());
        clazz.newInstance();
    }
}

//测试方式
//将classes下的Sample和CLDemo的class文件复制到测试目录,并将classes下的CLDemo.class文件删除
//然后运行Test类

//结果
/*
hashCode:705927765
Sample class loaded, class loader: sun.misc.Launcher$AppClassLoader@c387f44
Exception in thread "main" java.lang.NoClassDefFoundError: com/mishaki/classloader/csdn/CLDemo
        at com.mishaki.classloader.csdn.Sample.(Sample.java:7)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
        at java.lang.reflect.Constructor.newInstance(Unknown Source)
        at java.lang.Class.newInstance(Unknown Source)
        at com.mishaki.classloader.csdn.Test.main(Test.java:11)
Caused by: java.lang.ClassNotFoundException: com.mishaki.classloader.csdn.CLDemo
        at java.net.URLClassLoader.findClass(Unknown Source)
        at java.lang.ClassLoader.loadClass(Unknown Source)
        at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
        at java.lang.ClassLoader.loadClass(Unknown Source)
        ... 7 more

*/

可以打印出hashCode,但报了AppClassLoader不能找到CLDemo异常
这里有一个地方需要先确定,Sample是由AppClassLoader加载的,因为在当前classpath下,并没有将Sample.class文件删除,所以并不需要去其他地方加载该文件。
既然可以确定Sample是由AppClassLoader加载的,那剩下的问题就简单了,根据上面的当前类加载器的定义,加载了ClassX引用了ClassY,那么ClassX的类加载器就会去加载ClassY,这个时候CLDemo肯定就是由AppClassLoader去加载的,但又不能找到CLDemo.class文件,所以抛出异常。

//现在将当前项目下的Sample.class文件删除
public class Test {
    public static void main(String[] args) throws Exception{
        MyClassLoader cl = new MyClassLoader();
        cl.setPath(ClassLoaderUtil.LOADER_TEST_DIR);
        Class clazz = cl.loadClass("com.mishaki.classloader.csdn.Sample");
        System.out.println("hashCode:" + clazz.hashCode());
        clazz.newInstance();
    }
}

//结果
/*
loadClassData
hashCode:1829164700
Sample class loaded, class loader: com.mishaki.classloader.csdn.MyClassLoader@2a139a55
CLDemo class loaded, class loader: sun.misc.Launcher$AppClassLoader@c387f44
*/

可以看到,Sample使用的是ClassLoader是MyClassLoader
CLDemo使用的是AppClassLoader
原因是:在加载Sample类的时候,MyClassLoader发现parent没办法加载,这个时候只能自己加载
所以就到目标目录进行加载,并加载成功,这个时候加载CLDemo的时候就会继续使用该ClassLoader进行加载
但还是会先使用parent进行加载,由于当前项目存在CLDemo.class文件,所以最终使用AppClassLoader完成加载

不同命名空间引发的问题

//Person类
public class Person {
    private Person person;

    public void setPerson(Object obj) {
        this.person = (Person) obj;
    }
}

//案例一
//使用AppClassLoader加载class并调用初始化方法
//此时由于没有删除Person.class文件,也没有将路径指定为磁盘上的其他路径,而是当前项目的路径,所以是使用AppClassLoader进行加载
MyClassLoader loader1 = new MyClassLoader();
MyClassLoader loader2 = new MyClassLoader();

loader1.setPath(ClassLoaderUtil.CURRENT_DIR);
loader2.setPath(ClassLoaderUtil.CURRENT_DIR);

Class clazz1 = loader1.loadClass("com.mishaki.classloader.csdn.Person");
Class clazz2 = loader2.loadClass("com.mishaki.classloader.csdn.Person");
System.out.println(clazz1 == clazz2);

Object obj1 = clazz1.newInstance();
Object obj2 = clazz2.newInstance();

Method setPerson = clazz1.getMethod("setPerson",Object.class);
setPerson.invoke(obj1,obj2);

//结果:true,程序正常运行,没有发生异常
//因为两个loader最终都将任务交给了AppClassLoadewr

//案例二
//直接将parent指定为BoostrapClassLoader,所以会导致加载失败,最后使用自己定义的ClassLoader进行加载
MyClassLoader loader1 = new MyClassLoader(null);
MyClassLoader loader2 = new MyClassLoader(null);

loader1.setPath(ClassLoaderUtil.CURRENT_DIR);
loader2.setPath(ClassLoaderUtil.CURRENT_DIR);

Class clazz1 = loader1.loadClass("com.mishaki.classloader.csdn.Person");
Class clazz2 = loader2.loadClass("com.mishaki.classloader.csdn.Person");
System.out.println(clazz1 == clazz2);

Object obj1 = clazz1.newInstance();
Object obj2 = clazz2.newInstance();

Method setPerson = clazz1.getMethod("setPerson",Object.class);
setPerson.invoke(obj1,obj2);
//结果
/*
loadClassData
loadClassData
false
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at com.mishaki.classloader.csdn.Test2.main(Test2.java:44)
Caused by: java.lang.ClassCastException: com.mishaki.classloader.csdn.Person cannot be cast to com.mishaki.classloader.csdn.Person
	at com.mishaki.classloader.csdn.Person.setPerson(Person.java:7)
	... 5 more
*/
//Person不能转换成Person,这是因为clazz1和clazz2不是在同一个命名空间,所以没办法相互转换

你可能感兴趣的:(java,java)