Java-Day-35( 类加载 + 细化解释各阶段 )

发布时间 2023-07-24 21:51:04作者: 朱呀朱~

Java-Day-35

类加载

基本说明

反射机制是 java 实现动态语言的关键,也就是通过反射实现类动态加载

  • 静态加载:编译时加载相关的类,如果没有则报错,依赖性太强

    • 在非具备 idea 辅助型的工具里编写代码时
    import java.util.Scanner;
    public class test {
        public static void main(String[] args) throws ClassNotFoundException {
            Scanner scanner = new Scanner(System.in);
            String key = scanner.next();
            switch (key) {
                case "1":
                    Dog dog = new Dog(); // 静态加载类
                    dog.hello();
                    break;
                case "2":
                    System.out.println("输入的是2");
                    break;
                default:
                    System.out.println("啥也不是");
            }
        }
    }
    
    • 上述代码中,于代码文件 ( ClassLoad_zyz.java ) 所在目录 cmd 命令行中输入 javac ClassLoad_zyz.java 进行编译的话会报错
    • 因为:虽然不输入 "1" 的话是不会需要 Dog 类的使用创建的,但是由于是静态加载类,所以不导包的话,就算不需要也会在编译时的代码检查时报错
  • 动态加载:运行时加载需要的类,如果运行时不用该类,即使该类不存在也不会报错,降低了依赖性

    • 将上述代码的 case 2 进行修改
    case "2":
    	System.out.println("输入的是2");
    	Class cls = Class.forName("Person"); // 加载Person类,
    //                 但是在编译时不会加载,只有在代码执行、运行到这里的时候才会加载
    	Object o = cls.newInstance();
        Method m = cls.getMethod("hi"); // import java.lang.reflect.*;
    	m.invoke(o);
    	break;
    
    • 只导入 Dog 的包,没导入 Person 类的包的话,javac 编译是不会报错的,会在此目录生成 .class 文件,只有在 java ClassLoad_zyz 后输入 "2" 时,动态加载 Person 类的时候才会报错
    • 原因:此处是反射的方式,反射是动态加载,不执行到 forName() 就不会报错

类加载时机

  • 当创建对象时 ( new ) —— 静态加载
  • 当子类被加载时,父类也加载 —— 静态加载
  • 调用类中的静态成员时 —— 静态加载
  • 通过反射 —— 动态加载

过程图

image-20230718113404946

类加载三大阶段

  • 加载、连接、初始化

image-20230718150800992

  • 验证 —— 文件安全进行校验

  • 准备 —— 对静态变量进行默认的初始化并分配空间

  • 解析阶段 —— 符号引用转为字节引用

    • 连接完成后 —— 合并到 JRE 运行环境中,此时就是一种可运行的状态了
  • 前两个阶段 ( 加载和连接 ) 是 JVM 来控制的,只有初始化阶段才是认为可以指定的,如静态代码块、静态变量初始化等是程序员自己可以控制的代码了

    • 注意,是加载阶段的初始化,不是 new,此处主要针对静态资源 static

细化解释各阶段

加载阶段

  • JVM 在该阶段的主要目的是将字节码从不同的数据源 ( 可能是 class 文件、也可能是 jar 包,甚至网络 ) 转化为二进制字节流加载到内存中,并生成一个代表该类的 java.lang.Class 对象
    • 就是在先前讲过的 Class 类阶段的 —— 在方法区内加载二进制数据与堆内生成 Class 类对象

连接阶段 - 验证

  • 目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全

  • 包括:文件格式验证 ( 是否以魔数 oxcafebabe 开头 )、元数据验证、字节码验证和符号引用验证

    • javac 编译后的 .class 文件内容开头八位数就是 cafe babe
    • 追加载类源码:
    loadClass(String name) ——> loadClass(String var1, boolean var2) 内含SecurityManager就是验证
    
  • 可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,缩短虚拟机类加载的时间

连接阶段 - 准备

  • JVM 会在该阶段对静态变量分配内存并默认初始化 ( 对应数据类型的默认初始值,如:0、0L、null、false 等 )。这些变量所使用的内存都将在方法区中进行分配
//        属性 — 成员变量 - 字段
public int n1 = 10; // 实例属性,不是静态变量,因此准备阶段不会分配内存
public static int n2 = 20; // 静态变量,分配内存,默认初始化为0 (加载的初始化阶段才会真正执行此代码赋为20)
public static final int n3 = 30; // 是 static final 是常量,和静态变量不同,因为一旦赋值就不会再变了,所以此时就直接为30了

连接阶段 - 解析

  • 虚拟机将常量池内的符号引用替换为直接引用的过程
  • 到此步类都还没真正分配空间到内存中,所以类之间若有关系也只能是符号引用,记住了表面的相应的关系,只有真正用到加载完成类的时候才会由虚转实

初始化阶段

  • 到初始化阶段才真正开始执行类中定义的 Java 程序代码 —— 程序员可以控制了,此阶段是执行 < clinit >() 方法的过程
  • < clinit >() 方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,并进行合并
    • 注意:还是静态部分的
  • 虚拟机会保证一个类的 < clinit >() 方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 < clinit >() 方法,其他线程都需要阻塞等待,直到活动线程执行 < clinit >() 方法完毕
    • 类加载源码:synchronized (getClassLoadingLock(name)) { } 此机制的保证,使得某个类在内存中只有一份 Class 对象
public class test {
    public static void main(String[] args) {
//        1. 加载B类,生成B的class对象
//        2. 连接 num=0
//        3. 初始化阶段
//              自动先后顺序地收集(clinit方法)
        /*
            clinit(){
                System.out.println("静态代码块");
                // num = 300; 合并时此句就没有意义了
                num = 100;
            }
            最后合并后 num 为 100
         */
        
//        4. 如果 new 了的话,就比正常步骤多一步构造器的执行,输出时会在中间加一句:B()构造器
//        new B(); // 不加这一句的话就是只输出:静态代码块 100
        
        System.out.println(B.num); // 直接使用类的静态属性,也会导致类的加载 1.2.3.
//        静态代码块
//        100
    }
}

class B {
    static {
        System.out.println("静态代码块");
        num = 300;
    }
    static int num = 100;
    public B() {
        System.out.println("B()构造器");
    }
}