Java中类的加载顺序(ClassLoader)

在谈ClassLoader之前,先介绍一个常见异常:ClassNotFoundExcetpion ,原因:就是找不到指定的class。
常见的场景:

  1. 调用class的forName方法时,找不到指定的类
  2. ClassLoader 中的 findSystemClass() 方法时,找不到指定的类
  3. ClassLoader 中的 loadClass() 方法时,找不到指定的类

对于这个异常,它实质涉及到了java技术体系中的类加载。Java的类加载机制是技术体系中比较核心的部分,虽然它和我们直接打交道不多,但是对其背后的机理有一定理解有助于我们排查程序中出现的类加载失败等技术问题。

类的加载过程

一个java文件从被加载到被卸载这个生命过程,总共要经历5个阶段,JVM将类加载过程分为: (加链初使卸)
  加载->链接(验证+准备+解析)->初始化(使用前的准备)->使用->卸载

加载

首先通过一个类的全限定名来获取此类的二进制字节流;其次将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;最后在java堆中生成一个代表这个类的Class对象,作为方法区这些数据的访问入口。总的来说就是查找并加载类的二进制数据

链接:

  1. 验证:确保被加载类的正确性
  2. 准备:为类的静态变量分配内存,并将其初始化为默认值;
  3. 解析:把类中的符号引用转换为直接引用

类的初始化

类什么时候才被初始化

  1. 创建类的实例,也就是new一个对象
  2. 访问某个类或接口的静态变量,或者对该静态变量赋值
  3. 调用类的静态方法
  4. 反射(Class.forName(“com.kfzx.load”))
  5. 初始化一个类的子类(会首先初始化子类的父类)
  6. JVM启动时标明的启动类,即文件名和类名相同的那个类

类的初始化顺序

  1. 如果这个类还没有被加载和链接,那先进行加载和链接
  2. 假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)
  3. 加入类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。
  4. 总的来说,初始化顺序依次是:(静态变量、静态初始化块)–>(变量、初始化块)–> 构造器;

如果有父类,则顺序是:父类static方法 –> 子类static方法 –> 父类构造方法- -> 子类构造方法

类的加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个这个类的java.lang.Class对象,用来封装类在方法区类的对象。如:


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

  1. 从本地系统直接加载
  2. 通过网络下载.class文件
  3. 从zip,jar等归档文件中加载.class文件
  4. 从专有数据库中提取.class文件
  5. 将Java源文件动态编译为.class文件

加载器

JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:

加载器介绍

  1. BootstrapClassLoader(启动类加载器)
      负责加载$JAVA_HOMEjre/lib/rt.jar里所有的class,加载System.getProperty(“sun.boot.class.path”)所指定的路径或jar。
  2. ExtensionClassLoader(标准扩展类加载器)
      负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar-Djava.ext.dirs指定目录下的jar包。加载System.getProperty(“java.ext.dirs”)所指定的路径或jar。
  3. AppClassLoader(系统类加载器)
      负责记载classpath中指定的jar包及目录中class
  4. CustomClassLoader(自定义加载器)
      属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现。

类加载器的顺序

  1. 加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
  2. 在加载类时,每个类加载器会将加载任务上交给其父,如果其父找不到,再由自己去加载。
  3. Bootstrap Loader(启动类加载器)是最顶级的类加载器了,其父加载器为null

继承的加载顺序

由于static块会在首次加载类的时候执行,因此下面的例子就是用static块来测试类的加载顺序。

class A{
    static{
        System.out.println("A static");
    }
}
class B extends A{
    static{
        System.out.println("B static");
    }
}
class C extends B{
    private static D d = new D();
    static{
        System.out.println("C static");
    }
}
class D{
    static{
        System.out.println("D static");
    }
}
public class ExtendTest {
    public static void main(String[] args) {
        C c = new C();
    }
}

在上面的例子中,类C继承B,B继承A,而C有依赖于D。因此当创建C的时候,会自动加载C继承的B和依赖的D,然后B又会加载继承的A。只有A加载完,才能顺利的加载B;BD加载完,才能加载C。这就是类的加载顺序了。所以程序的输出为

A static
B static
D static
C static

应该注意的是,C类初始化D类时有static关键字,如果将static关键字去掉,将变成

A static
B static
C static
D static

也就是说,所有的变量初始化完,才会执行构造方法

扫描二维码关注公众号,回复: 5284803 查看本文章

在类的加载过程中,只有内部的变量创建完,才会去执行这个类的构造方法。

class A2{
    B2 b2 = new B2();
    static{
        System.out.println("A static");
    }
    public A2() {
        System.out.println("A2()");
    }
}
class B2{
    C2 c2 = new C2();
    D2 d2 = new D2();
    static{
        System.out.println("B static");
    }
    public B2() {
        System.out.println("B2()");
    }
}
class C2{
    static{
        System.out.println("C static");
    }
    public C2() {
        System.out.println("C2()");
    }
}
class D2{
    static{
        System.out.println("D static");
    }
    public D2() {
        System.out.println("D2()");
    }
}
public class VarTest {
    public static void main(String[] args) {
        A2 a2 = new A2();
    }
}

在上面的例子中,A2里面有B2变量,B2则有C2、D2变量。因此类的加载还是先读取到哪个,就执行相应的静态块。
当依赖的对象都定义完,才会执行构造方法:

A static
B static
C static
C2()
D static
D2()
B2()
A2()

静态成员与普通成员类的加载区别

在类的加载过程中,静态成员类的对象,会优先加载;而普通成员类的对象则是使用的时候才回去加载

class A3{
    B3 b3 = new B3();
    static C3 c4 = new C3();
    static{
        System.out.println("A3");
    }
}
class B3{
    static{
        System.out.println("B3");
    }
}
class C3{
    static{
        System.out.println("C3");
    }
}
public class StaticTest {
    public static void main(String[] args) {
        A3 a3 = new A3();
    }
}

输出:

C3
A3
B3

类构造方法的顺序

class A{
    public A() {
        System.out.println("A");
    }
}
class B extends A{
    public B() {
        System.out.println("B");
    }
}
class C extends B {
    private D d1 = new D("d1");
    private D d2 = new D("d2");
    public C() {
        System.out.println("C");
    }
}
class D {
    public D(String str) {
        System.out.println("D "+str);
    }
}
public class ExtendTest {
    public static void main(String[] args) {
        C c = new C();
    }
}

执行结果:

A
B
D d1
D d2
C

结论:

  1. 首先会调用基类的构造方法
  2. 其次,调用成员的构造方法
  3. 最后,调用自己的构造方法

总结

  1. 所有的类都会优先加载基类
  2. 静态成员的初始化优先
  3. 成员初始化后,才会执行构造方法
  4. 静态成员的初始化与静态块的执行,发生在类加载的时候。
  5. 类对象的创建以及静态块的访问,都会触发类的加载。

猜你喜欢

转载自blog.csdn.net/TTTZZZTTTZZZ/article/details/87029708