字节码和类加载器

字节码和类加载器

Jvm,即 java 虚拟机,是 java 能实现"一次编写、到处执行"的基础

不管是为了面试,还是为了深入理解 java,我们都有必要对 jvm 进行深度学习

Jvm 的整体架构,可以通过下面这张图进行概括:

jvm

我们后面会根据这张图,去一点点学习 jvm 的知识

这一篇文章,我们就讲解一下其中的 字节码类加载器 的相关知识

文章涉及知识点

一、类字节码

字节码文件,是 jvm 可读的文件形式,文件后缀是 .class

我们写的 .java 文件,需要经过 javac 编译,才能编译成字节码,供 jvm 处理

现在 jvm 也不再只支持 java,由 java 衍生出很多其他语言,但大体方式都是差不多的

编译成字节码文件

我们使用指令 javac 就可以对 .class 文件进行编译

javac Main.java

编译过后,就会多出一个 Main.class 文件

打开来看一下:

所有字节码文件的头四个字节,都是魔数,它的作用,就是确定该文件是一个字节码文件

这里为什么魔数要选择 cafe babe,我认为是在16进制中,想用 a-f 拼接出和 java 相关的单词,

Main.class 文件

二、类加载机制

1、类的生命周期

类的生命周期

类加载的过程包括了加载验证准备解析初始化五个阶段。在这五个阶段中,加载验证准备初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持 java 的动态绑定(即反射)

2、生命周期不同阶段讲解

1)加载(查找并加载类的二进制数据)

在加载阶段,虚拟机需要完成一下三件事:

  • 通过一个类的全限定名来获取其定义的二进制字节流。

  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

  • 在Java堆中生成一个代表这个类的java.lang.Class对象,作为堆方法区中这些数据的访问入口。

类加载的过程

加载阶段是可控性最强的阶段,开发人员可以使用系统提供的类加载器完成加载,也可以自定义自己的类加载器完成加载

有下述这些方法,可以加载 .class 文件:

  • 从本地系统中直接加载
  • 通过网络下载.class文件
  • 从zip,jar等归档文件中加载.class文件
  • 从专有数据库中提取.class文件
  • 将Java源文件动态编译为.class文件
2)连接(确保被加载的类的正确性)
1- 验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

2 - 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

  • 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。

  • 这里所设置的初始值通常情况下是数据类型默认的零值(如00Lnullfalse等),而不是被在Java代码中被显式地赋予的值。

    假设一个类变量的定义为: public static int value = 3;那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的put static指令是在程序编译后,存放于类构造器<clinit>()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。

3 - 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对接口字段类方法接口方法方法类型方法句柄调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。

直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

3)初始化

初始化,为类的静态变量赋予正确的初始值(连接过程中的准备,只会为静态变量赋一个0值,初始化还是要在这个阶段进行的),JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

  • 声明类变量是指定初始值
private static int id = 1;
  • 使用静态代码块为类变量指定初始值
private static int a;
static {
	  a=1;
}

jvm 的初始化步骤:

  • 假如这个类还没有被加载和连接,则程序先加载并连接该类
  • 假如该类的直接父类还没有被初始化,则先初始化其直接父类
  • 假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化的时机:

  • 创建类的实例,也就是new的方式
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射(如Class.forName(“com.pdai.jvm.Test”))
  • 初始化某个类的子类,则其父类也会被初始化
  • Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类

3、类加载器

1)类加载器层次

类加载器层次

**BootstrapClassLoader:**由 c++/c 语言实现,嵌套在 jvm 中,程序中无法直接获取,其只加载 java、javax、sun 开头的类,说白了,就是加载 jdk 自带的包

**ExtClassLoader:**加载 jre/lib/ext 目录下的包,说白了,就是加载我们项目中引入的第三方 jar 包

**AppClassLoader:**加载我们自己写的类

**UserClassLoader:**用户类加载器,类加载器可以是我们用户自定义的,上面三个类加载器,只能加载本地的,如果我们有加载网络上的类的需求,我们就需要自定义加载器,实现对应功能

2)寻找类加载器

我们可以通过代码,找寻类加载器

public class GetClassLoader {
    public static void main(String[] args) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        System.out.println(loader);
        System.out.println(loader.getParent());
        System.out.println(loader.getParent().getParent());
    }
}

类加载器

3)怎么进行类加载

1、命令行启动应用时候由JVM初始化加载

2、通过Class.forName()方法动态加载

3、通过ClassLoader.loadClass()方法动态加载

Class.forName()和ClassLoader.loadClass()区别:

  • Class.forName(): 将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
  • ClassLoader.loadClass(): 只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
  • Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

4、JVM 类加载机制

类加载机制有四个:

**全盘负责:**当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入

**父类委托:**先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类

**缓存机制:**缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

**双亲委派机制:**如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

1)双亲委派机制

虽然有四个,但是我们还是重点去了解双亲委派

1 - 双亲委派机制讲解

我们的类加载器,由浅至深,分别是 AppClassLoader、ExtClassLoader、BootstrapClassLoader

双亲委派,就是说子类加载器很懒,总是会让父类加载器去加载类,如果父类加载器也加载不到该类,子类才会去加载

这也是为什么我们自己写一个 java.lang.String,使用的还是 jdk 中的 String

双亲委派

使用双亲委派,能够:

  • 防止重复加载某个类:

父类加载过了,子类就不会去加载了

  • 保证了安全性:

因为Bootstrap ClassLoader在加载的时候,只会加载JAVA_HOME中的jar包里面的类,如java.lang.Integer,那么这个类是不会被随意替换的,除非有人跑到你的机器上, 破坏你的JDK。

2 - 不同层次的类加载器是继承关系吗

不是,子类加载器和父类加载之间是 组合关系

image-20210804194203176

3 - 类加载器的实现
protected Class<?> loadClass(String var1, boolean var2) 
  throws ClassNotFoundException {
  synchronized(this.getClassLoadingLock(var1)) {
    // 检查类加载器有没有被加载
    Class var4 = this.findLoadedClass(var1);
    if (var4 == null) {
      long var5 = System.nanoTime();
      
      try {
        if (this.parent != null) {
          var4 = this.parent.loadClass(var1, false);
        } else {
          var4 = this.findBootstrapClassOrNull(var1);
        }
      } catch (ClassNotFoundException var10) {
      }

      // 如果还是没有找到
      if (var4 == null) {
        long var7 = System.nanoTime();
        var4 = this.findClass(var1);
        PerfCounter.getParentDelegationTime().addTime(var7 - var5);
        PerfCounter.getFindClassTime().addElapsedTimeFrom(var7);
        PerfCounter.getFindClasses().increment();
      }
    }

    if (var2) {
      this.resolveClass(var4);
    }

    return var4;
  }
}
4 - 实现自己的类加载器

通过上面类加载器的实现,我们可以看到,我们只要重写 ClassLoader 的findClass()方法即可

public class MyClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 实现
    }
}


//...

// 使用
public static void main(String[] args) {
  MyClassLoader myClassLoader = new MyClassLoader();

  Class<?> aClass = myClassLoader.loadClass("<full class name>");
  // ...
}
5 - 怎么破坏双亲委派

先去看看 ClassLoader 的 loadClass 方法

会发现,如果这个类在当前类加载中没有加载的话,会丢给父加载器去加载

loadClass方法

了解这件事,我们就知道该怎么破坏双亲委派了

只要我们自己实现类加载器(继承 ClassLoader),然后重写 loadClass()方法,让它不要去使用双亲委派机制即可

然后我们就要去使用我们自己实现的这个 ClassLoader 就行了

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

FARO_Z

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

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

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

打赏作者

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

抵扣说明:

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

余额充值