JVM 类加载器详解(加载详情与加载器类型)

我不是女神ヾ 2022-12-18 11:59 280阅读 0赞

加载

将类的字节码载入方法区中,内部即底层是采用 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、 验证:验证类是否符合 JVM规范,安全性检查
2、 准备:为 static 变量分配空间,设置默认值

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

使用一个简单的变量声明进行测试,可以发现变量的声明和赋值时分开的。

在这里插入图片描述
3、解析
在类的初始化时如果这个类没有被使用,那么这个类是不会主动加载的。

初始化

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

使用下面的java代码进行测试:加载A类,查看输出,B类是否会被初始化,也就是说在B类当中的输出语句是否会被执行。答案时很显然的,B类的输出语句是没有输出的。

  1. package Class_load.load;
  2. import java.io.IOException;
  3. public class analysis_class {
  4. public static void main(String[] args) throws ClassNotFoundException, IOException {
  5. ClassLoader classloader = analysis_class.class.getClassLoader();
  6. // loadClass 方法不会导致类的解析和初始化
  7. Class<?> A = classloader.loadClass("Class_load.load.A");
  8. }
  9. }
  10. class A {
  11. B b = new B();
  12. }
  13. class B {
  14. static {
  15. System.out.println("hello");
  16. }
  17. }

发生的时机:概括得说,类初始化是懒惰的。

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

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

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
  • 类对象.class 不会触发初始化
  • 创建该类的数组不会触发初始化
  • 类加载器的loadClass方法
  • Class.forName的参数2为false时
检测:使用字节码对以下代码进行分析
  1. public class practice {
  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; // Integer.valueOf(20)
  12. static {
  13. System.out.println("init E");
  14. }
  15. }

在先对上方的变量进行加载,在对Integer包装类型进行初始化的时候会触发类的初始化,所以说init E的输出会在20之前输出。

类加载器

在这里插入图片描述
启动类加载器

用 Bootstrap 类加载器加载类:使用下述代码作为测试:

  1. public class start_up {
  2. public static void main(String[] args) throws ClassNotFoundException {
  3. Class<?> aClass = Class.forName("Class_load.loader.Son");
  4. System.out.println(aClass.getClassLoader());
  5. }
  6. }
  7. class Son {
  8. static {
  9. System.out.println("Son init");
  10. }
  11. }

使用java命令 java -Xbootclasspath/a:. Class_load.loader.start_up 对类进行加载,

  • -Xbootclasspath 表示设置 bootclasspath
  • 其中 /a:. 表示将当前目录追加至 bootclasspath 之后

扩展类加载器

使用上方相同的代码,直接运行,在这里会直接初始化加载Son这个类,并且会打印App对象,但在这里如果我们在jdk的扩展类当中加上一个Son的jar包,这时就会加载扩展类的jar包文件。

双亲委派模式

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

在这里插入图片描述

线程上下文类加载器

我们在使用 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(); println("JDBC DriverManager initialized");
  8. }

先不看别的,看看 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() 方法:

在这里插入图片描述
在这里插入图片描述
先看第二点发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此可以顺利完成类加载
再看第一点它就是大名鼎鼎的 Service Provider Interface (SPI)

自定义类加载器

什么时候需要使用自定义类加载器?

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

自定义加载类的步骤:

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

运行期优化

即时编译=>分层编译

使用以下代码进行测试:

  1. public class Jit1 {
  2. // -XX:+PrintCompilation
  3. public static void main(String[] args) {
  4. for (int i = 0; i < 200; i++) {
  5. long start = System.nanoTime();
  6. for (int j = 0; j < 1000; j++) {
  7. new Object();
  8. }
  9. long end = System.nanoTime();
  10. System.out.printf("%s%d%s\t%d\n","次数 = ",i,"。 时间 = ",(end - start));
  11. }
  12. }
  13. }

在进行循环创建对象的时候,到后续进行创建新的对象的时候时间会缩短,这是为什么呢?

JVM 将执行状态分成了 5 个层次:

  • 0 层,解释执行(Interpreter)
  • 1 层,使用 C1 即时编译器编译执行(不带 profiling)
  • 2 层,使用 C1 即时编译器编译执行(带基本的 profiling)
  • 3 层,使用 C1 即时编译器编译执行(带完全的 profiling)
  • 4 层,使用 C2 即时编译器编译执行

在这里使用一个JVM参数 -XX:-DoEscapeAnalysis 关闭即时编译器,这是我们所花费的时间就会变长,也就是在上述的过程中,是不会进入C2。

即时编译器(JIT)与解释器的区别

  • 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
  • JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
  • 解释器是将字节码解释为针对所有平台都通用的机器码
  • JIT 会根据平台类型,生成平台特定的机器码

对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由来),优化之刚才的一种优化手段称之为【逃逸分析】,

即时编译=>方法内联

使用以下代码段进行测试:在执行一千次方法调用:

  1. import java.util.Random;
  2. import java.util.concurrent.ThreadLocalRandom;
  3. public class Jit2 {
  4. public static void main(String[] args) {
  5. int x = 0;
  6. for (int i = 0; i < 500; i++) {
  7. long start = System.nanoTime();
  8. for (int j = 0; j < 1000; j++) {
  9. x = square(9);
  10. }
  11. long end = System.nanoTime();
  12. System.out.printf("%d\t%d\t%d\n", i, x, (end - start));
  13. }
  14. }
  15. private static int square(final int i) {
  16. return i * i;
  17. }
  18. }

运行代码可以发现在执行179次的时候用时第一次出现0,这个时候发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置:

  1. System.out.println(9 * 9);

在这里插入图片描述
早这里我们可以使用JVM参数 -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining 查看内联方法和详细参数。运行代码查看,在@28的地方提示Squre方法进行了内联

在这里插入图片描述
并且还可以使用一个参数 -XX:CompileCommand=dontinline,*Jit2.square 表示禁用方法内联,这个参数后面接上类名和方法名即可,再一次运行代码,会发现这个时候就不会出现时间为0的情况,因为在这个时候没有进行方法内联。

在这里插入图片描述
反射优化

使用以下代码段进行测试:

  1. import java.io.IOException;
  2. import java.lang.reflect.InvocationTargetException;
  3. import java.lang.reflect.Method;
  4. public class Reflect {
  5. public static void foo() {
  6. System.out.println("foo...");
  7. }
  8. public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
  9. Method foo = Reflect.class.getMethod("foo");
  10. for (int i = 0; i <= 16; i++) {
  11. System.out.printf("%d\t", i);
  12. foo.invoke(null);
  13. }
  14. System.in.read();
  15. }
  16. }

foo.invoke 前面 0 ~ 15 次调用使用的是 MethodAccessor 的 NativeMethodAccessorImpl 实现
在这里插入图片描述
当调用到第 16 次(从0开始算)时,会采用运行时生成的类代替掉最初的实现。

发表评论

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

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

相关阅读

    相关 ——

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

    相关 JVM详解

    前言 在上一篇中,通过下面这幅图大致了JVM整体的内部运行结构图,在JVM的结构中,类加载子系统作为连接外部class文件与真正将class文件加载到运行时数据区,承担着

    相关 JVM详解

    首先来了解一下字节码和class文件的区别: 我们知道,新建一个java对象的时候,JVM要将这个对象对应的字节码加载到内存中,这个字节码的原始信息存放在classpat

    相关 JVM过程

    我们知道在jvm中是通过各类形形色色的类加载器对class文件进行加载后才能进行使用的,而且jvm中能提供动态加载的特性也全是依靠类加载的机制,它已经成为了Java体系中