JVM - 类加载

概述

Java虚拟机(JVM)不与包括Java语言在内的任何程序语言绑定,只与“Class文件”(字节码)这种特定的二进制文件格式关联。而Java虚拟机可以运行在各种不同硬件平台和操作系统上,这也是说Java语言跨平台的原因。虚拟机与程序语言的关系如下图:
各种语言与虚拟机关系

Class文件结构定义

ClassFile {
    u4             magic; //魔数,代表这是一个可以被虚拟机接收的Class文件,而不是靠文件扩展名确定:0xCAFEBABE
    u2             minor_version;//次版本号
    u2             major_version;//主版本号;高版本JDK可以向下兼容以前版本的class文件,大于虚拟机版本号的class文件(高版本编译器生成的class文件)将执行不了
    u2             constant_pool_count;//常量池的数量
    cp_info        constant_pool[constant_pool_count-1];//常量池,常量池里面放的是字面量和符号引用,字面量就是一些如文本字符串,声明为final的常量值等,符号引用主要包括package,类和接口名,字段名,方法类型等等
    u2             access_flags;//Class 的访问标记
    u2             this_class;//当前类
    u2             super_class;//父类
    u2             interfaces_count;//接口
    u2             interfaces[interfaces_count];//一个类可以实现多个接口
    u2             fields_count;//Class 文件的字段属性
    field_info     fields[fields_count];//一个类可以有多个字段
    u2             methods_count;//Class 文件的方法数量
    method_info    methods[methods_count];//一个类可以有个多个方法
    u2             attributes_count;//此类的属性表中的属性数
    attribute_info attributes[attributes_count];//属性表集合
}

//字段表,方法表结构:
field_info/method_info{
    u2             access_flags;//作用域,如private
    u2             name_index;//常量池引用,表示字段/方法名
    u2             descriptor_index;//常量池引用,字段和方法描述
    u2             attributes_count;//字段/方法的额外属性
    attribute_info attributes[attributes_count];
}

类加载

类的生命周期

类的生命周期

加载

在加载阶段,虚拟机要完成三件事:

  • 通过一个类的全限定名来获取定义此类的二进制字节流(有多种方式,例如从zip包读取,如jar,war,jsp文件生成,运行时计算生成等)
  • 将这个字节流所代表的静态存储结构转化为方法区运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

验证

  • 文件格式验证,例如魔数验证,版本号大小验证等等
  • 元数据验证
    • 是否有父类,是否继承了不允许继承的类如final类等等
    • 不是抽象类,是否实现了父类中要求实现的方法
    • 是否与父类矛盾
      ...
  • 字节码验证,通过数据流,控制流分析,确定程序语义合法。
  • 符号引用验证,如符号引用通过全限定名能否找到对应类等等

准备

为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。

注意此时的变量初始值是数据变量类型的“零值”,例如:

public static int value = 123;

此时value的值为0,因为目前还没有执行任何java方法(赋值为123是执行类构造器<clinit>()方法做的),但是如果是被final修饰,在准备阶段就会被赋值为ConstantValue属性指定的初始值。

解析

Java虚拟机将常量池内的符号引用,替换为直接引用(指针,相对偏移量等)的过程。

初始化

执行类构造器<clinit>()方法。<clinit>()方法是Javac编译器的自动生成物和实例构造器<init>()不同。

  • 初始化顺序是由源文件初始化顺序决定
  • Java虚拟机保证父类的<clinit>()会比子类的<clinit>()先执行
  • 如果一个类没有静态语句块或者变量的赋值,编译器可以不为该类生成<clinit>()方法
  • 线程安全的,只有一个线程能执行这个类的<clinit>()方法

卸载

卸载类,即该类Class对象能成功被GC
卸载类需要满足三个要求:

  • 该类的所有实例对象都被GC
  • 该类没有在任何地方被引用,即其Class对象不可达
  • 该类的类加载器的实例不可达,即该类的ClassLoader不可达

类加载器

比较两个类是否相等

任何一个类,都是由该类本身和它的类加载器一起确立其在JVM中的唯一性。 比较两个类是否相等(equals(), isInstance(),isAssignableFrom()),只有他们在同一类加载器下加载的前提才有意义,否则必然不相等。

Java的类加载器

  • 启动类加载器 Bootstrap ClassLoader
    使用C++开发,加载<JAVA_HOME>/lib目录中的jar 和 -Xbootclasspath参数指定的类
  • 扩展类加载器 Extension ClassLoader
    加载<JAVA_HOME>/lib/ext目录的jar 和使用-Djava.ext.dir指定的类
  • 应用程序加载器 App ClassLoader
    加载用户类路径下的类
  • 用户自定义加载器 User/Custom ClassLoader

类加载器

双亲委派模型 Parent Delegation Model

双亲委派模型是类加载的过程,总结两句话就是自底向上检查类是否被加载,自顶向下尝试加载类。如下图:
双亲委派

源码:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name); // You will probably want to override this method when you instantiate a class loader subclass in your application -- oracle

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

双亲委派小结

结论1 任何一个类的加载最终都委托到启动类加载器

结论2 每个加载器都有自己加载的类范围,如启动类加载器只加载JDK核心库,因此并不是父类加载器都会加载成功,父类加载器无法加载时,此时会自己加载。

双亲委派的好处

保证所有的类都尽可能地由顶层类加载器加载,保证了加载类的唯一性和类的实现关系;也保证了Java的核心API不被窜改。

posted @   rachel_aoao  阅读(35)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~
点击右上角即可分享
微信分享提示