虚拟机类加载机制

875 阅读6分钟

日常开发中,我们写的Java代码是一个个.java文件,每个.java文件就是一个类(class)。这些文件都需要经过编译,才能成为计算机可运行的.class文件。这些class文件在程序运行时是怎么运行的呢?我们就来一探究竟!
Java语言中,类的加载、连接和初始化均在程序运行时完成,这样虽然比较耗性能,但是提高了程序的灵活性。比如,一个接口的实现可以等到运行时才指定;用户可以自定义类加载器来加载二进制流作为程序的一部分。

class文件 全名称为Java class文件,主要在平台无关性和网络移动性方面使Java更适合网络。它在平台无关性方面的任务是:为Java程序提供独立于底层主机平台的二进制形式的服务。

这些编译后的.class文件为程序的提供服务,什么时候提供服务?需要经历那些过程呢?

类的加载时机

  • 类加载到内存,再卸载出内存,整个生命周期包括:
    • 加载(Loading
    • 验证(Verification
    • 准备(Preparation
    • 解析(Resolution
    • 初始化(Initialization
    • 使用(Using
    • 卸载(Unloading

虚拟机规范严格规定了以下5中情况必须对类初始化:
1)遇到newgetstaticputstaticinvokestatic4条字节码指令时,触发初始化(加载、验证、准备均在此之前)。 new - 使用new关键字实例化对象时
getstatic- 读取一个类的静态字段
putstatic- 设置一个类的静态字段
invokestatic- 调用一个静态方法
2)使用反射API对类进行反射调用
3)子类初始化需先初始化父类
4)虚拟机启动需要指定一个主类(包含main方法)
5)当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类未初始化,则先初始化。

以上5中行为称为对类进行主动引用 ;其他情况都为被动引用

类加载过程

加载

虚拟机主要完成以下3方面:

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

验证

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

文件格式验证

验证字节流是否符合Class文件的格式规范,并且能够被当前版本的虚拟机处理

元数据验证

对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息 (元数据(其实就是类)数据类型校验)

字节码验证

通过数据流和控制流分析,确定语义是合法的、符合逻辑的 (类的方法体进行校验分析)

符号引用验证

对类自身以外(常量池中各种符号引用)的信息进行匹配性校验

准备

正式为类变量分配内存并设置类变量初始值的阶段,所有的变量使用的内存都在方法区进行分配。

  1. 这时进行内存分配的的仅包括类变量(即static修饰的变量),而不包括实例变量,实例变量会在对象实例化时随着对象一起分配在Java堆中。
  2. 这里的初始值是数据类型的零值。变量的赋值在初始化阶段才执行。
  3. 如果类字段的字段属性表中存在ConstantValue属性(类属性被final修饰),那么在准备阶段就会被初始化为ConstantValue属性所指定的值。

解析

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

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

初始化

类初始化阶段是类加载过程的最后一步。类加载过程中,用户程序可以通过自定义类加载器参与之外,基本都是虚拟机主导和控制的。初始化阶段才开始执行.class文件。 准备阶段,变量已经初始化赋值一次(仅仅是static修饰的变量),初始化阶段则按照程序员的主观计划(.java生成的.class文件)进行变量及其他资源的初始化。 另外一个角度阐述:初始化阶段是执行类构造器<clinit>()方法的过程。

  • <clinit>()方法是虚编译器自动收集类中所有类变量的赋值动作静态代码块中的语句合并产生的。编译器的收集顺序是按照源文件的代码顺序进行收集的。静态语句块中的只能访问到静态语句块之前的变量,不能访问到之后的变量;但是可以给定义在之后的变量赋值。
  • <clinit>()方法与<init>()方法不同,虚拟机会保证父类的<clinit>()会在子类之前进行,故不需要像<init>()方法一样显示调用父类的<clinit>()方法
  • <clinit>()方法不是必须的,如果类或接口没有静态语句块或者变量赋值操作,则不生成此方法。

使用

卸载

类加载器

"通过一个类的全限定名来获取描述此类的二进制字节流",让程序自己决定如何获取所需要的类。完成这个功能的代码块称为"类加载器"。

类加载器的分类

Java虚拟机的角度


Java开发人员角度

类加载器双亲委派模型

应用程序都是由启动类加载器、扩展类加载器、应用程序类加载器,也有可能有自定义类加载器相互配合完成类的加载。


双亲委派模型(Parents Delegation Model)工作过程:
每个类加载器都是"啃老族",收到类加载的请求后,自己并不获取尝试加载类,而是将这个请求委派给自己的父类加载器(这里的父类不是继承的关系,而是以组合的关系复用父加载器的代码)去完成,每一个层次的类加载器均如此,直至传递至启动类加载器。如果父类加载器实在无法完成请求,子类才会自己尝试去加载。

双亲委派模型并不是强制性的约束模型,而是推荐的类加载器实现方式。

以上内容来自《深入理解Java虚拟机》第2版 周志明(著)学习笔记。如有不完善之处,请不吝赐教!