虚拟机类加载机制

来源:互联网 发布:汽配软件 编辑:程序博客网 时间:2024/06/16 15:11

虚拟机类加载机制

@(jvm)[java, jvm, 虚拟机执行子系统]

  • 虚拟机类加载机制
  • 类加载的时机
  • 类加载的过程
    • 1 加载
    • 2 验证
    • 3 准备
    • 4 解析
    • 5 初始化
  • 类加载器
    • 1 类与类加载器
    • 2 双亲委派模型
    • 3 双亲委派模型的破坏

1. 类加载的时机

类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期包括加载、验证、准备、解析、初始化、使用、卸载,其中验证、准备、解析统称为连接
解析阶段在某些情况下可以在初始化阶段后再开始,这时为了支持Java语言的动态绑定
这些阶段会依次开始,但是不是依次进行

以下5种情况立即对类进行初始化:—-主动引用

  1. 使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候
  2. 使用java.lang.reflect包的方法对类进行反射调用
  3. 当初始化一个类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类)
  5. 当使用JDK1.7 的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic, REF_putStatic, REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化

被动引用:此时不会触发类加载

  1. 通过子类引用父类的静态字段,不会导致子类的初始化。对于静态字段,只有直接定义这个字段的类才会被初始化
  2. 通过数组定义来引用类,不会触发此类的初始化
  3. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。因为在编译阶段通过常量传播优化,已经将其他类的常量放在了本类的常量池中

对接口触发初始化的情况与类基本相同,不同点在上述第3种:
当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父类接口全部都完成了初始化,只有在真正使用到父接口的时候才会初始化

2. 类加载的过程

2.1. 加载

加载阶段jvm需要完成3件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
    获取途径:从zip(如jar,ear, war)包,网络中(Applet),运行时计算生成(动态代理)。。。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

一个非数组类的加载过程可控性很强,可以使用系统提供的引导类加载器完成,也可以由用户自定义的类加载器完成(重写一个类加载器的loadClass())
一个数组类本身不通过类加载器创建,而是由java虚拟机直接创建

加载完成后,jvm外部的二进制字节流就按照jvm所需的格式存储在方法区中,然后在内存中实例化一个java.lang.Class类的对象。(并没有明确在java堆中,对于HotSpot虚拟机来说,Class对象比较特殊,它虽然是对象,但是存放在方法区中)

2.2. 验证

这一阶段的目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全,验证阶段的工作量在JVM的类加载子系统中占了相当大的一部分。

  1. 文件格式验证
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证

2.3. 准备

这一阶段的目的是正式为类变量分配内存并设置类变量初始化,这些变量所使用的内存都将在方法区分配
注意:是为静态变量分配内存,不包括实例变量,此时初始化的值为0
`public static final int a = 1234;对于static final类型的变量直接初始化为设定的值。

2.4. 解析

这一阶段的目的是JVM将常量池内的符号引用替换为直接引用的过程

符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要无歧义即可
直接引用:可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄

解析动作主要针对**类、接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符等**7类符号引用

2.5. 初始化

这一阶段根据程序员通过程序制定的主观计划去初始化变量和其他资源,即初始化阶段是执行类构造器<clinit()>方法的过程

<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
<clinit>()方法不需要显式地调用父类构造器,JVM会保证子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。
由于父类的<clinit>()方法先执行,意味着父类中定义的静态语句块要优先于子类的变量赋值操作
<clinit>()对于类或接口不是必需的,如果类中没有静态语句块和赋值操作,就不会生成<clinit>()方法
接口中不能使用静态语句块,但任然有变量初始化的赋值操作,所以接口也会生成<clinit>()方法。但是接口不需要先执行父接口的<clinit>()方法,只有当父接口中定义的变量使用时父接口才会初始化。
JVM会保证一个类的<clinit>()在多线程环境中被正确的加锁、同步,如果多个线程同时初始化一个类,那么只有一个线程会执行,其他会挂起。

3. 类加载器

定义:JVM设计团队把类加载阶段中“通过一个类的全限定名来获取描述此类的二进制字节码”这个动作放到JVM外部去实现,以便让应用程序自己决定如何去获取所需要的类。

3.1. 类与类加载器

类加载器作用:
1. 类加载阶段加载类
2. 类加载器和类本身一同确立其在JVM中的唯一性

判断两个类是否相等:只有这两个类是由同一个类加载器加载的前提下才有意义,否则即使两个类来源于同一个Class文件,被同一个JVM加载,只要加载它们的类加载器不同,那这两个类就必定不相同。

3.2. 双亲委派模型

从jvm的角度来说,只存在两类加载器,一类是由c++实现的启动类加载器,是jvm的一部分,一类是由java语言实现的应用程序加载器,独立在jvm之外,并且全都继承自抽象类java.lang.ClassLoader

从开发人员的角度,有3中系统提供的类加载器:
1. 启动类加载器(Bootstrap ClassLoader):由C++代码实现,负责加载存放在%JAVA_HOME%\lib目录中的,或者通被-Xbootclasspath参数所指定的路径中的,并且被java虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库,即使放在指定路径中也不会被加载)类库到虚拟机的内存中,启动类加载器无法被java程序直接引用。
2. 扩展类加载器(Extension ClassLoader):由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
3. 应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现,负责加载用户类路径classpath上所指定的类库,是类加载器ClassLoader中的getSystemClassLoader()方法的返回值,开发者可以直接使用应用程序类加载器,如果程序中没有自定义过类加载器,该加载器就是程序中默认的类加载器

双亲委派模型:除了顶层的启动类加载器外,其余所有的类加载器都应当有自己的父类加载器。这里类加载器的父子关系是使用组合关系来复用父加载器的代码

工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送给顶层的启动类加载器,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

好处:Java类随着它的类加载器一起具备了一种带有优先级的层次关系。—以Object举例

实现代码:位于java.lang.ClassLoader的loadClass()中。

3.3. 双亲委派模型的破坏

  1. 如果定义自己的类加载器时直接覆盖loadClass()方法,会破坏双亲委派模型,建议覆盖findClass()中。
  2. 基础类要调用会用户的代码,会破坏双亲委派模型。由此引入线程上下文类加载器(Thread Context ClassLoader),让父类加载器请求子类加载器去完成类加载的动作。典型例子:JNDI, JDBC, JCE等。
  3. 用户堆程序动态性的追求,如代码热替换、模块热部署等。
原创粉丝点击