• 虚拟机类加载机制


    类加载时机

    类的生命周期分为加载、连接(验证、准备、解析)、初始化、使用、卸载几个阶段
    类的生命周期

    在什么情况下需要开始类加载过程的第一个阶段“加载”,《Java虚拟机规范》中并没有进行强制约束。
    但是对于初始化阶段,《Java虚拟机规范》 则是严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

    1. 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始 化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有:
      • 使用new关键字实例化对象的时候。
      • 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。
      • 调用一个类型的静态方法的时候
    2. 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
    3. 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
    4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
    5. 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
    6. 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
    类加载过程

    Java虚拟机中类加载过程包括加载、验证、准备、解析和初始化这五个阶段。

    加载

    在加载阶段虚拟机主要完成3件事情:

    • 通过类的全限定名来获取此类的二进制字节流
    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
    • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

    获取二进制字节流不一定要从本地文件中获取,也可以从压缩包(jar包、war包)、网络、运行时计算生成(JSP文件生成对应的Class文件)、加密文件(反编译保护措施)、数据库中获取

    加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,这两个阶段的开始时间仍然保持着固定的先后顺序。

    连接-验证

    这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全

    验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证。

    连接-准备

    这一阶段是为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值。

    假设一个类变量的定义为:

    public static int value = 123;

    变量value在准备阶段过后的初始值为0而不是123,在之后的初始化阶段才会赋值为123。

    如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值,上面类变量value的定义修改为:

    public static final int value = 123;

    编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据Con-stantValue的设置将value赋值为123。

    连接-解析

    解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程

    初始化

    初始化阶段就是执行类构造器()方法的过程。 这时Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。

    ()并不是程序员在Java代码中直接编写的方法,它是Javac编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。

    编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量。

    public class Test {
    
        static int i = 1;
    
        static {
            i = 0;              // 给变量赋值可以正常编译通过
            System.out.print(i); //编译通过,输出 0
        }
    
        public static void main(String[] args) {
            //输出 0
            System.out.println(i); 
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    定义在它之后的变量,在前面的静态语句块可以赋值但是不能访问

    public class Test {
    
        static {
            i = 0;              // 给变量赋值可以正常编译通过
    //        System.out.print(i); 这句编译器会报错“非法向前引用”
        }
    
        static int i = 1;
    
        public static void main(String[] args) {
            //输出 1
            System.out.println(i); 
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    Java虚拟机会保证先执行父类的()方法

    接口中不能使用静态语句块,但仍然有静态变量初始化的赋值操作,因此接口与类一样都会生成()方法。但接口与类不同的是,执行接口的()方法不需要先执行父接口的()方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化。此外,接口的实现类在初始化时也一样不会执行接口的()方法。

    Java虚拟机必须保证一个类的()方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行完毕()方法。

    类加载器

    “通过一个类的全限定名来获取描述该类的二进制字节流”这个动作是在Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动 作的代码被称为“类加载器”(Class Loader)。

    比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

    public class Test {
        public static void main(String[] args) throws Throwable {
    
            ClassLoader myLoader = new ClassLoader() {
                @Override
                public Class<?> loadClass(String name) throws ClassNotFoundException {
                    try {
                        String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                        InputStream is = getClass().getResourceAsStream(fileName);
                        if (is==null){
                            return super.loadClass(name);
                        }
                        byte[] b = new byte[is.available()];
                        is.read(b);
                        return defineClass(name, b, 0, b.length);
                    } catch (IOException e) {
                        throw new ClassNotFoundException(name);
                    }
                }
            };
            Object obj = myLoader.loadClass("com.hls.codebase.learn.empty.config.Test").newInstance();
    
            System.out.println(obj.getClass());
            System.out.println(obj instanceof com.hls.codebase.learn.empty.config.Test);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    输出:
    com.hls.codebase.learn.empty.config.Test false

    类型检查的时候返回了false是因为Java虚拟机中同时存在了两个ClassLoaderTest类,一个是由虚拟机的应用程序类加载器所加载的,另外一个是由自定义的类加载器加载的,虽然它们都来自同一个Class文件,但在Java虚拟机中仍然是两个互相独立的类。

    双亲委派模型

    站在Java虚拟机的角度来看,只存在两种不同的类加载器:
    一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;
    另外一种就是其他所有的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。

    站在Java开发人员的角度来看,类加载器是三层类加载器、双亲委派的类加载架构,

    类加载器双亲委派模型

    • 启动类加载器(Bootstrap Class Loader):这个类加载器负责加载存放在\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的核心库,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载类库加载到JVM的内存中用于提供JVM自身需要的类。
    • 扩展类加载器(Extension Class Loader):这个类加载器负责加载存放\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库,是在类sun.misc.Launcher$ExtClassLoader 中以Java代码的形式实现的。开发者可以直接在程序中使用扩展类加载器来加载放置在ext目录里的自定义Class文件。
    • 应用程序类加载器(Application Class Loader):这个类加载器负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器,由sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystemClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

    各种类加载器之间的层次关系被称为类加载器的“双亲委派模型(Parents Delegation Model)”。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。

    双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

    这样做好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。
    反之,如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱。

    用以实现双亲委派的代码,全部集中在java.lang.ClassLoader的loadClass()方法:

        protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded
                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 thrown if class not found
                        // from the non-null parent class loader
                    }
    
                    if (c == null) {
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        long t1 = System.nanoTime();
                        c = findClass(name);
    
                        // this is the defining class loader; record the stats
                        sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                        sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                        sun.misc.PerfCounter.getFindClasses().increment();
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    破坏双亲委派模型
    1. 历史原因:

    双亲委派模型在JDK 1.2之后才被引入,但是类加载器的概念和抽象类 java.lang.ClassLoader则在Java的第一个版本中就已经存在,面对已经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协,
    为了兼容这些已有代码,无法再以技术手段避免loadClass()被子类覆盖的可能性,只能在JDK 1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。

    1. 双亲委派模型的缺陷:

    双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类型之所以被称为“基础”,是因为它们总是作为被用户代码继承、调用的API存在,但是基础类型又要调用回用户的代码就没有办法了?

    接口:java.sql.Driver,定义在java.sql包中,包所在的位置是:jdk\jre\lib\rt.jar中,java.sql包中还提供了其它相应的类和接口比如管理驱动的类:DriverManager类,很明显java.sql包是由BootstrapClassloader加载器加载的;
    而接口的实现类com.mysql.jdbc.Driver是由第三方实现的类库,由AppClassLoader加载器进行加载的,DriverManager再获取链接的时候必然要加载到com.mysql.jdbc.Driver类,这就是由BootstrapClassloader加载的类使用了由AppClassLoader加载的类,很明显和双亲委托机制的原理相悖。

    SPI打破双亲委派机制:SPI的全名为Service Provider Interface,主要是应用于厂商自定义组件或插件中,系统中抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。
    面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。 为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。
    Java SPI就是提供这样的一个机制:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。
    而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。
    基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定,jdk提供服务实现查找的一个工具类:java.util.ServiceLoader。
    mysql驱动的spi机制

    以上截图展示了SPI使用的三要素:

    • 实现类的java包位置要放在主程序的classpath中;
    • 在实现类的jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
    • 主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
    1. 程序动态性要求:

    “动态性”指的是:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等。
    OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构。

  • 相关阅读:
    关于保存和恢复 View 层次结构,系统的工作流程是这样的
    C++11 ——— lambda表达式
    Hvv--知攻善防应急响应靶机--Web2
    Spring+SpringMVC+Jsp实现新冠肺炎疫苗接种管理系统
    基于SkyEye的CAN总线通信测试
    做了8年前端,感谢那些优秀的后端,陪伴我工作,教会我成长
    小程序如何反编译
    C语言--结构体(内容超级详细)
    基于C语言学生成绩管理系统的设计与开发
    SpringBoot SpringBoot 运维实用篇 2 配置高级 2.4 自定义配置文件
  • 原文地址:https://blog.csdn.net/qq_31603875/article/details/127407960