类加载、连接、初始化——加载

加载:根据类名寻找类或接口的二进制表示,并通过二进制表示创建类或者接口。
连接:获取类或者接口并将其组合到虚拟机的运行时状态以便执行。
初始化:执行类或者接口的初始化方法<cinit>

首先我们看看类和接口的二进制表示是怎么转换成符号引用的?

1 常量池

虚拟机规范中的描述是,常量池就是JVM维护的一个运行时数据结构,主要用作Java等传统编程语言的的符号表。

1.1 常量池构建时间
类或接口的二进制形式表示的constant_pool表用于在类或者接口创建的时候构建常量池。

1.2 常量池相关的几个数据结构

CONSTANT_Class_info
类或者接口的符号引用来自于类和接口的二进制表示形式中的CONSTANT_Class_info数据结构,其名称通过Class.getName获得。

  • 对于非数组类或者接口类,其名称为类或者接口的二进制名称。
  • 对于一个n维数组类,其名称由n个ASCII形式的 ‘[’ 符号开始,然后跟着元素类型的表现形式。
    如果元素类型为原始类型,那么就由域描述符代表。
    如果元素类型为引用类型,那么就由ASCII形式的"L"符跟着元素类型的二进制名称再跟着ASCII形式的";"字符表示。

CONSTANT_Fieldref_info
类或者接口中的域的符号引用来自于类和接口的二进制表示形式中的CONSTANT_Fieldref_info数据结构,这一引用返回域名称、域描述符以及该域所在类的符号引用。

CONSTANT_Methodref_info
类中的方法的符号引用来自于类的二进制表示形式中的CONSTANT_Methodref_info数据结构,这一引用返回方法名称、域描述符以及该域所在类的符号引用。

CONSTANT_InterfaceMethodref_info
接口中的方法的符号引用来自于接口的二进制表示形式中的CONSTANT_InterfaceMethodref_info数据结构,这一引用返回方法名称、域描述符以及该域所在接口的符号引用。

CONSTANT_MethodHandle_info
方法句柄的符号引用来自于类或接口的二进制表示形式中的CONSTANT_MethodHandle_info数据结构,这一引用返回一个类或者接口的域的符号引用、或者一个类的方法、或者一个接口的方法,取决于方法句柄的类型。

CONSTANT_MethodType_info
方法类型的符号引用来自于类或接口的二进制表示形式中的CONSTANT_MethodType_info数据结构,这一引用返回一个方法描述符。

CONSTANT_InvokeDynamic_info
调用点说明符的符号引用来自于类或接口的二进制表示形式中的CONSTANT_InvokeDynamic_info数据结构,这一引用给出:
一个为启动方法调用指令 invokedynamic 指令 (§invokedynamic)服务的符号引用
一系列的符号引用(类、方法类型、方法句柄)、string literal、作为静态方法的参数的运行时常量值
一个方法名或者方法描述符

string literal字符串文字是一个String类实例的引用,来自于类或接口的二进制表示形式中的CONSTANT_String_info结构,给出构成字符串文字的Unicode编码点的序列。在Java编程语言中,要求具有相同编码点序列的字符串文字必须引用同一个String类实例。而且,如下一个等式值为true
(“a” + “b” + “c”).intern() == “abc”

理解了这点就能很容易的解答字符串相等不相等的问题。

为了派生出一个字符串文字,JVM会检查CONSTANT_String_info给的编码点序列:如果String.intern先前在String类实例上已经被调用过,并且其Unicode编码点的序列和CONSTANT_String_info的编码点序列相同,那么派生字符串文字的结果就是一个指向相同String类实例的引用。否则的话,包含CONSTANT_String_info中编码点序列的String类实例将被创建,然后字符串文字派生的结果就是拿到一个指向String类实例的引用。最终,新的String实例的intern方法会被调用。

CONSTANT_Integer_info、CONSTANT_Float_info、CONSTANT_Long_info、CONSTANT_Double_info
运行时常量值由以上数据结构派生而来。

CONSTANT_NameAndType_info、CONSTANT_Utf8_info
这些constant_pool表中的结构仅在派生以上提到的对象的符号引用时间接使用。

2、JVM启动

JVM怎么启动的呢?JVM通过创建指定的初始化类来启动,通过bootstrap类加载器加载,然后初始化它,并调用public class method void main(String[])方法。对构成main方法的JVM指令的执行会引发其它类或者接口的连接及后续的类创建、以及对额外方法的调用。在一个JVM的实现中,初始化类可通过命令行参数提供,并且实现也可以有选择地在指定初始化类时设置类加载器来加载应用。

3、创建与加载

类的创建包括方法区中JVM特定实现的内部表现形式的构造。

触发类的创建:

  • 被其他接口或者类通过自身运行时常量池引用。
  • 通过特定JSE平台类库的方法调用(例如反射)。

对于非数组类,通过类加载器加载该类的二进制表现形式的文件来创建。对于数组类,通过JVM来创建,而不是类加载器。

类加载器有两种形式,bootstrap类加载器和自定义类加载器,用户定义的类加载器都是抽象类ClassLoader的子类实例。为什么会有自定义类加载器呢?这是用来丰富类加载行为的,可以用自定义类加载器加载来自于网络、即时生成、加密文件中提取等不同源头的类。

类加载器可以通过直接加载某个类直接定义该类,也可以委派给其它类加载器。如果类加载器直接加载类,则该加载器称为该类的定义加载器。另外一点是,某个类加载器创建了一个类,不管是通过直接加载还是委派,都称该类加载器为该类的初始加载器。

在运行期间,类和接口并不只由类名决定,而是由二进制名称和对应的定义类加载器共同决定。每个类和接口都属于一个特定的运行时包,这个包也由二进制包名和该类对应的类加载器共同确定。

JVM通过三种方式创建类或者接口:

  • 对于非数组类和接口:
    如果类或者接口 D 触发了类或者接口的创建,如果 D 由bootstrap类加载器定义,那么bootstrap类加载器初始化类的加载。
    如果 D 由用户定义类加载器加载,那么由同一个自定义类加载器初始化该类的加载。
  • 对于数组类和接口:
    由JVM直接创建该类,没有类加载器,但是 D 的定义类加载器,在创建数组类C的过程中也会被用到。

在类加载过程中,如果发生错误,会抛一个LinkageError异常。如果JVM在验证和resolution阶段尝试加载一个未初始化的类,那么该类的类加载器便会抛出一个ClassNotFoundException异常,JVM随即又抛NoClassDefFoundError异常。

类加载器的特性:

  • 给定一个类名,类加载器总能返回同一个Class对象。
  • 如果类加载器L1委托类加载器L2加载类C,那么对于是C的直接父类或接口、域、方法或者构造函数的参数、方法的返回值的任意类型T,L1和L2都应返回同一个Class对象。
  • 如果用户自定义的类加载器预加载了二进制表示的类或者接口,或者把相关的一组类都一起加载了,那么它仅在程序中不进行预取或者组加载可能出现的点上来反映加载错误。

3.1 Bootstrap类加载器

首先,JVM会确定是否已经存在类或者接口名为N,其初始加载器为bootstrap类加载器L的记录,如果已经存在,则无需加载。否则,JVM会将N传递给Bootstrap类加载器上的方法调用,去寻找平台相关的名称为N的类。通常,将使用分层文件系统中的文件来表示类或接口,并且将在文件的路径名中编码类或接口的名称。

3.2 自定义类加载器

首先,JVM会确定是否已经存在类或者接口名为N,其初始加载器为自定义加载器L的记录,如果已经有记录,没有必须加载并创建类。
如果没有记录,JVM会在加载器L上调用loadClass(N),返回值便是要创建的类或者接口。然后JVM便将加载器L记录为对应类或者接口的初始加载器。

为加载一个类或者接口,自定义类加载器L需要做如下两个操作:

  • 类加载器创建代表类或者接口C的ClassFile结构的字节码数组;调用ClassLoader的defineClass方法。对defineClass的调用触发JVM使用ClassFile结构的字节码数组派生出类或者接口C。
  • 类加载器L可以将类加载任务委派给另一个类加载器L1,伴随着将类名N传递给委派加载器的loadClass方法。

不管是哪个操作,因任何原因无法加载,必须抛ClassNotFoundException异常。

3.3 创建数组类

加载数组类的既可以是自定义加载器也可以是Bootstrap类加载器,如果对应组件类型为N的数组类C和其初始加载器L已经被记录,那么无需创建该数组类。否则会进行如下动作:

  • 1)如果组件类型是引用类型,那么前边描述的类加载方式适用该组件类型的类加载。
  • 2)JVM创建一个新的特定组件类型及维度的数组类。这里又做如下区分:
    如果组件类型是引用类型,那么数组类被标记为由组件类型的类加载器定义。否则,数组类被标记为由bootstrap类加载器定义。但是不管哪种情况,加载器均会被记录为数组类的初始加载器。

注:如果组件类型是引用类型那么数组类的访问权限由组件类型的访问权限确定,不然的话,数组类的访问权限就为public。

3.4 加载约束

有时候一个类或者接口,会有对其他类或者接口的域或者方法进行符号引用的动作。

那么加载约束主要是保障,在域或者方法的描述符中提到的任何类型名称N,不管是由加载器L1加载还是由加载器L2加载,都表示同一个类或者接口。

这一点由虚拟机来保证,在准备和解析期间公开加载约束,具体通过记录类和接口对应的初始加载器来强制约束。之后JVM立即检查是否违反加载约束,如果是,JVM抛LinkageError异常。

仅有四种情况是违反加载约束的:
1)存在名为N的类C及其初始加载器L的记录
2)存在名为N的类C1及其初始加载器L1的记录
3)有强制约束集定义的等价关系表示NL=NL1
4)C != C1

4、从类文件表示形式派生类

通俗的讲,也就是从我们写的java类对应的.class文件来创建一个非数组类的Class object,具体有如下步骤:
1)检查是否有对应的类和初始加载器的记录,如果有,则违反原则,报LinkageError。否则进行下一步

2)解析类文件,先进行格式检查:如果不是ClassFile结构的文件,报ClassFormatError;如果是不支持的major或者minor版本,加载器报UnsupportedClassVersionError;如果解析出的类名不是所需的类,报NoClassDefFoundError或其子异常。

3)如果类有直接父类,其处理过程后边专门说。如果是一个接口,必须让Object作为其直接父类,而且Object类必须已经被加载。仅有Object没有直接父类。
如果加载时发现类的直接父类实际上是一个接口,报IncompatibleClassChangeError。
如果加载时发现类的任一个父类是其本身,报ClassCircularityError。

4)如果类有直接继承接口,其处理过程后也留到后边文章说。
如果发现继承的实际上不是一个接口,报IncompatibleClassChangeError。
如果父类或者父接口是其本身,报ClassCircularityError。

5)JVM标记加载器L作为被加载类C的定义类加载器,并记录加载器L为C的初始类加载器。

发布了95 篇原创文章 · 获赞 5 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_43878293/article/details/104258298