类加载过程整理

类加载的流程分7个阶段

分别是:加载-->验证-->准备-->解析-->初始化-->使用-->卸载

其中:加载-->验证-->准备,初始化,卸载这5个的加载顺序不能变

类的加载

  1. 通过类的全限定名获取类的二进制字节流
    1. 获取字节流可以从jar、zip、war等文件
    2. 网络中获取
    3. 运行时计算生成,如动态代理
    4. 其他文件生成,如jsp
    5. 数据库读取,可以程序安装到数据库实现代码在集群的分发
  2. 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.long.Class对象,放在方法区中,作为程序访问这些类型数据的外部入口,c

数组比较特殊:数组不是类加载器创建的,是虚拟机创建的

验证

验证的主要目的是保证class文件中的字节流信息符合虚拟机要求,验证包含以下几点

1.文件格式验证,验证通过后才会在方法区存储

  • 是否魔数0XCAFEBABE开头:魔数是class文件的标志,是一个固定值: 0XCAFEBABE 。 他是判断一个文件是不是class格式的文件的标准, 如果开头四个字节不是0XCAFEBABE,那么它不是class文件, 不能被JVM识别。
  • 主次版本号是否支持:紧接着魔数的四个字节是class文件的次版本号和主版本号,当前虚拟机版本是否支持
  • 常量类型是不是被支持,以及闻不见是否被附加或被删除的其他信息等

2.元数据验证:主要是语义验证,保证语义信息符合java语言的规范,这种一般在编辑器可以识别

  • 类是否有父类
  • 类是否继承了不允许继承的类,重写了不允许重写的方法等
  • 非抽象类是否实现了父类或接口要求实现的全部方法等等

3.字节码验证:主要通过对数据流和控制流的分析,确定语义合法,符合逻辑

  • 任何时刻的操作数栈的数据类型和指令代码匹配,不能出现栈中放入int类型,使用是按long类型载入
  • 指令跳转不会到方法体意外
  • 方法体内的类型转换有效等,如子类赋值给父类安全,父类赋值给之类不允许

4.符号引用验证:发生在解析阶段,是对类自身以外的信息进行匹配性校验

  • 类引用的类能找到对应的类
  • 引用的类方法是都在引用类中存在
  • 访问权限是否合法等

准备

准备阶段:在方法区中为类变量(static修饰的变量)分配内存,并设置初始值

  • 初始化:static初始值不是变量的复制,例如static int value = 123,初始值是0;不是123,赋值123的putstatic指令编译后,存放于类构造器<clinit>()方法中,所以赋值为123是在初始化阶段执行
  • 对于final常量会直接赋值123(字段存在CobstantValue属性都会初始化为指定的值)

解析

将常量池中的符号引用转变为直接引用

  • 符号引用:引用以一组符号来描述
  • 直接引用:直接指向目标的指针、偏移量等能定位到目标的句柄

解析动作主要针对,类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符等

  • 类或接口解析:查找对应类的全限定名,并交给引用类的加载器加载,可能触发一系列加载动作
  • 字段:先进行类解析,然后在找对应类变量,有就返回,没有就去找父类接口,直到找到再返回,如果都没有找到,抛出notSuchFieldError异常
  • 类方法解析:先进行类解析,找到类后才能继续
    • 引用方法时发现是接口,报错IncompatibleClassChange
    • 在类中找到方法返回,没找到,则递归去父类中找,直到找到为止,都没有找到报错NoSuchMethodError
  • 接口方法:先解析接口引用,成功继续
    • 发现是类,抛出异常
    • 现在接口中找,有就直接返回,没有递归父接口查找,能找到就返回结束,没有找到报错

初始

初始化是类加载的最后一步

类初始化是执行类构造器<clinit>()的过程,

  • <clinit>()方法是编辑器自动收集类中的所有类变量赋值动作和静态语句块合并生成的,执行顺序和代码中定义的顺序相同,因此静态变量只能访问定义在前面的静态变量
  • 执行类的<clinit>()方法会之前先执行父类的<clinit>()方法,因此父类中的静态语句块先于子类之行
  • <clinit>()方法不是必须的,如果类或接口没有静态变量和静态语句块,可以没有
  • 接口中不能使用静态语句块,但是有变量初始化赋值操作,接口和类一样生成<clinit>()方法,但是接口不需要先执行父<clinit>()方法,只有当父接口中定义的变量使用时,父接口才会初始化
  • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁,同步:即当多个线程去初始化一个类时,只会有一个线程执行<clinit>()方法
  • 虚拟规定,5种情况必须对类继续进行初始化,也可以概括为,对一个类的主动引用时会触发初始化
    1. 遇到 new(创建对象) 、getstatic(获取静态变量)、putstatic(设置一个类的静态字段)、invokestatic(调用静态方法)指令时必须初始化
    2. java.lang.reflect包方法对类进行反射调用时,如果类没有初始化,先进行初始化
    3. 初始化类时,父类还没有初始化,先初始化父类
    4. 虚拟机启动指定执行的主类,虚拟机会先初始化主类(main方法的类)
    5. 实例解析的结果包含REF_getstatic、REF_putStativ、REF_invokeStatic的方法句柄,如果方法对应的类没有初始化,先进行初始化
  • 被动引用不会触发初始化
    1. 数组定义,如果定义class Super;定义Super[] supers = new Supers[10],不会触发
    2. 静态变量调用会初始化定义静态变量的类:通过子类名直接调用父类的静态变量,父类会初始化,子类不一定会出初始化
      例如
      class Super{
      static{
          System.out.print("Super class init")
      }
      public static  value = "imstatic"
      }
      
      class Son extends Supper{
          static{
              System.out.print("Son class init")
          }
      }
      
      class TestMian{
          public static void main(String[] args){
              System.out.print(Son.value)
          }
      }
      
      输出结果:Super class init  没有Son class init

       

    3. 常量引用不会初始化类
      例如
      class Test01{
          static{
              System.out.print("Test01 class init");
          }
      public static final String HELLO = "hello world";
      }
      
      class Testmain{
          public static void main(String[] args){
               System.out.print(Test01.HELLO );
          }
      }
      不会输出:Test01 class init
      因为 常量HEllO在编译阶段通过常量优化,将常量的值存储到NotInitialization类的常量池中,以后Test01.HELLO的引用实际转化为 NotInitialization类对自身常量池的引用

       

 

<think>嗯,用户想了解Java的类加载过程。我记得类加载是Java虚拟机加载类的重要步骤,但具体细节可能需要回忆一下。首先,类加载过程应该有几个阶段吧?比如加载、验证、准备、解析、初始化这些。但我不太确定每个阶段的具体内容,需要仔细理清楚。 首先,加载阶段,是不是把.class文件加载到内存里,生成Class对象?这时候类加载器起作用,可能有不同的类加载器,比如启动类加载器、扩展类加载器、应用类加载器,还有自定义的。用户可能需要知道双亲委派机制,但问题可能集中在过程本身,而不是加载器结构。不过可能需要简要提及。 接下来是验证,这部分是确保字节码符合规范,防止恶意代码。比如文件格式验证,元数据验证,字节码验证,符号引用验证。这些步骤可能属于验证阶段,但具体顺序是否正确呢? 然后是准备阶段,这里为类变量分配内存并设置初始值。比如static变量会被赋予默认值,而不是代码中的赋值。比如int类型初始化为0,而不是用户定义的123。这时候需要注意初始化和准备阶段的区别,初始化阶段才会执行代码中的赋值。 解析阶段是把符号引用转为直接引用。符号引用可能指的是类、方法、字段的名称,而直接引用是具体的内存地址。这个过程可能在初始化之前或之后进行,取决于虚拟机的实现,比如有些解析可能延迟到使用的时候。 初始化阶段是执行类构造器<clinit>方法,包括静态变量赋值和静态代码块。这里要确保多线程环境下的正确执行,比如加锁。这时候才会把static变量设置为用户指定的值。需要强调初始化是类加载的最后一步,这时候类才完全可用。 可能用户还关心触发初始化的条件,比如new实例、访问静态变量或方法、反射调用等。这些情况会触发类的初始化,但要注意被动引用不会触发,比如子类引用父类的静态字段,不会导致子类初始化。 另外,类加载过程中的各个阶段顺序是否严格?比如加载、验证、准备、初始化的顺序是固定的,但解析可能在初始化之后,这是否正确?比如在Java虚拟机规范中,解析阶段可以在初始化之前或之后,具体视情况而定。 需要检查是否有遗漏的阶段,或者是否有错误的地方。比如卸载阶段属于类生命周期的结束,但通常类加载过程指的是加载到初始化,所以用户的问题可能不涉及卸载。 总结一下,回答应该分阶段介绍加载、验证、准备、解析、初始化,每个阶段的作用,可能涉及类加载器的角色,双亲委派机制,以及各阶段的例子,比如准备阶段的默认值,初始化的赋值。同时需要提到触发初始化的条件和被动引用的情况,避免混淆。确保术语正确,比如<clinit>和<init>的区别,前者是类构造器,后者是实例构造器。 可能用户是Java初学者,需要简明扼要的解释,所以结构要清晰,步骤明确,用例子辅助说明。比如准备阶段的例子,说明初始值和用户赋值的区别。或者解析阶段的符号引用转为直接引用的例子,比如方法调用时的解析。 另外,可能需要注意类加载过程的异常处理,比如ClassNotFoundException,NoClassDefFoundError,这些异常出现的位置和原因,但可能不属于过程本身,所以可以简单提及或者略过。 最后,确认是否要提到自定义类加载器的可能性,但问题主要是关于过程,所以可能不需要深入,但可以提到类加载器的作用。比如双亲委派模型如何影响类加载过程,确保安全性,避免重复加载等。 总之,整理出各个阶段,每个阶段的功能,顺序,示例,以及相关的注意事项,如触发条件、被动引用等。确保回答准确且易于理解,避免过于技术性的术语,但需要正确使用术语。</think>Java 的类加载过程是 JVM 将 `.class` 文件加载到内存,并转换为 `Class` 对象的过程。这一过程分为以下阶段,按严格顺序执行: --- ### 1. **加载(Loading)** - **作用**:查找并加载类的二进制字节流(如 `.class` 文件)。 - **关键点**: - 通过类全限定名获取二进制数据(可从文件、网络、JAR 包等来源)。 - 生成对应的 `java.lang.Class` 对象,作为方法区的访问入口。 - **类加载器**负责此过程(如启动类加载器、应用类加载器等)。 --- ### 2. **验证(Verification)** - **作用**:确保字节码合法且符合 JVM 规范,防止安全漏洞。 - **步骤**: 1. **文件格式验证**:检查魔数、版本号等是否符合规范。 2. **元数据验证**:校验语义(如是否继承 final 类)。 3. **字节码验证**:确保方法逻辑合法(如类型转换正确)。 4. **符号引用验证**:确认引用的类、方法、字段是否存在。 --- ### 3. **准备(Preparation)** - **作用**:为类变量(static 变量)分配内存并设置初始值。 - **示例**: ```java public static int value = 123; ``` - 准备阶段会为 `value` 分配内存并赋默认值 `0`,而非 `123`。 - **例外**:若字段被 `final` 修饰(如 `static final`),则直接赋代码中的值。 --- ### 4. **解析(Resolution)** - **作用**:将常量池中的**符号引用**替换为**直接引用**(内存地址)。 - **符号引用**:以文本形式描述引用的目标(如 `java/lang/Object`)。 - **直接引用**:指向目标在内存中的指针或偏移量。 - **延迟解析**:某些解析可能发生在代码首次执行时(如方法调用)。 --- ### 5. **初始化(Initialization)** - **作用**:执行类构造器 `<clinit>()` 方法,完成静态变量赋值和静态代码块。 - **关键点**: - `<clinit>` 由编译器自动生成,合并所有 `static` 变量赋值和静态代码块。 - JVM 保证初始化过程在多线程下被正确加锁(线程安全)。 - **触发条件**: - `new` 实例化对象、访问静态变量/方法。 - 反射调用(如 `Class.forName()`)。 - 主类(包含 `main()` 的类)会自动初始化。 --- ### 6. **使用与卸载(Usage & Unloading)** - **使用**:类完成加载后,可通过 `Class` 对象创建实例、调用方法等。 - **卸载**:当类不再被引用且满足条件时,JVM 会回收其内存(由垃圾回收机制管理)。 --- ### 🌰 示例与注意事项 - **被动引用**:某些情况不会触发初始化: ```java class Parent { static int value = 1; } class Child extends Parent {} // 访问 Child.value 只会初始化 Parent,不会初始化 Child ``` - **类加载器双亲委派**:加载类时优先委派父加载器,确保核心类库的安全性。 --- ### 总结流程 ``` 加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载 ``` 每个阶段紧密衔接,确保类能被正确加载并安全执行。理解这一过程对解决 `ClassNotFoundException`、`NoClassDefFoundError` 等问题至关重要。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值