类加载(装载)过程

Java虚拟机通过装载连接初始化一个Java类型,使该类型可以被正在运行的Java程序所使用。
Java类型要么由启动类装载器装载,要么通过用户自定义类装载器装载

1、装载

就是把二进制的Java类型读入Java虚拟机中。
装载阶段分三个基本步骤,要装载一个类型,Java虚拟机必须:

  • 通过该类型的完全限定名,产生一个代表该类型的二进制数据流
  • 解析这个数据流在方法区内的内部数据结构
  • 创建一个表示该类型的java.lang.Class类的实例

Java虚拟机规范并没有说Java类型的二进制数据流应该怎样产生。下面是一些可能的产生"类型的二进制数据"的方式:

  • 从本地文件系统装载一个Java class文件(Java class文件:八位字节的二进制流,是对Java程序二进制文件的精确定义,每一个Java class文件都对一个Java类或Java接口做出了全面概述)
  • 通过网络下载一个Java class文件
  • 从一个ZIP,JAR,CAB或者其他某种归档文件中提取Java class文件
  • 从一个专有数据库中提取Java class文件
  • 把一个Java源文件动态编译为class文件格式
  • 动态为某个类型计算其class文件数据
  • 使用上述任何方法,但是使用不同于Java class文件的其他二进制文件格式

装载步骤的最终产品就是这个Class类的实例对象,它成为Java程序与内部数据结构之间的接口。

要访问该类型的信息(它们是存储在内部数据结构中的),程序就要调用该类型对应Class实例对象的方法。

这样一个过程,就是把一个类型的二进制数据解析为方法区中的内部数据结构,并在堆上建立一个Class对象的过程,这被称为"创建"类型。

2、连接

就是把这种已经读入虚拟机的二进制形式的类型数据合并到虚拟机的运行时状态中去。连接分为验证准备解析三个步骤。

2.1 验证

确保Java类型数据格式正确并且适用于Java虚拟机使用
在正式的验证阶段需要完成的候选检查在下面列出:
首先列出确保各个类之间二进制兼容的检查:

  • 检查final的类不能拥有子类
  • 检查final的方法不能被覆盖
  • 确保在类型和超类型之间没有不兼容的声明(比如两个方法拥有同样的名字,参数在数量,顺序,类型上都相同,但是返回类型不同)

当这些检查需要查看其他类型的时候,只需要查看超类型。因为超类型在子类初始化前被初始化,所以这些类已经被装载了。

当实现父接口的子类被初始化后,不需要初始化父接口,然而当实现父接口的子类被装载后,该父接口必须被装载(它们不会被初始化,只是被装载了,可能被某些虚拟机实现可选地连接了)。

总之,装载一个类的时候,它的所有超类都会被装载。在验证期间,这个类和它的所有超类之间都需要确保互相之间仍然二进制兼容。

  • 检查所有的常量池入口相互之间一致
  • 检查常量池中的特殊字符串(类名,字段名,方法名,字段描述符和方法描述符)是否符合格式
  • 检查字节码的完整性

上述所列出最复杂的步骤就是检查字节码的完整性,所有Java虚拟机都必须设法为它们执行的每个方法检查字节码的完整性。

2.2 准备

负责为类变量分配所需要的内存,设置默认初始值,但在初始化阶段之前,类变量都没有被初始化为真正的初始值。

在准备阶段,虚拟机把类变量新分配的内存根据类型设置为默认值。(此处不同于初始化阶段的赋值)

Tip:Java虚拟机不太支持boolean,在内部,boolean常常被实现为一个int,会被默认的置为0,就是boolean取false值,因此boolean类变量就算它们在内部是被作为int实现的,也总是被初始化成false。

此外,在准备阶段,Java虚拟机实现可能也为一些数据结构分配内存。目的是提高运行程序的性能

2.3 解析

此步骤在类型的常量池中寻找类,接口,字段,方法的符号引用,把这些符号引用转换为直接引用的过程。

注意:虚拟机的实现可以推迟解析这一步,它可以在当运行中的程序真正使用某个符号引用时再去解析它(把符号引用转换为直接引用)

当验证,准备和解析(可选)步骤完成后,该类型就已经为初始化做好了准备。

Java虚拟机规范允许类装载器缓存Java类型的二进制表现形式,在预料某个类型将要被使用时就装载他,或者把这些类型装载到一些相关的分组里面。

如果一个类装载器在预先装载时遇到问题,无论如何,它应该在该类型被首次主动使用时报告该问题(通过抛出一个LinkageError异常的子类)。
如果这个类一直没有被程序主动使用,那么该类装载器将不会报错误

3、初始化

在初始化期间,都将给类变量赋以适当的初始值。

所有的类变量初始化语句和类型的静态初始化器都被Java编译器收集到一起,放到一个特殊的方法中。

对于类来说,这个方法被称为类初始化方法,对于接口来说,它被称为接口初始化方法。

在类和接口的Java class文件中,它被称为clinit(),通常的Java程序无法调用此方法,这种方法只能被Java虚拟机调用,专门把类型的静态变量设置为正确的初始化值。
初始化一个类包括两个步骤:

  • 如果类存在直接超类的话,且超类没有被初始化,就先初始化超类
  • 如果类存在一个类初始化方法,就执行此方法

当初始化一个类的直接超类的时候,也是包含这两个方法,因此,第一个被初始化的类永远是Object,然后是被主动使用的类上所有继承的超类,超类总是在子类之前被初始化。

初始化接口并不需要初始化它的父接口,因此初始化一个接口只需一步:如果接口存在接口初始化方法,就执行此方法。

clinit()方法的代码并不显式的调用超类的clinit()方法,在Java虚拟机调用类的clinit()方法之前,它必须确认超类的clinit()方法已经被执行了。

注意:并非所有的类都需要在它们的class文件中拥有一个clinit()方法。
如果类没有声明任何类变量,也没有静态初始化语句,那么他就不会有clinit()方法。

如果类声明了类变量,但是没有明确使用类变量初始化语句或者静态初始化语句初始化它们,那么该类不clinit()方法。

如果类仅包含静态final变量的类变量初始化语句,而且这些类变量初始化语句采用编译时常量表达式,类也不会有clinit()方法。

只有那些的确需要执行Java代码来赋予类变量正确初始值的类才会有类初始化方法。(如static final修饰的常量就不会使用clinit()方法)
(这里可能有点绕,多理解几遍)

Java虚拟机严格定义了初始化的时机,所有的Java虚拟机实现必须在每个类或接口首次主动使用时初始化。下面六种情形符合主动使用的要求:

  1. 当创建某个类的新实例时(或者通过在字节码中执行new指令;或者通过不明确的创建,反射,克隆或者反序列化)
  2. 当调用某个类的静态方法时(即在字节码中执行invokestatic指令时)
  3. 当使用某个类或接口的静态字段,或者对该字段进行赋值时(即在字节码中,执行getstatich或putstatic指令时),用final修饰的静态字段除外,它被初始化为一个编译时的常量表达式。
  4. 当调用Java API中的某些反射方法时,比如类Class中的方法或者java.lang.reflect包中的方法
  5. 当初始化某个类的子类时(某个类初始化时,要求它的超类已经被初始化了)
  6. 当虚拟机启动时某个被标明为启动类的类(即含有main()方法的那个类)

除上述情形外,所有其他使用Java类型的方法都是被动使用,它们都不会导致java类型的初始化。


再次注意

  1. 任何一个类的初始化都要求它的父类预先初始化,而接口的初始化,并不要求它的父接口预先被初始化。
  2. Java虚拟机的实现可以根据需要在更早的时候装载以及连接类型,没有必要一直等到首次主动使用时才装载和连接它。无论如何,如果一个类型在它的首次主动使用之前还没有被装载和连接的话,那它必须在此时被装载和连接,这样它才能被初始化。










    如有错误,欢迎指正


    参考文献:深入Java虚拟机
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值