JVM-类加载机制

本文详细介绍了JVM的类加载过程,包括加载、连接(验证、准备、解析)、初始化三个步骤。类加载主要由ClassLoader完成,涉及字节码验证、内存分配、符号引用转换等。文章还探讨了双亲委派模型、SPI机制以及如何突破和实现热更新。
摘要由CSDN通过智能技术生成

目录

类装载流程

加载

连接

初始化

ClassLoader


类装载流程

class 通常以文件的形式存在,被jvm装载后才能被程序使用。装载 class 可以分为加载,连接(验证,准备,解析)和初始化这 3 个步骤。

加载

class 只有在必须要使用的时候才会被装载。jvm规定, 一个类或接口在初次使用前, 必须要进行初始化。这里指的使用如下:

  1. 作为启动虚拟机, 含有 main 方法的那个类。
  2. 初始化子类时, 要求先初始化父类。
  3. 调用反射方法。
  4. 创建一个类的实例:new 关键字、反射、克隆、反序列化。
  5. 当调用类的静态方法时, 即使用了字节码 invokestatic 指令;当使用类或接口的静态字段时(final 常量除外), 比如, 使用 getstatic 或者 putstatic 指令。

jvm在加载类时通过ClassLoader完成以下工作:

  • 通过类的全名, 获取类的二进制数据流。
  • 解析类的二进制数据流为方法区内的数据结构。
  • 创建 java.lang.Class 类的实例, 表示该类型。

连接

当类加载到系统后, 就开始连接操作, 验证是连接操作的第一步。它的目的是保证加载的字节码是合法、合理并符合规范的。

格式检查

判断类的二进制数据是否是符合格式要求和规范。比如是否以魔数开头, 主版本和小版本号是否在当前 Java 虚拟机的支持范围内, 数据中每一个项是否都拥有正确的长度等等。

语义检查

JVM会进行字节码的语义检查, 比如是否所有的类都有父类的存在(在 Java 里, 除了 Object 外, 其他类都应该有父类), 是否一些被定义为 final 的方法或者类被重载或继承了, 非抽象类是否实现了所有抽象方法或者接口方法, 是否存在不兼容的方法(比如方法的签名除了返回值不同, 其他都一样, 这种方法会让虛拟机无从下手调度), 但凡在语义上不符合规范的, 虚拟机也不会给予验证通过。

字节码验证

JVM会进行字节码验证, 字节码验证也是验证过程中最为复杂的一个过程。它试图通过对字节码流的分析, 判断字节码是否可以被正确地执行。比如, 在字节码的执行过程中, 是否会跳转到一条不存在的指令, 函数的调用是否传递了正确类型的参数, 变量的赋值是不是给了正确的数据类型等。 检测在特定的字节码处, 其局部变量表和操作数栈是否有着正确的数据类型。但是, 100% 准确地判断一段字节码是否可以被安全执行是无法实现的, 因此, 该过程只是尽可能地检查出可以预知的明显的问题。如果在这个阶段无法通过检查, 虚拟机也不会正确装载这个类。但是, 如果通过了这个阶段的检查, 也不能说明这个类是完全没有问题的。

符号引用验证

校验器还将进行符号引用的验证。Class 文件在其常量池会通过字符串记录自己将要使用的其他类或者方法。因此, 在验证阶段, 虚拟机就会检查这些类或者方法确实是存在的, 并且当前类有权限访问这些数据, 如果一个需要使用类无法在系统中找到, 则会抛出 NoClassDefFoundError, 如果一个方法无法被找到, 则会抛出 NoSuchMethodError。

准备阶段JVM就会为这个类分配相应的内存空间, 并设置默认值。仅当一个字段被标记为 static final 时,才会在准备阶段直接赋值,其他字段都会被 JVM 先赋上默认值。

解析阶段的工作就是将类、接口、字段和方法的符号引用转为直接引用。符号引用就是一些字面量的引用, 和虚拟机的内部数据结构和内存布局无关,解析阶段就是将这些字符串描述的引用关系,转变为指针描述的引用关系。

初始化

初始化阶段的重要工作是执行类的初始化方法 <clinit>。方法 <clinit> 是由编译器自动生成的, 它是由类静态成员的赋值语句以及 static 语句块合并产生的。

这里引出<clinit>方法和<init>

<clinit>

Java在编译之后会在字节码文件中生成<clinit>方法,称之为类构造器,类构造器同实例构造器一样,也会将静态语句块,静态变量初始化,收敛到<clinit>方法中,收敛顺序为:

1. 父类静态变量初始化 
2. 父类静态语句块 
3. 子类静态变量初始化 
4. 子类静态语句块

<init>

Java在编译之后会在字节码文件中生成<init>方法,称之为实例构造器,该实例构造器会将语句块,变量初始化,调用父类的构造器等操作收敛到<init>方法中,所谓收敛到<init>方法中的意思就是,将这些操作放入到<init>中去执行,收敛顺序(这里只讨论非静态变量和语句块)为: 

1. 父类变量初始化 
2. 父类语句块 
3. 父类构造函数 
4. 子类变量初始化 
5. 子类语句块 
6. 子类构造函数

<clinit>方法是在类加载过程中执行的,而<init>是在对象实例化执行的,所以<clinit>一定比<init>先执行。所以整个顺序就是: 
1. 父类静态变量初始化 
2. 父类静态语句块 
3. 子类静态变量初始化 
4. 子类静态语句块 
5. 父类变量初始化 
6. 父类语句块 
7. 父类构造函数 
8. 子类变量初始化 
9. 子类语句块 
10. 子类构造函数

实例化一个类的四种途径:(跟加载类的途径是一个级别)
1. 调用 new 操作符
2. 调用 Classjava.lang.reflect.Constructor 对象的newInstance()方法
3. 调用任何现有对象的clone()方法
4. 通过 java.io.ObjectInputStream 类的 getObject() 方法反序列化

ClassLoader

BootStrap ClassLoader(启动类加载器)、Extension ClassLoader(扩展类加载器)和 App ClassLoader(应用类加载器, 也称为系统类加载器)。

启动类加载器负责加载系统的核心类, 比如 rt.jar 中的 Java 类。扩展类加载器用于加载 %JAVA_HOME%/lib/ext/*.jar 中的 Java 类。应用类加载器用于加载用户类, 也就是用户程序的类。自定义类加载器用于加载一些特殊途径的类, 一般也是用户程序类。

双亲委派模型

系统中的 ClassLoader 在协同工作时, 默认会使用双亲委托模式。即在类加载的时候, 系统会判断当前类是否己经被加载, 如果已经被加载, 就会直接返回可用的类, 否则就会尝试加载, 在尝试加载时, 会先请求双亲处理, 如果双亲请求失败, 则会自己加载。

通常情况下, 启动类加载器中的类为系统核心类, 包括一些重要的系统接口, 而在应用类加载器中, 为应用类。按照这种模式, 应用类访问系统类自然是没有问题, 但是系统类访问应用类就会出现问题。比如, 在系统类中, 提供了一个接口, 该接口需要在应用中得以实现, 该接口还绑定一个工厂方法, 用于创建该接口的实例, 而接口和工厂方法都在启动类加载器中。这时, 就会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题。拥有这种问题的组件有很多, 比如 JDBC、Xml Parser 等。

在 Java 平台中, 把核心类(rt.jar)中提供外部服务, 可由应用层自行实现的接口, 通常可以称为 Service Provider Interface, 即 SPI。对于这类问题,一般的解决方案是,在 META-INF/services 目录下,以目标接口名创建一个文件,文件内记录该接口的所有实现类。

meta-inf-example

这样系统通过读取 jar 包中的 META-INF/services/TargetService 文件就能获取到所有实现类类名。紧接着,在使用这部分系统接口前,我们要将应用 ClassLoader 存入 ThreadLocal 变量中,这个 ThreadLocal 对象就是 java.lang.Thread.contextClassLoader。这样系统类库就能从 ThreadLocal 中获取应用 ClassLoader,并用它加载 META-INF/services/TargetService 中的实现类并使用。

突破双亲模型

双亲模式的类加载方式是虚拟机默认的行为, 但并非必须这么做, 通过重载 ClassLoader 可以修改该行为。事实上, 不少应用软件和框架都修改了这种行为, 比如 Tomcat 和 OSGi 框架, 都有各自独特的类加载顺序。

热更新

热更新是指在程序的运行过程中, 不停止服务, 只通过替换程序文件来修改程序的行为。热更新的关键需求在于服务不能中断, 修改必须立即表现在正在运行的系统之中。基本上大部分脚本语言都是天生支持热更新的, 比如 PHP, 只要替换了 PHP 源文件, 这种改动就会立即生效, 而无须重启 Web 服务器。但对 Java 来说, 热更新并非天生就支持, 如果一个类已经加载到系统中, 通过修改类文件, 并无法让系统再来加载并重定义这个类。因此, 在 Java 中实现这一功能的一个可行的方法就是灵活运用 ClassLoader。

在 Java 中,不同的 ClassLoader 加载的同名类属于不同的类型,不能相互转化和兼容,即两个不同的 ClassLoader 加载同一个类,在虚拟机内部,会认为这两个类是完全不同的。利用这种特性,每当我们想要进行热更新时,只需要新建一个 ClassLoader 然后用这个新的 ClassLoader 去加载更新后的 Class 文件,然后从这个新 ClassLoader 中创建类的对象,然后运行这个新对象的方法,就达到了热更新的效果。

package bbm.jvm;

public class Test {
    public void test() {
        System.out.println("test");
    }
}

为了达到上述目的,我们要实现自己的 ClassLoader,让其加载 Test 类时,从指定的文件中读取字节码。然后在测试函数中,我们不断的创建新的 HotUpdateClassLoader 实例,使其每次实例化时都是基于最新的 Test 文件,然后我们调用新 Test 实例的 test 方法。并观察其是否发生变化。

package bbm.jvm;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

public class HotUpdateClassLoader extends ClassLoader{

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = this.findLoadedClass(name);
        if (clazz == null) {
            try(FileInputStream fileInputStream = new FileInputStream("/path/to/Test.class");
                FileChannel fileChannel = fileInputStream.getChannel();
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                WritableByteChannel writableByteChannel = Channels.newChannel(byteArrayOutputStream)) {
                ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                while (true) {
                    int i = fileChannel.read(byteBuffer);
                    if (i == 0 || i == -1) {
                        break;
                    }
                    byteBuffer.flip();
                    writableByteChannel.write(byteBuffer);
                    byteBuffer.clear();
                }
                byte[] bytes = byteArrayOutputStream.toByteArray();
                clazz = defineClass(name, bytes, 0, bytes.length);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return clazz;
    }

    public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException, InterruptedException {
        while (true) {
            HotUpdateClassLoader classLoader = new HotUpdateClassLoader();
            Class clz = classLoader.loadClass("bbm.jvm.Test");
            Test demo = (Test) clz.newInstance();
            demo.test();
            Thread.sleep(10000);
        }
    }
}

执行起测试函数后,我们改写 Test 类,让其打印 test1, 然后重新编译 Test 类。

package bbm.jvm;

public class Test {
    public void test() {
        System.out.println("test1");
    }
}

重新编译 Test 类之后,测试函数的输入发生了变化,开始打印 test1,也就是说我们的热更新测试成功了。 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值