引言

  上一篇文章中,对于JVM做了一个基础的概念性介绍,远远达不到需要我们掌握的程度,在上篇文章的末尾我给出了虚拟机的结构图。今天我们就来深入学习一下第一部分,类加载子系统(Class Loader SbuSystem)。为了便于阅读,我将类加载子系统的图放到了这里,便于阅读和理解。

  图中我再加载部分多画了一个虚线框的用户自定义类加载器,这个后面会做解释。同时在外部添加了使用和卸载两部分,主要是为了向展示类的生命周期,但是实际上他们不是类加载子系统中的部分。

类的生命周期

  类从被加载到虚拟机内存中开始,一直到卸载出内存为止,整个生命周期如下图所示。其中验证、准备、解析三个阶段统称为连接阶段。在这个生命周期中,加载->验证->准备->初始化->卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序开始(通常是交叉的混合式进行的)。你可能已经发现,我少说了一个解析阶段,这个阶段的顺序有时候不是按照图中的顺序进行的,有时候会在初始化阶段开始之后再解析,这是为了支持Java的动态绑定特性。

  图中我加了几种语言如Groovy,JRuby等其他的语言,这是为了说明运行在JVM上的不一定是Java语言,也可能是其他语言文件转换成的.class文件。Java虚拟机在JDK1.7-1.8时候,已经支持了语言无关。总之就是JVM接收的.class文件不一定就仅仅是Java语言。

类加载过程

  类的加载过程包含了加载,连接,初始化三个过程。类加载器通过一个类的“全限定名”来获取描述此类的二进制字节流,全限定名即:包名+类名+类加载器ID。每一个类加载器都有一个独立的命名空间,所以比较两个类相等,是建立在通一个类加载器的前提下的。关于类加载器后面会专门做介绍,下图是类的加载过程。

  • 加载: 如图中所示,加载阶段虚拟机需要做三件事:
  • 1、通过一个类的全限定名来获取;
  • 2、将这个字节流所代表的静态存储结构(我理解的是类和它包含结构)转换为方法区运行时数据结构;
  • 3、在内存中生产一个代表这个类的java.lang.Class对象,作为方法区这个类各种数据的访问入口。

  • 连接: 连接阶段又细分为了下面的三个小阶段:
  • 验证:

    验证字节流是否符合Class文件格式规范;对字节码描述的信息进行语义分析,保证符合Java语言的规范;对类的方法等进行校验;对于符号引用转为直接引用时的校验(这个转化动作发生在解析阶段)。

  • 准备:

    正式为类分配内存并设置类变量初始值,这些变量所使用的内存都将在方法区中进行分配。这里的内存分配仅包含被static修饰的类变量,不包括实例变量,实例变量将在对象实例化的时候随对象分配在Java堆。这个所说的初始值通常是指0,例如定义了一个变量:public static int value =10,那么value在准备阶段初始化的值是0,而不是10,赋值为10的阶段是在初始化的时候进行的。

  • 解析:

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

  • 初始化: 初始化是类在加载过程的最后一步,前面的步骤中,除了加载阶段用户应用程序可以自定义类加载器之外,其余动作全部由虚拟机主导和控制。

类的加载时机

  虚拟机对于类何时加载没有做严格的约束,但是对于初始化阶段,虚拟机严格约束了“有且仅有”下列5中情况必须立即对类进行初始化(在初始化之前肯定是要进行类的加载,验证,准备的):

  • 遇到new、get static、put static、invoke static 这4条字节码指令时,如果类没有进行初始化,则需要先对其进行初始化。通俗一点说就是,用new实例化一个对象的时候,读取或设置静态字段时(除了final修饰的常量),调用一个类的静态方法时,都会触发初始化操作。
  • 使用java.lang.reflect包的方法对类进行反射调用时,如果类没有初始化,需要先对其进行初始化操作。
  • 初始化一个类时,如果其父类还没有初始化,需要先触发其父类初始化。
  • 当虚拟机启动时,用户需要指定一个执行的主类(包含了main()方法的那个类),虚拟机会先初始化这个主类。
  • 使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStaticREF_putStaticREF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先进行初始化。

以上5中情况,成为对一个类进行主动引用。除此之外,所有的引用类的方式都不会触发初始化,这叫被动引用。下面举例子说明什么是被动引用。

1. 子类引用父类的静态字段,不会导致子类初始化。对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过子类引用父类中的静态字段,不会被初始化。下面的例子会输出“Super class init!”,而不会输出“Sub class init!”

public class SuperClass {
  static{
    System.out.println("Super class init!");
  }
  public static int vale = 123;
}

public class SubClass {
  static{
    System.out.println("Sub class init!");
  }
}

public class NotInitialClass {
  public static void main(String[] args){
    System.out.println(SubClass.value);
  }
}

2. 通过数组引用类,也不会触发类的初始化。数组类本身不通过类加载器创建,它是由Java虚拟机直接创建,但是数组类与类加载器由密切的关联,因为数组的元素类型,最终需要类加载器去创建。下面的代码复用了上面例子中的代码

public class NotInitialClass {
  public static void main(String[] args){
    SuperClass[] array = new SuperClass[10];
  }
}

3. 常量在编译阶段会存入调用类的常量池,本质上没有直接引用到定义常量的类,所以也不会触发初始化操作。

public class ConstantClass {
  static {
    System.out.println("ConstantClass init!");
  }
  public static final String CONS_FIELD = "test const";
}

public class NotInitialClass {
  public static void main(String[] args){
    System.out.println(ConstantClass.CONS_FIELD);
  }
}

类加载器

  从开篇的图中我们可以看到类加载的时候包含了Bootstrap ClassLoaderExtension ClassLoaderApplication ClassLoader三种类加载器。从虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器由c++实现是虚拟机自身的一部分;另一种就是其他所有的类加载器,这些都是由java语言实现,独立于虚拟机外部,继承自抽象类java.lang.ClassLoader

  • Bootstrap ClassLoader: 负责加载存放在\lib目录中的类库加载到虚拟机内存中,或者是加载被`-Xbootclasspath`参数所指定的路径中,且被虚拟机识别的类库加载到虚拟机中(仅按照文件名识别,如:rt.jar,名字不符合的即时放在\lib目录下也不会被加载)。
  • Extension ClassLoader: 这个类加载器由sun.misc.launcher$ExtClassLoader实现,负责加载\lib\ext目录中的类库,或者被`java.ext.dirs`系统变量所指定的路径中的所有类库。
  • Application ClassLoader: 这个加载器由sun.misc.launcher$AppClassLoader实现,负责加载用户类路径(classpath)上指定的类库。一般程序中默认就是用的这个。由于这个加载器是ClassLoadergetSystemClassLoader()方法的返回值,所以也叫做系统类加载器。

双亲委派模型

  所谓双亲委派模型就是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是将其委派给父类加载器去完成,每一层次的类加载器都是如此,因此所有的加载请求最终应该传递到顶层的启动类加载器(Bootstrap ClassLoader)中,只有当父类反馈自己无法加载时,才有子加载器尝试自己去加载。其工作流程如如下所示:

总结

  这篇文章可能需要结合下一篇虚拟机运行时数据区进行结合着看,对于运行时的堆,方法区,虚拟机栈等都将在下一篇中进行介绍。

参考

1.《深入理解JVM(二)》
2.博客:https://blog.csdn.net/isunn/article/details/50493852