Java虚拟机--类加载器

Updated on in 程序人生 with 0 views and 0 comments

一、类生命周期

image.png

1.1 加载

  • 通过类全限定名获取它的二进制字节流
    • 从ZIP压缩包、JAR、EAR、WAR中获取
    • 从网络中获取,例如Web Applet
    • 运行时计算生成,如动态代理技术
    • 从数据库读取
    • 从加密文件中获取
  • 将这个字节流所代表得静态存储结构转化为方法区得运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据得访问入口

1.2 链接

1.2.1 验证

验证阶段确保加载的类的信息符合JVM规范,不会对JVM造成危害。这包括对字节码的结构、常量池中的索引、数据类型等进行检查。

  • 文件格式验证
    • 魔数是否是CAFEBABE
    • 主次版本是否在虚拟机支持范围内
    • ......
  • 元数据验证
    • 这个类是否有父类(除了java.lang.Object外,其他类都有父类)
    • 父类是否继承了不允许被继承的类
    • ......
  • 字节码验证
    • 确保类型转化是有效的,例如把父类对象赋值给子类数据类型就是无效的
    • ......
  • 符号引用验证
    • 符号引用中的类、字段、方法的可访问性(private\protected\public\)是否可以被当前类访问
    • 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段
    • .......

符号引用验证无法通过虚拟机会抛出java.lang.IncompatibleClassChangeError的子类,如Java.lang.IllegalAccessError\java.lang.NoSuchFieldError\java.lang.NoSuchMethodError等

1.2.2 准备

准备阶段为类的静态变量分配内存并在方法区中设置默认值,但并不执行任何代码,如静态初始化块(static {})不会被执行。

public static int value = 123;

上述代码在准备过后初始值是0,value赋值123的动作在类初始化时才执行

但是如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段就会被初始化

public static final int value = 123;

上述代码准备阶段过后value的值就是123

1.2.3 解析

解析阶段将常量池中的符号引用替换为直接引用,直接引用不在使用编号二十使用内存地址访问数据

eg:

public class HsdbDemo {
    public static int i=123;
    public static void main(String[] args) throws IOException {
        new HsdbDemo();
        System.in.read();
    }
}

这个类的字节码文件中,父类索引了一个常量池中的数据

image.png

image.png

image.png

然后我们启动这个方法,进入这个线程浏览类

image.png

可以看到这里是实际的内存地址

image.png

1.3 初始化

初始化阶段执行类构造器 <clinit>()方法,这会执行类的静态初始化块和静态变量赋值,使类准备好供应用程序使用,clinit方法对接口或类来说不是必须的,如果没有静态语句块也没有静态变量赋值,就没有clinit方法

1.3.1 clinit方法

clinit方法是编译器自动收集类中的所有静态类变量赋值动作和静态语句块中的语句合并产生的

如下的类中

public class Test {

    static int i=1;
    static {
        i = 0;
    }
}

生成的字节码文件中会有一个clinit方法

image.png

1.3.2 非法向前应用变量

静态语句块中只能访问到定义在静态语句块之前的变量,定义在他之后的变量,在前面的静态语句块可以赋值,但是不能访问,如果强行访问就会报非法向前引用的错误

image.png

1.3.3 静态代码块和静态变量赋值执行顺序

clinit中执行静态代码块和给静态变量赋值的顺序是按照我们代码的顺序来的,代码中先给静态变量赋值,那么clinit就会先给静态变量赋值,要是代码先执行了静态代码块,clinit就会先执行静态代码块

如下,先给静态变量赋值

public class Test {

    static int i=1;
    static {
        i = 0;
    }
}

上述类的clinit方法的字节码指令如下

image.png

  1. 常量1压入操作数栈
  2. 常量1出栈并赋值给编号为#2的静态变量,#2在常量池中对应的就是变量i
  3. 常量0压入操作数栈
  4. 常量0出栈并赋值给编号为#2的静态变量

执行完静态变量值为0

再来看执行静态代码块的情况

public class Test {

    static {
        i = 0;
    }
    static int i=1;
}

上述类的clinit方法的字节码指令如下

image.png

  1. 常量0压入操作数栈
  2. 常量0出栈并赋值给编号为#2的静态变量
  3. 常量1压入操作数栈
  4. 常量1出栈并赋值给编号为#2的静态变量

执行完静态变量值为1

1.3.4 父类clinit方法

clinit方法与构造函数不同,不需要显示调用父类构造器,java虚拟机会保证子类clinit方法执行前,父类clinit方法已经执行完毕,因此java虚拟机执行的第一个clinit方法的类型肯定是java.lang.Object

因此父类静态语句块执行优先于子类变量的赋值操作,下述代码输出结果为2


public class Test {
    static class Parent{
        public static int A = 1;
        {
            A = 2;
        }
    }
    static class Sub extends Parent{
        public static int B = A;
    }
    public static void main(String[] args){
        System.out.println(Sub.B);
    }
}

1.3.5 类和接口中的clinit

接口不能使用静态代码块,但是仍然有静态变量赋值操作,所以还是会有clinit方法,但是接口的clinit方法不需要先执行父接口的clinit方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化,接口的实现类在初始化时,也不会执行接口的clinit方法

1.3.6 clinit同步问题

如果有多个线程同时初始化一个类,此时只有一个线程会执行这个类的clinit方法,其他线程都要阻塞等待,直到clinit方法执行完毕

如下代码,static代码中执行while循环,其他线程就会阻塞

public class DeadLoopClassTest {

    static class DeadLoopClass{
        static {
            if(true){
                System.out.println(Thread.currentThread().getName()+"执行clinit");
                while(true){
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Runnable script = () -> {
            System.out.println(Thread.currentThread().getName()+" 开始");
            DeadLoopClass dlc = new DeadLoopClass();
            System.out.println(Thread.currentThread().getName()+" 结束");
        };
        Thread th1 = new Thread(script);
        th1.setName("任务1");
        Thread th2 = new Thread(script);
        th2.setName("任务2");
        th1.start();
        th2.start();

        while(true){
            System.out.println("任务1状态   "+th1.getState());
            System.out.println("任务2状态   "+th2.getState());
            Thread.sleep(3000);
        }
    }
}

对于静态代码块而言,你不需要担心显式的同步问题,因为JVM已经处理了这些细节。但是,如果你在静态代码块中访问了非静态成员或进行了其他可能引起并发问题的操作,那么你可能需要考虑使用synchronized关键字或其他同步机制来保护这些操作,确保线程安全

1.3.7 初始化时机

以下几种方式会导致类的初始化

访问一个类的静态变量或者而静态方法,注意变量是final修饰并且等号右边不会触发初始化,因为准备阶段就已经初始化了

下述代码中,访问了静态变量,执行了初始化

public class HelloWorld {
    public static void main(String[] args){
        System.out.println(TestStatic.i);
    }
}
class TestStatic{
    static{
        System.out.println("初始化...");
    }
    public static int i=0;
}

结果为

image.png

然后把静态变量用final修饰

public class HelloWorld {
    public static void main(String[] args){
        System.out.println(TestStatic.i);
    }
}
class TestStatic{
    static{
        System.out.println("初始化...");
    }
    public static final int i=0;
}

输出结果为

image.png

调用Class.forName(String className)

public class HelloWorld {
    public static void main(String[] args) throws ClassNotFoundException {
        Class.forName("TestStatic");
    }
}
class TestStatic{
    static{
        System.out.println("初始化...");
    }
    public static final int i=0;
}

输出

image.png

new一个对象

public class HelloWorld {
    public static void main(String[] args) throws ClassNotFoundException {
        TestStatic testStatic = new TestStatic();
    }
}
class TestStatic{
    static{
        System.out.println("初始化...");
    }
    public static final int i=0;
}

输出

image.png

执行main方法的当前类

public class HelloWorld {
    static{
        System.out.println("main初始化");
    }
    public static void main(String[] args) throws ClassNotFoundException {

    }
}
class TestStatic{
    static{
        System.out.println("初始化...");
    }
    public static final int i=0;
}

输出

image.png

1.3.8 不会进行初始化的情况

image.png

二、类加载器

2.1 定义

类加载器是Java虚拟机提供给应用程序去获取类和接口字节码数据的技术,通过一个类得全限定名来获取描述该类得二进制字节流

每一个类加载器都有一个独立的类名称空间,比较两个类是否相等,只有在这两个类是由同一个同一个类加载器加载得前提下才有意义,否则,即使这两个类来源于同一个class文件,被同一个java虚拟机加载,只要加载他们的类加载器不同,拿这两个类必定不相等

这里的相等,包括代表类的Class对象的equals()方法,isAssignableFrom()方法,instance()方法的返回结果

参考下述代码演示了不同类加载器对instanceof 运算结果的影响

package jvm;

import java.io.IOException;
import java.io.InputStream;

/**
 * @author Swimming Dragon
 * @description: TODO
 * @date 2024年08月12日 10:32
 */
public class ClassLoaderTest {
    public static void main(String[] args) throws Exception{
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
                InputStream is = getClass().getResourceAsStream(fileName);
                if(is == null){
                    return super.loadClass(name);
                }
                try {
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name,b,0,b.length);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        };
        Object obj = myLoader.loadClass("jvm.ClassLoaderTest").newInstance();
        System.out.println(obj.getClass());
        System.out.println(obj instanceof jvm.ClassLoaderTest);

    }
}

这里输出结果如下

image.png

2.2 分类

image.png

类加载器分为两个大类

  • 启动类加载器(Bootstrap ClassLoader),C++语言实现,是虚拟机得一部分
  • 其他所有类加载器,由Java语言实现,独立存在于虚拟机外部,全部继承自抽象类java.lang.ClassLoader

2.2.1 启动类加载器

负责加载存放在<JAVA_HOME>\lib目录或者被-Xbootclasspath参数所指定的路径中存放类库,如rt.jar,tools.jar等

启动类加载器在虚拟机中实现的,代码中无法获取到,class类源码中的getClassLoader方法中也说明了启动类加载器加载的类调用这个方法会返回null

image.png

如下代码所示

public class BootstrapClassLoaderDemo {
    public static void main(String[] args){
        System.out.println(String.class.getClassLoader());
    }
}

输出结果为

image.png

2.2.2 其他类加载器

image.png

2.2.2.1 扩展类加载器

这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码形式实现的,负责加载<JAVA_HOME>\lib\ext目录或者被java.ext.dirs系统变量指定的路径中的类库,使用-Djava.ext.dirs=目录,这种方式会覆盖原始目录,因此需要指定原始目录和自定义目录,eg:-Djava.ext.dirs=<JAVA_HOME>\lib\ext;自定义目录;

2.2.2.2 应用程序类加载器

这个类加载器是在类sun.misc.Launcher$AppClassLoader中以Java代码形式实现的,负责加载用户类路径上的所有的类库

2.3 双亲委派机制

2.3.1 定义

双亲委派模型要求除了顶层得启动类加载器外,其余的类加载器都有自己的父类加载器

image.png

这里的父子关系并不是通过继承类声明的,而是在类加载器中通过字段属性指定了父类加载器

image.png

2.3.2 作用

  • 保证类加载得安全性,避免恶意代码替换jdk中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性
  • 避免同一个类被多次加载,如Object类,他放在rt.jar,无论哪一个类加载器要加载这个类,最重都是委派给启动类加载器进行加载,保证了Object类在程序的各种类加载器环境中都是同一个类

2.3.3 工作过程

如果一个类加载器收到类加载的请求,检查自己是否已经加载过,没有的话他首先不会自己去尝试加载这个类,而是把请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层得启动类加载器中,只有当父加载器反馈自己无法完成这个请求时,子加载器才会尝试自己去完成加载

image.png

2.3.4 打破双亲委派机制

image.png

2.3.4.1 自定义类加载器

自定义类加载器中,可以直接重写loadClass()方法,取消loadClass()方法中调用父类加载器加载的过程,直接自己加载,以此来直接打破双亲委派机制

除此之外,可以重写findClass方法而不是去重写loadClass方法,这样既可以按自己的意愿去加载类,也可以保证自定义的类加载器符合双亲委派规则

2.3.4.2 线程上下文类加载器

双亲委派模型解决了各个类加载器协作时基础类型一致得问题,基础类型经常被用户代码继承、调用,但在某些情况下基础类型也需要回调用户的代码

典型的例子就是JNDI服务,他的代码由启动类加载器来完成加载,但是他需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码,但是这些代码往往是由AppClassLoader加载的,启动类无法加载这些代码

线程上下文类加载器(Thread Context ClassLoader)通过java.lang.Thread的setContextClassLoader()方法进行设置,如果创建线程时未设置,他会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,这个类加载器默认就是应用程序类加载器,如下图所示

image.png

有了线程上下文加载器,JNDI服务就可以使用这个加载器去加载SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,Java中涉及SPI的加载基本上都是通过这种方式完成,例如JNDI、JDBC等

当SPI服务提供者多于一个的时候,代码就只能根据具体提供者的类型来硬编码判断,为了消除这种极不优雅的方式,JDK6提供了java.util.ServiceLoader类,以META-INF/services中的配置信息,辅以责任链模式,才算是给SPI的加载提供了一种相对合理的解决方案

image.png

2.3.4.3 OSG模块化i热部署

OSGi实现模块化热部署的关键是他自定义的类加载器机制的实现,每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉

当收到类加载请求时,OSGi按照下面的顺序进行搜索:

  1. 检查是否属于java.包:
    • 如果类或者资源是在包 java.*中,那么会交由父级类加载器(通常是启动类加载器Bootstrap ClassLoader)代理完成 。
  2. 检查是否在启动代理序列中
    • 如果类或者资源在启动代理序列(org.osgi.framework.bootdelegation)中定义,那么也会交由父级代理完成 。
  3. 检查是否在Import-Package中声明
    • 如果类或资源属于在 Import-Package中声明的包,或者是在先前的加载过程中动态导入的,那么请求将转发到导出该包的bundle的类加载器 。
  4. 检查是否在Require-Bundle中声明
    • 如果类或资源位于通过 Require-Bundle导入的包中,那么请求将按照在bundle的manifest中指定的顺序委派给其他bundle的类加载器 。
  5. 搜索当前bundle的内部类路径
    • 如果在前面的步骤中都没有找到类或资源,则会搜索当前bundle的内部类路径(Bundle Class Path)。
  6. 搜索Fragment Bundle
    • 如果在当前bundle中仍未找到,则会查找附加在当前bundle上的fragment bundle的内部类路径。Fragment bundle是OSGi 4引入的概念,它是一种不完整的bundle,必须要附加到一个host bundle上才能工作;fragment能够为host bundle添加类或资源,在运行时,fragment中的类会合并到host bundle的内部classpath中 。
  7. 尝试动态导入
    • 如果类或资源位于通过 DynamicImport-Package导入的包中,则会尝试进行动态导入。如果找到了合适的导出者,并且建立了连接,则请求将交由导出该包的bundle的类加载器处理 。

三、Java模块化系统

3.1 定义

3.1.1 模块化概念

Java模块化将Java应用程序划分为一组模块,每个模块具有自己的边界和依赖关系。模块化使得开发人员能够更好地组织和管理复杂的Java项目,并提供更好的封装和可重用性

JDK9中引入了Java模块化系统(Java Platform Module System,JPMS),Java的模块定义包含以下内容:

  • 依赖其他模块的列表
  • 导出的包列表,即其他模块可以使用的列表
  • 开放的包列表,即其他模块可以反射访问模块的列表
  • 使用的服务列表
  • 提供服务的实现列表

3.1.2 模块化原理

Java模块化的核心原理是将应用程序划分为一组模块,并通过 module-info.java文件来定义模块的信息和依赖关系。module-info.java文件位于模块的根目录下,它使用 module关键字声明模块的名称,并通过 requiresexports关键字定义模块的依赖关系和导出的包。通过这种方式,可以清晰地定义模块之间的依赖关系,避免类路径冲突,并提供更好的封装性和可重用性 。

在JDK9之前,如果类路径中缺失了运行时依赖的类型,那就只能等程序运行到发生该类型的加载、链接时才会爆出异常,在JDK9之后,如果启用了模块化进行封装,模块就可以声明对其他模块的显示依赖,这样Java虚拟机就能在启动时验证应用程序开发阶段设定好的依赖关系在运行期是否完备,如果缺失就直接启动失败

可配置的封装隔离机制还解决了原来类路径上跨jar文件的public类型的可访问性问题

3.2 模块兼容性

模块化系统在jdk9引入,为了支持老版本的程序也可以访问到标准类库模块中导出的包,模块化系统按照以下规则加载

  • JAR文件在类路径的访问规则:所有类路径下的Jar文件和其他资源文件,都自动打包在一个匿名模块(Unnamed Module),这个匿名模块可以看到和使用类路径上所有的包,JDk系统模块中所有的导出包,以及模块路径上所有模块中导出的包
  • 模块在模块路径的访问规则:模块路径下具有匿名模块只能访问到他依赖定义中列明里来的模块和包,匿名模块里所有的内容对具名模块来说都是不可见的,即具名模块看不见传统JAR包的内容
  • JAR文件在模块路径的访问规则:如果把一个传统的、不包含模块定义的JAR文件放置到模块路径中,他就会变成一个自动模块,自动模块默认依赖于整个模块路径中的所有模块,因此可以访问到所有模块导出的包,自动模块也默认导出自己所有的包

java --list-modules可以查看模块列表

3.3 模块化下的类加载器

扩展类加载器(Extension Class Loader)被平台类加载器(Platform Class Loader)取代,取消了<JAVA_HOME>\lib\ext目录;

新版本jdk中取消了<JAVA_HOME>\jre目录,可以使用jlink命令组合构建程序运行所需要的JRE,在jdk9中通过jlink实现,例如:

jlink --module-path $JAVA_HOME/jmods --add-modules java.base,java.sql,java.xml --output my-custom-jre

模块化后的jdk安装目录如下有个jmods文件夹

image.png

下面存放的就是jdk的默认71个模块

image.png

平台类加载器和应用程序类加载器都不在派生自java.net.URLClassLoader,现在启动类加载器、平台类加载器、应用程序类加载器全部继承于jdk.internal.loader.BuiltinClassLoader

image.png

image.png

jdk9中任然位处三层类加载器和双亲委派的架构,但类加载器的委派关系也发生了变动,当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到一个系统的模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载

image.png

image.png


标题:Java虚拟机--类加载器
作者:wenyl
地址:http://www.wenyoulong.com/articles/2024/07/20/1721405382576.html