3、JVM入门学习日记(day03)--JVM类加载机制:双亲委派模型

1,095 阅读12分钟

第二章:类加载机制--双亲委派模型

1、引入

Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。

比如,我们现在这里有这么这一个问题:自定义新建一个java.lang包,尝试自定义写一个与rt.jar类库中已有类重名的类String,里面有如下程序

package java.lang;
    
public class String {
	//在初始化阶段,类加载的时候,就会加载这个静态代码块
	static{
   		System.out.println( "我是自定义的String类的静态代码块" )
	}
}

然后,再在另外一个普通包下创建一个程序

package com.atguigu.java1;
    
public class StringTest{
   public static void main(String[] args){
      String str = new String();//双亲委派模式加载String类
      System.out.println("hello")
    }
}

结果:会发现该自定义的String类可以正常编译,但是用于不会加载运行。执行StringTest类这个程序,输出结果为”hello”,不会输出自定义lang包下String类静态代码块的内容,从结果证明这个里面新new的String类是内部核心API的那个java.lang,仍然加载的是JDK 自带的 String 类,不是我们自定义的String类。从这里就体现出了双亲委派机制,并且如果真的是用自定义的,程序就很容易出现风险,因为可以随意更改,注入。

2、工作原理

  • 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
  • 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
  • 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

image.png

我们再以上面String的例子进行讲解:

加载StringTest这个类的时候,我们需要加载String这个类,由于我们自定义了一个String类,按理来说应该由系统类加载器加载,但是由于双亲委派机制,当这个自定义的类的加载器【系统类加载器】收到了类加载请求,它并不会自己先去加载,而是一直向上委托给引导类加载器,又因为引导类加载器本身就是加载以java等开头的包下的类,因此此时这个String就是以java.lang开头的包【自定义的java.lang包】下的类,能够处理,就不会向下委托了【反向委派】。所以证明这个里面新new的String类是内部核心API的那个java.lang,而非自定义的。

那如果是这样的呢?

StringTest strtest = new StringTest();

则原理还是一样,当这个自定义的类StringTest的加载器【系统类加载器】收到了类加载请求,它并不会自己先去加载,而是一直向上委托给引导类加载器,但是引导类加载器也处理不了,就会向下委托,一直委托到系统类加载器才能处理,因此这个StringTest类是由系统类加载器加载

再来看一个程序【定义在自定义的java.lang包下】

package java. lang;
public class String {
    //在初始化阶段,类加载的时候,就会加载这个静态代码块
    static{
        System.out.println("我是自定义的String类的静态代码块");
    }
    public static void main(String[] args) {
        System.out.println("hello");
    }
}

结果:

错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
  public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application

原因: 因为我们想要执行main方法,则main方法所在的这个String类就应该被加载,但是由于这个String类最终是交给引导类加载器加载的,加载完后就执行main方法,因为引导类加载器本身就是加载java核心API类库的,而核心API里面的String根本就没有main方法,就忽略了我们自定义的这个String类里面的信息,就会报错。

3、双亲委派机制举例

当我们加载jdbc.jar 用于实现数据库连接的时候,首先我们需要知道的是 jdbc.jar是基于SPI接口进行实现的,所以在加载的时候,会进行双亲委派,最终从引导类加载器中加载 SPI核心类,然后在加载SPI接口类,接着在进行反向委派,通过线程上下文类加载器进行实现类 jdbc.jar的加载。

image.png

4、双亲委派机制的优势

双亲委派模型在JDK1.2被引入,它不是一个具有强制性约束的模型,而是java设计者推荐给开发者们的一种类加载器实现机制的最佳实践。通过上面的例子,我们可以知道,双亲机制可以

  • 避免类的重复加载:防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。

  • 保护程序安全,防止核心API被随意篡改:保证核心.class不能被篡改。通过委托方式,不会去篡改java核心的.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

    • 自定义类:java.lang.String
      • 自定义类:java.lang.ShkStart(报错:阻止创建java.lang开头的类)
package java.lang;
public class ShkStart {
    public static void main(String[] args) { 
        System.out.println("hello!");
    }
}

会报以下错误错,就是出于保护机制,java.lang包下不允许我们自定义类

image.png

5、为什么要设计这种机制

这种设计有个好处是,如果有人想替换系统级别的类:比如String.java,篡改它的实现,在这种双亲委派机制下,这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。

“父委派模型”保证了系统级别的类的安全性,使一些基础类不会受到开发人员“定制化”的破坏。

如果没有使用父委派模型,而是由各个类加载器自行加载的话,如果开发人员自己编写了一个称为java.lang.String的类,并放在程序的ClassPath中,那系统将会出现多个不同的String类,Java类型体系中最基础的行为就无法保证。应用程序也将会变得一片混乱。

6、沙箱安全机制

联想360沙箱安全机制

image.png

Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?

沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。

自定义string类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的string类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制的一种体现。

所有的Java程序运行都可以指定沙箱,可以定制安全策略。

  1. 首先进行一个字节码校验器:确保java类文件遵循java语言规范,这样可以帮助java程序实现内存保护,但并不是所有的类文件都会经过字节码校验,比如核心类。
  2. 接下来类装载器:其中类装载器在3个方面对java沙箱起作用,它防止恶意代码去干涉善意的代码,它守护了被信任的类库边界。它将代码归入保护域,确定了代码可以进行哪些操作。
  3. 虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由java虚拟机为每个类装载器维护的,他们互相之间甚至不可以见。类装载器采用的机制是双亲委派模式。

7、类的主动使用和被动使用

7.1、如何判断两个class对象是否相同

在JVM中表示两个class对象是否为同一个类存在两个必要条件:

  • 类的完整类名必须一致,包括包名。
  • 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。

换句话说,在JVM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。

7.2、对类加载器的引用

JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的【动态连接】。

7.3、类的主动使用和被动使用

Java程序对类的使用方式分为:主动使用和被动使用。

主动使用,又分为七种情况:

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

除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化

详细参考第一章内容:

8、破坏双亲委派模型

8.1、双亲委派模型的代码实现

双亲委派模型对于保证Java程序的稳定运行极为重要,但是它的实现很简单,双亲委派模型的代码实现集中在java.lang.ClassLoader的loadClass()方法当中。代码逻辑如下:

  1. 首先检查类是否被加载,没有则调用父类加载器的loadClass()方法;
  2. 若父类加载器为空,则默认使用启动类加载器作为父加载器;
  3. 若父类加载失败,抛出ClassNotFoundException 异常后,再调用自己的findClass() 方法尝试进行加载。

loadClass方法源代码如下:

protected synchronized Class<?> loadClass(String name, boolean resolve) throws Clas sNotFoundException{
    //1首先检查类是否被加载
    Class c = findLoadedClass(name);
    if (c ==null) {
        try {
            if (parent != null) {
                //2没有则调用父类加载器的loadClass()方法;
                c = parent.loadClass(name,false);
            } else
                //3若父类加载器为空,则默认使用启动类加载器作为父加载器;
                c = findBootstrapClass0(name);
        }
    } catch (ClassNotFoundException e) {
        //4、若父类加载失败,则抛出ClassNotFoundException
    }
    if(c==nul1){
        //5 父类加载器无法完成加载请求,再调用自己的f indClass() 方法进行类加载
        c = findClass(name);
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}

8.2、破坏双亲委派模型

上文提到过双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个模型,但也有例外,到目前为止,双亲委派模型主要出现过3较大规模的“被破坏”情况。【详细内容看书】

9、总结

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

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。因此,使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处:类随着它的类加载器一起具备了一种带有优先级的层次关系:

例如类java.lang.Object,它存放于rt.jar中,由启动类加载器加载。双亲委派模型保证任何类加载器收到的对java.lang.Object的加载请求,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。

相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并用自定义的类加载器加载,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。比如上面的用户自定义类String