反射 p3 类加载

发布时间 2023-07-25 18:19:57作者: 凉白茶

类加载

基本说明

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

  1. 静态加载:编译时加载相关的类,如果没有则报错,依赖性太强。
  2. 动态加载:运行时加载相关的类,如果运行时不用该类,即使不存在该类,也不会报错,降低了依赖性。
  • 代码演示:

    import java.util.*;
    import java.lang.reflect.*;
    
    public class ClassLoad_{
    	public static void main(String[] args) throws Exception{
    		Scanner scan = new Scanner(System.in);
    		String key = scan.next();
    
    		switch(key){
    			case "1":
    				Dog dog = new Dog();//静态加载,依赖性很强
    				dog.cry();
    				break;
    			case "2":
    				//反射 -> 动态加载
    				Class cls = Class.forName("Person");//加载Person类[动态加载]
    				Object o = cls.newInstance();
    				Method m = cls.getMethod("hi");
    				m.invoke(o);
    				System.out.println("OK");
    				break;
    			default:
    				System.out.println("do nothing....");
    		}
    
    	}
    }
    
    
    /* 未写Dog类时,DOS窗口运行结果,编译错误
    C:\Users\86199\Desktop>javac ClassLoad_.java
    ClassLoad_.java:11: 错误: 找不到符号
                                    Dog dog = new Dog();//静态加载,依赖性很强
                                    ^
      符号:   类 Dog
      位置: 类 ClassLoad_
    ClassLoad_.java:11: 错误: 找不到符号
                                    Dog dog = new Dog();//静态加载,依赖性很强
                                                  ^
      符号:   类 Dog
      位置: 类 ClassLoad_
    注: ClassLoad_.java使用了未经检查或不安全的操作。
    注: 有关详细信息, 请使用 -Xlint:unchecked 重新编译。
    2 个错误
    */
    
    
    //因为 new Dog() 是静态加载,因此必须编写Dog类,否则在编译过程就会报错
    //Person类是动态加载,所以没有编写Person类在编译过程也不会报错,
    //只有在动态加载该类时才会报错
    
    class Dog{
    	public void cry(){
    		System.out.println("小狗汪汪叫");
    	}
    }
    
    
    /* 未写Person类时,DOS窗口运行结果,运行时报错
    C:\Users\86199\Desktop>java ClassLoad_
    1
    小狗汪汪叫
    ===============================================================
    C:\Users\86199\Desktop>java ClassLoad_
    2
    Exception in thread "main" java.lang.ClassNotFoundException: Person
            at java.net.URLClassLoader.findClass(Unknown Source)
            at java.lang.ClassLoader.loadClass(Unknown Source)
            at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
            at java.lang.ClassLoader.loadClass(Unknown Source)
            at java.lang.Class.forName0(Native Method)
            at java.lang.Class.forName(Unknown Source)
            at ClassLoad_.main(ClassLoad_.java:16)
    */
    
    
    class Person{
    	public void hi(){
    		System.out.println("hi....");
    	}
    }
    
    /* 写了Peson类后,DOS窗口运行结果
    C:\Users\86199\Desktop>java ClassLoad_
    1
    小狗汪汪叫
    =================================================
    C:\Users\86199\Desktop>java ClassLoad_
    2
    hi....
    OK
    */
    

类加载时机

  1. 当创建对象时(new),静态加载;
  2. 当子类被加载时,父类也加载,静态加载;
  3. 调用类中的静态成员时,静态加载;
  4. 通过反射,动态加载;

类加载流程

类加载流程图

  • 类加载过程图

  • 类加载各阶段完成的任务

类加载五个阶段

  1. 加载阶段(Loading)

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

  2. 连接阶段(Linking)

    1. 验证(Verification):

      • 目的:是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全;
      • 包括:文件格式验证(是否以魔数 oxcafebabe 开头)、元数据验证、字节码验证和符号引用验证;
      • 可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,缩短虚拟机加载的时间;
    2. 准备(Preparation):

      JVM 会在该阶段对静态变量,分配内存并默认初始化(对应数据类型的默认初始值,如0、0L、null、false等)。这些变量所使用的内存都将在方法区中进行分配。

      package com.hspedu.classload_;
      
      /**
       * @author: 86199
       * @date: 2023/6/2 21:19
       * @description: 说明类加载夹的连接阶段(Linking)-准备(Preparation)
       */
      public class ClassLoad02 {
      }
      
      class A{
          //属性-字段-成员变量
          //分析类加载阶段的连接阶段(Linking)-准备,属性是如何处理的
          //1. n1是实例属性,不是静态变量,因此在准备阶段是不会分配内存的
          //2. n2是静态属性,分配内存,n2 默认初始化为0,而不是20,20是在连接阶段之后的初始化阶段进行的
          //3. n3是static final 是常量
          public int n1 = 1;
          public static int n2 = 2;
          public final static int n3 = 3;
      }
      
    3. 解析(Resolution):虚拟机将常量池内的符号引用替换为直接引用的过程。

  3. 初始化(Initialization):

    • 到初始化阶段,才是真正的开始执行类中定义的Java程序代码,此阶段是执行()方法的过程;

    • ()方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,并进行合并;

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

      源码:

    案例演示:

    package com.hspedu.classload_;
    
    /**
     * @author: 86199
     * @date: 2023/6/2 22:19
     * @description: 演示类加载——初始化阶段(Initialization)
     */
    public class ClassLoad03_ {
        static {
            System.out.println("ClassLoad03_ 静态代码块被执行");
        }
        public static void main(String[] args) throws ClassNotFoundException {
            //1. Loading:加载B类,并生成B类的Class对象
            //2. Linking:num = 0
            //3. Initialization:
            //      依次收集类中所有的静态变量和静态代码块中的语句入clinit()中,并合并
            /*
                clinit(){
                    System.out.println("B 静态代码块被执行");
                    //num = 300;
                    num = 100;
                }
                合并:num = 300, num = 100  --> num = 100
             */
    //        new B();//也会使B类加载,但是B类加载只会执行一遍
    //        System.out.println(B.num);//直接使用B类的静态属性,也会导致B类加载
    
            //看看类加载时,是有同步机制控制的
            /*
                protected Class<?> loadClass(String name, boolean resolve)
                    throws ClassNotFoundException
                {
                    synchronized (getClassLoadingLock(name)) {
                    //拿到一个同步锁,正因为有这个机制,才能保证某个类在内存中只有一份Class对象
                    }
                }
             */
            Class<?> aClass = Class.forName("B");
        }
    }
    
    class B{
        static {
            System.out.println("B 静态代码块被执行");
            num = 300;
        }
    
        static int num = 100;
    
        public B() {//构造器
            System.out.println("B() 构造器被执行...");
        }
    }