JVM_6_类加载

类加载阶段

加载

  • 将类的字节码载入方法区中,内部采用 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对应的对象(xxx.class)是存储在堆中,元空间中有 _java_mirror的引用
  • 可以通过前面介绍的 HSDB 工具查看

image-20210926172814179

链接

验证

验证类是否符合 JVM规范,安全性检查

用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行

image-20210928161915682

准备

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

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

源码

// 演示final对静态变量的影响
public class Demo3_2 {
    static int a;
    static int b = 10;
    static final int c = 20;
    static final String d = "hello";
    static final Object e = new Object();
}

字节码

image-20210928163706674

image-20210928163835843

解析

类加载时,类的字节码载入方法区。解析时,将常量池中的符号引用解析为直接引用

// 解析的含义
public class Demo3_3 {
    public static void main(String[] args) throws ClassNotFoundException, IOException {
        ClassLoader classLoader = Demo3_3.class.getClassLoader();
        // loadClass 方法不会导致类的解析和初始化,只会加载(类的字节码载入方法区)
        Class<?> c = classLoader.loadClass("P3.C");

        // new C() 加载、解析、初始化
//        C c = new C();
        System.in.read();
    }
}

class C {
    D d = new D();
}

class D{

}

启用HDBS工具

cd /Library/Java/JavaVirtualMachines/jdk1.8.0_291.jdk/Contents/Home
java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB 
使用classLoad方法加载C

image-20210928170308580

进入类C的常量池

image-20210928170647752

  • 使用类加载器ClassLoader的classLoad方法,只会加载C这个类。

  • 加载即将类的字节码载入方法区中,但是也仅仅是字节码,不会对字节码做处理

  • 所以常量池中,类D仅仅是一个符号引用,并不知道它要引用哪个地址

使用new C()

image-20210928171204746

  • 使用new C(),new会触发加载、解析
  • 解析会将常量池中的符号引用解析为直接引用

初始化

()V 方法

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

发生的时机

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

  • main 方法所在的类,总会被首先初始化

  • 首次访问这个类的静态变量或静态方法时

  • 子类初始化,如果父类还没初始化,会引发

  • 子类访问父类的静态变量,只会触发父类的初始化

  • Class.forName

  • new 会导致初始化

不会导致类初始化的情况
  • 访问类的 static fifinal 静态常量(基本类型和字符串)不会触发初始化
    • 在准备阶段就已经赋值了
  • 类对象.class 不会触发初始化
    • 在类加载阶段就已经生成了java_mirror即类对象.class
  • 创建该类的数组不会触发初始化
  • 类加载器的 loadClass 方法
  • Class.forName 的参数 2 为 false 时

实验

class A { 
  static int a = 0; 
  static { 
    System.out.println("a init"); 
  } 
}
class B extends A { 
  final static double b = 5.0; 
  static boolean c = false; 
  static { 
    System.out.println("b init"); 
  } 
}

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

public class Load3 { 
  static { 
    System.out.println("main init"); 
  }
  
  public static void main(String[] args) throws ClassNotFoundException { 
    // 1. 静态常量(基本类型和字符串)不会触发初始化 
    System.out.println(B.b); 
    // 2. 类对象.class 不会触发初始化 
    System.out.println(B.class); 
    // 3. 创建该类的数组不会触发初始化 
    System.out.println(new B[0]); 
    // 4. 不会初始化类 B,但会加载 B、A 
    ClassLoader cl = Thread.currentThread().getContextClassLoader(); cl.loadClass("cn.itcast.jvm.t3.B"); 
    // 5. 不会初始化类 B,但会加载 B、A 
    ClassLoader c2 = Thread.currentThread().getContextClassLoader(); Class.forName("cn.itcast.jvm.t3.B", false, c2); 
    
    // 1. 首次访问这个类的静态变量或静态方法时 
    System.out.println(A.a); 
    // 2. 子类初始化,如果父类还没初始化,会引发 
    System.out.println(B.c); 
    // 3. 子类访问父类静态变量,只触发父类初始化 
    System.out.println(B.a); 
    // 4. 会初始化类 B,并先初始化类 A 
    Class.forName("cn.itcast.jvm.t3.B"); 
  } 
}

练习1

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

public class Load4 { 
  public static void main(String[] args) { 
    System.out.println(E.a); 
    System.out.println(E.b); 
    System.out.println(E.c); 
  } 
}

class E { 
  public static final int a = 10; 
  public static final String b = "hello"; 
  public static final Integer c = 20; // Integer.valueOf(20)
  
  static {
    // 如果E初始化了会打印此语句
    System.out.println("init E");
  }
}

输出结果

image-20210928174449677

字节码

image-20210928175005455

image-20210928175044888

练习2

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

public class Demo3_5 {
  public static void main(String[] args) {
    Singleton.test(); // 打印test
    // 只有显示地调用了getInstance才会去声明那个单例变量
    Singleton.getInstance(); // 打印lazy holder init
  }
}

class Singleton {
  public static void test() {
    System.out.println("test");
  }
  // 别的类调用不了构造方法,保证单例
  private Singleton() { 
  } 
  // 内部类中保存单例 
  private static class LazyHolder { 
    static final Singleton INSTANCE = new Singleton();
    static {
      System.out.println("lazy holder init");
    }
  }
  
  // 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员
  // 不调用就不会触发L啊这样Holder的加载链接初始化,保证懒加载
  public static Singleton getInstance() { 
    return LazyHolder.INSTANCE; 
  } 
}

以上的实现特点是:

  • 懒惰实例化,第一次使用到的是否才会加载

  • 初始化时的线程安全是有保障的

类加载器

以 JDK 8 为例:

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

启动类加载器

用 Bootstrap 类加载器加载类:

package cn.itcast.jvm.t3.load; 

public class F { 
  static { 
    System.out.println("bootstrap F init"); 
  } 
}

执行

package cn.itcast.jvm.t3.load; 

public class Load5_1 { 
  public static void main(String[] args) throws ClassNotFoundException { 
    Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F"); 
    System.out.println(aClass.getClassLoader()); 
  } 
}

输出

E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:. cn.itcast.jvm.t3.load.Load5 
bootstrap F init # 说明进行了初始化
null	# 获取不到类加载器,说明类加载器是启动类加载器(C++代码编写的java获取不到)
  • -Xbootclasspath 表示设置 bootclasspath
  • 其中 /a:. 表示将当前目录追加至 bootclasspath 之后
  • 可以用这个办法替换核心类
    • java -Xbootclasspath:< new bootclasspath >
    • java -Xbootclasspath/a:<追加路径>
    • java -Xbootclasspath/p:<追加路径>

扩展类加载器

package cn.itcast.jvm.t3.load; 
public class G { 
  static { 
    System.out.println("classpath G init"); 
  } 
}

执行

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

输出

classpath G init # 说明进行了初始化
sun.misc.Launcher$AppClassLoader@18b4aac2	# G为ClassPath下的类,默认用应用类加载器

写一个同名的类

package cn.itcast.jvm.t3.load; 
public class G { 
  static { 
    System.out.println("ext G init"); 
  } 
}

打个 jar 包,放在JAVA_HOME/jre/lib/ext目录下,以便让扩展类加载器加载此类

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

重新执行 Load5_2

输出

ext G init 
# 扩展类加载器优先于应用加载器
sun.misc.Launcher$ExtClassLoader@29453f44

双亲委派模式

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

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

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { 
  synchronized (getClassLoadingLock(name)) { 
    // 1. 检查该类是否已经加载 
    Class<?> c = findLoadedClass(name); 
    if (c == null) { 
      long t0 = System.nanoTime(); 
      try {
        if (parent != null) { 
          // 2. 有上级的话,委派上级 (递归调用)
          loadClass c = parent.loadClass(name, false); 
        } else { 
          // 3. 如果没有上级了(ExtClassLoader),则委派 BootstrapClassLoader(没有就没有了,不会再向上递归)
          c = findBootstrapClassOrNull(name); 
        } 
      } catch (ClassNotFoundException e) {
      }
      
      if (c == null) { 
        long t1 = System.nanoTime(); 
        // 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展,重写了的)去加载
        c = findClass(name); 
        // 5. 记录耗时
        sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
        sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); 
        sun.misc.PerfCounter.getFindClasses().increment(); 
      } 
    }
    if (resolve) { 
      resolveClass(c); 
    }
    return c; 
  } 
}

例如

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

执行流程为:

  1. sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有

  2. sun.misc.Launcher@AppClassLoader // 2 处,委派上级sun.misc.Launcher@ExtClassLoader.loadClass()

  3. sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有

  4. sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派BootstrapClassLoader查找

  5. BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有

  6. sun.misc.Launcher@ ExtClassLoader // 4 处,调用自己的 findClass 方法,是在JAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有,回到sun.misc.Launcher@AppClassLoader 的 // 2 处

  7. 继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的findClass 方法,在classpath 下查找,找到了

线程上下文类加载器

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

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

也是可以让 com.mysql.jdbc.Driver 正确加载的,你知道是怎么做的吗?

让我们追踪一下源码:

public class DriverManager { 
  // 注册驱动的集合 
  private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>(); 
  
  // 初始化驱动 
  static { 
    loadInitialDrivers(); 
    println("JDBC DriverManager initialized"); 
  }

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

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() 方法:

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; 
  }
  
  // 1)使用 ServiceLoader 机制加载驱动,即 SPI 
  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); 
  
  // 2)使用 jdbc.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); 
      // 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器 
      Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); 
    } catch (Exception ex) { 
      println("DriverManager.Initialize: load failed: " + ex); 
    } 
  } 
}

先看 2)发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此可以顺利完成类加载,破坏了双亲委派机制,直接让应用程序类加载器加载。

再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)

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

image-20210929113909395这样就可以使用

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

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

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

接着看 ServiceLoader.load 方法:

public static <S> ServiceLoader<S> load(Class<S> service) { 
  // 获取线程上下文类加载器 
  ClassLoader cl = Thread.currentThread().getContextClassLoader(); 
  return ServiceLoader.load(service, cl); 
}

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

private S nextService() { 
  if (!hasNextService()) throw new NoSuchElementException(); 
  String cn = nextName; 
  nextName = null; 
  Class<?> c = null; 
  try {
    // loader即线程上下文类加载器
    c = Class.forName(cn, false, loader); 
  } catch (ClassNotFoundException x) { 
    fail(service,"Provider " + cn + " not found"); 
  }
  if (!service.isAssignableFrom(c)) { 
    fail(service, "Provider " + cn + " not a subtype"); 
  }try {
    S p = service.cast(c.newInstance()); 
    providers.put(cn, p); 
    return p; 
  } catch (Throwable x) { 
    fail(service, "Provider " + cn + " could not be instantiated", x); 
  }
  throw new Error(); 
  // This cannot happen 
}

自定义类加载器

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

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

步骤:

  1. 继承 ClassLoader 父类

  2. 要遵从双亲委派机制,重写 findClass 方法

    • 注意不是重写 loadClass 方法,否则不会走双亲委派机制
  3. 读取类文件的字节码

  4. 调用父类的 defifineClass 方法来加载类

  5. 使用者调用该类加载器的 loadClass 方法

示例:

准备好两个类文件放入 E:\myclasspath,它实现了 java.util.Map 接口,可以先反编译看一下:

image-20210929152121272

image-20210929152132821

自定义类加载器

image-20210929152912527

测试代码

image-20210929152843381

输出结果

image-20210929152851545

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值