JVM_11 类加载与字节码技术 (类加载与类的加载器)

电玩女神 2023-01-21 09:29 96阅读 0赞

在这里插入图片描述

学习视频链接:黑马程序员JVM完整教程

1. 类加载阶段

1.1 加载阶段

  • 将类的字节码载入方法区中,内部采用 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 工具查看

在这里插入图片描述

1.2 链接阶段

验证

验证类是否符合 JVM规范,安全性检查,阻止不合法的类继续运行。用 UE 等支持二进制的编辑器修改 HelloWorld.class的魔数,在控制台运行:

  1. E:\git\jvm\out\production\jvm>java cn.itcast.jvm.t5.HelloWorld
  2. Error: A JNI error has occurred, please check your installation and try again
  3. Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value
  4. 3405691578 in class file cn/itcast/jvm/t5/HelloWorld
  5. at java.lang.ClassLoader.defineClass1(Native Method)
  6. at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
  7. at
  8. java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
  9. at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
  10. at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
  11. at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
  12. at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
  13. at java.security.AccessController.doPrivileged(Native Method)
  14. at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
  15. at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
  16. at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
  17. at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
  18. at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)

准备

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

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

解析

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

  1. /** * 解析的含义 */
  2. public class Load2 {
  3. public static void main(String[] args) throws ClassNotFoundException,IOException {
  4. ClassLoader classloader = Load2.class.getClassLoader();
  5. // loadClass 方法不会导致类的解析和初始化
  6. Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C");
  7. // new C();
  8. System.in.read();
  9. }
  10. }
  11. class C {
  12. D d = new D();
  13. }
  14. class D {
  15. }

1.3 初始化阶段

< init()> V 方法

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

发生的时机

概括得说,类初始化是【懒惰的】

  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

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

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

测试代码:

  1. class A {
  2. static int a = 0;
  3. static {
  4. System.out.println("a init");
  5. }
  6. }
  7. class B extends A {
  8. final static double b = 5.0;
  9. static boolean c = false;
  10. static {
  11. System.out.println("b init");
  12. }
  13. }

验证(测试时请先全部注释,每次只执行其中一个)

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

1.4 练习

从字节码分析,使用 a,b,c 这三个常量是否会导致 E 初始化:

  1. public class Load4 {
  2. public static void main(String[] args) {
  3. System.out.println(E.a);
  4. System.out.println(E.b);
  5. System.out.println(E.c);
  6. }
  7. }
  8. class E {
  9. public static final int a = 10;
  10. public static final String b = "hello";
  11. public static final Integer c = 20;
  12. }

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

  1. public final class Singleton {
  2. private Singleton() { }
  3. // 内部类中保存单例
  4. private static class LazyHolder {
  5. static final Singleton INSTANCE = new Singleton();
  6. }
  7. // 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员
  8. public static Singleton getInstance() {
  9. return LazyHolder.INSTANCE;
  10. }
  11. }

以上的实现特点是:

  • 懒惰实例化
  • 初始化时的线程安全是有保障的

2. 类加载器

以 JDK 8 为例:































名称 加载哪的类 说明
Bootstrap ClassLoader(启动类加载器) JAVA_HOME/jre/lib 无法直接访问
Extension ClassLoader(扩展类加载器) JAVA_HOME/jre/lib/ext 上级为 Bootstrap,显示为 null
Application ClassLoader(应用程序类加载器) classpath 上级为 Extension
自定义类加载器 自定义 上级为 Application

类加载器的优先级(由高到低):启动类加载器 -> 扩展类加载器 -> 应用程序类加载器 -> 自定义类加载器

2.1 启动类加载器

用 Bootstrap 类加载器加载类:

  1. package cn.itcast.jvm.t3.load;
  2. public class F {
  3. static {
  4. System.out.println("bootstrap F init");
  5. }
  6. }

执行:

  1. package cn.itcast.jvm.t3.load;
  2. public class Load5_1 {
  3. public static void main(String[] args) throws ClassNotFoundException {
  4. Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F");
  5. // aClass.getClassLoader():获得aClass对应的类加载器
  6. System.out.println(aClass.getClassLoader());
  7. }
  8. }

输出:

  1. E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:. cn.itcast.jvm.t3.load.Load5
  2. bootstrap F init
  3. null
  • -Xbootclasspath 表示设置 bootclasspath
  • 其中 /a:. 表示将当前目录追加至 bootclasspath 之后
  • 可以有以下几个方式替换启动类路径下的核心类:

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

在这里插入图片描述

2.2 扩展类加载器

  1. package cn.itcast.jvm.t3.load;
  2. public class G {
  3. static {
  4. System.out.println("classpath G init");
  5. }
  6. }

程序执行:

  1. public class Load5_2 {
  2. public static void main(String[] args) throws ClassNotFoundException {
  3. Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
  4. System.out.println(aClass.getClassLoader());
  5. }
  6. }

输出结果:

  1. classpath G init
  2. sun.misc.Launcher$AppClassLoader@18b4aac2 // 这个类是由应用程序加载器加载

写一个同名的类:

  1. package cn.itcast.jvm.t3.load;
  2. public class G {
  3. static {
  4. System.out.println("ext G init");
  5. }
  6. }

打个 jar 包:

  1. E:\git\jvm\out\production\jvm>jar -cvf my.jar cn/itcast/jvm/t3/load/G.class // 将G.class打jar包
  2. 已添加清单
  3. 正在添加: cn/itcast/jvm/t3/load/G.class(输入 = 481) (输出 = 322)(压缩了 33%)

将 jar 包拷贝到JAVA_HOME/jre/lib/ext扩展类加载器加载的类必须是以jar包方式存在),重新执行 Load5_2

输出:

  1. ext G init
  2. sun.misc.Launcher$ExtClassLoader@29453f44 // 这个类是由扩展类加载器加载

2.3 双亲委派模式

所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则。

注意:这里的双亲,翻译为上级似乎更为合适,因为它们并没有继承关系

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

例如:

  1. public class Load5_3 {
  2. public static void main(String[] args) throws ClassNotFoundException {
  3. Class<?> aClass = Load5_3.class.getClassLoader()
  4. .loadClass("cn.itcast.jvm.t3.load.H");
  5. System.out.println(aClass.getClassLoader());
  6. }
  7. }

执行流程为:

  • sun.misc.Launcher$AppClassLoader // 1 处, 开始查看已加载的类,结果没有
  • sun.misc.Launcher$AppClassLoader // 2 处,委派上级 sun.misc.Launcher$ExtClassLoader.loadClass()
  • sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有
  • sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派 BootstrapClassLoader 查找
  • BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有
  • sun.misc.Launcher$ExtClassLoader // 4 处,调用自己的 findClass 方法,是在JAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有,回到 sun.misc.Launcher$AppClassLoader 的 // 2 处
  • 继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 findClass 方法,在 classpath 下查找,找到了

2.4 线程上下文类加载器

我们在使用 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. }

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

  1. System.out.println(DriverManager.class.getClassLoader());

打印 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)使用 ServiceLoader 机制加载驱动,即 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, ClassLoader.getSystemClassLoader());
  39. } catch (Exception ex) {
  40. println("DriverManager.Initialize: load failed: " + ex);
  41. }
  42. }
  43. }

先看 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. // 获取线程上下文类加载器
  3. ClassLoader cl = Thread.currentThread().getContextClassLoader();
  4. return ServiceLoader.load(service, cl);
  5. }

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由 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, "Provider " + cn + " not found");
  11. }
  12. if (!service.isAssignableFrom(c)) {
  13. fail(service, "Provider " + cn + " not a subtype");
  14. }
  15. try {
  16. S p = service.cast(c.newInstance());
  17. providers.put(cn, p);
  18. return p;
  19. } catch (Throwable x) {
  20. fail(service, "Provider " + cn + " could not be instantiated", x);
  21. }
  22. throw new Error(); // This cannot happen
  23. }

2.5 自定义类加载器

问问自己,什么时候需要自定义类加载器:

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

步骤:

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

  • 后续会陆续更新,如果对大家有帮助,请三连支持一下!
  • 有问题欢迎评论区留言,及时帮大家解决!

在这里插入图片描述

发表评论

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

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

相关阅读

    相关 机制

    类加载器: 执行”通过一个类的全限定名来获取描述此类的二进制字节流“这个步骤的代码模块称为“类加载器“,类加载器虽然只用于实现类的加载动作,但是他的作用不是仅限于此,...

    相关 jvm

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

    相关 ——

    虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作