面试必问:请你详细说说类加载流程,类加载机制及自定义类加载器

当程序使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、链接、初始化三个步骤对该类进行类加载。

类的加载、链接和初始化

类的加载

类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象。类的加载过程是由类加载器来完成,类加载器由JVM提供。我们开发人员也可以通过继承ClassLoader来实现自己的类加载器。

加载的class来源:

  • 从本地文件系统内加载class文件
  • 从JAR包加载class文件
  • 通过网络加载class文件
  • 把一个java源文件动态编译,并执行加载。

类的链接

通过类的加载,内存中已经创建了一个Class对象。链接负责将二进制数据合并到 JRE中。链接需要通过验证、准备、解析三个阶段。

验证

验证阶段用于检查被加载的类是否有正确的内部结构,并和其他类协调一致即是否满足java虚拟机的约束。

准备

类准备阶段负责为类的类变量(static修饰)分配内存,并设置默认初始值。(如果final static一起修饰,并且变量是使用基础类型或String的字面量进行赋值的则会在该阶段赋具体值,而不是初始值)

解析

我们知道,引用其实对应于内存地址。思考这样一个问题,在编写代码时,使用引用,方法时,类知道这些引用方法的内存地址吗?显然是不知道的,因为类还未被加载到虚拟机中,你无法获得这些地址。

举例来说,对于一个方法的调用,编译器会生成一个包含目标方法所在的类、目标方法名、接收参数类型以及返回值类型的符号引用,来指代要调用的方法。

解析阶段的目的,就是将这些符号引用解析为实际引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必会触发解析与初始化)。

类的初始化

类的初始化阶段,虚拟机主要对类变量进行初始化。虚拟机调用<clinit>方法,进行类变量的初始化(链接阶段的解析已经赋默认值)。

java类中对类变量进行初始化的两种方式:

  • 在定义时初始化
  • 在静态初始化块内初始化

<clinit>方法相关

虚拟机会收集类及父类中的类变量及类方法组合为<clinit>方法,根据定义的顺序进行初始化。虚拟机会保证子类的<clinit>执行之前,父类的< clinit>方法先执行完毕。

因此,虚拟机中第一个被执行完毕的<clinit>方法肯定是java.lang.Object方法。

public class Test {
    static int A = 10;
    static {
        A = 20;
    }
}

class Test1 extends Test {
    private static int B = A;
    public static void main(String[] args) {
        System.out.println(Test1.B);
    }
}
//输出结果
//20

从输出中看出,父类的静态初始化块在子类静态变量初始化之前初始化完毕,所以输出结果是20,不是10。

如果类或者父类中都没有静态变量及方法,虚拟机不会为其生成<clinit>方法。

接口与类不同的是,执行接口的<clinit>方法不需要先执行父接口的<clinit>方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>方法。

public interface InterfaceInitTest {
    long A = CurrentTime.getTime();
}

interface InterfaceInitTest1 extends InterfaceInitTest {
    int B = 100;
}

class InterfaceInitTestImpl implements InterfaceInitTest1 {
    public static void main(String[] args) {
        System.out.println(InterfaceInitTestImpl.B);
        System.out.println("---------------------------");
        System.out.println("当前时间:"+InterfaceInitTestImpl.A);
    }
}

class CurrentTime {
    static long getTime() {
        System.out.println("加载了InterfaceInitTest接口");
        return System.currentTimeMillis();
    }
}
//输出结果
//100
//---------------------------
//加载了InterfaceInitTest接口
//当前时间:1560158880660

从输出验证了:对于接口,只有真正使用父接口的类变量才会真正的加载父接口。这跟普通类加载不一样。

虚拟机会保证一个类的<clinit>方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只有一个线程去执行这个类的<clinit>方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>方法完毕。

public class MultiThreadInitTest {
    static int A = 10;
    static {
           System.out.println(Thread.currentThread()+"init MultiThreadInitTest");
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Runnable runnable = () -&g
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值