【JVM学习一】Java类加载机制

本博客内容是博主学习时自己整理的,如有错误,欢迎大家积极指出

参考博客地址:JVM 类加载机制

类的生命周期

在这里插入图片描述

  • 加载
    1. 通过包名+类名来获取此类的二进制字节流(即将class字节码加载到内存中)
    2. 将字节流中的静态数据结构转化为方法区的运行时数据结构(静态变量,静态代码块,常量池等)
    3. 在内存中(一般是堆)生成一个代表这个类的java.lang.Class对象,作为方法区这个类访问的入口
      补充: 第一步中的获取类方法仅是一种方式,还可以通过zip、网络、数据库等方式获取
  • 连接
    • 验证

      • 目的:确保Class字节流信息不会威胁到虚拟机
      • 四种验证:
        • 文件格式验证:验证字节流是否符合Class规范、主次版本号是否在当前虚拟机范围内,常量池中常量是否有不被支持的
        • 元数据验证:对字节码描述信息进行分析,如是否存在父类、是否继承了不能被继承的类等
        • 字节码验证:整个过程最复杂的阶段;通过数据流和控制流的分析,确定程序语义是否正确。主要针对方法体的验证(类型转换是否正确、跳转指定是否正确)
        • 符号引用验证:在后面的解析过程中发生,确保解析动作能正常执行
    • 准备

      • 目的:为static修饰得变量分配内存地址并设置类变量得初始值,这些变量使用的内存都在方法区进行分配
        补充:
      1. 这里进行分配的仅包括类变量(static修饰的变量),不包括实例变量。实例变量将会在对象实例化时,随着对象一起分配在Java堆中。
      2. 初始化值通常为0值,赋值是在初始化中进行的
      3. 基本数据类型初始化0值表
        在这里插入图片描述
    • 解析

      • 将JVM常量池内的符号引用替换为直接引用的过程(这一步可能在初始化之前,也可能在初始化之后)
      • 解析大体分为:
        • 类或接口解析
        • 字段解析
        • 类方法解析
        • 接口方法解析
          补充:
        1. 解释符号引用:java类在编译成class文件时,虚拟机不知道其确切地址,便用符号代替,解析就是将符号替换为真正地址
        2. 常出现在这个阶段的异常:
          • NoSuchFieldError:根据继承关系从下往上,找不到相关字段时的报错。(字段解析异常)
          • IllegalAccessError:字段或者方法,访问权限不具备时的错误。(类或接口的解析异常)
          • NoSuchMethodError:找不到相关方法时的错误。(类方法解析、接口方法解析时发生的异常)
  • 初始化:
    • 主要是对类中的static{}进行操作,对应字节码()方法。(对于类或接口不是必须)
    • 虚拟机规定必须对类进行初始化的6种情况
      • 遇到new、getstatic、putstatic、invokestatic字节码指令,如果类没有进行初始化,则初始化该类。会出现这些指令的情况
        • new 实例化对象时
        • 读取或者设置一个静态字段时(被final修饰,在编译器已经进入常量池的字段除外)
        • 调用一个类的静态方法时
      • 通过java.lang.reflect包进行反射调用时,如果类没有初始化,则需要对其进行初始化
      • 当初始化一个类时,其父类还未初始化时,则需要对其父类进行初始化
      • 当虚拟机启动时,需要指定主类(含mian()方法),虚拟机将初始化此类
      • JDK1.7使用动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic、REF_putsattic、REF_invokestatic的方法句柄,并且这个方法句柄所对应的类没有初始化,则需要对其进行初始化
      • 如果接口中定义了jdk1.8加入的默认方法(接口被default修饰),如果实现这个接口的类被初始化,那么接口应该在其之前被初始化
      • 补充:
        • 动态语言:
        • 句柄:
  • 使用
  • 卸载

类加载器

  • 作用
    • 执行使用之前的整个过程(加验准解出)
  • 种类
    • 在这里插入图片描述

    • 启动类加载器(Bootstrap ClassLoader)

      • 加载器的核心类库,rt.jar、resources.jar、charsets.jar,由C++编写,随JVM启动
    • 扩展类加载器(Extension ClassLoader)

      • 用于加载lib/ext目录下的的jar和.class文件
      • 是一个Java类,继承自URLClassLoader
      • 可通过系统变量java.ext.dirs指定目录
    • 应用程序类加载器(Application ClassLoader)

      • 我们写的Java类的默认加载器,也叫System ClassLoader
      • 一般用来记载classpath下的其他jar和.class文件
      • 我们写的代码一般会首先尝试使用这个类加载器进行加载
    • 自定义类加载器(User ClassLoader)

      • 个性化的定义一些功能扩展
  • 双亲委派模式
    • 除了Bootstrap CLassloader之外,其他的类加载器都应该有自己的父类。这里的父类不是继承和是一种叫组合的方式

    • java.lang.ClassLoader中实现双亲委派模式代码在这里插入图片描述

    • 流程:

      1. 在已经加载的类中查找是否存在,存在则返回
      2. 不存在,委托父类加载器进行加载(第二步的递归),如果父类没有加载,就委托父类的父类加载,直到根类加载器
      3. 如果根加载器都无法加载则抛出ClassNotFoundException异常,然后再调用findClass进行加载
    • 通过上面的流程我们可以发现,如果要自定义类加载器,只需要重写findClass方法即可,不需要重写loadClass方法

    • 双亲委派模式的优点

      • 安全性,避免Java核心api被篡改;比如用户通过自定义类加载器加载了一个自己写的java.lang.String类到JVM中,那么就可能造成病毒代码植入
      • 双亲委派模式从最顶端的加载器开始执行,则始终不会加载自定义的String
      • 避免重复加载,即避免同样的字节码多次加载
      • 补充:
        • 上述避免重复加载只是针对同一个类加载器来说,要比较两个类是否相等,只有在这两个类是同一个类加载器加载的前提下才有意义,否则,即使是同一个类被不同类加载器加载那么他们也一定是不相等的
        • 这里的相等包括Class对象的equal()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况
      • 自定义类加载器
        • 为什么需要自定义类加载器
          • 加密;因为Java代码可以很轻松的进行反编译,所以你不想让别人看你的源码,你就可以将编译后的代码进行某种加密,这时ClassLoader是无法进行加载的,所以需要自定义类加载器先解密然后进行加载
          • 非标准来源的代码;比如字节码是放在云、数据库等地方的
        • 不破坏双亲委托模式的自定义类加载器,只需要重写findClass()方法即可
  • 破坏双亲委派模式
    • Java SPI(service provider interface) 和 tomcat 就是破坏双亲委派模式的例子
      • SPI模式讲解(以JDBC为例子):jdk提供了一套由三方实现或扩展的API,三方厂商可以通过这些API对应用进行补充或扩展
        • 以前我们连接数据库的步骤,其中我们会通过Class.forName()方法去加载驱动类到内存;那么具体在这过程中做了些什么事呢?在这里插入图片描述

        • 8.0之前手动加载驱动的流程

          1. 在这里插入图片描述

          2. 在这里插入图片描述

          3. 在这里插入图片描述

        • Mysql8.0后我们可以使用SPI自动注册,不用手动Class.forName()去加载在这里插入图片描述

        • SPI是怎么自动注册的

          • 在这里插入图片描述

          • 加载驱动过程【因为博主用的JDK17,和上面博客链接中的流程有所不同,17中加了些加载时候对权限的判定】在这里插入图片描述

          • 从加载驱动的源码中可以看到,多处的地方都用到了Class.forName(),那么Class.forName()是怎么破坏双亲委托模式的呢?

            • ensureDriversInitialized()方法中有一段代码ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

            • 点进其中的load方法ClassLoader cl=Thread.currentThread().getContextClassLoader();,可以看到在这里插入图片描述

            • 接下来的步骤在这里插入图片描述
              在这里插入图片描述
              在这里插入图片描述

            • 可以从上面的步骤看到,最后通过指定Class.forName()的类加载器,实现破坏双亲委托模式【这只是自动注册实现破坏双亲委托模式,其实CLass.forName()中就是使用Caller的加载器进行加载的,可以debug看看】

      • 总结:
        • 实现双亲委派模式
          1. 在 jar 包的 META-INF/services 目录下创建一个以"接口全限定名"为命名的文件,内容为实现类的全限定名
          2. 接口实现类所在的 jar 包在 classpath 下
          3. 主程序通过 java.util.ServiceLoader 动态状态实现模块,它通过扫描 META-INF/services 目录下的配置文件找到实现类的全限定名,把类加载到 JVM
          4. SPI 的实现类必须带一个无参构造方法
        • 技术实现:主要是靠接编程+策略模式+配置文件组合实现的动态加载机制,使用java.util.ServiceLoader类进行的动态加载。流程如下:在这里插入图片描述

结束语

博主才开始看源码,确实看得有点头大,里面有很多东西还是一脸懵逼,不过看一遍下来还是收益良多!还有一点要说的就是jdk版本不一样,很多逻辑有出入,不过好像原理都差不多!

  • 18
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值