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高并发编程详解:多线程与架构设计
外国新能源的对决!福特VS特斯拉究竟谁更值得买?如果是在以往,消费者花30万买车,目光往往会放在传统豪华品牌的燃油车上,当然现在也有很多用户也会这样做,但是自从新源车异军突起,越来越多的用户将目光瞄准了这类车型,除了政策福利外,
重温山村老尸,果然有不少值得思考的地方,童年阴影名不虚传我了解,这是很多小宝贝的童年阴影,所以很贴心的P了一张图,希望能缓解你的紧张感。这片子是1999年出的,我第一次看的时候已经是2008年,并没有留下什么太深的印象,我很难理解为何很
宝宝裹膜而出近日,怀化市第一人民医院产科诞生了一对孕36周双胞胎的宝宝,出生时还裹着一层保护膜。她们以这样奇特方式降生,其实是医生有意为她们穿上的保护膜。当天早上9点多,在科主任张远方的带领下
抵制低俗,正能量诗歌之九数千年农民默默奉献,地位卑微食不果腹,今日麦芒,愿天下农人在希望的田野上,枕着星光梦想富裕成真。麦芒所愿望窗听雨栀子花开,麦芒飘香裹着湿气。清茶怡然着雅人,农人汗水在烈日中氤氲。陇
遗精正常吗?快看这里女性在睡眠过程中或者性刺激下,阴道分泌物大量增加有粘液排出,但一般不会被注意,而男性则不同,很多男性朋友因遗精来就诊,忧心忡忡,担心是不是自己身体出现了病变?会不会影响自己的生育能
北上广富婆圈一身名牌,全是A货大家好,又到周五。欢迎回来D姐的时尚频道上周,我们聊了LV专柜售假的事,不少姐妹cueD姐聊聊奢侈品高仿。那么今天直接就来到底有多少富婆名媛背假包?奢侈品假货市场有多大?假包能有多
凡人修仙传庆祝播放量破八亿,但却被路人群嘲,这是为何?目前小说改编的动漫很多,其中不乏精品之作,比如斗破苍穹凡人修仙传等,最近某站庆祝凡人播放量突破八亿,但实则反观凡人粉丝并不高兴,并且还有其他人进行嘲讽,说这点播放量单集不过一千万,
全系降价的奥迪Q7真的太香了,外观大气,内饰豪华,2。0T动力强劲奥迪Q7终于迎来了大降价,作为奥迪的旗舰SUV,在国内市场是很有地位和资历的,更是豪华SUV的标杆级车型,不仅性能出众,而且口碑扎实,是很多男人的努力的目标,在新车上市后价格也有了
香港汇丰及恒生两大银行的网上系统出现故障,绝对不能等闲视之文谢悦汉香港汇丰银行及恒生银行的网上系统昨早一度出现故障,数码银行及自动柜员机服务受到影响,又适逢昨日是赛马日,令到很多马迷抱怨和不快。作为全港两大银行(恒生是汇丰属下银行)及市民
苹果将在明日凌晨正式发布iOS16系统据国外媒体报道,苹果(AAPL。US)将在6月7日凌晨1点正式发布iOS16系统,发布前夕其公布了一组最新数据,在过去四年推出的设备中,iOS15的普及率达到了89,表现远超分析师
距终止上市仅剩1天!退市西水10cm涨停,上交所出手中新经纬6月6日电6日晚间,上交所官方微博发布关于退市西水股票异常交易情况的通报称,6月6日,退市西水股票价格出现异动,个别投资者在交易过程中存在拉抬股价以涨停价大额申报等影响市场