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格式的基础从网络中获取,这种场景最典型的应用就是WebApplet运行时计算生成,这种场景使用得最多的就是动态代理技术,在java。lang。reflect。Proxy中,就是用了ProxyGenerator。generateProxyClass()来为特定接口生成形式为Proxy的代理类的二进制字节流由其他文件生成,典型场景是JSP应用,由JSP文件生成对应的Class文件从数据库中读取,这种场景相对少见些,例如有些中间件服务器(如SAPNetweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发可以从加密文件中获取,这是典型的防Class文件被反编译的保护措施,通过加载时解密Class文件来保障程序运行逻辑不被窥探。。。。。。连接阶段验证 验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。 验证阶段大致会完成下面四个阶段的验证动作:文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求字节码验证:整个验证过程中最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的符号引用验证:校验行为发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段解析阶段中发生 该阶段开发者可控性弱,而且对我们学习Java开发意义不大,再次不多赘述,想要更详细的学习具体每阶段验证的内容,可以阅读《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》连接阶段准备 在准备阶段,会为类中定义的类变量(即静态变量,使用static修饰的变量)分配内存并设置类变量初始值,从概念上讲,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区本身是一个逻辑上的区域,在JDK7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;而在JDK8及之后,类变量则会随着Class对象一起存放在Java堆中,这时候类变量在方法区就完全是一种对逻辑概念的表述了。 需要注意区分类变量和实例变量两个概念:类变量:是被static修饰的成员变量,准备阶段分配内存、设置初始值的就是这些变量实例变量:没有被static修饰的成员变量,这些变量是在类实例化时,随着对象的创建一起分配内存到堆中 在此阶段,类变量设置初始值包括两种情况: 1、编译后value具有ConstantValue属性 当类变量是使用static和final修饰,并且代码中赋值类型为基本数据类型或字符串(这里的字符串是双引号赋值,不是newString()),如下所示:publicclassDemo{基本数据类型privatestaticfinalintNUM10;字符串privatestaticfinalStringSTR字符串;} 使用命令javapvpDemo。class查看编译后的Class文件,如下所示:省略{privatestaticfinalintNUM;descriptor:Iflags:(0x001a)ACCPRIVATE,ACCSTATIC,ACCFINALConstantValue:int10privatestaticfinaljava。lang。StringSTR;descriptor:LjavalangString;flags:(0x001a)ACCPRIVATE,ACCSTATIC,ACCFINALConstantValue:String字符串省略}SourceFile:Demo。java 可以看到两个变量均生成了ConstantValue属性,对于这种变量,设置初始值就是当前代码中设置的值。 2、不具有ConstantValue属性 当类类变量只使用static修饰,或使用static和final修饰,但代码中赋值类型为引用数据类型,如下所示:publicclassDemo{只有static修饰privatestaticintnum10;赋值为引用数据类型privatestaticfinalStringSTRnewString(字符串);} 使用命令javapvpDemo。class查看编译后的Class文件,如下所示:省略{privatestaticintnum;descriptor:Iflags:(0x000a)ACCPRIVATE,ACCSTATICprivatestaticfinaljava。lang。StringSTR;descriptor:LjavalangString;flags:(0x001a)ACCPRIVATE,ACCSTATIC,ACCFINAL省略}SourceFile:Demo。java 这类类变量没有生成ConstantValue属性,对于这类变量,设置初始值就是对应类型的默认初始值,比如类变量num初始值为0,STR初始值为null,具体如下: 各种数据类型初始化零值连接阶段解析 该阶段开发者可控性弱,而且对我们学习Java开发意义不大,再次不多赘述,想要更详细的学习具体每阶段验证的内容,可以阅读《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》初始化阶段 类的初始化阶段是类加载过程的最后一个步骤,之前介绍的几个类加载的动作里,除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导控制。直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。 在初始化阶段,最主要做一件事,就是执行类构造器()方法(clinit:classinitialize前几个字母的简写),该方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的,如下所示:publicclassInitStageTest{privatestaticfinalStringSTATICCONSTANT静态常量;privatestaticStringCLASSVARIABLE类变量;static{CLASSVARIABLE类变量重新赋值;}} 对上面的类进行编译,执行命令javapvpInitStageTest。class查看字节码,如下所示:省略上面内容{privatestaticfinaljava。lang。StringSTATICCONSTANT;descriptor:LjavalangString;flags:ACCPRIVATE,ACCSTATIC,ACCFINALprivatestaticjava。lang。StringCLASSVARIABLE;descriptor:LjavalangString;flags:ACCPRIVATE,ACCSTATICpubliccom。haichun。jvm。InitStageTest();descriptor:()Vflags:ACCPUBLICCode:stack1,locals1,argssize10:aload01:invokespecial1MethodjavalangObject。init:()V4:returnLineNumberTable:line6:0LocalVariableTable:StartLengthSlotNameSignature050thisLcomhaichunjvmInitStageTest;static{};descriptor:()Vflags:ACCSTATICCode:stack3,locals0,argssize00:new7classjavalangString3:dup4:ldc9String静态常量6:invokespecial11MethodjavalangString。init:(LjavalangString;)V9:putstatic14FieldSTATICCONSTANT:LjavalangString;12:ldc20String类变量14:putstatic22FieldCLASSVARIABLE:LjavalangString;17:ldc25String类变量重新赋值19:putstatic22FieldCLASSVARIABLE:LjavalangString;22:returnLineNumberTable:line8:0line10:12line13:17line14:22}SourceFile:InitStageTest。java 上面输出的信息中static{}其实就是方法 为了更加直观的看到,可以使用IDEA插件jclasslibBytecodeViewer查看类的字节码信息,如下所示: 根据方法的字节码可以看到,在该方法中有如下内容:【09】静态常量赋值(除了静态常量的值可以在编译器确定的以外,这类变量具有ConstantValue属性,在连接阶段准备阶段赋值完成,其他类变量只是赋值为默认零值)【1214】类变量赋值【1719】执行静态代码块。 通过插件可以看到另外一个方法(对应javap命令打印的publicom。haichun。jvm。InitStageTest()),该方法为实例构造器,该方法是在类实例化时调用。要区别方法,方法为类构造器,是在类加载过程初始化阶段调用。 实例构造器,其实就是我们代码中定义的构造函数,如果有多个构造函数,就会有多个方法。blockquote 方法与方法不同的是,不需要显示的调用父类类构造器(方法首先会调用父类实例构造器,如publicom。haichun。jvm。InitStageTest()命令中1:invokespecial1调用Object的构造器),而是由JVM保证子类方法执行前,父类的先执行完成,因此,JVM第一个执行的方法的类型一定是Object。 由于父类方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作,因此下面示例中字段B的值将会是2不是1:staticclassParent{publicstaticintA1;static{A2;}}staticclassSubextendsParent{publicstaticintBA;}publicstaticvoidmain(String〔〕args){System。out。println(Sub。B);} 方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成方法。 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成方法。但接口与类不同的是,执行接口的方法不需要先执行父接口的方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化。此外,接口的实现类在初始化时也一样不会执行接口的方法。 参考文献 深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明 Java高并发编程详解:多线程与架构设计