【深入理解java虚拟机】 - 类加载器与双亲委派模型

x33g5p2x  于2022-07-26 转载在 Java  
字(3.7k)|赞(0)|评价(0)|浏览(261)

前言

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

类加载器用于实现类的加载动作,对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

在比较两个类是否“相等”时,如果两个类来源于同一个 Class文件且被同一个Java虚拟机加载,但只要加载它们的类加载器不同,那这两个类就必定不相等。因此,比较两个类是否“相 等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。

类加载器的分类

从JVM角度来看,只存在两种不同的类加载器:

  1. 启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;
  2. 其他所有类加载器:这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。

但从我们开发者的角度来看,可以把类加载器细化如下:

  • 启动类加载器(Bootstrap Class Loader):即上面所述的类加载器。这个类加载器负责加载存放在 <JAVA_HOME>\lib 目录 ,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机的内存中。
    启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时, 如果需要把加载请求委派给引导类加载器去处理,那直接使用null代替即可。
  • 扩展类加载器(Extension Class Loader):这个类加载器是在类sun.misc.Launcher$ExtClassLoader 中以Java代码的形式实现的。它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所 指定的路径中所有的类库。
    这是一种Java系统类库的扩 展机制,JDK的开发团队允许用户将具有通用性的类库放置在ext目录里以扩展Java SE的功能,在JDK 9之后,这种扩展机制被模块化带来的天然的扩展能力所取代。由于扩扩展类加载器是由Java代码实现 的,开发者可以直接在程序中使用扩展类加载器来加载Class文件。
  • 应用程序类加载器(Application Class Loader):这个类加载器由 sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystem- ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径 (ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。
    如果应用程序中没有 自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
  • 用户自定义类加载器(User Class Loader)

双亲委派模型

Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的就是双亲委派模型

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加 载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的 加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请 求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承的关系来实现的,而是通常使用组合关系来复用父加载器的代码。

双亲委派模型源码

双亲委派模型实现位于java.lang.ClassLoader中的loadClass(java.lang.String, boolean)方法中,具体如下:

protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            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
                    // to find the class.
                    long t1 = System.nanoTime();
                    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()方法,若父加载器为空则默认使用启动类加载器作为父加载器。假如父类加载器加载失败, 抛出ClassNotFoundException异常的话,才调用自己的findClass()方法尝试进行加载。

沙箱安全机制

沙箱安全机制就是将java代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源的访问,通过这样的措施来保证对代码的有效隔离,防止对系统造成破坏。

例如:
我们自定义一个String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang \string.class)。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。

自定义类加载器

在Java的日常应用程序开发中,类的加载几乎是由这三种类加载器互相配合来完成加载的,如果用户认为有必要,还可以加入自定义的类加载器来进行拓展,典型的如增加除了磁盘位置之外的Class文件来源,或者通过类 加载器实现类的隔离、重载等功能。

为什么要自定义类加载器

自定义类加载器可以实现以下功能:

  • 隔离加载类
  • 修改类加载的方式
  • 扩展加载源
  • 防止源码泄漏

实现方式

  1. 通过继承抽象类java.lang.classLoader类的方式,实现自己的类加载器,以满足一些特殊的需求。
  2. 在JDK1.2之前,在自定义类加载器时,总会去继承classLoader类并重写loadclass ()方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadclass()方法,而是建议把自定义的类加载逻辑写在findclass ()方法中。
  3. 在编写自定义类加载器时,如果没有太过于复杂的需求可以直接继承URLClassLoader类,这样就可以避免自己去编写findclass ()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

类的主动使用和被动使用

类的主动使用和被动使用区别在于是否会导致类的初始化,导致类的初始化为类的主动适用,不导致类的初始化为类的被动使用。

主动使用有以下7中情况:

  1. 创建类的实例
  2. 访问某个类或接口的静态变量,或者对该静态变量赋值
  3. 调用类的静态方法
  4. 反射(比如: Class.forName ( “com.atguigu . Test” ) )初始化一个类的子类
  5. Java虚拟机启动时被标明为启动类的类
  6. JDK 7开始提供的动态语言支持:
  7. java . lang . invoke.MethodHandle实例的解析结果REF getstatic、REF_putstatic、REF_invokestatic句柄对应的类没有初始化,则初始化

除以上7种情况外都是被动使用。

相关文章