03 | 从覆盖 JDK 的类开始掌握类的加载机制


从覆盖 JDK 的类开始掌握类的加载机制

面试题

  • 我们能够通过一定的手段,覆盖 HashMap 类的实现么?
  • 有哪些地方打破了 Java 的类加载机制?
  • 如何加载一个远程的 .class 文件?怎样加密 .class 文件?

类加载过程

现实中并不是说,把一个文件修改成 .class 后缀,就能够被 JVM 识别。类的加载过程非常复杂,主要有这几个过程:加载、验证、准备、解析、初始化

在这里插入图片描述

加载

加载的主要作用是将外部的 .class 文件,加载到 Java 的方法区内。

验证

验证阶段在虚拟机整个类加载过程中占了很大一部分,不符合规范的将抛出 java.lang.VerifyError 错误。像一些低版本的 JVM,是无法加载一些高版本的类库的,就是在这个阶段完成的。

准备

从这部分开始,将为一些类变量(静态变量)分配内存,并将其初始化为默认值。此时,实例对象还没有分配内存,所以这些动作是在方法区上进行的。

方法区存储了已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

下面两段代码,code-snippet 1 将会输出 0,而 code-snippet 2 将无法通过编译。

code-snippet 1public class A {

         static int a ;

         public static void main(String[] args) {

             System.out.println(a);

         }

     }

 code-snippet 2public class A {

     public static void main(String[] args) {

         int a ;

         System.out.println(a);

     }

 }

类变量有两次赋初始值的过程,一次在准备阶段,赋予初始值;另外一次在初始化阶段,赋予程序员定义的值。

因此,即使程序员没有为类变量赋值也没有关系,它仍然有一个默认的初始值。但局部变量就不一样了,如果没有给它赋初始值,是不能使用的。

解析

解析在类加载中是非常非常重要的一环,是将符号引用替换为直接引用(实际引用)的过程。

在 class 文件被加载至 Java 虚拟机之前,这个类无法知道其他类及其方法、字段所对应的具体地址,甚至不知道自己方法、字段的地址。因此,每当需要引用这些成员时,Java 编译器会生成一个符号引用。 在运行阶段,这个符号引用一般都能够无歧义地定位到具体目标上。

举例来说,对于一个方法调用,编译器会生成一个包含目标方法所在类的名字、目标方法的名字、接收参数类型以及返回值类型的符号引用,来指代所要调用的方法。

解析阶段的目的,正是将这些符号引用解析成为实际引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化)。

解析阶段负责把整个类激活,串成一个可以找到彼此的网,过程不可谓不重要。那这个阶段做的工作大体可以分为:

  • 类或接口的解析
  • 类方法解析
  • 接口方法解析
  • 字段解析

几个经常发生的异常,就与这个阶段有关。

  • java.lang.NoSuchFieldError 根据继承关系从下往上,找不到相关字段时的报错。
  • java.lang.IllegalAccessError 字段或者方法,访问权限不具备时的错误。
  • java.lang.NoSuchMethodError 找不到相关方法时的错误。

解析过程保证了相互引用的完整性,把继承与组合推进到运行时。

初始化

初始化指的就是对类的静态变量进行初始化。

public class A {

     static int a = 0 ;

     static {

         a = 1;

         b = 1;

     }

     static int b = 0;
     public static void main(String[] args) {

         System.out.println(a);

         System.out.println(b);

     }

 }

结果是 1 0。a 和 b 唯一的区别就是它们的 static 代码块的位置。

这就引出一个规则:static 语句块,只能访问到定义在 static 语句块之前的变量。所以下面的代码是无法通过编译的。

static {

         b = b + 1;

 }

 static int b = 0;

第二个规则:JVM 会保证在子类的初始化方法执行之前,父类的初始化方法已经执行完毕。
所以,JVM 第一个被执行的类初始化方法一定是 java.lang.Object。另外,也意味着父类中定义的 static 语句块要优先于子类的。

类的初始化和对象的初始化之间的差别

public class A {

     static {

         System.out.println("1");

     }
     public A(){

         System.out.println("2");

         }

     }
     
     public class B extends A {

         static{

         System.out.println("a");

     }
     public B(){

         System.out.println("b");

     }
     public static void main(String[] args){

         A ab = new B();

         ab = new B();

     }

 }

static 字段和 static 代码块,是属于类的,在类的加载的初始化阶段就已经被执行。类信息会被存放在方法区,在同一个类加载器下,这些信息有一份就够了,所以上面的 static 代码块只会执行一次,它对应的是方法。

而对象初始化就不一样了。通常,我们在 new 一个新对象的时候,都会调用它的构造方法,来初始化对象的属性。每次新建对象的时候,都会执行。
在这里插入图片描述
所以,上面代码的 static 代码块只会执行一次,对象的构造方法执行两次。

准备和初始化阶段的区别

准备阶段是将类中的静态变量设定为默认值,而在初始化阶段,是将静态变量初始化为实际的值。

案例(代码):

public class Test1 {
    public static void main(String[] args) {
        TestA.getInstance();

    }
}

class TestA  {
    
    private static int number2;

   public static TestA testA=new TestA();

   private TestA(){
       number1++;
       number2++;
   }
   
   private static int number1=0;
   
   
   public static TestA  getInstance(){
       System.out.println("number1 = "+number1);
       System.out.println("number2 = "+number2);
       return testA;
   }

}

案例(结果):

number1 = 0
number2 = 1
  Process finished with exit code 0

疑问:number1 的结果为什么为 0?

  1. 准备阶段,首先将静态变量从上往下 设置成默认值 ,那么这时 number2=0、testA=null、number1=0。
  2. 代码从上往下执行,执行到构造方法这里时,number1 依旧是默认值,并没有初始化实际的值。
  3. testA 执行完成后,就该对 number1 初始化实际值的了。number1 实际值是 0,所以初始化后 number1 还是为 0,将原来的1给覆盖了。

类加载器

整个类加载过程任务非常繁重,虽然这活儿很累,但总得有人干。类加载器做的就是上面 5 个步骤(加载、验证、准备、解析、初始化)的事。

Bootstrap ClassLoader

启动类加载器,这是加载器中的大 Boss,任何类的加载行为,都要经它过问。它的作用是加载核心类库,也就是 rt.jar、resources.jar、charsets.jar 等。当然这些 jar 包的路径是可以指定的,-Xbootclasspath 参数可以完成指定操作。

这个加载器是 C++ 编写的,随着 JVM 启动。

Extention ClassLoader

扩展类加载器,主要用于加载 jre/lib/ext 目录下的 jar 包和 .class 文件同样的,通过系统变量 java.ext.dirs 可以指定这个目录。

这个加载器是个 Java 类,继承自 URLClassLoader。

Application ClassLoader

应用加载器,这是我们写的 Java 类的默认加载器,有时候也叫作 System ClassLoader,一般用来加载 classpath 下的其他所有 jar 包和 .class 文件,我们写的代码,会首先尝试使用这个类加载器进行加载。

Custom ClassLoader

自定义加载器,支持一些个性化的扩展功能。

双亲委派机制

双亲委派机制是指除了顶层的启动类加载器以外,其余的类加载器,在加载之前,都会委派给它的父加载器进行加载。这样一层层向上传递,直到祖先们都无法胜任,它才会真正的加载。

除了启动类加载器,每一个加载器都有一个parent,并没有所谓的双亲。

在这里插入图片描述

这个模型的好处在于 Java 类有了一种优先级的层次划分关系。 比如 Object 类,这个毫无疑问应该交给最上层的加载器进行加载,即使是你覆盖了它,最终也是由系统默认的加载器进行加载的。如果没有双亲委派模型,就会出现很多个不同的 Object 类,应用程序会一片混乱。

如何替换 JDK 的类

当 Java 的原生 API 不能满足需求时,比如修改 HashMap 类,就必须要使用到 Java 的 endorsed 技术。我们需要将自己的 HashMap 类,打包成一个 jar 包,然后放到 -Djava.endorsed.dirs 指定的目录中。注意类名和包名,应该和 JDK 自带的是一样的。但是,java.lang 包下面的类除外,因为这些都是特殊保护的。

因为我们上面提到的双亲委派机制,是无法直接在应用中替换 JDK 的原生类的。但是,有时候又不得不进行一下增强、替换,比如你想要调试一段代码,或者比 Java 团队早发现了一个 Bug。所以,Java 提供了 endorsed 技术,用于替换这些类。这个目录下的 jar 包,会比 rt.jar 中的文件,优先级更高,可以被最先加载到。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

久违の欢喜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值