JVM类加载过程
Java 虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程被称作虚拟机的类加载机制。
与那些在编译时需要进行连接的语言不同,在 Java 语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略让 Java 语言进行提前编译会面临额外的困难,也会让类加载时稍微增加一些性能开销, 但是却为 Java 应用提供了极高的扩展性和灵活性,Java 天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。例如,编写一个面向接口的应用程序,可以等到运行时再指定其实际的实现类,用户可以通过 Java 预置的或自定义类加载器,让某个本地的应用程序在运行时从网络或其他地方上加载一个二进制流作为其程序代码的一部分。这种动态组装应用的方式目前已广泛应用于Java 程序之中,从最基础的 Applet、JSP 到相对复杂的 OSGi 技术,都依赖着 Java 语言运行期类加载才得以诞生。
之前写的 Java 实战案例:Java 类隔离应用:多 Jar 包支持,就是充分利用了 Java 这个特性实现的。
在正式学习下面内容之前,避免因表达而造成的歧义,约定如下内容:类型:后文中提到类型,指类或接口,如果需要精确表达类或接口,会特意说明Class 文件:并非只磁盘中编译后的 Class 文件,而是二进制字节流,可能是从磁盘文件读取,也可能是网络、数据库、内存或动态生成的等
对于类加载过程,包括加载、连接、初始化三个阶段,每个阶段的作用简单概括如下:
类加载过程三个阶段要注意类加载过程和类加载阶段两个名词的区别:
类加载过程:是类加载的整个流程,包括加载阶段、连接阶段、初始化阶段
类加载阶段:只是类加载过程的第一个阶段加载阶段
在加载阶段,JVM 需要完成下面三件事:通过一个类的全限定名来获取定义此类的二进制字节流将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
类加载的最终产物就是堆内存中的 Class 对象,对于同一个 ClassLoader 来说,不管某个类被加载多少次,对应到堆内存中,只有一个 Class 对象。如下图所示:
虚拟机规范要求类的加载通过类的全限定类名来获取二进制字节流,并没有严格规范获取的途径,这就给开发者很大的想象空间,除了我们平时常见的 Class 文件以外,还会有如下几种形式:从ZIP压缩包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础从网络中获取,这种场景最典型的应用就是Web Applet运行时计算生成,这种场景使用得最多的就是动态代理技术,在 java.lang.reflect.Proxy 中,就是用了 ProxyGenerator.generateProxyClass() 来为特定接口生成形式为"*$Proxy"的代理类的二进制字节流由其他文件生成,典型场景是 JSP 应用,由 JSP 文件生成对应的 Class 文件从数据库中读取,这种场景相对少见些,例如有些中间件服务器(如 SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发可以从加密文件中获取,这是典型的防 Class 文件被反编译的保护措施,通过加载时解密 Class 文件来保障程序运行逻辑不被窥探......连接阶段 - 验证
验证是连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java虚 拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
验证阶段大致会完成下面四个阶段的验证动作:文件格式验证:验证字节流是否符合Class文件格式的规范, 并且能被当前版本的虚拟机处理元数据验证:对字节码描述的信息进行语义分析, 以保证其描述的信息符合《Java语言规范》 的要求字节码验证:整个验证过程中最复杂的一个阶段, 主要目的是通过数据流分析和控制流分析, 确定程序语义是合法的、 符合逻辑的符号引用验证:校验行为发生在虚拟机将符号引用转化为直接引用的时候, 这个转化动作将在连接的第三阶段——解析阶段中发生
该阶段开发者可控性弱,而且对我们学习 Java 开发意义不大,再次不多赘述,想要更详细的学习具体每阶段验证的内容,可以阅读《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》连接阶段 - 准备
在准备阶段,会为类中定义的类变量(即静态变量,使用 static 修饰的变量)分配内存并设置类变量初始值,从概念上讲,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区本身是一个逻辑上的区域,在JDK 7 及之前,HotSpot 使用永久代来实现方法区时,实现是完全符合这 种逻辑概念的;而在 JDK 8 及之后,类变量则会随着 Class 对象一起存放在 Java 堆中,这时候"类变量在方法区"就完全是一种对逻辑概念的表述了。
需要注意区分类变量和实例变量两个概念:类变量:是被 static 修饰的成员变量,准备阶段分配内存、设置初始值的就是这些变量实例变量:没有被 static 修饰的成员变量,这些变量是在类实例化时,随着对象的创建一起分配内存到堆中
在此阶段,类变量设置初始值包括两种情况:
1、编译后 value 具有 ConstantValue 属性
当类变量是使用 static 和 final 修饰,并且代码中赋值类型为基本数据类型或字符串(这里的字符串是双引号赋值,不是 new String()),如下所示:public class Demo { // 基本数据类型 private static final int NUM = 10; // 字符串 private static final String STR = "字符串"; }
使用命令 javap -v -p Demo.class 查看编译后的 Class 文件,如下所示:// 省略 { private static final int NUM; descriptor: I flags: (0x001a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL ConstantValue: int 10 private static final java.lang.String STR; descriptor: Ljava/lang/String; flags: (0x001a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL ConstantValue: String 字符串 // 省略 } SourceFile: "Demo.java"
可以看到两个变量均生成了 ConstantValue 属性,对于这种变量,设置初始值就是当前代码中设置的值。
2、不具有 ConstantValue 属性
当类类变量只使用 static 修饰,或使用 static 和 final 修饰,但代码中赋值类型为引用数据类型,如下所示:public class Demo { // 只有 static 修饰 private static int num = 10; // 赋值为引用数据类型 private static final String STR = new String("字符串"); }
使用命令 javap -v -p Demo.class 查看编译后的 Class 文件,如下所示:// 省略 { private static int num; descriptor: I flags: (0x000a) ACC_PRIVATE, ACC_STATIC private static final java.lang.String STR; descriptor: Ljava/lang/String; flags: (0x001a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL // 省略 } SourceFile: "Demo.java"
这类类变量没有生成 ConstantValue 属性,对于这类变量,设置初始值就是对应类型的默认初始值,比如类变量 num 初始值为 0,STR 初始值为 null,具体如下:
各种数据类型初始化零值连接阶段 - 解析
该阶段开发者可控性弱,而且对我们学习 Java 开发意义不大,再次不多赘述,想要更详细的学习具体每阶段验证的内容,可以阅读《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》初始化阶段
类的初始化阶段是类加载过程的最后一个步骤,之前介绍的几个类加载的动作里,除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导控制。直到初始化阶段,Java 虚拟机才真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。
在初始化阶段,最主要做一件事,就是执行类构造器 () 方法(clinit:class initialize 前几个字母的简写),该方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的,如下所示:public class InitStageTest { private static final String STATIC_CONSTANT = "静态常量"; private static String CLASS_VARIABLE = "类变量"; static { CLASS_VARIABLE = "类变量重新赋值"; } }
对上面的类进行编译,执行命令 javap -v -p InitStageTest.class 查看字节码,如下所示:// 省略上面内容 { private static final java.lang.String STATIC_CONSTANT; descriptor: Ljava/lang/String; flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL private static java.lang.String CLASS_VARIABLE; descriptor: Ljava/lang/String; flags: ACC_PRIVATE, ACC_STATIC public com.haichun.jvm.InitStageTest(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return LineNumberTable: line 6: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/haichun/jvm/InitStageTest; static {}; descriptor: ()V flags: ACC_STATIC Code: stack=3, locals=0, args_size=0 0: new #7 // class java/lang/String 3: dup 4: ldc #9 // String 静态常量 6: invokespecial #11 // Method java/lang/String."":(Ljava/lang/String;)V 9: putstatic #14 // Field STATIC_CONSTANT:Ljava/lang/String; 12: ldc #20 // String 类变量 14: putstatic #22 // Field CLASS_VARIABLE:Ljava/lang/String; 17: ldc #25 // String 类变量重新赋值 19: putstatic #22 // Field CLASS_VARIABLE:Ljava/lang/String; 22: return LineNumberTable: line 8: 0 line 10: 12 line 13: 17 line 14: 22 } SourceFile: "InitStageTest.java"
上面输出的信息中 static {} 其实就是 方法
为了更加直观的看到,可以使用 IDEA 插件 jclasslib Bytecode Viewer 查看类的字节码信息,如下所示:
根据 方法的字节码可以看到,在该方法中有如下内容:【0 - 9】静态常量赋值(除了静态常量的值可以在编译器确定的以外,这类变量具有 ConstantValue 属性,在连接阶段 - 准备阶段赋值完成,其他类变量只是赋值为默认零值)【12 - 14】类变量赋值【17 - 19】执行静态代码块。
通过插件可以看到另外一个方法 (对应 javap 命令打印的 public om.haichun.jvm.InitStageTest()),该方法为实例构造器,该方法是在类实例化时调用。要区别 方法, 方法为类构造器,是在类加载过程初始化阶段调用。
实例构造器,其实就是我们代码中定义的构造函数,如果有多个构造函数,就会有多个 方法。
方法与 方法不同的是,不需要显示的调用父类类构造器( 方法首先会调用父类实例构造器,如 public om.haichun.jvm.InitStageTest() 命令中 1: invokespecial #1 调用 Object 的构造器),而是由 JVM 保证子类 方法执行前,父类的先执行完成,因此,JVM 第一个执行的 方法的类型一定是 Object。
由于父类 方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作,因此下面示例中字段 B 的值将会是 2 不是 1:static class Parent { public static int A = 1; static { A = 2; } } static class Sub extends Parent { public static int B = A; } public static void main(String[] args) { System.out.println(Sub.B); }
方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成 方法。
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成 方法。但接口与类不同的是,执行接口的 方法不需要先执行父接口的 方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化。此外,接口的实现类在初始化时也一样不会执行接口的 方法。
参考文献
深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明
Java高并发编程详解:多线程与架构设计
一线调研丨一家德国企业的五连投开年以来,外资加速布局中国。北京的一家德国企业,新一年的投资正在落地。而在过去的三年里,这家企业已连续四次增资中国。在北京经济技术开发区,生产压缩机的德国企业比泽尔,正在召开新一年
物流保通保畅民航保障航班环比增长0。5最新的数据显示,2月12日,全国货运物流有序运行,民航保障航班环比增长0。5。根据国务院物流保通保畅工作领导小组办公室监测汇总数据显示,2月12日,国家铁路货运继续保持高位运行,运
47次赔付超120万理赔金新华保险托起客户生命的希望一份保单,一份生存的希望,一份厚重的守护。先后赔付47次,累计理赔金额超120万元,新华保险为不幸罹患肝癌的40岁C女士托起生命的希望,从患病治疗到成功肝脏移植手术,一路为生命撑起
家国情怀共情才能满堂红2月8日,电影满江红票房突破40。67亿,荣登中国内地影史票房榜第八名。这部影片火爆的原因固然有多种,但作品本身所蕴含的精神内核价值无疑是关键因素。在艺术创作中,只有实现深沉的家国
周末下乡种菜,听起来很有诗意资料图一到周末,杨瑞在车后座装进羊粪鱼肠等肥料,以及铲土用的小工具和手套,和家人从上海市区驱车15分钟抵达上海闵行区梅陇镇的一处农场。这里有她花钱租的菜地,地里是她一家人种下的萝卜
稳定的预期是经济发展的硬通货预期是市场主体对未来的判断,将影响经济行为。预期决定市场主体接下来是否增加用工增加投资,也决定了人们是否愿消费能消费敢消费,稳经济的关键在于稳预期。进一步改善预期,有助于经济发展目
姑侄情,姐妹情,母女情贾远满的大爱人生湖南日报新湖南客户端通讯员李干记者李治沧桑的面容黝黑的皮肤长满厚茧的双手瘦弱的身躯腿疾走路不利索这是外人对贾远满的第一印象。正是这样一位普通的妇女,在平常日子里,展现了真情大爱二十
作业帮科技企业为传统市场带来创新搭载聪明学智能系统的作业帮AI学习桌。作业帮供图2月9日,作业帮举办AI学习桌新品沟通会,宣布其独家开创的AI学习桌在推出4个月内跻身天猫书桌单品GMV第四名,并发布新款AI学习桌
海峡链入选赛迪研究院2022区块链技术创新典型企业名录2023年1月10日,由重庆市渝中区人民政府重庆市经济和信息化委员会重庆市文化和旅游发展委员会指导,重庆文博会组委会联合重庆数字经济产业园管委会赛迪区块链研究院等单位举办的元宇宙数
企示录风云浙商郑永刚留给企业的三个经营启示杉杉官网首页背景是一片杉树林,2月11日晚间,这片原本苍翠浓郁的树林变成黑白。当晚,杉杉企业创始人郑永刚逝世。杉杉控股发布讣告杉杉企业创始人杉杉控股董事局主席郑永刚先生因突发心脏疾
院校介绍法国AD教育集团,欧洲最大的创意类艺术高校联盟法国AD教育集团法国AD教育集团(ADEducation)是法国艺术高等教育的领军者,集团提供奢侈品设计动画电子游戏遗产珠宝香水等领域的高等教育课程。AD教育集团目前拥有5000多