面试必问的 JVM 类加载机制,你真的了解吗?

Posted 李小立Flag

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试必问的 JVM 类加载机制,你真的了解吗?相关的知识,希望对你有一定的参考价值。

前言

本篇文章带来JVM一个重要的知识点,可能同学们对JVM内存管理有过或多或少的了解,但也没有想过,我们写的java代码是如何被JVM虚拟机载入内存的呢?带着疑问,读完本篇文章,你将收获满满。

探索类加载机制

1.加载的过程

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析3个部分统称为连接。

  1. 加载: 加载是类加载过程中的一个阶段

    • 通过类的完全限定名,查找此类字节码文件
    • 利用字节码文件创建Class对象.
    • 会在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的入口。
  2. 验证: 连接阶段的第一步,这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。从整体上看,验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

  3. 准备: 半初始化,该阶段是正式为类变量(为static修饰)进行内存分配,并设置初始值(0或null或false)

    注意这里所说的初始值概念,例如int初始值是0,

    public static int port= 8080;
    

    实际上变量 在准备阶段过后的初始值为 0 而不是 8080,将 port 赋值为 8080 的 put static 指令是程序被编译后,存放于类构造器 < client > 方法之中。

  4. 解析: 该阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。(符号引用可以抽象理解占位符,直接引用说明载入JVM内存存在真正引用关系)

  5. 初始化: 初始化可以说是类加载阶段的最后一步,到了初始化阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员的赋值去初始化变量或者资源。

    初始化阶段是执行类构造器< client>方法的过程 < clinit> 不是程序员在 Java 代码中直接编写的方法,而是由 Javac 编译器自动生成的。虚拟机会保证子< client>方法执行之前,父类的方法已经执行完毕,如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成< client>()方法。

以下几种情况不会执行类初始化:

  • 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
  • 定义对象数组,不会触发该类的初始化。
  • 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
  • 通过类名获取 Class 对象,不会触发类的初始化。
  • 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
  • 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作。

2. 类加载器的种类


JDK自带有三个类加载器:bootstrap ClassLoader、ExtClassLoader、AppClassLoader。

  1. BootStrapClassLoader(启动类加载器) 启动类加载器,由C++实现,没有父类,是ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%lib下的jar包和 class文件(主要加载rt.jar)。
  2. ExtClassLoader(扩展类加载器) 是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext文件夹下的jar包和 class类。
  3. AppClassLoader(应用程序类加载器) 是自定义类加载器的父类,负责加载classpath下的类文件。系统类加载器,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

3. 什么是双亲委派

    当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class)子类加载器才会尝试自己去加载
    采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。

优点:

  1. 避免类的重复加载
  2. 避免Java的核心API被篡改

类加载的源码如下(java.lang.ClassLoader):

protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
  
      synchronized (getClassLoadingLock(name)) 
          // 先从缓存查找该class对象,找到就不用重新加载
          Class<?> c = findLoadedClass(name);
          if (c == null) 
              long t0 = System.nanoTime();
              try 
                  if (parent != null) 
                      //如果找不到,则委托给父类加载器去加载
                      c = parent.loadClass(name, false);
                   else 
                  //如果没有父类,则委托给启动加载器去加载
                      c = findBootstrapClassOrNull(name);
                  
               catch (ClassNotFoundException e) 
                  // ClassNotFoundException thrown if class not found
                  // from the non-null parent class loader
              

              if (c == null) 
                  // If still not found, then invoke findClass in order
                  // 如果都没有找到,则通过自定义实现的findClass去查找并加载
                  c = findClass(name);

                  // this is the defining class loader; record the stats
                  sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                  sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                  sun.misc.PerfCounter.getFindClasses().increment();
              
          
          if (resolve) //是否需要在加载时进行解析
              resolveClass(c);
          
          return c;
      
  

正如loadClass源码里所展示,当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载去的父加载器去加载,倘若没有父加载则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用findClass()方法去加载自定义的类加载器加载。

4.有哪些场景破坏了双亲委派模型?

目前比较常见的场景主要有:

  1. Tomcat 的多 Web 应用程序
  2. OSGI 实现模块化热部署
  3. JDBC 使用线程上下文类加载器加载 Driver

5. 为什么要破坏双亲委派模型?

双亲委派模型无法满足业务需求了,我们就拿tomcat举例子。
我们都知道Tomcat 容器可以同时部署多个 Web 应用程序,每个应用都有自己的类,如果不同应用公用一个类加载器,就可能会造成类的冲突(类全限名一样,但方法属性完全不一样,每个应用对应一个类加载器的实例 其实相当于物理隔离)

6. tomcat是如何破坏双亲委派模型

    既然 Tomcat 不遵循双亲委派机制,那么如果我自己定义一个恶意的HashMap或者String,会不会有风险呢?

    很明显,tomcat稳定运行这么多年,显然不会存在这样的问题,tomcat虽然打破双亲委派模型,但是顶层的class loader还是一样的。

破坏双亲委派模型的思路都比较类似,其实原理非常简单,上方源码中loadClass的方法修饰符是 protected,只需要两步

  1. 继承 ClassLoader,Tomcat 中的 WebAppClassLoader 继承 ClassLoader 的子类 URLClassLoader。
  2. 重写 loadClass 方法,实现自己的逻辑,不要每次都先委托给父类加载,例如可以先在本地加载,这样就破坏了双亲委派模型了。

每个Tomcat的WebAppClassLoader优先加载自己的目录下的class文件,不会传递给父类加载器,加载不到时再交给commonClassLoade走双亲委派

破坏双亲委派的总结

  • 为了避免类冲突,不能出现一个应用中加载的类库会影响另一个应用的情况。每个 webapp 项目中各自使用的类库要有隔离机制
  • 不同 webapp 项目支持共享某些类库

7. JDBC破坏双亲委派模型

类加载器受到加载范围的限制,在某些情况下父类加载器无法加载到需要的文件,这时候就需要委托子类加载器去加载class文件

JDBC 功能相关的基础类是由 Java 统一定义的,在 rt.jar 里面,例如 DriverManager,也就是由 Bootstrap ClassLoader 来加载,而 JDBC 的实现类是在各厂商的实现 jar 包里,例如 mysql 是在 mysql-connector-java 里,各种数据厂商也会有不同的jar包,根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类,此时只加载了rt.jar的Driver接口,并没有装载mysql实现类,这时就需要由子类加载器去加载Driver实现,这就破坏了双亲委派模型

  • Java 中提供了线程上下文类加载器
//设置或者获取当前线程的上下文类加载器
 ClassLoader cl = Thread.currentThread().getContextClassLoader();
  Thread.currentThread().setContextClassLoader(this.loader);

设置或者获取当前线程的上下文类加载器。如果创建线程时没有设置,则会继承父线程的,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器(Application ClassLoader)。

所以,JDBC 可以通过线程上下文类加载器,来实现父类加载器“委托”子类加载器完成类加载的行为,这个就明显不遵守双亲委派模型了,不过这也是双亲委派模型自身的缺陷导致的。

最后

我是小立,一个坚持分享干货的后端博主。

以上是关于面试必问的 JVM 类加载机制,你真的了解吗?的主要内容,如果未能解决你的问题,请参考以下文章

面试必问的 JVM 类加载机制,你真的了解吗?

面试必问的 JVM 类加载机制,你真的了解吗?

面试必问的 JVM 类加载机制,你懂了吗?

面试必问的 JVM 类加载机制,你懂了吗?

面试必问:JVM类加载机制详细解析

面试必问的HashMap,你真的了解吗?