Java基础——深入理解类的加载

1. 什么是类的加载?

Java虚拟机将描述类的数据从.Class文件装入虚拟机内存,并对数据进行验证,准备,解析和初始化,最终形成Java虚拟机可以直接使用的Java类型的过程为类的加载

2. 类加载的阶段?

运行期间。
不是编译期间哦,类的所有工作都是在程序运行期间完成的,虽然这给Java虚拟机编译带来额外的困难,但是这种动态加载和动态链接却实现了Java的动态扩展这一语言特性

3.类加载的时机?

3.1 :boom:最重要的一点 —— 类的生命周期:boom:

:star:加载(loading)
:star:链接(Linking)

  • :small_orange_diamond:验证(Verification)
  • :small_orange_diamond:准备(Preparation)
  • :small_orange_diamond:解析(Resolution)

:star:使用(Using)
:star:卸载(Unloading)

加载,验证,准备,初始化和卸载这五个阶段的开始顺序是确定的,而解析则不一定,在某些情况下会在类的初始化阶段之后开始!!

3.2 类初始化的六大时机:clock2:

加载,验证和准备已经开始
:one:遇到new,getstatic,putstatic或invokestatic这四条字节码指令

  • 对于第一个new指令毋庸置疑,新创建实例必定会涉及类初始化
  • 对于二三指令就是获取或者设置一个静态字段
  • 对于第四条指令就是调用静态方法

:two:当使用java.lang.reflect包的方法进行反射调用时会进行相应的类初始化
:three:当初始化类的时候,如果发现父类没有被初始化则先初始化父类!
:four:当Java虚拟机启动时,会首先进行main主类进行初始化
剩下两种可以去了解
:boom: 注意,Java虚拟机规范规定,有且只有以上六种情况才会进行类的初始化,这六种情况被称为对一个类型的主动引用

:raised_hand:既然有主动引用,就有对应的被动引用,举例说明

/**
情况一:通过子类引用父类的静态字段不会触发子类的初始化!!!
**/
public class Main {
    public static void main(String[] args){
        System.out.println(subclass.age);
    }
}
class parent{
    public static int age  = 0; //静态字段
    static {
        System.out.println("parent is loading!");
    }
}
class subclass extends  parent{
    static {
        System.out.println("subclass is loading!");
    }
}
/**
情况二:通过数组定义来引用类,不会触发类的初始化!!!
**/
public class Main {
    public static void main(String[] args){
        parent[] parents = new parent[10];
        System.out.println(subclass.age);
    }
}
class parent{
    public static int age  = 0; //静态字段
    static {
        System.out.println("parent is loading!");
    }
}
/**
情况三:常量!!!
**/
public class Main {
    public static void main(String[] args){
        parent[] parents = new parent[10];
        System.out.println(subclass.age);
    }
}
public class parent{
    public final static int age  = 0; //常量字段
    static {
        System.out.println("parent is loading!");
    }
}
解释一下:上述代码并没有打印 parent is loading! 原因在于,常量在编译阶段通过传播优化,已经将此常量的值存入在Main类的常量池种,实际上对parent类中常量的引用都转为对Main中常量的引用!!!

:facepunch:以上就是类的初始化时机和一些尽管看起来觉得需要进行初始化实际上并没有进行初始化的情况!!!

4.类加载的详细过程?:wc:

:one:加载

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流中苏代表的静态存储结构转换为方法去的运行时数据集
  • 在内存中生成一个代表这个类的java.lang.Class对象(仅此一份)!作为方法去这个类的各种数据的访问入口
    注意:数组的加载与以上步骤不同,但最终降维还是要进行类的加载过程的!

:two:验证

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证
    详细过程自己看书!

:three:准备

准备阶段主要是正式为类中定义的变量(即静态变量,static修饰的变量)分配内存并设置类变量初始值的阶段!注意此时并不分配实例变量!实例变量会随着对象实例化一起分配在Java堆中!

public static int age = 19;
注意,类加载中的准备阶段并不会立马赋值为19,而是先赋初始值为0!!!!!直到进行类加载的初始化阶段才会进行真正的赋值!!!

:four:解析

解析阶段就是Java虚拟机将常量池内的符号引用替换为直接引用的过程!!
详细过程自己看书

:five: 初始化

进行准备阶段变量已经赋值过一次的系统要求的初始零值(即静态变量和常量),按照程序员主观意愿进行初始化类变量和其他资源!
换一种说法就是:执行类构造器 clinit()方法的过程:raising_hand:

  • clinit()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,按出现顺序收集!
  • clinit()方法与类的构造函数不同,它不需要显式调用父类的构造器,Java虚拟机会保证在子类 clinit()方法执行之前去执行父类的!因此在Java虚拟机中,第一个被执行 clinit()方法的一定是java.lang.object
  • clinit()方法对于类和接口并不是必须的!如果一个类中没有静态语句块,大可不必生成这个方法!
  • Java虚拟机必须保证一个类的 clinit()方法在多线程环境中被正确的加锁同步!如果多个线程进行类的初始化,那么就只会有一个线程执行!所以如果这个方法耗时很长,可能会导致很隐蔽的阻塞!!!!:imp:

5.类加载器

类加载器即是在类的加载过程种,通过获取类的全限定名来获取描述该类的二进制字节流,完成加载动作,但是其意义又远不止于类的加载!

在Java虚拟机中,对于任意一个类,都必须由类的加载器和这个类本身一同确立其在Java虚拟机中的唯一性!言外之意,如果两个类来源于同一个Class文件,被同一个Java虚拟机加载,如果加载他们的类加载器不同,那么这两个类一定是不同的!:boom:

注意:这里的不同,包括代表类的Class对象的equals方法以及isInstance方法,同时也包括使用了 instanceof 关键字所做对象所属关系判断!!!

5.2 双亲委派模型

自JDK1.2 以来,Java一直保持这三层类加载、双亲委派模型的类加载架构:facepunch:

  • 启动类加载器(BootStrap Class Loader):主要负责加载存放在 JAVA_HOME\lib 目录下
  • 扩展类加载器(Extension Class Loader)
  • 应用程序类加载(Application Class Loader):负责加载用户类路径(class path)上所有的类库,如果应用程序没有自定义类加载器,一般这个类加载器就是默认的加载器

Java基础——深入理解类的加载

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,不过这里的类加载器之间的父子关系不是以继承来实现的,而是以组合关系来复用父加载器!!:raising_hand:

:arrow_right:结合上图,我们再来看下源码!!:eyes:

private final ClassLoader parent; 
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查请求的类是否已经被加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {//父加载器不为空,调用父加载器loadClass()方法处理
                        c = parent.loadClass(name, false);
                    } else {//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                   //抛出异常说明父类加载器无法完成加载请求
                   //但并不做任何处理,只是继续向下运行!
                }

                if (c == null) {
                    long t1 = System.nanoTime();
                    //自己尝试加载
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c; //返回自己的加载结果给子类!
        }
    }

从上面的过程我们可以看出,双亲委派模型的工作过程::boom:

  1. 当需要加载类的时候,首先判断这个类是否加载过了!
  2. 没有加载过 ,ok, 递归向上传递,检查对应类加载器是否加载过这个类!
  3. 如果到了最顶层的启动类加载都没有加载,那就从启动类加载器开始尝试加载,即递归返回,每层加载器尝试加载!

双亲委派模型的优点:
双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。因为越基础的类由越顶层的类加载加载,

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!