一. 什么是类加载器
类加载器 classLoader
- 类加载器:负责将.class文件(存储在硬盘上的物理文件)加载到内存中,是类加载器把类的字节码文件加载到内存当中的。
二. 类加载的过程
每个编写的”.java”拓展名类文件都存储着需要执行的程序逻辑,这些”.java”文件经过Java编译器编译成拓展名为”.class”的文件,”.class”文件中保存着Java代码经转换后的虚拟机指令,当需要使用某个类时,虚拟机将会加载它的”.class”文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程称为类加载。
类在什么时候会被加载到内存中呢?
1.是不是在虚拟机一启动的时候,所有的类就加载到内存当中了呢?
- 其实并不是这样的,类加载的时机有以下六个。
类加载时机:
- 创建一个类的实例对象的时候,比如说我要创建Student学生类对象的时候,这个时候才会把Student类对应的字节码文件加载到内存当中。
- 调用类的类方法。这里的类方法,就是指静态方法,而静态方法是用类名直接调用的,不需要创建对象,所以在调用静态方法的时候也会加载这个类的字节码文件。
- 访问类或者接口的类变量,或者为该类变量赋值。这里的类变量,其实就是指静态变量,静态变量也是用类名直接调用的,此时不需要创建对象,所以在调用静态变量的时候也会加载这个类的字节码文件。
- 使用反射方式来强制创建某个类或接口对应的java.lang.Class类对象。当使用反射来创建一个类对象的时候,这个类的字节码文件也会加载到内存当中。
- 初始化某个类的子类,说白了就是创建子类对象。比如说我要创建一个Student类的对象,那此时就需要把Student它的父类的字节码文件也加载到内存当中。
- 直接使用java.exe命令来运行某个主类。那这个就是运行一个类的时候,需要把这个类的字节码文件加载到内存当中。
总结:用到就加载,不用不加载。类是在用到的时候才加载,不用是不加载的。
类加载的过程:
- 加载:类加载过程的一个阶段:通过一个类的完全限定名查找此类的字节码文件,并利用字节码文件创建一个Class对象
- 验证:目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
- 准备:为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
-
解析:主要将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针
- 初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器(静态代码块)和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。
-
这便是类加载的5个过程,而类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例。
- 加载:
- 通过一个类的全限定名来获取定义此类的二进制字节流,简答理解就是类加载器首先会通过包名 + 类名,这个就叫做全限定名,通过它来找到这个字节码文件,因为不同的包下有可能会有相同的类名,所以我们是通过包名 + 类名去找的,那找到字节码文件之后,就需要准备用流来进行传输。那因为在Java当中,所有的数据传输都是以流的形式进行传输的。
- 那加载呢,还会做第二件事情,将这个字节流所代表的静态存储结构转化为运行时数据结构,那这个简单理解就是通过刚刚得到的流把字节码文件去加载到内存当中。那此时,字节码文件已经到内存里面了。
- 还有第三件事情,在内存中生成一个代表这个类的Class对象,任何类被使用时,系统都会为之建立一个Class对象。那这句话的意思就是当一个类加载到内存之后,是不能随便乱放的,此时虚拟机会创建这个类的Class对象来存储类中对应的内容。
- 一开始class文件也就是字节码文件,一定时存储在硬盘上的,首先通过包名+类名找到我想要加载的class字节码文件,然后创建一个流,再通过这个流将字节码文件加载到内存当中,但是加载到内存当中之后,你是不能随便乱放的,此时在内存中就要创建一个class对象,用来存储类的成员信息。
- 以Student为例,在内存当中创建的就是Student类的class对象,在这个对象里面会存储类的成员信息(成员变量以及成员方法)。
- 总结:当加载完成后,字节码文件已经到内存中了,并且在内存中会创建一个class对象,用来存储类中的成员信息。
2.验证:
- 链接阶段的第一步验证,这一阶段为了确保class文件字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。简答理解就是,看以下文件中的信息是否符合虚拟机规范,有没有一些安全隐患,这个就是验证。
3. 准备:
- 链接的第二步准备,就是负责为类的类变量,也就是静态变量去分配内存空间,并且设置默认初始化值。那简单理解就是在刚刚创建的class对象当中初始化静态变量。
- 比如说我Student里面有一个静态的变量school学校,那么在内存当中,就相当于在class对象中为school去开辟了一个空间,赋值为null。
4. 解析:
- 链接的第三步,解析,是将类的二进制数据流中的符号引用替换为直接引用。
- 当一个字节码文件加载到内存中的时候,虚拟机会创建一个class对象,假设这一个对象的地址值时0x0011,那对象里面会记录类的成员信息,这些是第一步加载做的事情,但是这些成员信息,它是有类型的,假设这里的Student它的成员变量有String类型的name和int类型的age,这里的String是一个引用数据类型,也就是说String它是一个别的类的名字,那么在加载Student的时候String这个类有没有加载我不知道,加载到了哪里我也是不知道的。
- 所以在最初加载Student类的时候,是不知道String这个类是在哪里的,所以这个时候,这里的String它其实是用符号替代的,那这个就叫做符号引用,因为找不到,所以先用符号替代。
- 假设这里的符号是且这样的一个符号,那么到了现在进行到了解析这一步的时候,此时就会实际的去找String这个类它在哪里,并且会把这个临时的符号变成实际的String的引用,那假设String的class对象它的地址值是0x0022,那就把上面的这个符号变成0x0022,所以呢这个就叫做符号引用变成直接引用。
- 那么简单理解一下解析这一步,如果本类中用到了其他的类,那么此时就需要找到对应的类就可以了。
5. 初始化
- 类加载的最后一步初始化,比较专业的解释呢,就是根据程序员通过程序程序制定的主观计划去初始化类变量和其他资源。那简单理解就是给静态变量进行赋值以及其他资源进行初始化。
- 那假设在Student类当中有静态变量school,它的值为“传智大学”,那此时就会把传智大学这个值赋值给school这个变量,覆盖原来的默认值null,那这个就是初始化要做的事情。
类加载过程回顾:
三. 类加载器的分类
- 绝大部分的Java程序,都会使用的到以下的三种系统提供的类加载器相互配合执行的,在必要时,我们还可以自定义类加载器。
- 第一个启动(Bootstrap)类加载器,它是虚拟机内置的类加载器,启动类加载器主要加载的是JVM自身需要的类,底层是用C++语言实现的,是虚拟机自身的一部分,当虚拟机启动的时候,它会自动地进行启动。
- 第二种平台(Platform)类加载器,负责加载JDK中一些特殊的模块。
- 第三个叫做系统(System)类加载器,它也被称之为应用程序类加载器,负责加载用户类路径上所指定的类库。开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader类当中的静态方法getSystemClassLoader()方法可以获取到系统类加载器。
- 在一般情况下,我们自己写的大部分代码就是用第三种系统类加载器去加载到内存当中的,而这些类加载器,它都有各自的加载范围,什么样的类,该用哪一种加载器去加载,这个在Java当中已经规定好了,不需要我们自己再写额外的代码去实现了。
- 需要注意的是,Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
四. 理解类加载器的双亲委派模型 / 模式
双亲委派模型 / 模式工作原理
- 要学习类加载器的双亲委派模型,我们就需要回忆一下我们刚刚学习的三种类加载器,启动类加载器(Bootstrap ClassLoader),平台类加载器(Platform ClassLoader)和系统类加载器(System ClassLoader),其中用的最多的恶就是系统类加载器,那如果说有必要,我们还可以再加入自定义类加载器,那么这些类加载器之间的层次关系,我们称之为类加载器的双亲委派模型。
- 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,这个是最基本的一个条件。
- 比如说自定义类加载器,它的父类就是系统类加载器,而系统类加载器它的父类就是平台类加载器,而平台类加载器的父类是启动类加载器,启动类加载器它是最顶层的,所以它上面就没有父类加载器了。
- 请注意双亲委派模式中的父子关系并非通常所说的类继承关系,并非我们在代码当中写的extends继承,而是在逻辑上的继承。
- 假设现在想要用最下面的类加载器去加载一个字节码文件,它首先不会自己去尝试加载,而是把这个加载任务委派给父类的加载器去完成,也就是自定义类加载器会交给系统类加载器,而系统类加载器它还会委托给平台类加载器去完成,那平台类加载器继续委托给最上面的启动类加载器去完成,而启动类加载器是最顶层的了,所以它不会再往上委托了,因此,所有的加载请求最终都会传递到最顶层的启动类加载器当中,所以这个传递委托的关系,我们也认为是逻辑上的继承。那么这些类加载器都有各自的加载范围,当父类加载器无法完成这个加载请求的时候,它就会一层一层的往下返回,启动类加载器会返回给平台加载器,继续返回给系统加载器,最后再返回给自定义类加载器一层一层往下返回,那此时下面的子加载器才会尝试自己去加载我们想要的字节码文件,在这个模型当中,自定义类加载器我们平时用的不都有,最为常见的就是类加载器System ClassLoader。
- 双亲委派模式是在Java 1.2后引入的,其工作原理的是,如果一个类加载器收到了类加载求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这不就是传说中的实力坑爹啊?那么采用这种模式有啥用呢?
双亲委派模式优势
- 采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子类加载器ClassLoader再加载一次。
- 其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
代码验证双亲委派模型:
package com.gch.d11_class_loader;
/**
获取类加载器
*/
public class ClassLoaderDemo {
public static void main(String[] args) {
// 1.获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
// 2.获取系统类加载器的父加载器 - - - 平台类加载器
ClassLoader platformClassLoader = systemClassLoader.getParent();
// 3.获取平台类加载器的父加载器 - - - 启动类加载器
ClassLoader bootstrapClassLoader = platformClassLoader.getParent();
System.out.println("系统类加载器:" + systemClassLoader); // 系统类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println("平台类加载器:" + platformClassLoader); // 平台类加载器:sun.misc.Launcher$ExtClassLoader@14ae5a5
System.out.println("启动类加载器:" + bootstrapClassLoader); // 启动类加载器:null
}
}
ClassLoader类的两个重要方法:
package com.gch.d11_class_loader;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
目标:掌握ClassLoader类的两个重要方法
*/
public class ClassLoaderAPI {
private static String name = null;
public static void main(String[] args) throws IOException {
/**
* 获取系统类加载器
* @return ClassLoader:返回系统类加载器
* public static ClassLoader getSystemClassLoader();
*/
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
/**
* 利用获取到的加载器去加载一个指定的资源文件
* @Param String name:资源文件的路径
* @Return InputStream:返回值是一个字节输入流,文件中的数据都在这个流当中
* public InputStream getResourceAsStream(String name);
*/
InputStream is = systemClassLoader.getResourceAsStream("prop.properties");
// 创建属性集集合对象
Properties properties = new Properties();
// 加载属性文件中的键值对数据到属性对象properties中去
properties.load(is);
// 在类加载器当中,文件一定要建在src这个文件夹下
System.out.println(properties);
// 释放资源
is.close();
}
}
五. 类与类加载器
类与类加载器
在JVM中表示两个class对象是否为同一个类对象存在两个必要条件:
- 类的完整类名必须一致,包括包名。
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
也就是说,在JVM中,即使这个两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的,这是因为不同的ClassLoader实例对象都拥有不同的独立的类名称空间,所以加载的class对象也会存在不同的类名空间中
了解class文件的显示加载与隐式加载的概念
所谓class文件的显示加载与隐式加载的方式是指JVM加载class文件到内存的方式
显示加载指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象。
而隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。在日常开发以上两种方式一般会混合使用,这里我们知道有这么回事即可。