首页 > 其他分享 >JVM——类加载机制

JVM——类加载机制

时间:2023-08-12 20:32:32浏览次数:54  
标签:初始化 JVM 阶段 超类 机制 执行 加载

任何一个类型在使用之前都必须经历过完整的加载、连接和初始化3个类加载步骤。一旦一个类型成功经历过这3个步骤之后,它就可以随时随地被使用了,开发人员可以在程序中访问和调用它的静态类成员信息(比如:静态方法、静态字段),或者使用new关键字为其创建对象实例。当然从一个类型被加载进JVM中开始算起,直至最终被卸载出内存为止,它的整个生命周期也就随之而结束了。

1、类加载器

类加载器是JVM执行类加载机制的前提。简单来说,类加载器的主要任务就是根据一个类的全限定名来读取此类的二进制字节流到JVM内部,然后转换为一个与目标类对应的java.lang.Class对象实例。

无论类加载器的类型如何划分,在程序中我们最觉的类加载器始终只有3个,如下所示:

  • Bootstrap ClassLoader;
  • ExtClassLoader;
  • AppClassLoader;

upload successful

BootStrap ClassLoader 也称之为启动类加载器,它由C++语言编写并嵌套在JVM内部,主要负责加载“JAVA_HOME/lib”目录中的所有类型,或者由选项“-Xbootclasspath”指定路径中的所有类型。ExtClassLoaderAppClassLoader派生于ClassLoader,并且都是采用Java语言进行编写的,前者主要负责加载“JAVA_HOME/lib/ext”扩展目录中的所有类型,而后者则主要负责加载ClassPath目录中的所有类型。

如果当前的类加载器无法满足我们的需求时,便可以在程序中编写自定义类加载器来重新定义类的加载规则,以便实现一些自定义的处理逻辑。在程序中编写一个自定义类加载器只需要继承抽象类ClassLoader并重写其findClass()方法即可。使用时调用loadClass()方法来实现类加载操作。

2、双亲委派模型(Parents Delegation Model)

除了启动类加载器之外,程序中每一个类加载器都应该拥有一个超类加载器,比如AppClassLoader的超类加载器就是ExtClassLoader,而开发人员自己编写的自定义类加载器的超类就是AppClassLoader。当一个类加载器接收到一个类加载任务的时候,它并不会立即展开加载,而是将加载任务委派给它的超类加载器去执行,每一层的类加载器都采用相同的方式,直至委派给最顶层的启动类加载器为止。如果超类加载器无法加载委派给它的类时,便会将类的加载任务退回给它的下一级类加载器去执行加载。

由于Java虚拟机规范并没有明确要求类加载器的加载机制一定要使用双亲委派模型,只是建议采用这种方式。比如在Tomcat中,类加载器所采用的加载机制就和传统的双亲委派模型有一定区别,当缺省的类加载器接收到一个类的加载任务时,首先会由它自行加载,当它加载失败时,才会将类的加载任务委派给它的超类加载器去执行,这同时也是Servlet规范推荐的一种做法。

双亲委派模型代码如下:**

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查类是否已经加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 如果存在超类加载器,就委派给超类加载器执行加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        // 如果不存在超类加载器,就直接委派给顶层的启动类加载器执行加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 如果找不到类,则引发ClassNotFoundException
                    // 当抛出 ClassNotFoundException 异常时,意味着超类加载器加	载失败
                }

                if (c == null) {
                    // 如果超类加载器无法加载时,则自行加载
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

自定义类加载器

package com.jvm;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class MyClassLoader extends ClassLoader{

    private String byteCode_path;

    public MyClassLoader(String byteCode_path) {
        this.byteCode_path = byteCode_path;
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return super.loadClass(name);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] value = null;
        BufferedInputStream in = null;
        try {
            in = new BufferedInputStream(new FileInputStream(byteCode_path + name + ".class"));
            value = new byte[in.available()];
            in.read(value);
        }  catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        // 将byte数组转为一个类的 Class 对象实例
        return defineClass(value, 0, value.length);
    }

    public static void main(String[] args) throws ClassNotFoundException {
        MyClassLoader classloader = new MyClassLoader("/Users/jarvan/");
        System.out.println("加载目标类的类加载器->" + classloader.loadClass("NumberTest").getClassLoader().getClass().getName());
        System.out.println("当前类加载器的超类加载器->" + classloader.getParent().getClass().getName());
    }
}

输出如下:

加载目标类的类加载器->com.jvm.MyClassLoader
当前类加载器的超类加载器->jdk.internal.loader.ClassLoaders$AppClassLoader

3、类的加载过程

类加载器所执行的加载操作仅仅只是属于JVM中类加载过程中的一个阶段,一个完整的类加载过程必须经历加载、连接和初始化这3个步骤。

upload successful

加载阶段:

简单来说,类的加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到 JVM 内部,并存储在运行时内存区中的方法区内,然后将其转换成一个与目标类型对应的 java.lang.Class 对象实例,这个 Class 对象在日后就会作为方法区中该类的各种数据的访问入口

连接阶段:

连接阶段要做的事情就是将已经加载到 JVM 中的二进制字节流的类数据信息合并到 JVM 的运行时状态中。

连接阶段由验证、准备和解析3个阶段构成,其中验证阶段的主要任务就是验证类数据信息是否符合 JVM 规范,是否是一个有效的字节码文件,而验证的内容涵盖了类的类数据信息的格式验证、语义分析、操作验证等;

准备阶段的主要任务就是为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,因此实例变量将不在此操作范围内);

解析阶段的主要任务是将常量池中所有的符号引用全部转换为直接引用,不过 Java 虚拟机规范并没有明确要求解析阶段一定要按顺序执行,因此解析阶段可以等到初始化之后再执行

初始化:

初始化阶段中 JVM 会将一个类中所有被 static 关键字标示的代码统统执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖掉之前在准备阶段中 JVM 为其设置的初始值,当然如果没有显示赋值静态变量,那么所持有的值仍是之前的初始值;如果执行的是 static 代码块,那么在初始化阶段中,JVM 会执行 static 代码块中的所有操作。

一个类或者接口应该在首次主动使用时执行初始化操作

  • 为一个类型创建一个新的对象实例时(new关键字、反射或序列化)
  • 调用一个类型的静态方法时(即在字节码中执行invokestatic指令)
  • 调用一个类型或接口的静态字段,或者对这些静态字段执行赋值操作时(即在字节码中,执行getstatic或者putstatic指令),不过使用final关键字修饰的静态字段除外,它被初始化为一个编译时的常量表达式
  • 调用 Java API 中的反射方法时(比如 java.lang.Class 中的方法或者 java.lang.reflect 包中其他类的方法)
  • 初始化一个类的派生类时(Java 虚拟机规范明确要求初始化一个类时,它的超类必须提前完成初始化操作)
  • JVM 启动包含 main() 方法的启动类时

尽管一个类在初始化之前必须要求它的超类提前完成初始化操作,但对于接口而言并不适用。只有在某个接口中声明的非常量字段被使用时,该接口才会被初始化,而不会因为实现这个接口的派生接口或派生类要初始化而被初始化。

演示一个类发生的变量

package com.jvm;

public class LoadingTest {
    public static LoadingTest obj = new LoadingTest();
    public static int value1;
    public static int value2 = 0;

    public LoadingTest() {
        value1 = 10;
        value2 = value1;
        System.out.println("before valuel->" + value1);
        System.out.println("before value2->" + value2);
    }

    public static void main(String[] args) throws Exception {
        System.out.println("after valuel->" + value1);
        System.out.println("after value2->" + value2);
    }
}

程序输出如下

before valuel->10
before value2->10
after valuel->10
after value2->0

将示例代码中的代码位置调换顺序

public static int value1;
public static int value2 = 0;
public static LoadingTest obj = new LoadingTest();

程序输出如下

before valuel->10
before value2->10
after valuel->10
after value2->10

原因

当类加载器将 Loading 类加载进 JVM 内部后,会在方法区中生成一个与该类型对应的 java.lang.Class 对象实例。

当进入到准备阶段时,JVM 便会为 Loading 类中的3个静态变量分配内存空间,并为其设置初始值(value1和value2为0,obj为null)。

当经历到类加载过程的初始化阶段时,程序最终的输出结果就会和代码的执行顺序有关了。

在上述示例中,静态变量obj是优先初始化的,所以初始化后value1和value2的值都是10。接下来 JVM 会检查静态变量value1是否也需要执行初始化,由于value1并没有显示的进行赋值,因此将会跳过它到静态变量value2上,这里显示赋值为value2=0等于重新覆盖掉之前在构造方法中的赋值操作,这就是会输出为0的原因,位置调换后,最后赋值的为10所以和预期一致。

1、加载阶段

类加载器根据一个类的全限定名来读取此类的二进制字节流到 JVM 内部,并存储在运行时内存区中的方法区内,然后将其转换成一个与目标类型对应的 java.lang.Class对象实例,这个 Class 对象在日后就会作为方法区中该类的各种数据的访问入口。

2、验证阶段

验证阶段中的主要任务是检查当前正在加载的字节码文件是否符合 JVM 规范,是否一是个有效的字节码文件,如果不是一个有效的字节码文件 JVM 就将会抛出 java.lang.VerifyError异常。

格式验证检查字节码文件中的前四个字节是否为 0xCAFEBABE,如果高版本 JDK 编译的字节码文件不能在低版本的 JVM 中运行,否则 JVM 会抛出java.lang.UnsupportedClassVersionError异常

只有当成功通过格式验证之后,类加载器才会成功将类的二进制数据信息加载到方法区中。而后续的其他验证操作都直接在方法区中进行。接下来 JVM 就会开始执行下一阶段的验证任务

  • 检查一个被标记为 final 的类型是否包含派生类;
  • 检查一个类中的 final 方法是否被派生类进行重写;
  • 确保超类与派生类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)

当成功执行完语义验证后,JVM 会进入操作验证阶段。此阶段中JVM会对类型的方法执行验证,确保一个类的方法在执行时,不会对JVM产生不良的影响,不会因此导致JVM的进程出现崩溃(比如入栈2个int类型后,却把它们当做long类型去操作)。

验证阶段最后一步,对常量池中的各种符号引用执行验证,在JVM的具体实现中连接阶段中的解析操作往往发生在初始化之后,因此这一阶段的验证会在解析阶段中才会执行。简单来说解析阶段的主要任务是将常量池中所有的符号引用全部转换为直接引用,那么符号引用验证的主要任务就是验证需要被转换为直接引用的这些符号引用是否正确。比如是否能否通过符号引用中通过字符串描述的全限定名定位到指定的类型上,或者是符号引用中的类成员信息的访问修饰符是否能够被当前类执行访问操作等

3、准备阶段

此阶段对存放在方法区中的类数据信息的类变量执行初始化,为类中的所有静态变量分配内存空间,并为其亩一个初始值(由于还没有产生对象,因此实例变量将不在此操作范围内)

JVM 内部实现其实并不支持boolean类型,在JVM内部,boolean类型往往被实现为一个int类型,初始值为0也就代表着false。

4、解析阶段

由于Java虚拟机规范并没有明确要求解析阶段一定要按照顺序执行,因此解析阶段可以等到初始化之后再执行。

解析阶段的主要任务就量将字节码常量池中的符号引用全部转换为直接引用,包括类、接口、方法和字段的符号引用。

5、初始化阶段

类加载过程中的最后一个阶段就是初始化,初始化阶段中, JVM 会将一个类中所有被 static 关键字标示的代码统统执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖掉之前在准备阶段中 JVM 为其设置的初始值,当然如果没有显示赋值静态变量,那么所持有的值仍是之前的初始值;如果执行的是 static 代码块,那么在初始化阶段中,JVM 会执行 static 代码块中的所有操作。

标签:初始化,JVM,阶段,超类,机制,执行,加载
From: https://blog.51cto.com/u_11906056/7061404

相关文章

  • 10 张图帮你搞定 TensorFlow 数据读取机制
    一、tensorflow读取机制图解首先需要思考的一个问题是,什么是数据读取?以图像数据为例,读取数据的过程可以用下图来表示:假设我们的硬盘中有一个图片数据集0001.jpg,0002.jpg,0003.jpg……我们只需要把它们读取到内存中,然后提供给GPU或是CPU进行计算就可以了。这听起来很容易,但事实远没有......
  • JVM之字节码的编译原理
    JVM之字节码的编译原理Java最初诞生的目的就是为了在不依赖特定的物理硬件和操作系统环境下运行,那么也就是说Java程序实现跨平台我的基石其实就是字节码。Java之所以能够解决程序的安全性问题、跨平台移植性等问题,最主要的原因就是Java源代码的编译结果并非是本地机器指令,而是字......
  • 记录--Loading 用户体验 - 加载时避免闪烁
    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助在切换详情页中有这么一个场景,点击上一条,会显示上一条的详情页,同理,点击下一条,会显示下一条的详情页。伪代码如下所示:我们定义了一个 switcher 模版,用户点击上一条、下一条时调用 goToPreOrNext 方法。该页面......
  • JVM之内存结构
    从整体上看JVM的内存分为两大类:线程私有的和线程共享的。线程私有:程序计数器虚拟机栈本地方法栈线程共享:堆区方法区程序计数器主要作用就是记住下一条JVM指令的执行地址。因为在多线程的情况下,同一个时间单核CPU只会执行一个线程中的方法,也就是说CPU会不断切换执行的......
  • JDK中动态库加载路径问题,一文讲清
    前言本周协助测试同事对一套测试环境进行扩容,我们扩容很原始,就是新申请一台机器,直接把jdk、resin容器(一款servlet容器)、容器中web应用所在的目录,全拷贝到新机器上,servlet容器和其中的应用启动没问题。以为ok了,等到测试时,web应用报错,初始化某个类出错。报错的类长下面这样:com.thi......
  • 10 张图帮你搞定 TensorFlow 数据读取机制
    在学习tensorflow的过程中,有很多小伙伴反映读取数据这一块很难理解。确实这一块官方的教程比较简略,网上也找不到什么合适的学习材料。今天这篇文章就以图片的形式,用最简单的语言,为大家详细解释一下tensorflow的数据读取机制,文章的最后还会给出实战代码以供参考。一、tensorflow读取......
  • Flutter中的加载指示器
    Flutter提供了多种加载指示器样式供选择。你可以使用CircularProgressIndicator以外的其他加载指示器样式来替换原有的加载指示器。以下是一些常见的加载指示器样式,你可以根据自己的需要选择其中之一:一、LinearProgressIndicator:线性进度指示器,呈现为水平进度条。LinearProgressInd......
  • Devexpress xtraTabControl1实现多标签页选项卡,关闭选项卡,刷新重新加载
    //选项卡Dictionary<string,XtraTabPage>dictXtraTabPage=newDictionary<string,XtraTabPage>();Dictionary<string,Form>dictXtraForm=newDictionary<string,Form>();publicvoidShowMDIForm(string......
  • HDFS工作流程与机制
    1、各个角色的职责主角色:NameNode从而,NameNode成为了访问HDFS的唯一入口从角色:DataNode主角色辅助角色:SecondaryNameNodeNameNode职责:DataNode职责:2、HDFS写数据流程(上传文件)流程图:PipeLine管道:HDFS文件系统的一种数据传输方式ACK应答响应:确认字符(计算机网络相......
  • Vue 路由懒加载
    1路由懒加载的原理路由懒加载是一种优化技术,用于延迟加载应用程序中的路由组件。它可以提高初始加载速度并减少资源消耗,特别适用于大型单页应用。1.1为什么要使用路由懒加载当应用程序包含多个页面和路由时,如果在初始加载时将所有路由组件都打包到一个文件中,会导致初始加载时......