类加载过程

在编写代码时,编辑的文件名后缀为.java,我们需要将其编译成.class为后缀的文件,之后加载到虚拟机中才能被运行和使用。

那么类是如何被加载的呢?

类的加载过程一共分为7个阶段,如下图:

加载

首先,虚拟机需要通过类的全限定名才能获取到定义此类的二进制字节流。
然后,将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构。
最后,在内存中生成一个代表该类的java.lang.Class对象,作为这个类在方法区的各种数据的访问入口。

虚拟机规范其实相对比较宽泛,并没有强制规范“二进制字节流一定要从.class文件中读取”,所以,也可使用很多其他的方式,诸如 压缩包内读取、网络上获取、其他文件生成、数据库中读取、运行时计算生成等。

验证

验证的目的主要是为了校验.class文件中获取的二进制字节流中包含的信息是否满足当前虚拟机的要求,并且不会对当前虚拟机产生安全方面的危害。

主要包含4个方面的验证:

  1. 文件格式验证(是否以魔数0xCAFEBABY开头、主次版本号是否在当前虚拟机处理范围内等)
  2. 元数据验证(是否有父类、父类是否继承了final修饰的类、如果非抽象类是否实现了其父类或者接口中所有要求的方法、是否重写了父类中final修饰的方法等)
  3. 字节码验证(通过数据流和控制流分析,判定程序语义是否是合法的、符合逻辑的;对方法体进行校验分析,却表被校验的类的方法在运行时不会做出危害虚拟机的安全事件。)
  4. 符号引用验证(符号引用中能否通过字符串描述的全限定名寻找到对应类;在指定的类中是否存在符合方法的字段描述以及简单名称所描述的方法和字段;符号引用中的类、字段、方法的访问性是否允许被当前类访问;)

准备

在这一阶段中,将正式为类变量分配内存,并设置其初始值。

注意

所谓的类变量,仅指被“static”修饰的变量,而不包括实例变量。

解析

在这一阶段中,虚拟机将常量池内的符号引用将转化为直接引用。

主要针对7类符号引用:

  1. 类及接口
  2. 字段
  3. 类方法
  4. 接口方法
  5. 方法类型
  6. 方法句柄
  7. 调用点限定符

初始化

Java虚拟机规范中强制定义了有且仅有5中情况必须立即对类进行初始化:

  1. 遇到了new、getstatic、putstatic、invokestatic4条字节码指令时
  2. 使用java.lang.reflect包的方法对类进行反射调用时
  3. 初始化一个类时,其父类尚未被初始化,需要先触发其父类的初始化
  4. 虚拟机启动时,用户需要指定一个要执行的主类时(即main()方法的所在类),虚拟机会先初始化这个类。
  5. 使用JDK 1.7的动态语言支持时

到了这一步,才真正开始执行类中定义的Java程序代码。
通俗来说就是执行类构造器<clinit>()方法的过程。

双亲委派模型

对于Java虚拟机的角度来讲,只存在两种不同的类加载器:

  • Bootstrap ClassLoader(启动类加载器),由C++实现,是虚拟机自身的一部分
  • 所有其他的类加载器,都是有Java语言实现,独立于虚拟机外部,并且都继承自抽象类java.lang.ClassLoader

但是对于Java程序员来说,可以更细致一些:

  • Bootstrap ClassLoader(启动类加载器) :主要负责将存放于 JAVA_HOME/lib 目录下的,并且能够按文件名被虚拟机识别的类库加载到虚拟机中。
  • Extension ClassLoader(拓展类加载器) :主要负责将存放于 JAVA_HOME/lib/ext 目录下的类库加载到虚拟机中。
  • Application ClassLoader(应用类加载器) :主要负责将存放在用户路径(ClassPath)下的类库加载到虚拟机中。

此三者类加载器互相配合进行加载,具有层级关系

最后,程序员们可以在应用类加载器之下自定义类加载器。

双亲委派模型工作过程:
类加载器收到了类加载委托请求,它首先不会去尝试自己加载该类,而是委派给父类加载器去加载,如果父类加载器在其搜索范围内没有找到所需的类而无法完成这个加载请求时,子类加载器才会尝试由自己去加载。