Fork me on GitHub

类加载

注意:所有文章除特别说明外,转载请注明出处.

类加载

[toc]

在Java程序中,类型的加载、连接和初始化过程都是在程序运行期间完成的。提供了更大的灵活性,增加了更多的功能。

Java 虚拟机与程序的生命周期

在如下几种情况下,Java虚拟机将结束生命周期:

1. 执行了System.exit()方法

2. 程序正常执行结束

3. 程序在执行过程中遇到异常或错误而异常终止

4. 由于操作系统出现错误而导致Java虚拟机进程终止

类的加载、连接与初始化

1. 加载:查找并加载类的二进制数据(就是将二进制形式的Java类型读入Java虚拟机中)

2. 连接:

    1. 验证:确保被加载的类的正确性

    2. 准备:为类的静态变量分配内存、并将其初始化为默认值(为类变量分配内存,设置默认值。但是到达初始化之前,类变量都没有初始化为真正的初始值。)

    3. 解析:将类中的符号引用转换为直接引用(解析过程是在类型的常量池中寻找类、接口、字段和方法的符号引用,将这些符号引用替换为直接引用的过程。)

3. 初始化:为类的静态变量赋予正确的初始值(为类的变量赋予正确的初始值)

    类的初始化步骤:

        1. 假如这个类还没有被加载和连接,那就先进行加载和连接

        2. 假如类存在直接父类,并且这个父类还没有被初始化,那么就先初始化直接父类

        3. 假如类中存在初始化语句,那就依次执行这些初始化语句

4. 类的实例化:

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

    Java编译器为它编译的每一个类都至少生成一个实例初始化方法,在Java的class文件中,这个实例初始化方法被称为<init>。针对源程序中每一个类的构造方法,Java编译器都产生一个<init>方法。

Java对类的使用方式

Java程序对类的使用方式有两种:主动使用与被动使用。所有Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才 初始化 它们。

1. 主动使用

    1. 创建类的实例
    2. 访问某个类或接口的静态变量,或者对该静态变量赋值
    3. 调用类的静态方法
    4. 反射
    5. 初始化一个类的子类
    6. Java虚拟机启动时被标明为启动类的类
    7. 动态语言支持

提示:调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

2. 被动使用

    除了上面的七种主动使用方式之外,其它方式不会导致类被初始化

提示:所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化它们。

类的加载

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

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

类加载器是用来将类加载到Java虚拟机中,在jdk1.2之后采用双亲委托机制,该机制能够更好的保证Java平台的安全。在此机制中,出Java虚拟机自带的根类加载器之外,其余的类加载器都有且只有一个父类加载器。当Java程序请求加载器loader1加载Sample类时,loader1首先委托自己的父加载器去加载Sample类,如果父类加载器能加载,则由父类加载器完成加载任务,否则才由加载器loader1本身加载Sample类。

加载.class文件方式
1. 从本地系统中直接加载

2. 通过网路下载.class文件

3. 从zip等归档文件中加载

4. 将Java源文件动态编译为.class文件

...

提示:对于静态字段来说,只要直接定义了该字段的类才会被初始化。当一个类在初始化时,要求其父类全部都已经初始化完成。

-XX:+TraceClassLoading:用于追踪类的加载信息并打印出来。

-XX:+<option>,表示开启option选项
-XX:-<option>,表示关闭option选项
-XX:<option>=<value>,表示将option选项值设置为value

由上面的命令可以在控制台得到即使static中没有被初始化,但是在Java中已经被加载出来过的。

常量在编译阶段会被存入到调用这个常量方法所在类的常量池中。本质上,调用类并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化。

提示:助记符:

ldc表示将int,float或是String类型的常量值从常量池中推送至栈顶。

bipush表示将单字节(-128-127)的常量推送至栈顶。

sipush表示将一个短整型的常量值(-32768-32767)推送至栈顶。

iconst_1表示将int类型1推送至栈顶。(iconst_1 - iconst_5)

anewarry表示创建一个引用类型的(如类、接口、数组)数组,并将其引用值压入栈顶

newarray表示创建一个指定的原始类型(如int float char等)的数组,并将其引用值压入栈顶。

当一个常量的值并非编译期间可以确定的,那么其值就不会被放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,会导致这个被初始化。

对于数组实例来说,其类型是由JVM在运行期间动态生成的,表示 [运行类的类型 这种形式。动态生成的类型,其父类类型是Object。对于数组来说,JavaDoc经常将构成数组的元素的Component,实际就是数组降低一个维度后的类型。

提示:当一个数组类的类型是原生类型时,表示该类的类加载器是没有的(null)。

当一个接口在初始化时,并不要求其父接口都完成初始化。只有在真正使用到父接口的时候(如引用接口中所定义的常量时),才会初始化。

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

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

所以一个父接口并不会因为它的子接口或者实现类的初始化而初始化,只有当程序首次使用特定接口的静态变量时才会导致该接口的初始化。

类的初始化时机

只有当程序访问的 静态变量 或 静态方法 确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用。

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

两种类型的类加载器

类加载器并不需要等到某个类被“首次主动使用”时在加载它。

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

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

类的验证内容:1. 类文件的结构检查。 2. 语义检查。 3. 字节码检查。 4. 二进制兼容性验证。

1. Java虚拟机自带的加载器

由上到下的类加载器:

1. 根类加载器(Bootstrap)

    该加载器没有父加载器,它负责加载虚拟机的核心类库,如:java.lnag.*等。根类加载器从系统属性 sun.boot.class.path 所指定的目录中加载类库。该加载器的实现依赖底层操作系统,是虚拟机实现的一部分,并没有继承java.lang.ClassLoader类。

        $JAVA_HOME中jre/lib/rt.jar里所有的.class,是由c++实现,不是classloader的子类。

2. 扩展类加载器(Extension)

    它的父加载器是根类加载器。它从java.ext.dirs系统属性所指定的目录中加载类库。或者从jdk的安装目录的 jre\lib\ext子目录(扩展目录)下加载类库。如果用户创建的jar文件放在这个目录下,也会自动由扩展类加载器加载。该加载器是java.lang.ClassLoader类的子类。

        负责加载Java平台的扩展功能的jar包。包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包。

3. 系统(应用)类加载器(System)

    或称为应用类加载器,它的父加载器是扩展类加载器。它从环境变量 classpath或者系统属性 java.class.path 所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器。该加载器是 java.lang.ClassLoader 类的子类。

        负责加载classpath中指定的jar包及目录中class。
2. 用户自定义的类加载器
1. java.lang.ClassLoader的子类

    所有用户自定义的类加载器都应该继承该类。

2. 用户可以定制类的加载方式
当前类加载器(Current ClassLoader)

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

线程上下文加载器(Context ClassLoader)

线程上下文类加载器是从JDK1.2开始引入的,类Thread中的getContextClassLoader()与setContextClassLoader(ClassLoader cl)分别用来获取和设置上下文类加载器。

如果没有通过setContextClassLoader(ClassLoader cl)进行设置的话,线程将继承其父线程的上下文类加载器。Java应用运行时的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过该类加载器来加载类和资源。

线程上下文类加载器的重要性
SPI(Service Provider Interface) - 服务提供接口

父ClassLoader可以使用当前线程Thread.currentThread().getContextLoader()所指定的ClassLoader加载的类。这就改变了父ClassLoader不能使用子ClassLoader或是其它没有直接父子关系的ClassLoader加载的类的情况,即改变了双亲委托模型。

提示:线程上下文类加载器就是当前线程的Current ClassLoader。

在双亲委托模型下,类加载是由下至上的,即下层的类加载器会委托上层进行加载。但是对于SPI来说,有些接口是Java核心库所提供的,而Java核心库是由启动类加载器来加载的,而这些接口的实现却来自于不同的jar包(厂商提供),Java的启动类加载器是不会加载其它来源的jar的,这样传统的双亲委托模型就无法满足SPI的要求。而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载。

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

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

try{
    Thread.currentThread().setContextClassLoader(targetTccl);
    myMethod();
}finally{
    Thread.currentThread().setContextClassLoader(classLoader);
}

在myMethod里面则调用了Thread.currentThread().getContextClassLoader(),获取当前线程的上下文类加载器做某些事情。

如果一个类由类加载器A加载,那这个类的依赖类也是由相同的类加载器加载的(如果该依赖类之前没有被加载过的话)。

提示:ContextClassLoader的作用就是为了破坏Java的类加载委托机制。

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

类加载器的双亲委托机制

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

1. 自底向上检查类是否已经加载

2. 自顶向下尝试加载类

public Test16(String classLoaderName) {
    super();//将系统类加载器当做该类加载器的父加载器
    this.classLoaderName = classLoaderName;
}

public Test16(ClassLoader parent, String classLoaderName) {
    super(parent);//显示指定该类加载器的父加载器
    this.classLoaderName = classLoaderName;
}

如果有一个类加载器能够成功加载到我们的Test类,那么称此类加载器是定义类加载器,而所有能够成功返回Class对象引用的类加载器(包括定义类加载器)都称为初始类加载器。

类加载器双亲委托模型的优点
1. 可以确保Java核心类库的类型安全:所有的Java应用都至少会引用java.lang.Object类,也就是在运行期,java.lang.Object这个类会被加载到Java虚拟机中。如果这个加载过程是由Java应用自己的类加载器所完成,那么很可能就会在JVM中存在很多版本的java.lang.Object类,而且这些类之间还是不兼容的,互相不可见的(正是命名空间在发挥作用)。借助于双亲委托机制,Java核心类库中的类的加载工作都是由启动类加载器来统一完成,从而确保了Java应用所使用的都是同一个版本的Java核心类库,它们之间是相互兼容的。

2. 可以确保Java核心类库所提供的类不会被自定义的类所替代。

3. 不同的类加载器可以为相同名称(binary name)的类创建额外的命名空间。相同名称的类可以并存在Java虚拟机中,只需要不同的类加载器来加载它们即可。不同的类加载器所加载的类之间是不兼容的,这就相当于在Java虚拟机内部创建一个又一个相互隔离的Java类空间,这类技术在很多框架中都得到了实际应用。

命名空间

每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成的。

在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。

在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。

关于命名空间的说明
1. 子加载器所加载的类能够访问父加载器所加载的类。

2. 父加载器所加载的类无法访问子加载器所加载的类。
不同类加载器命名空间关系

同一个命名空间内的类是相互可见的,子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器加载的类能看见父加载器加载的类。如:系统类加载器加载的类能看见根类加载器加载的类。

由父加载器加载的类不能看见子加载器加载的类。如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类互相不可见。

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

内建于JVM中的启动类加载器会加载 java.lang.ClassLoader以及其它的Java平台类,当JVM启动时,一块特殊的机器码会运行,它会加载扩展类加载器与系统类加载器,这块特殊的机器码叫作启动类加载器(Bootstrap)。

提示:启动类加载器并不是Java类,而其它的加载器则都是Java类。启动类加载器是特定于平台的机器指令,它负责开启整个加载过程。所有类加载器(除了启动类加载器)都被实现为Java类,不过,总归要有一个组件来加载第一个Java类加载器,从而让整个加载过程能够顺利进行下去,加载第一个纯Java类加载器就是启动类加载器的职责。启动类加载器还会负责加载供JRE正常运行所需要的基本组件,这包括java.util与java.lang包中的类等等。

获得ClassLoader的途径

  1. 获取当前类的ClassLoader -> clazz.getClassLoader();
  2. 获取当前线程上下文的ClassLoader -> Thread.currentThread().getContextClassLoader();
  3. 获取系统的ClassLoader -> ClassLoader.getSystemClassLoader();
  4. 获取调用者的ClassLoader -> DriverManager.getCallerClassLoader();

类的卸载

当类被加载、连接和初始化后,它的生命周期开始,当代表该类的class对象不再被引用,即不可触及时,class对象就会结束生命周期,该类在方法区内的数据也会被卸载,从而结束该类的生命周期。

一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。

由Java虚拟机自带的类加载器所加载的类在虚拟机的生命周期中,始终不会被卸载。而由用户自定义的类加载器可以被卸载。

Laucher 加载类加载器

源码分析

本文标题:类加载

文章作者:Bangjin-Hu

发布时间:2019年10月15日 - 09:22:26

最后更新:2020年03月30日 - 08:18:52

原始链接:http://bangjinhu.github.io/undefined/类加载/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

Bangjin-Hu wechat
欢迎扫码关注微信公众号,订阅我的微信公众号.
坚持原创技术分享,您的支持是我创作的动力.