虚拟机类加载机制

之前讲解了Java将代码编译成.class文件格式进行存储,class文件描述的各种信息,而这些信息都要最终加载到虚拟机中才能被运行和使用,虚拟机会将class文件到内存,并对数据进行校验,转换解析和初始化,最终形成能被虚拟机直接使用的Java类型。像C++这样的语言,连接阶段是在编译时完成的,而Java则是在程序运行时完成的,这样可提供较高的灵活性,具有动态拓展和动态连接。

类加载的时机

一个类,什么时候被JVM加载到内存中?

答:只有在第一次主动使用时,才会被加载。import不会发生加载。主动使用包括:

  1. 使用new关键字创建类的对象

  2. 使用类的静态成员(调用或赋值);调用类的静态方法

  3. 反射机制时,如ClassForName

类在内存中的生命周期可分为:加载,验证,准备,解析,初始化,使用,卸载这7个步骤。其中,验证,准备和解析三个部分统称为连接。

加载

在加载阶段,虚拟机需要完成三件事:

  • 通过一个类的全限定名来获取定义此类的二进制字节流

  • 将这个字节流所代表的静态存储记过转化成方法区的运行时数据结构

  • 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

这里的二进制字节流不一定从.class文件获取,也可以从Jar包,网络(applet)和其他文件(jsp)获取。加载时通过类加载器完成的。

验证

验证是连接的第一步主要保证字节流满足虚拟机的要求,不会影响虚拟机的安全。验证阶段在类加载子系统中占用了很大一部分,一般的虚拟机都会实现四个阶段的验证。

  • 文件格式验证:是否以魔数开头;主次版本号是够在虚拟机处理范围内;常量池的常量tag标志是否在范围内;索引的指向是否有效;CONSTANT_Utf8_INFO是否符合编码要求等

  • 元数据验证:语义分析要求符合Java规范,例如:类是否有父类;是否继承了不允许的父类;类的字段,方法是否和父类产生矛盾

  • 字节码验证:进行数据流和控制流分析,例如,类型转换是否合法,非法跳转(跳转到方法以外)等

  • 符号引用验证:确保解析动作能够正常执行,例如通过全限定名能够找到对应的类

在实施阶段就可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备

为类的变量分配内存并设置变量的初始值,这些内存都是在方法区的进行分配的,这些变量仅包括类变量(被static修饰的变量),不包括实例变量,通常情况下都是赋给对应数据类型的零值。我的理解是这里仅仅是给类变量分配内存空间而已。如public static in abc = 123, abc在方法区中被设置为0. 但是如果是常量变量,例如public static final abc = 123; ,则会立刻赋值给123,这也说明了static final变量必须在定义时初始化。

解析

将虚拟机中的常量池内的符号引用替换成直接引用,主要针对类或接口,字段,类方法,接口方法四类符号。

初始化

什么时候需要对类进行初始化?虚拟机规定了有且仅有四种情况必须对类进行初始化。

  1. 遇到new、获取静态变量(final常量除外)、为静态变量赋值以及调用静态方法时,如果类没有进行过初始化,则需要先触发其初始化。

  2. 使用java.lang.reflect包的方法对类进行反射调用的时候(Class.forName(…)),如果类还没有初始化,需要先触发对其的初始化。

3.当初始化一个类的时候,如果发现其父类还没有初始化,则需要先触发对其父类的初始化。

4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。

执行类构造器,初始化静态变量,静态块中的数据(一个类只会初始化一次),如在准备的阶段赋值的abc,这这个阶段被赋值为123.那么这个clinit方法是什么呢?它是有编译器自动收集类中所有的类变量的赋值动作和静态语句块(static{}块)合并产生的方法,顺序就是定义的顺序,并且保证,父类的clinit优先于子类的执行。并且clinit能够保证多线程同步。

这里补充一个概念的比较,类的初始化对象的实例化

类的初始化只会被执行一次,就是类被加载到虚拟机当中的时候,主要是对类变量(静态变量)赋初始值,函数的执行,类生命周期的一个阶段。而对象的实例化则是创建对象时发生的,过程会执行很多次构造器的调用。

使用

类的生命周期中绝大部分时间都处在被使用中,类的实例在堆中,类成员变量也在堆中,static变量,方法代码和方法表等在方法区中。每个类在堆中海油唯一的一个Class对象,存储着类信息,它作为对象访问方法区的入口如存在。

Class对象

每个类,虚拟机都会维护一个java.lang.Class对象,它在编译时保存在.class文件当中。在类加载过程中,类加载器中的defineClass 方法自动构造,它被加载到堆中。它保存着创建类对象的信息,也是对象实例连接方法区的桥梁。基本类型,数组同样有Class对象

那么,如何得到一个类的class对象呢?

  1. 对象的getClass()方法

如:

1
2
Cat t = new Cat();
Class clazz = t.getClass();

  1. Class类的中静态forName()方法

如:

1
Class clazz = Class.forName("java.util.List");
  1. 类型的class属性

如:

1
Class clazz = Ojbect.class;

有了类的Class对象,可以调用它的一些方法,如: clazz.getName();clazz.newInstance();clazz.getClassLoader()

卸载

当某个类不在需要时,在特定情况下会被卸载。特定情况必须是:该类所有的实例都被回收,该类的类加载器不可达。这样,当内存不足是,垃圾回收器会回收对应的Class对象和该类方法区的内存。

类加载器(ClassLoader)

通过一个类的全限定名来获取描述此类的二进制字节流。类加载器在上述流程中的加载阶段。

启动类加载器(Bootstrap ClassLoader)

这个类加载器负责将存放在\lib目录中的,或者被-Xbootclasspath参数指定的路径的类库加载至虚拟机内存中,由C++编写,无法被Java程序直接引用

拓展类加载器(Extension ClassLoader)

ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将jdk中jre/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器

应用程序类加载器(Application ClassLoader)

系统类加载器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径java -classpath或-Djava.class.path变量所指的目录下的类库加载到内存中。开发者可以直接使用系统类加载器。通过ClassLoader的getSystemClassLoader()方法的返回值,用户编写的类,默认是使用该类作为默认类加载器。

双亲委派机制

双亲委派模型要求除了顶层的启动类加载器,其余的类加载器都应该有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承关系来实现,而是使用组合关系来复用父加载器的代码。

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

优点:

  • 使得类具有一定优先级的层次关系,例如,Object类在rt.jar中,那么无论是哪一个类记载其要加载它,都会委派给启动类加载器加载,保证在各类加载器中得到同一个类

  • 防止内存中出现多份同样的字节码,相同的类加载器,不会加载同一个类两次,而不同的类加载器,会加载同一个类,从而有两份相同字节码的类

破坏双亲委派机制

  1. JDK 1.2发布之前:压根就没双亲委派模型,但是classloader在JDK 1.0就存在了。所以引入双亲委派模型也做一点妥协,具体的不用关心。反正现在出现的概率为0了

  2. 自身缺陷:双亲委派机制解决了各类加载器的基础类统一的问题,这些基础类,它们一般会作为被用户代码所调用的API,但如果是返回来,用户类提供API,供给基础类调用呢?比如JNDI(Java Naming Directory Interface),它是由启动类加载器加载的,目的是对资源进行集中的管理和查找,它需要调用独立厂商在ClassPath下的JNDI提供者的代码,因此,启动类加载器无法委托其他加载器去加载用户代码,因为它无法委托子加载器。 为此虚拟机团队设计了一个无奈之举:线程上下文类加载器。包括最常用的JDBC也是使用线程上下文类加载器。

  3. 动态替换:也就是所谓的热部署,典型就是OSGi

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。

针对第二点,一些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由引导类加载器来加载的;SPI的实现类是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。

而线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。首先,科同过java.lang.Thread类的setContextClassLoader()方法进行设置,JNDI服务科使用上下文类加载器加载所需要的SPI代码,父类加载器请求子类加载器完成加载动作。