【JVM】-- 类加载

野性酷女 2023-07-06 14:34 82阅读 0赞

文章目录

    • 1.类加载的阶段
      • 加载(Loading)
      • 连接(Linking)
      • 1.验证(Verification)
      • 2.准备(Preparation)
      • 3.解析(Resolution)
      • 初始化(Initialization)
      • 初始化时机
      • 面试题
      • 1.”e init “何时打印
      • 2.典型应用 - 完成懒惰初始化单例模式
    • 2.类加载器
      • 1.启动类加载器( Bootstrap ClassLoader))
      • 2.扩展类加载器(Extension ClassLoader)
      • 3.应用程序类加载器(Application ClassLoader )
      • 双亲委派机制
      • 线程上下文类加载器
      • 自定义类加载器

1.类加载的阶段

类从被加载到虚拟机内存开始,到被卸载出内存开始,其生命周期共包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段其中验证、准备和解析部分被统称为连接(Linking)。

图片

将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:

  • _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用
  • _super 即父类
  • _fields 即成员变量
  • _methods 即方法
  • _constants 即常量池
  • _class_loader 即类加载器
  • _vtable 虚方法表
  • _itable 接口方法表

注意:

  • instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror是存储在堆中
  • 可以通过前面介绍的 HSDB 工具查看

类被加载后,在Java虚拟机中的存储情况:

图片

有了以上知识,我们就开始讲解Java中类加在的几个阶段:

加载(Loading)

在加载阶段,虚拟机需要完成以下三件事情:

  • 1)通过一个类的全限定名来获取定义此类的二进制字节流。
  • 2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 3)在Java堆中生成一个代表这个类的java.lang.Class对象(该对象引用了元空间中的Java_mirror镜像),作为方法区这些数据的访问入口。

加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。

连接(Linking)

连接阶段是对验证(Verification)、准备(Preparation)、解析(Resolution)三个阶段的总称。

1.验证(Verification)

验证是连接阶段的第- -步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

尽管验证阶段是非常重要的,并且验证阶段的工作量在虚拟机的类加载子系统中占了很大一部分,不同的虚拟机对类验证的实现可能会有所不同,但大致上都会完成下面四个阶段的检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。

  • 1.文件格式验证

第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟

机处理。这一阶段可能包括下面这些验证点:

  • 是否以魔数0xCAFEBABE开头。
  • 主、次版本号是否在当前虚拟机处理范围之内。
  • 常量池的常量中是否有不被支持的常量类型( 检查常量tag标志)。
  • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
  • CONSTANT_ Utf8 info 型的常量中是否有不符合UTF8编码的数据。
  • Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。

这阶段的验证是基于字节流进行的,经过了这个阶段的验证之后,字节流才会进入内存的方法区中进行存储,所以后面的三个验证阶段全部是基于方法区的存储结构进行的。

  • 2.元数据验证

第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,这个阶段可能包括的验证点如下:

  • 这个类是否有父类(除了java.lang.Object 之外,所有的类都应当有父类)。
  • 这个类的父类是否继承了不允许被继承的类(被final 修饰的类)。
  • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
  • 类中的字段、方法是否与父类产生了矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都- -致,但返回值类型却不同等)。

第二阶段的主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。

  • 3.字节码验证

第三阶段是整个验证过程中最复杂的-一个阶段,主要工作是进行数据流和控制流分析。在第二阶段对元数据信息中的数据类型做完校验后,这阶段将对类的方法体进行校验分析。这阶段的任务是保证被校验类的方法在运行时不会做出危害虚拟机安全的行为,例如:

  • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似这样的情况:在操作栈中放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中。
  • 保证跳转指令不会跳转到方法体以外的字节码指令上。
  • 保证方法体中的类型转换是有效的,例如可以把-一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的。
  • 4.符号引用验证

最后一个阶段的校验发生在虛拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段一解析阶 段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性的校验,通常需要校验以下内容:

  • 符号引用中通过字符串描述的全限定名是否能找到对应的类。
  • 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。
  • 符号引用中的类、字段和方法的访问性( private、protected、 public、 default) 是否可被当前类访问。

符号引用验证的目的是确保解析动作能正常执行,如果无法通过符号引用验证,将会拋出一个java.lang. IncompatibleClassChangeError 异常的子类,如java.lang.IllegalAccessError. java.lang.NoSuchFieldError. java.lang.NoSuchMethodError 等。

验证阶段对于虛拟机的类加载机制来说,是-一个非常重要的、但不一定是必要的阶段。如果所运行的全部代码(包括自己写的、第三方包中的代码)都已经被反复使用和验证过,在实施阶段就可以考虑使用-Xverify:none 参数来关闭大部分的类验证措施,以缩短虛拟机类加载的时间。

2.准备(Preparation)

为 static 变量分配空间,设置默认值

  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
  • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成

使用代码验证:

  1. public class Load1 {
  2. private static int a;
  3. private static int b = 10;
  4. private static final int c = 20;
  5. private static final String str = "hello";
  6. private final String str2 = "world";
  7. private static final Object o = new Object();
  8. }

使用javap -v -p Load1.class查看其字节码文件:

  1. {
  2. private static int a;
  3. descriptor: I
  4. flags: ACC_PRIVATE, ACC_STATIC
  5. private static int b;
  6. descriptor: I
  7. flags: ACC_PRIVATE, ACC_STATIC
  8. private static final int c;
  9. descriptor: I
  10. flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
  11. ConstantValue: int 20
  12. private static final java.lang.String str;
  13. descriptor: Ljava/lang/String;
  14. flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
  15. ConstantValue: String hello
  16. private final java.lang.String str2;
  17. descriptor: Ljava/lang/String;
  18. flags: ACC_PRIVATE, ACC_FINAL
  19. ConstantValue: String world
  20. private static final java.lang.Object o;
  21. descriptor: Ljava/lang/Object;
  22. flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
  23. public wf.load.Load1();
  24. descriptor: ()V
  25. flags: ACC_PUBLIC
  26. Code:
  27. stack=2, locals=1, args_size=1
  28. 0: aload_0
  29. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  30. 4: aload_0
  31. 5: ldc #2 // String world
  32. 7: putfield #3 // Field str2:Ljava/lang/String;
  33. 10: return
  34. LineNumberTable:
  35. line 3: 0
  36. line 9: 4
  37. LocalVariableTable:
  38. Start Length Slot Name Signature
  39. 0 11 0 this Lwf/load/Load1;
  40. //这是个方法,该方法是JVM内部自动生成的,会把所以static修饰的代码写入其中,在类初始化时,进行初始化
  41. static { };
  42. descriptor: ()V
  43. flags: ACC_STATIC
  44. Code:
  45. stack=2, locals=0, args_size=0
  46. 0: bipush 10
  47. 2: putstatic #4 // Field b:I
  48. 5: new #5 // class java/lang/Object
  49. 8: dup
  50. 9: invokespecial #1 // Method java/lang/Object."<init>":()V
  51. 12: putstatic #6 // Field o:Ljava/lang/Object;
  52. 15: return
  53. LineNumberTable:
  54. line 6: 0
  55. line 10: 5
  56. }

从上面可以看出,static修饰的变量,如果有值会在初始化阶段解析。而被static 和final两个修饰符共同修饰的基本类型,以及字符串常量会在准备阶段就赋值。而修饰的其他变量也是在初始化阶段赋值。

3.解析(Resolution)

将常量池中的符号引用解析为直接引|用。

  • 符号引用(Symbolic References) :符号引用以一-组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不-定已经加载到内存中。
  • 直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虛拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。

初始化(Initialization)

()V 方法:Java内部的一个类,Java将一个类中的静态变量及静态代码块全部组合到一起组成这个方法。

O方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问。
0方法与类的构造函数(或者说实例构造器O方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的O方法执行之前,父类的(方法已经执行完毕。因此在虚拟机中第-一个被执行的0方法的类肯定是java.lang.Object.
由于父类的O 方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。

初始化即调用 ()V ,虚拟机会保证这个类的『构造方法』的线程安全

初始化时机

  • 一:遇到new,getstatic,putstatic或invokestatic这四个字节指令时,如果类没有进行过初始化,则需要先对其进行初始化。这四条指令生成的常见场景是:一:1.实用new关键字实例化对象的时候,2.读取或设置一个类的类的静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外),以及3.调用一个类的静态方法时候。
  • 二:使用java.lang.reflect的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发初始化。
  • 三:当初始化一个类时,如果发现父类还没有进行初始化,则需要先出发其父类初始化。
  • 四:当虚拟机启动时,用户需要指定一个执行类的主类(包括main方法的那个类),虚拟机会先初始化这个类。
  • 五:当JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandler实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的句柄方法,并且这个方法句柄对应的类没有进行过初始化,则需要先进行初始化出发。

这五种情况,虚拟机做了强制限定语:有且只有,这五种场景中的行为称为对一个类进行主动引用。除此之外,所有类的引用都不会触发初始化,称为被动引用。

不会导致类初始化的情况:

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
  • 类对象.class 不会触发初始化
  • 创建该类的数组不会触发初始化

验证(验证时,先把其他部分注释掉,只留下需要验证的代码,进行验证)

  1. public class Load3 {
  2. static {
  3. System.out.println("main init");
  4. }
  5. public static void main(String[] args) throws ClassNotFoundException {
  6. // 1. 静态常量(基本类型和字符串)不会触发初始化
  7. System.out.println(B.b);
  8. // 2. 类对象.class 不会触发初始化
  9. System.out.println(B.class);
  10. // 3. 创建该类的数组不会触发初始化
  11. System.out.println(new B[0]);
  12. // 4. 不会初始化类 B,但会加载 B、A
  13. ClassLoader cl = Thread.currentThread().getContextClassLoader();
  14. cl.loadClass("wf.load.B");
  15. // 5. 不会初始化类 B,但会加载 B、A
  16. ClassLoader c2 = Thread.currentThread().getContextClassLoader();
  17. Class.forName("wf.load.B", false, c2);
  18. // 1. 首次访问这个类的静态变量或静态方法时
  19. System.out.println(A.a);
  20. // 2. 子类初始化,如果父类还没初始化,会引发
  21. System.out.println(B.c);
  22. // 3. 子类访问父类静态变量,只触发父类初始化
  23. System.out.println(B.a);
  24. // 4. 会初始化类 B,并先初始化类 A
  25. Class.forName("wf.load.B");
  26. }
  27. }
  28. class A {
  29. static int a = 0;
  30. static {
  31. System.out.println("a init");
  32. }
  33. }
  34. class B extends A {
  35. final static double b = 5.0;
  36. static boolean c = false;
  37. static {
  38. System.out.println("b init");
  39. }
  40. }

面试题

1.”e init “何时打印

  1. public class Load2 {
  2. public static void main(String[] args) {
  3. System.out.println(E.a);
  4. System.out.println(E.str);
  5. System.out.println(E.anInt);
  6. }
  7. }
  8. class E{
  9. static final int a = 10;
  10. static final String str = "10";
  11. static final Integer anInt = 10;//自动装箱Integer.valueOf(10);
  12. static {
  13. System.out.println("e init");
  14. }
  15. }

在System.out.println(E.anInt);之前打印因为。Interger是一个包装类型,其赋值会被放在初始化时进行。
输出:

  1. 10
  2. 10
  3. e init
  4. 10

2.典型应用 - 完成懒惰初始化单例模式

  1. public class Load4 {
  2. public static void main(String[] args) {
  3. System.out.println(Singleton.test);
  4. System.out.println(Singleton.getInstance());
  5. }
  6. }
  7. final class Singleton{
  8. static final int test = 10;
  9. private static class LazyHolder{
  10. static final Singleton singleton = new Singleton();
  11. static {
  12. System.out.println("LazyHolder init");
  13. }
  14. }
  15. public static Singleton getInstance(){
  16. return LazyHolder.singleton;
  17. }
  18. }

输出:

  1. 10
  2. LazyHolder init
  3. wf.load.Singleton@4554617c

内部类的加载被延迟到使用的阶段。且JVM内部保证初始化的线程安全。

2.类加载器































名称 加载哪的类 说明
Bootstrap ClassLoader JAVA_HOME/jre/lib 无法直接访问
Extension ClassLoader JAVA_HOME/jre/lib/ext 上级为 Bootstrap,显示为 null
Application ClassLoader classpath 上级为 Extension
自定义类加载器 自定义 上级为 Application

1.启动类加载器( Bootstrap ClassLoader))

启动类加载器( Bootstrap ClassLoader):前面已经介绍过,这个类加载器负责将存放在\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虛拟机识别的(仅按照文件名识别,如rtjar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虛拟机内存中。启动类加载器无法被Java程序直接引用。

用 Bootstrap 类加载器加载类:

  1. package wf.load;
  2. public class Load5 {
  3. public static void main(String[] args) throws Exception{
  4. Class<?> aClass = Class.forName("wf.load.Loader");
  5. System.out.println(aClass.getClassLoader());
  6. System.out.println(aClass.getClassLoader().getParent());
  7. }
  8. }
  9. class Loader{
  10. static {
  11. System.out.println("Loader 被加载了。。。");
  12. }
  13. }

输出:

  1. E:\java\idea\ym\jvm\out\production\jvm>java -Xbootclasspath/a:. wf.load.Load5
  2. Loader 被加载了。。。
  3. null
  4. Exception in thread "main" java.lang.NullPointerException
  5. at wf.load.Load5.main(Load5.java:8)

启动类加载器由c++代码编写,使用Java不能直接引用,故输出为null,而它也是最顶层的类加载器,故它没有父类加载器,所以会包异常。
-Xbootclasspath 表示设置 bootclasspath。其中 /a:. 表示将当前目录追加至 bootclasspath 之后

可以用这个办法替换核心类

  • java -Xbootclasspath:
  • java -Xbootclasspath/a:<滞后追加路径>
  • java -Xbootclasspath/p:<置前追加路径>

2.扩展类加载器(Extension ClassLoader)

扩展类加载器(Extension ClassLoader): 这个加载器由sun.misc.LauncherSExtClassLoader实现,它负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

使用扩展类加载器加载类:

  1. package wf.load;
  2. public class Load5 {
  3. public static void main(String[] args) throws Exception{
  4. Class<?> aClass = Class.forName("wf.load.Loader");
  5. System.out.println(aClass.getClassLoader());
  6. System.out.println(aClass.getClassLoader().getParent());
  7. }
  8. }
  9. class Loader{
  10. static {
  11. System.out.println("Loader 被加载了。。。");
  12. }
  13. }

输出:

  1. sun.misc.Launcher$AppClassLoader@18b4aac2
  2. sun.misc.Launcher$ExtClassLoader@1540e19d

将Loader类的class文件打成jar包

  1. E:\java\idea\ym\jvm\out\production\jvm>jar -cvf my.jar wf/load/Loader.class
  2. 已添加清单
  3. 正在添加: wf/load/Loader.class(输入 = 486) (输出 = 340)(压缩了 30%)

然后的打成的jar包放入JAVA_HOME/jre/lib/ext目录下。再次运行程序
输出:

  1. Loader 被加载了。。。
  2. sun.misc.Launcher$ExtClassLoader@45ee12a7
  3. null

3.应用程序类加载器(Application ClassLoader )

应用程序类加载器(Application ClassLoader): 这个类加载器由sun.misc.LauncherSAppClassLoader来实现。由于这个类加载器是ClassLoader中的getSystemClassLoader(方法的返回值,所以- -般也称它为系统类加载器。它负责加载用户类路径(ClassPath).上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,- -般情况下这个就是程序中默认的类加载器。

在上述过程中,应用类加载器加载类,已经演示过这里不在演示

双亲委派机制

所谓双亲委派机制,指调用loadClass()方法时。类的查找规则:

  1. protected Class<?> loadClass(String name, boolean resolve)
  2. throws ClassNotFoundException {
  3. synchronized (getClassLoadingLock(name)) {
  4. // 1. 检查该类是否已经加载
  5. Class<?> c = findLoadedClass(name);
  6. if (c == null) {
  7. long t0 = System.nanoTime();
  8. try {
  9. if (parent != null) {
  10. // 2. 有上级的话,委派上级 loadClass
  11. c = parent.loadClass(name, false);
  12. } else {
  13. // 3. 如果没有上级了(ExtClassLoader),则委派
  14. BootstrapClassLoader
  15. c = findBootstrapClassOrNull(name);
  16. }
  17. } catch (ClassNotFoundException e) {
  18. }
  19. if (c == null) {
  20. long t1 = System.nanoTime();
  21. // 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
  22. c = findClass(name);
  23. // 5. 记录耗时
  24. sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
  25. sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
  26. sun.misc.PerfCounter.getFindClasses().increment();
  27. }
  28. }
  29. if (resolve) {
  30. resolveClass(c);
  31. }
  32. return c;
  33. }
  34. }

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每-一个 层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子在自己加载要不加载的类。

线程上下文类加载器

我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写

  1. Class.forName("com.mysql.jdbc.Driver")

也是可以让 com.mysql.jdbc.Driver 正确加载的,你知道是怎么做的吗?
让我们追踪一下源码:

  1. public class DriverManager {
  2. // 注册驱动的集合
  3. private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers
  4. = new CopyOnWriteArrayList<>();
  5. // 初始化驱动
  6. static {
  7. loadInitialDrivers();
  8. println("JDBC DriverManager initialized");
  9. }
  10. }

先不看别的,看看 DriverManager 的类加载器:

  1. public class Load6 {
  2. public static void main(String[] args) throws Exception{
  3. System.out.println(DriverManager.class.getClassLoader());
  4. }
  5. }

打印 null,表示它的类加载器是 Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类,但JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,这样问题来了,在
DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?

继续看 loadInitialDrivers() 方法:

  1. private static void loadInitialDrivers() {
  2. String drivers;
  3. try {
  4. drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
  5. public String run() {
  6. return System.getProperty("jdbc.drivers");
  7. }
  8. });
  9. } catch (Exception ex) {
  10. drivers = null;
  11. }
  12. // 1)使用ServiceLoaderl机制加载驱动,即SPI
  13. AccessController.doPrivileged(new PrivilegedAction<Void>() {
  14. public Void run() {
  15. ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
  16. Iterator<Driver> driversIterator = loadedDrivers.iterator();
  17. try{
  18. while(driversIterator.hasNext()) {
  19. driversIterator.next();
  20. }
  21. } catch(Throwable t) {
  22. // Do nothing
  23. }
  24. return null;
  25. }
  26. });
  27. println("DriverManager.initialize: jdbc.drivers = " + drivers);
  28. // 2)使用 jdbc.drivers 定义的驱动名加载驱动
  29. if (drivers == null || drivers.equals("")) {
  30. return;
  31. }
  32. String[] driversList = drivers.split(":");
  33. println("number of Drivers:" + driversList.length);
  34. for (String aDriver : driversList) {
  35. try {
  36. println("DriverManager.Initialize: loading " + aDriver);
  37. // 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器
  38. Class.forName(aDriver, true,
  39. ClassLoader.getSystemClassLoader());
  40. } catch (Exception ex) {
  41. println("DriverManager.Initialize: load failed: " + ex);
  42. }
  43. }
  44. }

先看 2)发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此可以顺利完成类加载
再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)

约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称

图片

这样就可以使用

  1. ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
  2. Iterator<接口类型> iter = allImpls.iterator();
  3. while(iter.hasNext()) {
  4. iter.next();
  5. }

来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:

  • JDBC
  • Servlet 初始化器
  • Spring 容器
  • Dubbo(对 SPI 进行了扩展)

接着看 ServiceLoader.load 方法

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

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由
Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类

LazyIterator 中:

  1. private S nextService() {
  2. if (!hasNextService())
  3. throw new NoSuchElementException();
  4. String cn = nextName;
  5. nextName = null;
  6. Class<?> c = null;
  7. try {
  8. c = Class.forName(cn, false, loader);
  9. } catch (ClassNotFoundException x) {
  10. fail(service,
  11. "Provider " + cn + " not found");
  12. }
  13. if (!service.isAssignableFrom(c)) {
  14. fail(service,
  15. "Provider " + cn + " not a subtype");
  16. }
  17. try {
  18. S p = service.cast(c.newInstance());
  19. providers.put(cn, p);
  20. return p;
  21. } catch (Throwable x) {
  22. fail(service,
  23. "Provider " + cn + " could not be instantiated",
  24. x);
  25. }
  26. throw new Error(); // This cannot happen
  27. }

自定义类加载器

1.为什么要使用自定义类加载器?

  • 1)想加载非 classpath 随意路径中的类文件
  • 2)都是通过接口来使用实现,希望解耦时,常用在框架设计
  • 3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

2.如何实现自定义类加载器?

    1. 继承 ClassLoader 父类
    1. 要遵从双亲委派机制,重写 findClass 方法注意不是重写 loadClass 方法,否则不会走双亲委派机制
    1. 读取类文件的字节码
    1. 调用父类的 defineClass 方法来加载类
    1. 使用者调用该类加载器的 loadClass 方法

    public class Load7 {

    1. public static void main(String[] args) throws ClassNotFoundException {
    2. MyClassLoad classLoad = new MyClassLoad();
    3. Class<?> aClass = classLoad.loadClass("Test");
    4. System.out.println(aClass.getClassLoader());
    5. }

    }

    class MyClassLoad extends ClassLoader{

    1. @Override
    2. protected Class<?> findClass(String name) throws ClassNotFoundException {
    3. String path = "F:\\" + name + ".class";
    4. Class<?> aClass = null;
    5. ByteOutputStream os = new ByteOutputStream();
    6. try {
    7. Files.copy(Paths.get(path),os);
    8. byte[] bytes = os.toByteArray();
    9. aClass = defineClass(name, bytes, 0, bytes.length);
    10. } catch (IOException e) {
    11. e.printStackTrace();
    12. throw new ClassNotFoundException("");
    13. }
    14. return aClass;
    15. }

    }

输出:

  1. wf.load.MyClassLoad@677327b6

加载成功。
注意:

如果出现

  1. Exception in thread "main" java.lang.ClassFormatError: Extra bytes at the end of class file Test
  2. at java.lang.ClassLoader.defineClass1(Native Method)
  3. at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
  4. at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
  5. at wf.load.MyClassLoad.findClass(Load7.java:28)
  6. at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
  7. at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
  8. at wf.load.Load7.main(Load7.java:13)

错误,有可能是在获取byte数组时,错误的实现方式导致了class文件和byte数组不一致,JVM检查不过。需要一个字节一个字节的写入byte数组。

发表评论

表情:
评论列表 (有 0 条评论,82人围观)

还没有评论,来说两句吧...

相关阅读

    相关 jvm

    1.类加载过程: 首先要加载某个类一定是出于某种目的,比如要运行java程序,那么久必须加载主类才能运行其中的方法,所以一般在这些情况下,如果类没有被加载,就会自动被加载

    相关 JvmJvm机制

    类加载时机 > 虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加

    相关 JVM-机制

    类加载过程     类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。它们开始的顺序如下图所示:

    相关 JVM 机制

    jvm将描述java类的.class的字节码文件加载到内存中,并对文件中的数据进行安全性校验、解析和初始化,最终形成可以被java虚拟机直接使用的java类型,这个复杂的过程为