java-虚拟机类加载机制

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,即为虚拟机的类加载机制。

1 类的生命周期

类从被加载到虚拟机内存到卸载出内存,整个生命周期如下图:

分为7个阶段。2、3、4合在一起称为连接阶段。

 

顺序:1、2、3、5、6、7顺序是确定的,4可以位于3后,也可以位于5后,这是为了支持动态绑定(晚期绑定)。

Java中,类的加载、连接和初始化过程在程序运行期间,而非编译期间完成的。该策略增加了类加载时的开销,但为应用程序提供了高度的灵活性。Java天生可以动态扩展的语言特性就是依赖这种机制实现的。

2 类加载器

类的加载阶段中,“通过一个类的全限定名获取描述此类的二进制字节流”这个动作(实现这个动作的代码模块称为“类加载器”),是在JVM外部实现的,来让应用程序决定如何获取所需的类。

每一个类加载器,都有一个独立的类命名空间。任意一个类,由加载它的类加载器和这个类本身确定其在JVM中的唯一性

JVM提供的3种类加载器:

 

  1. 启动类加载器负载加载$JAVA_HOME/lib目录中,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar,名字不符合的,放在这个目录下也不会加载)的类。
  2. 扩展类加载器加载$JAVA_HOME/lib/ext目录中,或通过java.ext.dirs系统变量锁指定的路径中的类库。
  3. 应用程序类加载器/系统类加载器加载用户路径(ClassPath)上指定的类库。
  4. 可以通过继承java.lang.ClassLoader实现自定义的类加载器。

 

3 类加载工作流程

JVM通过双亲委派模型进行类的加载

双亲委派模型的工作过程:一个类加载器收到类加载的请求,它首先不会自己加载这个类,而是会把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。只有当父类加载器在其无法加载(在它的搜索范围中没有找到要加载的类)时,子类加载器才会尝试自己加载。如果最终类加载器都没有找到这个类,会抛出ClassNotFoundException异常。

4 加载阶段

做三件事

  1. 通过一个类的全限定名来获取定义该类的二进制字节流
  2. 将这个字节流代表的静态存储结构转换为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的java.lang.Class对象,(对于HotSpot,Class对象虽然是对象,但是存放在方法区中)作为方法区这个类的各种数据的访问入口。

获取类的二进制字节流有很多途径,不一定从一个Class文件中获取,还可以有以下几种方式:

  1. 从ZIP包读取。(是JAR,EAR,WAR格式的基础)
  2. 从网络获取(例如Applet)
  3. 运行时计算生成(动态代理技术)
  4. 由其他文件生成。(比如JSP文件中生成对应的Class类)
  5. 从数据库中读取(比如中间件服务器SAP Netweaver可以把程序安装在数据中完成程序代码在集群间的分发)

5 验证阶段

确保Class文件中的字节流包含的信息符合当前虚拟机的要求,且不会危害虚拟机自身安全。

完成文件格式验证(确保字节流符合Class文件格式规范,且能被JVM理解)、元数据验证(对类的元数据信息进行语义校验)、字节码验证(对类的方法体进行语义校验)、符号引用验证(发生在将符号引用转为直接引用的时候,即解析阶段)。

文件格式验证是基于二进制字节流进行的,完成之后字节流会进入方法区进行存储。后面的三个验证都是基于方法区的存储结构进行的。

验证阶段是很重要,但非必须阶段。可以使用-Xverify:none关闭大部分的类验证措施,这样会缩短类加载的时间。

6 准备阶段

正式为类变量(被static修饰的变量)分配内存并设置类变量初始值(初始值为数据类型的零值,而非程序员指定的值。但是被static final修饰的类变量除外)。

被static final修饰的类变量,在编译阶段会在其字段属性表中生成ConstantValue属性,则该阶段会直接将该值设置为程序员指定的值。

7 解析阶段

将常量池内的符号引用替换为直接引用的过程。

符号引用就是常量池中的CONSTANT_Class_info等类型的常量。与JVM实现的内存布局无关,引用的目标也不一定加载到内存。虚拟机规范的Class文件中明确规定了各种符号引用的字面量。

直接引用,可以是直接指向目标的指针、相对偏移量或者能间接定位到目标的句柄。如果有了直接引用,那么引用的目标一定已经存在于内存中。

解析动作主要针对类或接口(对应常量池的CONSTANT_Class_info)、字段(对应常量池的CONSTANT_Fieldref_info)、类方法(对应常量池的CONSTANT_Methodref_info)、接口方法(对应常量池的CONSTANT_InterfaceMethodref_info)、方法类型(对应常量池的CONSTANT_MethodType_info)、方法句柄(对应常量池的CONSTANT_MethodHandle_info)、调用点限定符(对应常量池的CONSTANT_InvokeDynamic_info)7类符号引用进行。

8 初始化阶段

类加载的最后一步,前面的步骤除了类加载阶段用户可以通过自定义类加载器参与之外,其余动作都是JVM主导。该阶段,才真正执行类中定义的代码。

初始化阶段是类构造器<clinit>()执行的过程。该方法是由编译器自动收集类中的类变量的赋值动作和静态语句块中的语句合并(按照出现顺序合并)产生。JVM保证子类<clinit>()方法执行之前,父类<clinit>()方法已经执行完毕(执行接口的该方法不需要先执行父接口的该方法。只有当父接口定义的变量使用的时候,父接口才会初始化。接口的实现类的初始化的时候也不会执行接口的<clinit>()方法)。如果一个类中没有对静态变量的赋值也没有静态语句块,编译器可以不为这个类生成<clinit>()方法。

必须对类进行初始化的(有且仅有)5种情况

1、遇到new、getstatic、putstatic或invokestatic这4条字节码指令时。

    生成这4条指令的最常见场景是:

  •         使用new实例化对象时;
  •         读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)时;
  •         调用一个类的静态方法时;

2、使用java.lang.reflect包的方法对类进行反射调用的时候。

3、初始化类的父类如果没有进行初始化,则先触发器父类的初始化。(对于接口,不要求其父接口全部都初始化,只有在真正使用到父接口的时候,比如引用接口中定义的常量,父接口才会初始化)

4、虚拟机启动时,用户指定的执行主类(包含main()方法的类)会先被初始化。

5、使用JDK1.7时,一个java.lang.invoke.MethodHandle实例最后解析的结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄时。

此5种情况下的行为称为对一个类进行主动引用。其余所有情况都不会触发类的初始化,称为被动引用

典型的不会执行类的初始化情况

  1. 通过子类引用父类的静态字段,不会导致子类初始化。(子类被加载,父类被初始化)。
  2. 通过数组对象引用类,不会触发此类的初始化。(会触发虚拟机自动生成的对应的引用类的数组类发生初始化)
  3. 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,不会触发定义常量的类的初始化。
  4. 通过类名获取Class对象,不会触发类的初始化。
  5. 通过Class.forName加载指定类,如果指定参数initialize为false的时候,不会触发类的初始化。
  6. 通过ClassLoader默认的loadClass方法时,不会触发类的初始化。

9 OSGI-灵活的类加载器架构(动态模型系统)

OSGi(Open Service Gateway Initiative),是面向Java的动态模型系统,是Java动态化模块化系统的一系列规范。

动态改变构造

OSGi服务平台提供在多种网络设备上无需重启的动态改变构造的功能。为了最小化耦合度和促使这些耦合度可管理,OSGi提供一种面向服务的架构,它能使这些组件动态地发现对方。

模块化编程与热插拔

OSGi旨在为实现Java程序的模块化编程提供基础条件,基于OSGi的程序很可能可以实现模块级的热插拔功能,当程序升级更新的时候,可以只停用、重新安装然后启动程序的其中一部分,这对企业级程序开发来说非常具有诱惑力。

OSGi实现模块化热部署的关键是其自定义类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉。

模块(Bundle)和普通Java类库类似,存储的是java的包和class,一般以JAR格式封装,每个模块可以通过Export-Package描述声明导出发布的Package,也可以通过Import-Package声明所依赖的Package。这样,根据Package导入导出定义就可以构造Bundle间的委派和依赖。

在OSGi环境中,类加载器不再是双亲委派模型中的树状结构,而是复杂的网状结构,当收到类加载请求的时候,OSGi将按下面的顺序进行:

  1. 以java.*开头的类,委派给父类加载器加载。
  2. 否则,委派列表名单内的类,委派给父类加载器加载。
  3. 否则,Import列表中的类,委派给Export这个类的Bundle的类加载器加载。
  4. 否则,查找当前Bundle的Classpath,使用自己的类加载器加载。
  5. 否则,查找是否在自己的Fragment Bundle中,如果是,则委派给Fragment Bundle的类加载器加载。
  6. 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
  7. 否则,类查找失败。

并非所有的应用都适合采用OSGi做为基础架构。OSGi在提供强大功能的同时,引入了额外的复杂度,带来了线程死锁和内存泄漏的风险。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值