Java虚拟机的类加载机制

2023-11-06

Java虚拟机的类加载机制

Java虚拟机在程序执行过程中会动态加载类,所谓类的加载指的是将一个Class文件描述的Class对象加载到JVM中,形成一个Class对象的过程。这里”Class对象”更通用的指的是一个二进制字节流,并不一定以一个文件的形式存在,而Class对象可以表示类,也可以表示一个接口。

Java类的加载主要有“加载、验证、准备、解析、初始化”这几个过程,其中“验证、准备、解析”又被统称为链接过程。

类加载器

在开始描述类加载过程之前,我们先讲一下类加载器,类的加载过程中“通过一个类的全限定名来获取描述此类的二进制流”的过程是由类加载器实现的。

类与类加载器

对于任意一个类,都需要加载它的类加载器和这个类本身一同确立其在JVM中的唯一性,对于每一个类加载器都有一个独立的类名称空间。

这里说的唯一性包括了Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法返回的结果不同,也包括了instanceof运算符的判定结果。

类加载器类型

从JVM的角度来说,只存在两种不同的类加载器:
- Bootstrap ClassLoader 启动类加载器 这个类加载器是JVM自身一部分,在Java程序中是不可获得的。
- 其它类加载器 这些类加载器独立于JVM,位于JVM外部,并且全部继承java.lang.ClassLoader这个基类。

从用户的角度看,可以有四种类加载器:
- Bootstrap ClassLoader 启动类加载器 即上文提到的Bootstrap ClassLoader,这个类加载器负责加载Java语言的基本组件。进一步而言,它加载的类位于“%JAVA_HOME\lib”或者-Xbootclasspath指定的路径下。同时,它只将自己识别的(按文件名识别,如rt.jar)类库加载到JVM中。对于其它的类库,即使位于lib下也不会被加载。
- Extension ClassLoader 扩展类加载器 它加载位于“%JAVA_HOME\lib\ext”或java.ext.dirs系统变量指定路径下的所有类库。它对于用户来说是可获得的,用户可以直接使用这个类加载器。
- Application ClassLoader 应用程序类加载器 由于它是ClassLoader的getSystemClassLoader()方法的返回值,所以又被称为系统类加载器。它负责加载classpath上的类库已经类文件。这个类加载器是正常情况下程序默认使用的类加载器。它对于用户来说是可获得的,用户可以直接使用这个类加载器。
- Custom ClassLoader 自定义类加载器 用户可以继承java.lang.ClassLoader类来自定义自己的类加载器,选择从自定义的路径下去加载类。

双亲委托模型

双亲委托模型是JVM类加载器自行遵守的一种模式,它不是强制的,但是Extension ClassLoader和Application ClassLoader都遵守这个模式,用户自定义ClassLoader时也应该遵守这个模式。

所谓双亲委托模式指的是“当一个类加载器要加载一个类时,它首先会将加载请求交给它的父加载器,如果父加载器加载不成功,子加载器才会继续进行加载”。这样做的好处是,可以避免不同的类加载器加载同一个类形成多个代表同一个类的Class对象,从而避免不必要的麻烦。如Integer类只能会被Bootstrap ClassLoader加载,而不会被Extension ClassLoader或者Application ClassLoader加载。
这里写图片描述

加载

“加载”阶段,虚拟机主要完成:
- 1、通过一个类的权限定名来获取一个定义此类的二进制字节流。
- 2、将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构。
- 3、在内存中生成一个代表这个类的java.lang.Class对象,作为这个类的各种数据结构的访问入口。

这里需要加以说明的是数组类的加载,数组类的不是由类加载器创建的,而是由虚拟机自主创建。在创建一个数组类时,虚拟机会先加载这个数组类的元数据类型,如”[java.lang.Integer”一维数组、”[[java.lang.Integer”二维数组的元数据的类型都是”java.lang.Integer”,高维相似。加载完元数据后,虚拟机会生成数组类的Class对象,同时这个数组类将会和加载其元数据的类加载器的类名称空间相关联。同时数组类的访问修饰符与其元数据相同。

验证

验证是链接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害到虚拟机自身的安全。

准备

准备阶段是正式为类变量分配内存并且设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这里说的类变量是指类中被static修饰的变量。

另外这里说的”设置初始值“指的是“为变量的内存空间设零值”。程序逻辑意义上的初始化会在初始化阶段进行。如:

    public static int value = 123;

在准备阶段后,value的值为0,初始化阶段后value的值才会被设置为123。

但是有一种情况例外,即对拥有ConstantValue属性的类字段的初始化。这类变量,它的值在准备阶段就会被设置成用户初始化的值。

ConstantValue

类中static变量的初始化赋值分为两种:正常情况下是初始化阶段在类初始化方法<clinit>()中完成,另外一种是在准备阶段通过字段的ConstantValue赋值。
在实际程序中,只有static final的字段才会有ConstantValue属性,并且只限定于基本类型字符串常量。这里可以理解为这些常量值在编译器就会放置到Class文件的常量池中,可以直接赋值。

解析

解析主要负责将虚拟机常量池中的符号引用解析成直接引用。

初始化

初始化阶段是指根据用户的初始化逻辑将类变量设置成一定的值,初始化将做重点阐述。

初始化的时机

JVM虚拟机规范里详细定义了“当且仅当”如下5种开始初始化的情况:
- 遇到new、getstatic、putstatic、invokestatic字节码时,如果类没有初始化则进行初始化则进行初始化。它们分别对应使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()法的那个类),虚拟机会先初始化这个主类。
- 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

这5种情况被称为对一个类的主动引用。除此之外,所有引用类的方式都称为被动引用,不会触发初始化。以下是几种被动引用:
- 通过子类来引用父类的静态变量 这种情况下只会导致父类的初始化,而子类不会进行初始化。
- 引用数组类 创建数组对象时,只会导致数组类的初始化,而不会触发数组元类型的初始化。如:

    new Integer[10];

上述代码只会触发”[com.comple.ArrayBase”数组类的初始化(而它的初始化往往是不被注意到的),但并不会触发“com.comple.ArrayBase”的初始化。
- 引用一个类的static final静态常量(限于基本类型和String类型常量) 这种情况下不会导致被引用类的初始化。因为是静态常量,所以在编译期就会通过常量传播优化将静态常量放置到引用类的常量表中,对这个静态常量的引用就变成了对自身常量池的静态变量的引用了。举例子说:

    public class A{
        public static final String HELLO_WORLD = "Hello World!";
    }

    public class B{
        public static void main(String[] args){
            System.out.println(A.HELLO_WORLD);
        }
    }

上面这段代码,在编译期结束后就可以认为A和B之间就没有关系了,B引用的A的静态常量值被存在了B的常量池中,B对这个常量的引用这变为了对自身常量池中变量值的引用。所以B中对HELLO_WORLD的引用并不会导致A的初始化。

接口的初始化

接口中的变量都是static final的,所以如前面ConstantValue小节所述,接口内字段的初始化分为两种情况:
- 字段为基本类型和String类型常量。这种情况下,字段会带有ConstantValue属性,在准备阶段就会被设置成ConstantValue属性的值。同时,这种情况也对应着上面提到的第三种被动引用的情况,如果本字段是当前描述的这种情况,其它类来引用本字段不会触发当前类的初始化阶段。
- 字段为非基本类型或者String类型常量。这种情况如下面这段代码所示:

    public interface A{
        Object aObject = new Object();
    }

    public interface B extends A{
        Object bObject = A.aObject;
    }

上述这段代码中,接口B的初始化会导致接口A的初始化,因为虽然B中引用的是A的static final变量,但是A中的aObject字段并不带有ConstantValue属性,所以它需要在初始化阶段进行赋值,接口B的初始化也就触发了A的初始化。

接下来讨论一下父子接口的初始化顺序。父子接口的初始化和父子类的初始化不同,子接口的初始化并不会父接口的初始化。但这也不是绝对的,可以把上面这段代码改成下面这段代码:

    public interface A{
        Object aObject = new Object();
    }

    public interface B extends A{
        Object bObject = A.aObject;
    }

这段代码与上面一段的不同只在于B继承了A。这种情况下,B的初始化是会导致A的初始化的,但是实际上这与他们之间的继承关系并与关系,关键在于B引用了A中的不带ConstantValue属性的字段
再看两段代码:

    public interface A{
        Object aObject = new Object();
    }

    public interface B extends A{
        Object bObject = A.aObject;
    }

这里父子接口的初始化就完全没有联系了,子接口的初始化不会导致父接口的初始化。

    public interface A{
        String aString = "Hello World!";
    }

    public interface B extends A{
        String bString  = A.aString;
    }

上述这种情况就属于前文所述的被动引用的的第三种情况。

总结起来说,父子接口之间的初始化并无必然的联系,单纯的子接口的初始化不会触发父接口的初始化,具体会不会触发在于子接口是否引用了父接口中的不带ConstantValue属性的字段,这种关系又与不带继承关系的两个接口的初始化没有区别。

初始化的顺序

类或接口的初始化都由<clinit>()方法完成。JVM会搜集static块和变量初始化操作生成<clinit>()方法(接口只有变量初始化操作,没有static块)。<clinit>()中初始化的顺序和定义static块与变量初始化操作的顺序一致。
有关初始化顺序需要注意以下几点:
- static块可以为在其后定义的static变量赋值,但是不能访问它:

    public class Test{
        static{
            i = 0;
            System.out.println(i);
        }
        static int i = 1;
    }

上述这段代码中,static块为i赋值为0是可以的,但是System.out.println(i);语句在编译时则会出错,会提示“非法向前引用”。
- 子类的初始化会触发父类的初始化,并且父类的<clinit>()方法会在子类的<clinit>()方法执行前执行完毕。
- 子接口的初始化不会触发父接口的初始化,除非直接引用了父接口的static变量才会导致初始化。同样的,接口的实现类初始化也不会触发父接口的初始化,除非直接引用了父接口的static变量。
- <clinit>()在多线程环境中被正确地加锁、同步。如果多个线程同时初始化一个类,那么只有一个线程能调用<clinit>()方法,其它线程阻塞,直到活动线程执行<clinit>()方法执行完毕。阻塞线程唤醒之后将不再执行<clinit>()方法。


本文参照《深入理解Java虚拟机(第2版)》第7章。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java虚拟机的类加载机制 的相关文章

随机推荐

  • 量化择时——平均K线图双均线策略(第1部分—策略效果测算)

    文章目录 平均K线图概述 OHLC的计算方式 K线图走势对比 平均K线图阴阳线交易策略 交易规则 测算结论 双均线策略测算 测算规则 测算结论 平均K线图概述 平均K线图是蜡烛图的一种分支 在日本 Heikin意味着 平均 Ashi意味着
  • python root:code for hash md5 was not found.错误

    可能还会伴随一大堆其他错误 ERROR root code for hash md5 was not found Traceback most recent call last File usr local Cellar python 2
  • qt 怎么检测鼠标在不在某个控件上

    方式一 推荐 感觉这种事件过滤器的方法捕捉比较敏感 记得安装事件过滤器 this gt installEventFilter this protected bool eventFilter QObject obj QEvent event
  • k8s 配置 glusterFS 动态供给

    部署环境 Host IP k8s 版本 glusterFS版本 heketi版本 heketi client 版本 k8s master1 192 168 10 1 1 20 0 9 5 1 el7 heketi 8 0 0 1 heket
  • 短 URL 服务的设计与实现

    转载 https mp weixin qq com s DJM7KFFfgZ2AgfrrYHXSzQ 短url的好处有 短 短信和许多平台 微博 有字数限制 太长的链接加进去都没有办法写正文了 好看 比起一大堆不知所以的参数 短链接更加简洁
  • 如何查看端口是被哪个程序占用的

    一 开始 gt 运行 gt cmd 或者是window R组合键 调出命令窗口 二 输入命令 netstat ano 列出所有端口的情况 在列表中我们观察被占用的端口 比如是8080 首先找到它 三 查看被占用端口对应的PID 输入命令 n
  • C语言中输入输出重定,freopen()妙用。

    使用的理由 范围 如果输入数据很庞大 需要一次又一次的重新输入和调试时可采用本函数 freopen 函数 1 格式 FILE freopen const char filename const char mode FILE stream 2
  • window如何实时刷新日志文件

    1 安装windows git 下载地址 Git Downloading Package git scm com 2 打开git bash 输入tail exe f 日志文件路径
  • 19-Openwrt双固件升级

    在上一章节 Openwrt sysupgrade系统升级 中 我们描述了sysupgrade升级系统的过程 这种升级过程会直接firmware分区进行写入 无法保证系统的安全性 只要在写入过程突然断电就会出现系统写入失败 升级失败无法启动系
  • xml文件c语言读取函数,IDL读取XML文件

    使用IDL读取RADARSAT 2的数据 需要用到lutSigma xml文件中的定标常数来计算相关参量 本文需要提取lutSigma xml中的offset和gains参数 使用IDL来读取xml文件 并且提取特定的节点下的参数 经过实验
  • fifo复位问题

    一次笔者在调试K7和5EV模块通信时候遇到fifo状态异常问题 K7现象 full和empty均拉高 5EV现象 empty拉高 full拉低 但是写信号已经产生 问题原因 fifo的复位来的太早 而随路时钟来的太晚导致 因为fifo的写时
  • Host is not allowed to connect to this MySQL server解决方法

    先说说这个错误 其实就是我们的MySQL不允许远程登录 所以远程登录失败了 解决方法如下 在装有MySQL的机器上登录MySQL mysql u root p密码 执行use mysql 执行update user set host whe
  • 服务器内存型号2400,S26361-F3934-E511 8GB 1Rx4 PC4-2400 RX2540M2服务器内存

    S26361 F3934 E511 8GB 1Rx4 DDR4 PC4 2400 ECC Primergy CX2550M2 TX2560M2 RX2510M2 RX2530M2 RX2540M2 RX2560M2富士通服务器内存A3C40
  • nginx配置访问springboot服务

    一 idea中可通过 clean package打包命令 打好包 比如 端口为8080 服务访问地址为 前端打包文件为dist 访端口为8000 则可以这样配置nginx server listen 8000 location root h
  • OpenSSL在QT中的使用

    现在需要把OpenSSL集成到QT里面 本来是想直接把Cygwin的动态库和头文件直接拿来用的 没想到链接的时候报了一票错误 那好吧 重新自己build一个 这样来的也干净些 到官网上下载源码 根据里面的INSTALL W32一步步来 首先
  • 分类(6):不平衡和多分类问题

    原版 http www jianshu com p 15185f0ecb57 一 不平衡问题 1 不平衡数据 例如 一个产品生产的不合格产品数量会远低于合格产品数量 信用卡欺诈的检测中 合法交易远远多于欺诈交易 这时候 准确率的度量会出现一
  • windows安装pnpm后报错:pnpm : 无法将“pnpm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。

    使用 npm 方式 安装pnpm 命令如下 npm install g pnpm 安装完以后 执行pnpm v 查看版本号 pnpm v 执行完 发现报错 pnpm 无法将 pnpm 项识别为 cmdlet 函数 脚本文件或可运行程序的名称
  • 服务器被攻击了怎么办

    今天德迅云安全的我给大家讲讲服务器被攻击的那些事 首页给大家讲讲我一客户的亲身经历 我有个客户开设了电商平台 最近几年电商平台俨然已经成了老百姓的生活依赖 淘宝 京东 所以我这客户的小平台发展得也还不错 直到之前那段时间日子他遇上了大麻烦
  • 61 openEuler 22.03-LTS 搭建MySQL数据库服务器-管理数据库用户

    文章目录 61 openEuler 22 03 LTS 搭建MySQL数据库服务器 管理数据库用户 61 1 创建用户 示例 61 2 查看用户 示例 61 3 修改用户 61 3 1 修改用户名 61 3 2 修改用户示例 61 3 3
  • Java虚拟机的类加载机制

    Java虚拟机的类加载机制 Java虚拟机在程序执行过程中会动态加载类 所谓类的加载指的是将一个Class文件描述的Class对象加载到JVM中 形成一个Class对象的过程 这里 Class对象 更通用的指的是一个二进制字节流 并不一定以