面试经常问的Class加载过程详解

Class的装载流程

  1. 加载

    1. 获取二进制流(本地或者网络)
    2. 静态存储结构(class转换成的二进制流)转化为方法区的运行时数据结构
    3. 在Java堆里里面生成一个类对象,作为方法区的访问入口。
  2. 链接

  3. 验证:保证输入的输入的字节流能正确的存储在方法区中。

    • 验证class文件的标志(魔数)和class文件的版本号(JDK的版本号)

      RPC远程通讯的过程中,每次传输二进制流的时候也会使用魔数标注是正确的格式

    • 验证常量池的数据结构和字段表、方法表(class文件结构)

    • 元数据验证:父类验证、继承验证、final的验证(final修饰的数据是否被修改,子类是否集成final修饰的父类)现在编译器就会进行校验报错、抽象类、接口的验证

    • 符号引用的验证:类、方法、字段的访问性(private、public、protected、default)是否可以被当前类根据全限定名找到对应的数据并访问。

      找不到的话就会抛异常:IllegalAccessError(访问权限不对)、NoSuchFieldError、NoSuchMethodError…框架中bean不存在没有创建成功经常抛出这样的异常

  4. 准备:为类变量分配内存(方法区中)并设置类变量初始值。

    public static int value = 123;
    
    1. 通常情况:在准备阶段,value=0;把value赋值为123的JVM指令则是存放在clinit()中。
    public static final int value = 123;
    
    1. 特殊情况:value = 123;被final修饰的静态变量(可以直接放入到常量池中)在准备阶段直接被赋值为指定值。

      public static final Object value = new Object();
      

      则不会在准备阶段直接赋值为指定值

  5. 解析:将常量池内的符号引用(代码中定义的引用变量字符串)替换为直接引用(实际对象位置的指针)的过程。

    在Java运行时才能知道的实例对象的引用,在解析的时候并不会进行解析

    1. 类或接口的解析:

      public class ClassAnalysis {
          private D d = new D();
          private Integer[] i = new Integer[10];
      }
      
      class D {
      }
      

      符号引用d解析:JVM会把代表d的全限定名传递给ClassAnalysis的ClassLoader去加载类D。重新触发上诉步骤。

      符号引用i解析:元素类型为对象,i的描述符为[Ljava/lang/Integer,按照d一样解析,并且JVM会生成一个代表此数组维度和元素的数组对象。

    2. 字段解析:

      JVM会从字段表中解析出该字段所属的类or接口的符号引用C。

      1. 如果C本身就包含该字段,返回该字段的直接引用,结束。
      2. 否则C中实现了接口,按照继承关系从下往上递归搜索各个接口和它的父接口,找到就返回。
      3. 否则C(排除Object),按照继承关系从下往上递归搜索其父类,找到就返回。
      4. 否则查找失败,抛出NoSuchFieldError

      当一个同名字的字段同时出现在一个类(ClassAnalysis)的接口和父类中,编译器将拒绝编译

      代码示例:

      public class ClassAnalysis {
          private static D d = new D();
          private Integer[] i = new Integer[10];
      
          public static void main(String[] args) {
              System.out.println(d.temp);
          }
      }
      class A {
          public static int temp = 3;
      }
      interface B {
          int temp = 4;
      }
      interface C extends B {
          int temp = 4;
      }
      class D extends A implements C {
          // public static int temp = 1;
      }
      

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s09oRAvn-1574769908673)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20191126165251222.png)]

    3. 类方法解析

      和字段解析一样,找到该方法所属的类or接口的符号引用C

      1. 由于类方法和接口方法符号引用的常量类型定义是不一样的1。如果在类方法表2中发现C是个接口,则直接抛出IncompatiableClassChangeError
      2. 在本类C中查找是否有相匹配的方法,找到返回这个方法的直接引用。
      3. 在C的父类中递归查找,找到返回。
      4. 在C实现的接口列表以及他们的父接口中递归查找,找到返回。
      5. 否则查询失败,NoSuchMethodError
    4. 接口方法解析

      和类方法解析差不多

    在完成这些解析的时候,JVM还需要在完成之前进行符号引用验证(访问权限)。接口方法解析除外

  6. 初始化(执行类构造方法—>clinit()1

在准备阶段变量已经赋值一次系统初始值了,在初始化阶段,则通过程序代码去初始化类变量和其他资源。详解见clinit()和init()的区别

  1. 使用

  2. 卸载

    由JVM自带的类加载器所加载的类,在JVM的生命周期中,始终不会被卸载。JVM会始终引用这些类加载器,而这些类加载器则始终引用它们所加载的类的Class对象 。

用户自定义的类加载器加载的类是可以被卸载的。思考:Spring的bean在初始化的就会被全部加载?不担心被卸载吗?

Full GC,Class对象的实例对象的GCRoots引用不存在的时候,就会被标记清除,把该Class对象在方法区存储的二进制数据进行卸载,相对应的ClassLoader被卸载。

2.clinit()与init()的区别

明确一点万物皆对象即可

  1. clinit()是类对象的构造函数(在类中没有静态时可以没有),init()类的对象的构造函数(必须有)
  2. clinit()是JVM自动收集静态变量静态代码块(从上到下的顺序)组成的构造方法(也可以自定义参与)。
  3. init()是通过Class对象new Instance()创建实例对象的构造方法。
  4. clinit()执行的时候会首先保证父类的clinit()先执行完毕。由于接口中不能使用静态语句块,但仍然有静态变量初始化的赋值操作,因此接口与类一样都会生成clinit()方法。 但接口与类不同的是,执行接口的clinit()方法不需要先执行父接口的clinit()方法。 只有当父接口中定义的变量使用时,父接口才会初始化。 另外,接口的实现类在初始化时也一样不会执行接口的clinit()方法。
  5. JVM会保证一个类对象的初始化在多线程的环境中被正确的加锁。多个线程同时去初始化一个类的时候只有一个线程去执行,其他线程阻塞。当执行线程结束的唤醒其他线程,其他线程不会再次进入到clinit()。同一个类加载的情况下,一个类对象只能被初始化一次。一个对象的equals比较的前提下是:该对象的类对象是被同一个ClassLoader加载

接口中的静态变量都被默认为final修饰的,不允许改变。普通类的静态变量则不是。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iiPeesSz-1574769908674)(C:\Users\Administrator\Desktop\Java优化\mackdown\图片\interface.png)]

3.Class初始化六大时机—>只执行clinit()

  1. 创建类的实例(new xxxClass()|Class.newInstance()|constructor.newInstance());

    反射使用Class.newInstance();必须保证这个类已经加载和链接了;只能调用无参构造方法,且是public修饰的。

    **constructor.newInstance();**根据传入的参数,调用任意构造构造函数,也可以调用私有的构造。推荐使用。

    new 关键字,这个类可以没有被加载;使用任意构造方法。

    都new实例对象了,肯定执行了clinit()

  2. 访问类中的某个静态变量或者对静态变量进行赋值;只执行了clinit()

  3. 调用类的静态方法;只执行了clinit()

  4. Class.forName(“绝对路径”); 只执行了clinit()

  5. 完成子类的初始化,也会完成父类的初始化; 只执行了clinit()

  6. 该类是程序引入口(main或者test入口); 只执行了clinit()

    当访问的静态变量是被final修饰的并且是可以直接放入到常量池中,是不会触发类的初始化

    class Father {
        public static String strFather = "HelloJVM_Father";
    
        static {
            System.out.println("Father静态代码块");
            System.out.println(strFather);
        }
        
        public Father() {
            System.out.println("Father代码块");
        }
    }
    
    class Son extends Father {
        // 1.虽然被final修饰,但是调用了Test的静态属性a,触发了初始化
        public final static int test = Test.a;
    
        // 2.final修饰的静态变量为:固定的基础变量(可以直接放入到常量池)才不会触发类的初始化
        public final static String strSon = "1231";
    
        static {
            System.out.println("Son静态代码块");
        }
    }
    
    class Test {
        public static int a = 6;
        public int b;
    
    }
    
    public class InitativeUseTest2 {
        public static void main(String[] args) {
            //1.System.out.println(Son.test);
            //2.System.out.println(Son.strSon);
        }
    }
    
    

    1.运行结果:

    Father静态代码块
    HelloJVM_Father
    Son静态代码块
    6
    

    2.运行结果:

    1231
    

4.获取Class对象的四种方式

  1. Class clazz1 = xxxClass.class; //没有完成初始化过程

  2. Class clazz1 = instanceObj.getClass(); //对象都存在了,完成了初始化过程

  3. Class clazz1 = xxxClassLoader.loadClass(“全路径包名”); //没有完成初始化过程

  4. Class clazz1 = Class.forName(“全路径包名”); //完成初始化过程


  1. 接口方法会被额外标识,需要其实现类去覆盖重写。 ↩︎ ↩︎

  2. 类方法表详解可以百度查看,挣钱的时候也不会问 ↩︎

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值