Class文件介绍

Class文件介绍

1.Class文件都存了什么数据?
Class文件字节码结构组织示意图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6i4Lquey-1628239418394)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\f56d9bfd3953a51d6907839e62db027.png)]

class常量池如何存储数据
1.常量池的常量有哪些

分为字面量和符号引用

2.常量池的里面是怎么组织的?

cp_info:常量池项

constant_pool_count:常量池计数器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PU4zMarI-1628239418398)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\6cada608e15c38901e39825f9eb3963.png)]

注意:

  1. 常量池计数器是从1开始计数的,而不是从0开始的,如果常量池计数器值constant_pool_count=22,则后面的常量池项)(cp_info)的个数就为21(原因:在指定class文件规范的时候,将第0项常量空出来是有特殊考虑的,这样做是为了满足某些指向常量池的索引值的数据在特点的情况下表达“不引用任何一个常量池项”的意思,这种情况下可以将索引值设置成0来表示);
  2. 常量池项的索引是从1开始的,第一个常量池项(cp_info)的索引为1,最后一个常量池项(cp_info)的索引值为:constant_pool_count-1;
3.常量池项(cp_info)的结构是什么?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OvU11nlZ-1628239418399)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\ac1bb0f3004740eb4344d08c5d1efe0.png)]

没个常量池项(cp_info)都会对应记录看class文件中的某种类型的字面量。JVM虚拟机根据tag的值来确定是某个常量项(cp_info)表示什么类型的字面量。

JVM虚拟机规定了不同的tag值和不同类型的字面量对应关系如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5eYjeySS-1628239418402)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\f0c0cd9a9b1ce3058b755d6ae0b2970.png)]

所以根据cp_info中的tag不同的值,可以将cp_info更细化为一下结构体:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R7GmiFEm-1628239418403)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\6d1bc2fe56ac592201c492aac982347.png)]

现在让我们看一下细化了的常量池的结构会是类似下图所示的样子:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d0PSbi2f-1628239418405)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\dff189308d3cdc76a32bad1ac5a1453.png)]

4如何存储int和float数据类型的常量?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cQtHrP3T-1628239418405)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\0a31eb99dd474e8dfbd43fb121f7d00.png)]

编译器会将int和float分别包装成CONSTANT_Integer_info和CONSTANT_Float_info结构体,然后防止到常量池中

5.如何存储long和double数据类型的常量?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ytDFVUcZ-1628239418406)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\7cb0c12b769dfdb5bbe532cc4f3e81b.png)]

编译器会将long和double分别包装成CONSTANT_Long_info和CONSTANT_Double_info,然后放到常量池中。

6.如何存储String类型的字符串常量?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ASltXGLx-1628239418407)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\7d28456085f9195dfb4d5c5c05c5250.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1iLn3o1g-1628239418407)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\f15bb0f58255093b8adf9ef8997cd64.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kLJCjQhi-1628239418408)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\41afcb8e2cd835f19afe70ddb3a6a56.png)]

英文字符占一个字节,汉语占三个字节。

7.如何存储Class引用类型?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cVGEtTOP-1628239418409)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\15ec221f4f06ff9754352f42b3a4986.png)]

name_index的值是某个CONSTANT_Utf8_info结构体在常量池中的索引,对应的CONSTANT_Utf8_info结构体存储了对应的二进制形式的完全限定名称的字符串。

name_index是占有俩个字节,所以它的最大表示的索引是65535(2的16次方-1).也就是常说常量池中最多能够容纳65535个常量项。所以这就要求我们在定义java类时要注意类的大小,不能太大。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ezc3GLXS-1628239418409)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\eed46eae7ed0b37e3aeff6c72bb4de8.png)]

java规定所有的类都要继承自java.lang.object类,即所有类都是java.lang.Object的子类。JVM在编译类的时候,即使我们没有显示地写上extends java.lang.Object JVM编译器在编译的时候也会自动帮我们加上。

CONSTANT_Class_info结构体中的name_index值是2,则它指向了常量池的第二个常量池项,第二个常量池项必须是Constant_Utf8_info,它存储了以utf-8编码编码的“com/jvm/ClassTest”字符串。

命令:javap -V 类名,可以看到常量池中表示的常量池项

8.哪些字面量会进入到常量池中?
package com.jvm;

public class Test {
    private int int_num = 110;
    private char char_num = 'a';
    private short short_num = 120;
    private float float_num = 130.0f;
    private double double_num = 140.0;
    private byte byte_num = 111;
    private long long_num = 3333L;
    private long long_delay_num;
    private boolean boolean_flage = true;

    public void init() {
        this.long_delay_num = 5555L;
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N7DpbFJ8-1628239418410)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\0a456df01c6040448ef3156b0c536aa.png)]

结论
1.【final修饰】的8中基本类型的值会进入常量池
2.【非final类型】(包括static的)8中基本类型,只有【
double,float,long】的值会进入常量池
3.常量池中包含的字符串类型字面量(【双引号引起来的字符串值】)

符合引用和直接引用

Class文件中符合引用

在Java中,一个Java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。

比如 org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于Constant_class_info的常量来表示的)来表示Language类的地址。

直接引用(运行时内存中)

直接引用可以是:

  1. 直接指向目标的指针(比如,指向“类型”【Class对象】、类对象、类方法的直接引用可能是指向方法区的指针)
  2. 相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
  3. 一个能间接定位到目标的句柄。
引用替换的时机
  1. 类加载的解析阶段(需要将一部分符号引用转换为直接引用)
    符号引用替换为直接引用的操作发生在类加载过程(加载->链接(验证、准备、解析)->初始化)中的解析阶段,会将符号引用转换(替换)为对对应的直接引用,放入运行时常量池中。
  2. 运行期间(动态分派)
class中的特殊字符串
类的全限定名

比如Object类,在源文件中的全限定名是java.lang.Object.而class文件中的全限定名是将点号替换成“/”,也就是java/lang/Object.

各类型的描述符

对于字段的数据类型,其描述符主要有一下几种

  • **8中基本数据类型:**除了long和booean,其他都用对应单词的大写首字母表示。long用J表示,boolean用Z表示。
  • void:描述符是V。
  • 对象类型:描述符用字符L加上对象的全限定名表示,如String类型的描述符为Ljava/lang/String
  • 数组类型:每增加一个维度则在对应的字敦描述符前增加一个[,如一维数组int[]的描述符为[,二维数组String[][]的描述符为[[Ljava/lang/String

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L6t3ows2-1628239418411)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\afa1b698a17a05c1ce9fd20e1f19e2e.png)]

字段描述符

字段的描述符就是字段的类型所对应的字符或字符串。

如:

int i中,字段i的描述符就是I
Object o中,字段o的描述符就是Ljava/lang/Object;
double[][] d中,字段d的描述符就是 [[D
方法描述符

方法的描述符比较复杂,包括所有参数的类型列表和方法返回值,它的格式是这样的:

(参数1类型 参数2类型 参数3类型 。。。)返回值类型

注意事项:

不管是参数的类型还是返回值类型,都是使用对应字符和字符串来表示的,并且参数列表使用小括号括起来,并且各个参数类型直接没有空格,参数类别和返回值类型之间也没有空格。
方法描述符举例说明如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w0If4SG7-1628239418412)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\83f057bd8523184eca4d702ef4db840.png)]

特殊方法的方法名

类的构造方法和类型初始化方法。

构造方法就不用多说了,至于类型的初始化方法对应到源码中就是镜头初始块。也就是说,静态初始化块,在class文件是以一个方法表述的,这个方法同意有方法描述符和方法名,具体如下:

  • 类的构造方法的方法名使用字符串 表示
  • 静态初始化方法的方法名使用字符串 表示
  • 除了这俩种特殊的方法外,其他普通方法的方法名,和源文件中的方法名相同。
javap命令

javap是jdk自带的反解析工具。他的作用就是根据class字节码文件,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。

javap的用法格式:

javap <options> <classes>

其中classes就是你要反编译的class文件。

一般常用的是-v -l -c三个选项。

  • javap -v classxx,不仅会输出行号、本地变量信息、反编译汇编代码,还会输出当前类用到的常量池等信息。
  • javap -l 会输出行号和本地变量表信息
  • javap -c 会对当前class字节码进行反编译生成汇编代码。

类加载详解

1.类加载的时机

1.遇到new、getstatic、putstatic和invokestatic这四条指令时,如果对应的类没有初始化,则要对对应的类先进行初始化。

2.使用java.lagn.reflect包方法时对类进行反射调用的时候。

Class c = Class.forname("com.jvm.Student");

3.初始化一个类的时候发现其父类还没初始化,要先初始化其父类

4.当虚拟机开始启动时,用户需要指定一个主类,虚拟机会先执行这个主类的初始化。

2.类加载的过程

加载(loading)->连接Linking(验证->准备->解析)->初始化Initialization->使用Using->卸载Unloading

加载–class文件–>Class对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9ivC4KCO-1628239418413)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\dea935bf109e8ba999dcef29c7f1b65.png)]

加载的过程

在加载的过程中,JVM主要做3件事情:

  • 全限定名–>二进制字节流(class文件)
  • 字节流的静态存储结构–>方法去的运行时数据结构
  • 创建java.lang.Class对象

加载源

  • 本地class文件

  • zip包 jar、war、Ear等

  • 其他文件生产
    由JSP文件中生产对应的Class类

  • 数据库中
    将二进制字节流存储至数据库中,然后在加载时从数据库中读取.有些中间件会这么做,用来实现代码在集群间分发

  • 网络
    从网络中获取二进制字节流。典型就是Applet

  • 运行时计算生产
    动态代理技术,用ProxyGenerator.generateProxyClass为特定接口生成形式为“¥Proxy”的代理类的二进制字节流

类和数组加载的区别

数组也有类型,称为“数组类型” 如:

String[] str = new String[10];

这个数组的数组类型是[Ljava.lang.String,而String只是这个数组的元素类型。

数组类和非数组类的类加载是不同的,具体情况如下:

  • 非数组类:是由类加载器来完成。
  • 数组类:数组类本身不通过类加载器创建,它是由java虚拟机直接创建,但数组类与类加载器有很密切的关系,因为数组类的元素类型最终要靠类加载器创建。
加载过程的注意点
加载阶段和链接阶段是交叉的

类加载的过程中每个步骤的开始顺序都有严格限制,但每个步骤的结束顺序没有限制。也就是说,类加载过程中,必须按照如下顺序开始:

加载 -> 链接 ->初始化

但结束顺序无所谓,因此由于每个步骤处理时间的长短不一就会导致有些步骤会出现交叉。

【验证】–各种检查

验证阶段比较耗时,它非常重要但不一定必要(因为对程序运行期没有影响),如果所运行的代码已经被反复使用和验证过,那么可以使用-Xverify:none参数关闭,以缩短类加载时间。

验证的过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cgtMeIBR-1628239418414)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\a0a4cf2a3e146a20006434c9bb9af41.png)]

  • 文件格式验证
    验证字节流是否符合Class文件格式的规范,并且能被当前的虚拟机处理
    本验证阶段是基于二进制字节流进行的,只有通过本阶段验证,才被允许存到方法区
    后面的三个验证阶段都是基于方法区的存储结构进行,不会在直接操作字节流。

    印证【加载和验证】是交叉进行的:

    1.加载开始前,二进制字节流还没进方法去,而加载完成后,二进制字节流已经存入方法区
    2.而在文件格式验证前,二进制字节流尚未进入方法区,文件格式验证同之后才进入方法区
    
    也就是说,加载开始后,立即启动了文件格式验证,本阶段验证通过后,二进制字节流被转换成特定数据结构存储至方法区中,继而开始下阶段的验证和创建Class对象等操作
    
  • 元数据验证
    对字节码描述信息进行语义分析,确保符合Java语法规范。

  • 字节码验证
    本阶段是验证过程的最复杂的一个阶段。
    本阶段对方法体进行语义分析,保证方法在运行时不会出现危害虚拟机的事件。
    字节码验证将对类的方法进行校验分析,保证被校验的方法在运行时不会做出危害虚拟机的事,一个类方法题的字节码没有通过字节码验证,那一定有问题,但若一个方法通过了验证,也不能说明它一定安全。

  • 符号引用验证
    发生在JVM将符号引用转化为直接引用的时候,这个转化动作发生在解析阶段,对类自身以外的信息进行匹配校验,确保解析能正常执行。

【准备】–为静态成员变量分配内存并初始化0值
1.7之前方法区
1.7之后堆

准备阶段注意完成俩件事情:

  • 为已在内存中的类的静态成员变量分配内存
  • 为静态成员变量设置初始值,初始值为0、false、null等

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-672QCIJK-1628239418415)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\7e1a169a9e82454da3abf4ac70685df.png)]

仅仅[为类变量(即static修饰的字段变量)分配内存]()并且[设置该类变量的初始值,即零值](),

这里不包含用final修饰的static,因为final在编译的时候就会分配了(编译器的优化),同时这里也[不会为实例变量分配初始化]()。
[类变量(静态变量)]()会分配在[方法区]()中,而[实例变量]()是会随着对象一起分配到[Java堆]()中。

比如:

public static int x = 1000;

注意:

实际上变量x在准备阶段过后的初始值为0,而不是1000
将x赋值为1000的pustatic指令是程序被编译后,存放于类构造器方法之中

但是如果声明为:

public static final int x = 1000;
在编译阶段会为x生产ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将x复制为1000
[解析]–将符号引用替换为直接引用

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

解析动作主要针对类或接口、字段、类方法、接口发给发四类符号引用进行,分别对应于常量池中的
CONSTANT_Class_inf、CONSTANT_Fieldref_info、CONSTANT_Me
thodref_info、CONSTANT_InterfaceMethodref_info四种常量类型
类或接口的解析:

判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。

字段解析
  1. 会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段
  2. 如果有,则查找结束;
  3. 如果没有,则会安装继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口。
  4. 还没有,则安之继承关系从上往下递归搜索其父类,直至查找结束。(优先从接口来,然后是继承的父类,理论上是按照上述顺序进行搜索解析,但在实际应用中,虚拟机的编译器实现可能要比上述规范要求的更严格一些,如果有一个同名字段同时出现在该类的接口和父类中,或同时在自己或父类的接口中出现,编译器可能会拒绝编译)
类方法解析:

对类方法的解析与对字段解析的搜索步骤差不多,只是多了判断该方法所处的是类还是接口的步骤,而且对类方法的匹配搜索,是先搜索父类,在搜索接口。

接口方法解析:

与类方法解析步骤类似,只是搜索的是接口不会有父类,因此,只递归向上搜索父接口就行了。

[初始化]----调用方法

初始化过程的注意点

初始化阶段是执行类构造器()方法的过程。其实初始化过程就是调用类初始化方法的过程,完成对static修饰的类变量的手动赋值还有主动调用静态代码块。

  • 方法是编译器自动收集类中所有类变量的赋值动作和静态语句块中的预计合并产生的,编译器手机的顺序是由语句在源文件中出现的顺序所决定的
  • 静态代码块只能访问到出现在静态代码块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
  • 实例构造器需要显示调用父类构造函数,而类的不需要调用父类的类构造函数,虚拟机会确保子类的方法执行前已经执行完毕父类的方法,因此再JVM中第一个被执行的方法的类肯定是java.lang.Object.
  • 虚拟机会爆炸在多现场环境中一个类的方法被正确的加锁,同步。
类加载器介绍

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YHpp7K5i-1628239418416)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\9da96a64d111332bea4daac05647d8d.png)]

自定义类加载器

为生命要定义自己的类加载器呢?

因为java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果我们想加载其他位置的类或jar时,就只能自定义一个ClassLoader类了
如何定义类加载器?
继承ClassLoader类,重写findClass()方法,loadClass方法
自定义类加载器注意事项:
自定义类加载器需要去继承ClassLoader类。
JDK1.2之前是重写ClassLoader类中的loadClass方法
JDK1.2之后是重写ClassLoader类中的findClass方法
双亲委派机制
什么是双亲委派机制呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UU1oLxzx-1628239418416)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\e7907abba31993ed525844391a94d23.png)]

为什么要使用双亲委托这种模型呢?

考虑到安全因素,这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。

如何判定来个class是相同的?

JVM在判定俩个class是否相同时,不仅要判断来个类名是否相同,而且要判断是否由同一个类加载器实例加载的

Proxy.newProxyInstance(ClassLoader);

双亲委派机制源码
protected Class<?> loadClass(String name, boolean resolve)
 throws ClassNotFoundException
 {
 synchronized (getClassLoadingLock(name)) {
 // First, check if the class has already been
loaded
 Class<?> c = findLoadedClass(name);
 if (c == null) {
 long t0 = System.nanoTime();
 try {
 if (parent != null) {
 c = parent.loadClass(name, false);
 } else {
 c =
findBootstrapClassOrNull(name);
 }
 } catch (ClassNotFoundException e) {
 // ClassNotFoundException thrown if
class not found
 // from the non-null parent class
loader
 }
 if (c == null) {
 // If still not found, then invoke
findClass in order
 // to find the class.
 long t1 = System.nanoTime();
 
 
 
 c = findClass(name);
 
 // this is the defining class loader;
record the stats
 
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1
- t0);
 
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFro
m(t1);
 
sun.misc.PerfCounter.getFindClasses().increment();
 }
 }
 if (resolve) {
 resolveClass(c);
 }
 return c;
 }
 }

破坏双亲委派模型

双亲委派机制的破坏,在JDK发展的过程中是发生了多次

第一次破坏

在jdk1.2之前,那时候还没有双亲委派模型,不过已经有了ClassLoader这个抽象类,所以已经有人继承这个抽象类,重写loadClass方法来实现用户自己定义类加载器。

而在1.2的时候要引入双亲委派模型,为了向前兼容,loadClass这个方法还得保留着使之得以重写,新搞了个findClass方法让用户重写,并呼吁大家不要重写loadClass只要重写findClass.

这就是第一次对双亲委派模型的破坏,因为双亲委派的逻辑在loadClass上,但是有允许重写loadClass,重写了之后就可以破坏委派逻辑了。

第二次破坏

双亲委派机制,是一种自上而下的加载需求,越往上类越基础。在实际应用中双亲委派解决了java基础类同意加载的问题,但是却着手存在一定多多缺陷。jdk中的基础类作为典型的ali被用户调用,但是也存在被api调用用户代码的情况,典型的如SPI代码。

SPI机制简介

SPI的全面为Service Provider Interace,主要是应用于厂商自己定义组件或插件中,在java.util.ServiceLoader的文档里有详细的介绍,简单的总结额下java SPI机制的思想:

系统里的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦带里设计具体的实现类,就违法了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

DriverManager源码

static {
 loadInitialDrivers();
 println("JDBC DriverManager initialized");
 }
 
 private static void loadInitialDrivers() {
 String drivers;
 try {
 drivers = AccessController.doPrivileged(new
PrivilegedAction<String>() {
 public String run() {
 return
System.getProperty("jdbc.drivers");
 }
 });
 } catch (Exception ex) {
 drivers = null;
 }
 AccessController.doPrivileged(new
PrivilegedAction<Void>() {
 public Void run() {
 ServiceLoader<Driver> loadedDrivers =
ServiceLoader.load(Driver.class);
 Iterator<Driver> driversIterator =
loadedDrivers.iterator(); 
 try{
 while(driversIterator.hasNext()) {
 driversIterator.next();
 }
 } catch(Throwable t) {
 // Do nothing
 }
 return null;
 }
 });
 println("DriverManager.initialize: jdbc.drivers =
" + drivers);
 if (drivers == null || drivers.equals("")) {
 return;
 }
 String[] driversList = drivers.split(":");
 println("number of Drivers:" +
driversList.length);
 for (String aDriver : driversList) {
 try {
 println("DriverManager.Initialize: loading
" + aDriver);
 Class.forName(aDriver, true,
 ClassLoader.getSystemClassLoader());
 } catch (Exception ex) {
 println("DriverManager.Initialize: load
failed: " + ex);
 }
 }
 }

如果1出现SPI相关代码时,我们应该如何解决基础类去加载用户代码类呢?这个时候,jvm不得不妥协,推出现场上下文加载器的概念,去解决该问题。

现场上下文类加载器
public Launcher() {
 Launcher.ExtClassLoader var1;
 // 扩展类加载器
 try {
 var1 =
Launcher.ExtClassLoader.getExtClassLoader();
 } catch (IOException var10) {
 throw new InternalError("Could not create
extension class loader", var10);
 }
 // 应⽤类加载器/系统类加载器
 try {
 this.loader =
Launcher.AppClassLoader.getAppClassLoader(var1);
 } catch (IOException var9) {
 throw new InternalError("Could not create
application class loader", var9);
 }
 // 线程上下⽂类加载器
 
Thread.currentThread().setContextClassLoader(this.loader)
;
 String var2 =
System.getProperty("java.security.manager");
 if (var2 != null) {
 SecurityManager var3 = null;
 if (!"".equals(var2) &&
!"default".equals(var2)) {
 try {
 var3 =
(SecurityManager)this.loader.loadClass(var2).newInstance()
;
 } catch (IllegalAccessException var5) {
 } catch (InstantiationException var6) {
 } catch (ClassNotFoundException var7) {
 } catch (ClassCastException var8) {
 }
 } else var3 = new SecurityManager();
 }
 if (var3 == null) {
 throw new InternalError("Could not create
SecurityManager: " + var2);
 }
 System.setSecurityManager(var3);
 }
 }

获取线程上下文的源码

public static <S> ServiceLoader<S> load(Class<S>
service) {
 ClassLoader cl =
Thread.currentThread().getContextClassLoader();
 return ServiceLoader.load(service, cl);
 }

第三次破坏

这次破坏是为了满足热部署的需求,不停机更新这对企业来说至关重要,毕竟停机是大事。

OSGI就是利用自定义的类加载器机制来完成模块化热部署,而它实现的类加载机制就没有完全遵循自下而上的委托有很多平级之间的类加载器查找,具体就不展开了,有兴趣可以自行研究。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K7sjFNE6-1628239418417)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\b863d732ec6b002b18f23a2c86f0d18.png)]

再上图中,人工转换箭头线上的install、update、resolve、start、stop是通过在控制台输入对应命令来触发。Bundle生命周期过程之中的六种状态分别为:

  1. UNINSTALLED(未安装):状态值为整数1。次数Bundle中的资源是不可用的。
  2. INSTALLED(已安装):状态值为正数2.此时Bundle已经通过了OSGi框架的有效性校验并分配了undleID,本地资源以加载,但尚未对其以来关系进行解析处理。
  3. RESOLVED(以解析):状态值为正数4,此时Bundle已经完成了依赖关系解析并已经找到所有依赖包,而且自身导出的Package已可以被其它的Bundle导入使用。在此种状态的Bundle要么是已经准备好运行,要么就是被停止的。
  4. STARTING(启动中):状态值为整数8.此时Bundle的BundleActivator的start()方法已经被调用但是尚未返回。如果start()正常执行结束,Bundle将自动转换到ACTIVE状态;否则如果start()方法抛出了异常,Bundle将退回到RESOLVED状态。
  5. STOPPING(停止中):状态值为整数16.此时Bundle的BundleActivator的stop()方法已经被调用但是尚未返回。无论sto()是正常结束还是抛出了异常,在这个方法突出后,Bundle的状态都将转为RESOLVED。
  6. ACTIVE(已激活):状态值为正数32.Bundle处于激活状态,说明BundleActivator的start()方法已经执行完毕如果没有其他动作,Bundle将继续维持Active状态。
OSGI:Bundle解析

Class Loader在Bundle被正确解析(状态变成Resolved)之后创建,而只有Bundle中的所有包约束条件都满足后,对应的Bundle才能被正确解析完毕。

Bundle的解析由定义在MANIFEST.MF文件中的四个配置项定义:

  1. Import-Package:定义当前Bundle需要导入的其他包(Package),一般位于其他Bundle之中,并且被定义为Export-Package.
  2. Export-package:定义当前Bundle要导出的包。被导出的包中的类资源可以被其他Bundle导入,可以被定义到Import-Package中
  3. Required-Bundle:定义当前Bundle需要依赖Bundle.
  4. DynamicImport-Package:定义需要动态导入的包。这里定义的包在Bundle解析过程中不会被使用到,而是会在运行时被动态解析并加载。

OSGI:Bundle-ClassPath

在MANIFEST.MF文件中定义的Bundle-Classpath会描述Classpath范围告诉Classloader去哪里查找类。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VFpm5t0S-1628239418418)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\581b1e65ba52301d72214d151d7d744.png)]

OSGI:类加载机制

Bundle的加载策略,如何导入和导出代码是在OSGi规范的模块(Modules)层实现的。

OSGi框架对于每个Bundle(非Fragment Bundle)都会创建一个单独的类加载器(Class Loader),不过对于Class Loader的创建可能会延迟到真正需要它时才会发生。

Bundle的Class Loader搜索类资源的规则简要介绍如下:

  1. 如类资源属于java.*包,则将加载请求委托给父Class Loader;

  2. 如类资源定义在OSGi框架的启动委托列表中,则将加载请求委托给父Class Loader;例如:

    osgi.framework.bootdelegation=*
    osgi.framework.bootdelegation=sun.*,com.sun.*
    
  3. 如果类资源属于在Import-Package中定义的包,则框架会将加载请求委托给导出此包的Bundle的Class Loader;

  4. 如果资源属于在Require-Bundle中定义的Bundle,则框架会将加载请求委托给此Bundle的ClassLoader;

  5. Bundle搜索自己的Bundle0Classpath中定义的类资源;

  6. Bundle搜索属于该Bundle的Fragment的类资源;

  7. 判断是否找到导出(Export-Package)了对应资源,如果仍未找到进入动态导入查找

  8. 若类在DynamicImport-Package中定义,则开始尝试在运行环境中寻找符合条件的Bundle,框架会将加载请求委托给导出此包的Bundle的ClassLoader;

    DynamicImport-Package:*使用了星号来对所有资源进行通配,这也意味着对应的Bundle可以看到任何可能访问到的资源,这个选项应该尽量少地使用,除非别无他法
    
  9. 如果在经过上面一系列步骤后,仍然没有正确地加载到类资源,则OSGi框架会向外抛出类未发现异常。

总结

JVM的类加载机制,是自上而下的加载过程。

OSGi的类加载机制,既能在Bundle内部按照JVM的类机制去加载,本身也会按照Bundle之间横向委托的方式进行类加载,所以它是一种网状结构的类加载方式。
ootdelegation=*
osgi.framework.bootdelegation=sun.,com.sun.


3. 如果类资源属于在Import-Package中定义的包,则框架会将加载请求委托给导出此包的Bundle的Class Loader;

4. 如果资源属于在Require-Bundle中定义的Bundle,则框架会将加载请求委托给此Bundle的ClassLoader;

5. Bundle搜索自己的Bundle0Classpath中定义的类资源;

6. Bundle搜索属于该Bundle的Fragment的类资源;

7. 判断是否找到导出(Export-Package)了对应资源,如果仍未找到进入动态导入查找

8. 若类在DynamicImport-Package中定义,则开始尝试在运行环境中寻找符合条件的Bundle,框架会将加载请求委托给导出此包的Bundle的ClassLoader;

DynamicImport-Package:*使用了星号来对所有资源进行通配,这也意味着对应的Bundle可以看到任何可能访问到的资源,这个选项应该尽量少地使用,除非别无他法


9. 如果在经过上面一系列步骤后,仍然没有正确地加载到类资源,则OSGi框架会向外抛出类未发现异常。

#### 总结

JVM的类加载机制,是自上而下的加载过程。

OSGi的类加载机制,既能在Bundle内部按照JVM的类机制去加载,本身也会按照Bundle之间横向委托的方式进行类加载,所以它是一种网状结构的类加载方式。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值