类加载

一个类通常需要经过:类加载–> 使用 –> 卸载 的阶段。

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口

类加载过程包括三个阶段:加载(loading) + 连接(Linking) + 初始化(Initialization)。如下图
9f4cb31209d745eda780150b95ec8207_tplv-k3u1fbpfcp-watermark

类加载过程

加载

查找类的字节码,并创建该类的class对象

  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 通过这个字节流所代表对的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

连接

  1. 校验(Verify)

    校验时为了检验class文件的字节码是否符合虚拟机的要求,且不危害虚拟机

    • 文件校验:检验字节流是否符合class文件格式的规范,比如说JAVA规定class的二进制文件开头都是以0xKAFABABE开头的
    • 元数据校验
    • 字节码校验
    • 符号引用校验
  2. 准备(Prepare)

    为类变量分配内存并且设置该类变量的默认初始值,即零值。

    注意:

    • 不包含final修饰的static,final修饰的static在编译的时候就会分配了。
    • 不会为实例变量分配,类变量会分配在方法区中,实例变量会随着对象一起分配到堆中。
  3. 解析(Resolve)

    将常量池内的符号引用转换为直接引用(地址引用)

初始化

初始化阶段就是执行类构造方法< clinit >()的过程

  1. 在javac编译器编译成字节码的时候自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并为<clinit>()方法

  2. 构造器方法中的指令是按照语句在源文件中出现的顺序执行的。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Test {
    {
    num = 20;
    }
    private static int num = 10;
    public static void main(String[] args) {
    System.out.println(num);
    }
    }

    运行结果是10?还是20?

    ab2c894c87e6478082924acbe5402e04_tplv-k3u1fbpfcp-watermark

    解析:

    • 对于num,在准备阶段,会附上零值,也就是0
    • 然后在初始化阶段,执行的clinit方法,会先执行num = 20,然后执行num = 10。因为clinit中指令是按照语句在源文件中出现的顺序执行的。
  3. 如果该类有父类,jvm会保证子类的<clinit>执行前,父类的<clinit>已经执行完毕

  4. 虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁的。防止多次加载

java的类加载器

类加载就是通过类加载器来完成的。java的类加载器一共有如下几种分类

c6aa97a2e211459c9e54d728db7c5073_tplv-k3u1fbpfcp-watermark

启动类加载器(BootstrapClassLoader)

由c++编写,属于jvm的一部分,不继承于java.lang.ClassLoader。主要负责加载核心java库。存储在/jre/lib/rt.jar目录中的。同时出于安全考虑,只加载包名以java、javax、sun等开头的类

扩展类加载器(ExtensionsClassLoader)

sun.misc.Launcher$ExtClassLoader类实现,用来在/jre/lib/ext或者java.ext.dirs中指明的目录加载java的扩展库。Java虚拟机会提供一个扩展库目录,此加载器在目录里面查找并加载java类。

应用类加载器(AppClassLoader)

sun.misc.Launcher$AppClassLoader实现,一般通过(java.class.path或者Classpath环境变量)来加载Java类,也就是我们常说的classpath路径。通常我们是使用这个加载类来加载Java应用类,可以使用ClassLoader.getSystemClassLoader()来获取它。

自定义类加载器(UserDefineClassLoader)

除了上述java自带提供的类加载器,我们还可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器。

双亲委派机制

java加载一个类的时候,使用的是双亲委派机制

工作原理

  • 如果一个类加载器收到一个类加载请求。并不会直接自己去加载类。而是托付给父类加载器。
  • 如果父类加载器还存在父类加载器,则会继续向上委托。直到没有父类加载器。
  • 如果父类加载器无法加载完成该任务,子加载器才会去尝试加载。

例子

062c90b5343a4152bfb832ba185c3a35_tplv-k3u1fbpfcp-watermark

e71a41bd6af64cbab087e3e4dfe0ee69_tplv-k3u1fbpfcp-watermark
我自定义了一个java.lang.String类。然后在main新建一个对象。查看该类是否被加载。

c907d48d5c2547d8af73c80e96b123e6_tplv-k3u1fbpfcp-watermark
没有输出,说明自定义的类没有被加载。因为双亲委派机制,会把类加载请求托付给BootstrapClassLoaderBootstrapClassLoader可以加载java.lang.String。最终加载的是java核心库中的String。

双亲委派机制的好处

避免重复加载 + 避免核心类被篡改

  • 避免重复加载:如果父类加载器已经加载过该类了。就不加载了
  • 避免核心类被篡改:假设通过网络传递了一个java.lang.String的类,我们因为双亲委派机制,最终加载到的还是java核心类库中的String。