JVM之类加载机制

一、类加载机制

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

类加载的时机

那么类到底什么时候会加载呢?(初始化)
当对一个类进行主动引用的时候

主动引用

主动引用有5种情况:

  1. new实例化对象、读取或设置一个类的静态字段时、调用一个类的静态方法时
  2. 使用java.lang.reflect包的方法对类进行反射调用时
  3. 初始化一个类时,父类还没初始化,则先触发父类的初始化
  4. 虚拟机启动时,初始化包含main()方法的主类
  5. 使用JDK1.7的动态语言支持的时候,如java.lang.invoke.MethodHandle实例
被动引用

被动引用有以下几种情况:

  1. 子类调用父类的静态变量,子类不会被初始化,只有父类会被初始化。(只有定义静态字段的类才会被初始化)
  2. 通过数组来定义引用类,不会初始化
  3. 访问类的final常量(编译期静态常量),不会初始化
  4. 通过类名获取Class对象,不会触发类的初始化(其余两种getClass()和Class.forName()会)
    1
    2
    //类名.class获取Class对象
    Class clazz = A.class;

二、类加载过程

类的生命周期

image

类的生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。 验证、准备、解析这三个阶段称为连接。
解析阶段的顺序不是确定的,可以在初始化之后开始。

类加载过程包括加载、验证、准备、解析、初始化

加载

JVM完成以下3件事:

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

注:

  • 数组类本身不通过类加载器创建,由虚拟机直接创建
  • 内存中实例化的Class对象并不是在堆中,存放于方法区(HotSpot虚拟机)

    验证

    验证的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求。
    主要完成以下4个检验动作:
  1. 文件格式验证:如魔数、版本号、常量类型….
  2. 元数据验证:这个类是否有父类?是不是抽象类?是否继承了不允许被继承的类?
  3. 字节码验证:对类的方法体进行校验分析,确定程序语义是合法、符合逻辑的。
  4. 符号引用验证:(发生在解析阶段)符号引用中通过字符串描述的全限定名是否能找到对应的类?…

验证阶段是一个很重要但不一定是必要的阶段。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。(这里的类变量仅为Static修饰的变量,不包括实例变量,实例变量是在对象实例化时随着对象分配在堆中)

假设一个类变量定义为:

1
public static int value = 666;

那么在准备阶段过后,初始值为0,而不是666,因为把value赋值为666的动作在初始化阶段才会执行。

但是下面这种情况除外:

1
public static final int value = 666;

当用final修饰时,会在准备阶段赋值为666。

解析

解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。

符号引用:定义在Java虚拟机规范的Class文件格式中(与虚拟机内存布局无关)
直接引用:直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。(与虚拟机内存布局相关)

初始化

类初始化阶段是类加载过程的最后一步。初始化阶段才真正开始执行类中定义的Java程序代码。

在准备阶段,类变量被赋了初始值,在初始化阶段才会真正赋值。或者说,初始化阶段是执行类构造器()方法的过程。

()方法
  • ()方法是编译器自动收集类中的所有变量的赋值动作和静态语句块(static{})中的语句合并产生的,顺序由源文件顺序决定。
  • ()方法与类的构造函数不同(实例构造器()),它不需要显示地调用父类构造器,虚拟机保证子类()方法执行之前,父类()方法已经执行完毕。因此,JVM中第一个执行的()方法的类是java.lang.Object。
  • ()方法对于类或接口来说不是必须的,一个类没有静态语句块或者没有对变量的赋值操作,可以没有()方法
  • 接口中有变量初始化赋值的操作时,会生成()方法。但接口不需要执行父接口的()方法。接口的实现类在初始化时也不要=会执行接口的()方法。
  • 虚拟机保证一个类的()方法会在多线程情况下加锁、同步。只有一个线程会执行()方法成功。

三、类加载器

在加载阶段中,“通过一个类的全限定名来获取定义此类的二进制字节流”这个动作的实现是由类加载器来完成的。
这个动作在JVM外部实现,以便让应用程序自己决定如何去获取所需要的类。

简而言之,类加载器用于实现类的加载动作。

只有由同一个类加载器加载的两个类才相等。若同一个Class文件由不同的类加载器加载,则不相等。

3种类加载器

image

  • 启动类加载器(Bootstrap ClassLoader):负责加载\lib目录中,或者被-Xbootclasspath参数所指定的路径中,并且被虚拟机识别的如rt.jar等类库
  • 扩展类加载器(Extension ClassLoader):加载\lib\ext目录中的,或通过java.ext.dirs系统变量指定路径中的类库
  • 应用程序类加载器(Application ClassLoader):加载用户类路径(ClassPath)上指定的类库,也叫系统类加载器,程序默认的类加载器

双亲委派模型

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

双亲委派模型有什么好处?

保证了Java程序的稳定运行。
例如,java.lang.Object存放在rt.jar中,无论哪一个类加载器加载这个类,最终都会委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。