面试BAT必问的JVM,今天我们来说一说它类加载器的底层原理

类加载器的关系

类加载器的分类

  • JVM支持两种类加载器,一种为引导类加载器(Bootstrap ClassLoader),另外一种是自定义类加载器(User Defined ClassLoader)
  • 引导类加载器是由C/C++编写的无法访问到
  • Java虚拟机规定:所有派生于抽象类ClassLoader的类加载器都划分为自定义加载器
  • 最常见的类加载器只有三个(如上图所示)

用户自定义的类会被系统类加载器所加载,核心类库的类会被引导类加载器所加载

public class ClassLoaderTest {
    public static void main(String[] args) {
        //获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

        //获取其上层:扩展类加载器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d

        //获取其上层:获取不到引导类加载器
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader);//null

        //对于用户自定义类来说:默认使用系统类加载器进行加载
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

        //String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器进行加载的。
        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader1);//null
    }
}

系统自带的类加载器介绍

  • 启动类加载器(引导类加载器、Bootstrap ClassLoader)
    • 由c/c++语言实现的,嵌套在jvm内部
    • 用来加载java核心库
    • 并不继承java.lang.ClassLoader,没有父加载器
    • 为扩展类加载器和系统类加载器的父加载器
    • 只能加载java、javax、sun开头的类
  • 扩展类加载器(Extension ClassLoader)
    • java语言编写,sun.misc.Launche包下。
    • 派生于ClassLoader类,父类加载器为Bootstrap ClassLoader
    • 从java.ext.dirs系统属性指定的目录中加载类库或者加载jre/lib/ext子目录下的类库(用户可以在该目录下编写JAR,也会由此加载器所加载)
  • 系统类加载器(System ClassLoader\AppClassLoader)
    • 派生于ClassLoader,父类加载器为Extension ClassLoader
    • 负责加载classpath或者系统属性java.class.path指定路径下的类库
    • java语言编写,sun.misc.Launche包下。
    • 负责加载程序中默认的类,可以通过getSystemClassLoader()方法获取该类的加载器。
  • 用户自定义类加载器(后面详细介绍)
    • 隔离加载类
    • 修改类加载的方式
    • 扩展加载源
    • 防止源码泄漏(可以对字节码文件加密)
    • 继承ClassLoader类方式实现自定义类加载器

关于ClassLoader

  • ClassLoader是一个抽象类,其后的所有类加载器都继承此类

注:这些方法都不是抽象方法。

获取ClassLoader的路径

public class ClassLoaderTest2 {
  public static void main(String[] args) {
      try {
          //1.
          ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
          System.out.println(classLoader);
          //2.
          ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
          System.out.println(classLoader1);
          //3.
          ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();
          System.out.println(classLoader2);
      } catch (ClassNotFoundException e) {
          e.printStackTrace();
      }
  }
}

双亲委派机制(面试)

  • Java虚拟机对class文件采用的是按需加载的方式,当需要使用到这个类的时候才会对它的class文件加载到内存生成class对象,加载的过程中使用的双亲委派模式,即把请求交给父类处理。

  • 如果一个类加载器收到了类加载的请求,它不会自己加载,而是先把这个请求给自己的父类加载器去执行

  • 如果这个父类加载器还有父类加载器,则会再将请求给自己的父类加载器,依次递归到顶层的启动类加载器

  • 依次进行判断是否能完成委派(加载此类),若能完成委派则该类就由此加载器加载,若无法完成委派,则将委托给子类加载器进行判断是否能完成委派,依次递归到底层加载器,若期间被加载则完成加载阶段不会再递归(注)。

 注:类只能被一个加载器所加载。
  • 双亲委派的优势

  • 避免类的重复加载

  • 保护程序的安全,防止核心API被篡改

  • 例如:

  • 创建一个java.lang.String类,因为有双亲委派的机制,所以会将String类交给引导类加载器来判断是否能被加载。引导加载器判断可以加载此类(是核心类中的String),完成加载,则"恶意"写的String类无法生效,防止String类被恶意篡改。这里也称沙箱安全机制(保护java核心源代码)

package java.lang;
public class String {
    //
    static{
        System.out.println("我是自定义的String类的静态代码块");
    }
    //错误: 在类 java.lang.String 中找不到 main 方法
    public static void main(String[] args) {
        System.out.println("hello,String");
    }
}

//因为加载的是核心类的String,在String中找不到main方法

public class StringTest {

    public static void main(String[] args) {
        java.lang.String str = new java.lang.String();//无输出
        
        StringTest test = new StringTest();
        System.out.println(test.getClass().getClassLoader());
    }
}
package java.lang;

public class ShkStart {

    //错误:java.lang.SecurityException: Prohibited package name: java.lang
    public static void main(String[] args) {
        System.out.println("hello!");
    }
}

//因为java.lang包由引导类加载器加载,引导类中并没有此类,为了安全引导类

破坏双亲委派模型:

较大规模的破坏双亲委派模型的有3种:

  • 由于双亲委派模型是在JDK1.2之后才引入的,所以在JDK1.2之前是不符合双亲委派模型的:

  • ClassLoader这类在JDK1.0开始有存在的,在JDK1.2之前,ClassLoader中是通过私有方法loadClassInternal()去调用自己内部的loadClass()。为了满足双亲委派以及向下兼容,在JDK1.2后的ClassLoader类中,又为该类添加了protected的findClass()方法,JDK1.2之后就不推荐通过覆盖重写loadClass()方法了,而是在新添加的findClass()方法中书写自己的类加载逻辑,若loadClass()方法中的父类加载失败则会调用自己的findClass()方法。

  • 由于双亲委派模型的旨意是越核心的类越由高层的加载器所加载(上文提到过的String类),倘若这些核心类要去调用用户的基础类,例如JNDI服务(是对 资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代码,但启动类加载器不可能"认识"这些代码)

  • 为了解决调用问题,设计了一个线程上下文类加载器(Thread Context ClassLoader),这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

  • 有了这个上下文类加载器,就可以去加载所需要的SPI代码,实际上就是从父类加载器去请求子类加载器去完成类的加载驱动,违背了双亲委派的一般性规则。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。

  • 为了满足"热部署"、"动态部署"等功能而导致的。在OSGi(动态模块技术)环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求时,OSGi将按照顺序进行类搜索

关于类加载器的一些补充

1. JVM中判断一个类是否是同一个类有两个必要条件:
  • 这两个类的全限定名要一致
  • 这两个类被同一个类加载器加载。
2. 对类加载器的引用:
  • JVM必须知道一个类型是由引导类加载器(启动类加载器)加载的还是由用户类加载器加载的。
  • 如果一个类是由用户类加载器所加载的,那么JVM会将这个类加载器的一个引用作为类信息的一部分保存在方法区。
  • 当解析一个类型到另外一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
3. 类的主动使用和被动使用
  • 主动使用:
  • 创建类的实例
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射
    * 初始化一个类的子类
    * JVM启动时被标明为启动类的类
  • JDK 7 开始提供的动态代理:java.invoke.MethodHandle实例的解析结果,REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化、则初始化
    * 除以上7种情况,其他使用Java类的方式都为被动使用,被动使用不会导致类的初始化。

最后

大家看完有什么不懂的可以在下方留言讨论,也可以关注我私信问我,我看到后都会回答的。也欢迎大家关注我的公众号:前程有光,马上金九银十跳槽面试季,整理了1000多道将近500多页pdf文档的Java面试题资料放在里面,助你圆梦BAT!文章都会在里面更新,整理的资料也会放在里面。谢谢你的观看,觉得文章对你有帮助的话记得关注我点个赞支持一下!

posted @ 2020-08-24 14:00  前程有光  阅读(231)  评论(0编辑  收藏  举报