Java 类加载 —— 底层是如何实现的?

目录

1、加载

2、链接

2.1 验证阶段:验证类是否符合 JVM 规范,安全性检查

2.2 准备阶段:为 static 变量分配空间,设置默认值

2.3 解析:将常量池中的符号引用解析为直接引用。

3、初始化

4、类加载常见问题-练习

5、类加载常见问题-练习2


从类的生命周期而言,一个类包括如下阶段:

类加载阶段分类:加载、链接(验证/准备/解析)、初始化(<cinit>()V方法 / 发生的时机),如下图所示:


1、加载

  • 将类的字节码载入方法区中,内部采用 c++ 的 instanceKlass 描述 java 类,它的重要 field 域有:
    • _java_mirror 即 java 的类镜像,起到桥梁作用,例如对 String 来说,就是 String.class, 作用是把 kclass 暴露给 java 使用
    • _super 即父类
    • _fields 即成员变量
    • _methods 即方法
    • _constants 即常量池
    • _class_loader 即类加载器
    • _vtable 虚方法表

_itable 接口方法表

  • 如果这个类还有父类没有加载,先加载父类
  • 加载和链接可能是交替运行的

注意:

  • instanceKlass 这样的【元数据】是存储在方法区(1.8后的元空间内),但 _java_mirror是存储在堆中
  • 可以通过HSDB工具查看
实例对象、类镜像、instanceKlass之间的关系

2、链接

链接又可以分为3个小步骤,如下:

2.1 验证阶段:验证类是否符合 JVM 规范,安全性检查

用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行


2.2 准备阶段:为 static 变量分配空间,设置默认值

  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾(方法区),从 JDK 7 开始,存储于跟着类对象 _java_mirror 末尾(堆内存)
  • static 变量:分配空间 和 赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
  • 如果 static 变是是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成

解析:将常量池中的符号引用解析为直接引用

// 类加载分析 - 演示 final 对静态变量的影响
public class T02_ClassLoad_Final_Static {
    // 在准备阶段,仅仅只有分配空间,没有赋值
    static int a;
    // 在准备阶段,仅仅只有分配空间,赋值是在类的构造方法中
    static int b = 10;
    // 在准备阶段,20 属于ConstantValue,final+static 修饰的整型变量 在准备阶段就完成了赋值,编译器对其优化了
    static final int c = 20;
    // 在准备阶段,"hello" 也属于ConstantValue,static+final 修饰的字符串常量 在准备阶段就完成了赋值,编译器对其优化
    static final String d = "hello";
    // 在准备阶段,如果 static 变量是final的,但属于引用类型,那么赋值也会在初始化阶段完成
    static final Object e = new Object();
}

上述代码,通过:javap -v T02_ClassLoad_Final_Static.class进行反编译,得到如下字节码。


2.3 解析:将常量池中的符号引用解析为直接引用。

将常量池中的符号引用解析为直接引用。符号引用仅仅只是符号,并不知道类、方法、属性到底在内存的哪个位置;而直接引用就可以确切知道类、方法、属性在内存的中位置。

代码示例如下:

// 解析的含义
public class T03_ClassLoad_Resolved {
    public static void main(String[] args) throws ClassNotFoundException, IOException {
        ClassLoader classLoader = T03_ClassLoad_Resolved.class.getClassLoader();
        // 可以通过 HSDB工具进行查看:java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
        // loadClass 方法不会导致类的解析和初始化: JVM_CONSTANT_UnresolvedClass com.jvm.t09_class_load.D
        // D类没有被加载、解析
        Class<?> c = classLoader.loadClass("com.jvm.t09_class_load.C");

        // 下述会导致C类/D类的加载,JVM_CONSTANT_Class  com.jvm.t09_class_load.D
        // new C();
        System.in.read();
    }
}
// 类C
class C {
    D d = new D();
}
// 类D
class D {
    
}

3、初始化

<cinit>()v 方法,初始化是类加载的最后一个阶段

初始化即调用 <cinit>()v,虚拟机会保证这个类的 【构造方法】的线程安全。

发行的时机

概括的说,类初始化是【懒惰的】,下面是会导致类初始化的情况

  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

不会导致类初始化的情况

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
  • 类对象 .class 不会触发初始化,因为 .class 在加载阶段就已经生成,所以不会被触发
  • 创建该类的数组不会触发初始化
  • 类加载器的 loadClass 方法
  • Class.forName 的参数 2 initialize为 false 时
public class T04_ClassLoad_Init {
    static {
        // main 方法所在的类,总会被首先初始化
        System.out.println("main init");
    }

    public static void main(String[] args) throws ClassNotFoundException {
        // 不会导致类初始化的情况
        // 1、静态常量不会触发初始化
        System.out.println(B.b);
        // 2、类对象.class 不会触发初始化
        System.out.println(B.class);
        // 3、创建该类的数组不会触发初始化
        System.out.println(new B[0]);
        // 4、不会初始化类 B,但会加载 B、A
        ClassLoader c1 = Thread.currentThread().getContextClassLoader();
        c1.loadClass("com.jvm.t09_class_load.B");
        // 5、不会初始化类 B,但会加载 B、A
        ClassLoader c2 = Thread.currentThread().getContextClassLoader();
        Class.forName("com.jvm.t09_class_load.B", false, c2);

        // 导致类初始化的情况
        // 1、首次访问这个类的静态变量或静态方法时
        System.out.println(A.a);
        // 2、子类初始化,如果父类还没有初始化,会引发
        System.out.println(B.c);
        // 3、子类访问父类静态变量,只触发父类初始化
        System.out.println(B.a);
        // 4、会初始化类 B,并先初始化类 A
        Class.forName("com.jvm.t09_class_load.B");
    }
}

class A {
    static int a = 0;
    static {
        System.out.println("a init");
    }
}

class B extends A {
    final static double b = 5.0;
    static boolean c = false;
    static {
        System.out.println("b init");
    }
}

4、类加载常见问题-练习

从字节码分析,使用a,   b,  c  这三个常量是否导致致 E 初始化

// 类加载分析 - 练习
public class T05_ClassLoad_Practice {
    public static void main(String[] args) {
        System.out.println(E.a);
        System.out.println(E.b);
        System.out.println(E.c);
    }
}

class E {
    // a, b 是静态常量,一个是整型,一个字符串常量。是在类链接的准备阶段就已经完成了赋值
    public static final int a = 10;
    public static final String b = "hello";
    // c 是包装类型,20进行装箱,c 是在初始化阶段完成赋值
    public static final Integer c = 20; // Integer.valueOf(20)
    static {
        System.out.println("init E");
    }
}

5、类加载常见问题-练习2

典型应用 - 完成懒惰初始化单例模式

// 类加载分析 - 练习2
public class T05_ClassLoad_PracticeV2 {
    public static void main(String[] args) {
        //Singleton.test(); // 调用静态方法test,不会触发类的初始化
        Singleton.getInstance(); // 
    }
}

class Singleton {
    public static void test() {
        System.out.println("test");
    }

    private Singleton() {}

    // 静态内部类,好处就是可以访问外部类的资源:构造方法,虽然构造方法是私有的,但在静态内部类还是可以访问的
    // 内部类中保证单例
    private static class LazyHolder {
        private static final Singleton SINGLETON = new Singleton();
        static {
            System.out.println("lazy holder init");
        }
    }

    // 第一次调用,getInstance 方法,才会导致内部类加载和初始化其静态成员
    public static Singleton getInstance() {
        return LazyHolder.SINGLETON;
    }
}

以上实现的特点是:

  1. 懒惰实例化
  2. 初始化时的线程安全是有保障的

 


文章最后,给大家推荐一些受欢迎的技术博客链接

  1. Hadoop相关技术博客链接
  2. Spark 核心技术链接
  3. JAVA相关的深度技术博客链接
  4. 超全干货--Flink思维导图,花了3周左右编写、校对
  5. 深入JAVA 的JVM核心原理解决线上各种故障【附案例】
  6. 请谈谈你对volatile的理解?--最近小李子与面试官的一场“硬核较量”
  7. 聊聊RPC通信,经常被问到的一道面试题。源码+笔记,包懂

 


欢迎扫描下方的二维码或 搜索 公众号“10点进修”,我们会有更多、且及时的资料推送给您,欢迎多多交流!

                                           

       

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不埋雷的探长

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值