关于java:从零了解JVM先来看看类加载机制吧

10次阅读

共计 3944 个字符,预计需要花费 10 分钟才能阅读完成。

hello 我是宝哥, 明天咱们来聊聊 JVM 的类加载过程

要搞清楚 JVM, 首先要搞清楚几个问题:

  • jvm 起到什么作用?
  • 怎么加载 class 文件的?
  • 加载类时会反复吗? 程序是什么样的?

说到 jvm 那么不得不提 类的加载过程. 咱们先来理解下类是如何被一步一步加载到 jvm 的

类的加载过程

咱们先抽象的理解一下类加载的整个过程:

如上图所示,Java 源代码文件 (.java 后缀) 会被 Java 编译器编译为 字节码文件(.class 后缀)

而后由 JVM 中的类加载器加载各个类的字节码文件,加载结束之后,交由 JVM 执行引擎执行。

在整个程序执行过程中,JVM 用一段空间来存储程序执行期间须要用到的数据和相干信息,被称作为Runtime Data Area(运行时数据区),也就是咱们常说的 JVM 内存。

此时, 咱们能够全面的了解为:JVM 为咱们的 class 字节码提供了 加载 , 存储 , 执行 的环境.
(jvm 是 java 可跨平台运行的基石, 因为不同的零碎有不同的 jvm 实现, 都能够加载.class 字节码文件)

Java 的类加载机制

那么 ClassLoader 都做了什么呢?

咱们先来看看 类加载机制的定义:

虚拟机把形容类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终造成能够被虚拟机间接应用的 Java 类型,这就是虚拟机的类加载机制

这里有几个阶段比拟重要: 1. 加载 2. 连贯 3. 初始化

依据这 3 个阶段, 咱们能够分析出, 类的生命周期:

类的生命周期

  • 加载: 加载类的二进制字节流, 并且将动态存储构造转化为办法区的运行时数据结构, 而后在内存中生成一个代表此类的 Class 对象, 作为办法区这个类各种数据的拜访入口
  • 验证 : 验证是在 连贯 (Linking) 局部的第一步, 验证的目标是验证 Class 文件中的字节流合乎以后虚拟机的要求, 保障不会危害虚拟机.
  • 筹备 : 为 类变量 分配内存, 并且设置类变量初始值, 此时这此类变量所应用的内存都是在办法区中进行调配.
  • 解析: 解析是将符号援用替换为间接援用,解析动作针对类或接口,字段,类或接口的办法进行解析。
  • 初始化 : 初始化类或接口并且执行类或接口的初始化办法, 此时,它的生命周期就开始了

    • 初始化的机会:

      • 虚拟机标准规定了有且只有 5 种状况 必须 立刻 对类进行初始化
      • 1. 遇到 new、getstatic 和 putstatic 或 invokestatic 这 4 条字节码指令时,如果类没有进行过初始化,则须要先触发其初始化。对应场景是:应用 new 实例化对象、读取或设置一个类的动态字段(被 final 润饰、已在编译期把后果放入常量池的动态字段除外)、以及调用一个类的静态方法。
      • 2. 对类进行反射调用的时候,如果类没有进行过初始化,则须要先触发其初始化。
      • 3. 当初始化类的父类还没有进行过初始化,则须要先触发其父类的初始化。(而一个接口在初始化时,并不要求其父接口全副都实现了初始化)
      • 4. 虚拟机启动时,用户须要指定一个要执行的主类(蕴含 main() 办法的那个类),虚构机会先初始化这个主类。
      • 5. 当应用 JDK 1.7 的动静语言反对时,如果一个 java.lang.invoke.MethodHandle 实例最初的解析后果 REF_getStatic、REF_putStatic、REF_invokeStatic 的办法句柄,并且这个办法句柄所对应的类没有进行过初始化,则须要先触发其初始化。
  • 应用 : 此时咱们能够通过 new 关键字, 创立实例对象. 顺带一提, 一个 Class 对象总是会援用它的类加载器。调用 Class 对象的 getClassLoader() 办法,就能取得它的类加载器。由此可见,Class 实例和加载它的加载器之间为双向关联关系。
  • 卸载: 当类不再被援用或被垃圾回收器标记为已死对象时, 将会被回收, 然而 Java 虚拟机自身会始终援用类加载器,而这些类加载器则会始终援用它们所加载的类的 Class 对象,因而这些 Class 对象始终是可涉及的, 也就是说 jvm 自带的类加载器所加载的类, 在虚拟机还没有退出时, 始终不会被卸载, 当然也有特例 如: 咱们本人定义的类加载器的类是能够被卸载的.

ClassLoader 类加载器

类的唯一性

任意一个类,都须要由加载它的 类加载器 和这个 类自身 一起确立其在 Java 虚拟机中的唯一性

这句定义怎么了解呢?

两个类来源于同一个 Class 文件,被同一个虚拟机加载,然而 加载它们的类加载器不同,那这两个类也不相等

那有的小伙伴就有纳闷了, 还有很多类加载器吗?
emm.. 那加载的程序呢? 会不会反复加载了?

别急, 咱们理解下 双亲委派准则.

双亲委派准则

如果一个类加载器收到了类加载的申请,它首先不会本人去尝试加载这个类,而是把这个申请委派给父类加载器去实现,每一个档次的类加载器都是向上拜访,因而所有的加载申请最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈本人无奈实现这个加载申请(它的搜寻范畴中没有找到所需的类)时,子加载器才会尝试本人去加载。

这里举例几个面试会问的 classloader 职责:

  • Bootstrap ClassLoader: 根类加载器,负责加载 java 的外围类,它不是 java.lang.ClassLoader 的子类,而是由 JVM 本身实现;
  • Extension ClassLoader: 扩大类加载器,扩大类加载器的加载门路是 JDK 目录下 jre/lib/ext, 扩大类的 getParent()办法返回 null, 实际上扩大类加载器的父类加载器是根加载器,只是根加载器并不是 Java 实现的;
  • System ClassLoader: 零碎 (利用) 类加载器,它负责在 JVM 启动时加载来自 java 命令的 -classpath 选项、java.class.path 零碎属性或 CLASSPATH 环境变量所指定的 jar 包和类门路。程序能够通过 getSystemClassLoader()来获取零碎类加载器;
  • User Define ClassLoader: 用户自定义的 classloader, 自定义的加载器必须继承 ClassLoader。

它们的加载程序:

show me the code:

// 代码摘自《深刻了解 Java 虚拟机》protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // 首先,查看申请的类是否曾经被加载过了, 同时也解决了小伙伴的疑虑
        Class c = findLoadedClass(name);
        if (c == null) {
            try {if (parent != null) {c = parent.loadClass(name, false);
                } else {c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
            // 如果父类加载器抛出 ClassNotFoundException
            // 阐明父类加载器无奈实现加载申请
            }
            if (c == null) {
                // 在父类加载器无奈加载的时候
                // 再调用自身的 findClass 办法来进行类加载
                c = findClass(name);
            }
        }
        if (resolve) {resolveClass(c);
        }
        return c;
    }

那么咱们不禁要思考一下为何要用这种准则有何长处?

  • 1,一个当然是防止反复加载,晋升性能
  • 2,防止了外围类被用户篡改(例如我在用户自定义的 classloader 中加载一个 String 类去笼罩自带的 String 类, 因为先让父类加载, 我定义的程序在后. 不会呈现笼罩胜利的问题)

这里有几个点小伙伴须要留神, 不然要被面试官吊打了:

  1. 类加载器之间的父子关系不会以继承的关系来实现, 他们尽管都继承于抽象类 java.lang.ClassLoader 然而他们的关系是组合关系, 应用组合关系复用父类的加载器.
  2. Bootstrap 类加载器是用 C++ 实现的,不继承于抽象类 java.lang.ClassLoader, 它是虚拟机本身的一部分,如果获取它的对象,将会返回 null;
  3. jvm 自带的类加载器所加载的类, 在虚拟机还没有退出时, 始终不会被卸载, 咱们本人定义的类加载器, 加载的类是能够被卸载的.

毁坏双亲委派准则

当然类加载器的双亲委派准则是能够被毁坏的, 毁坏它是因为双亲委派模型本身缺点导致,他没有方法解决用户根底类又要从新调用户类的代码。为了解决这个问题就有了线程上下文加载器,例如 JNDI、JDBC、JCE 等

举个 Tomcat 的例子:

每个 Tomcat 的 webappClassLoader 加载本人的目录下的 class 文件,不会传递给父类加载器。tomcat 之所以造了一堆本人的 classloader,大抵是出于上面三个起因:

  • 对于各个 webapp 中的 class 和 lib,须要互相隔离,不能呈现一个利用中加载的类库会影响另一个利用的状况,而对于许多利用,须要有共享的 lib 以便不浪费资源。
  • 与 jvm 一样的安全性问题。应用独自的 classloader 去装载 tomcat 本身的类库,免得其余歹意或无心的毁坏;
  • 热部署。置信大家肯定为 tomcat 批改文件不必重启就主动从新装载类库而惊叹吧。

毁坏双亲委派的形式

双亲委派机制准则在 loadclass 办法中。
只须要绕开 loadclass 办法中即可。

  1. 自定义类加载器,重写 loadclass 办法
  2. 应用 SPI 机制绕开 loadclass 办法。以后线程设定关联类加载器

对于 SPI 在我另外一篇文章 https://mp.weixin.qq.com/s/2U…


讲了那么久的加载, 此时咱们才刚刚一只脚踏进 JVM 的大门, 前面咱们将剖析 JVM 运行时数据区. 大家继续关注java 宝典 公众号, 咱们下章再聊

最近我创立了一个微信 - 学习群, 有酷爱学习的小伙伴能够进群探讨

关注公众号:java 宝典

正文完
 0