• java高级——类加载机制


    简述

    JVM把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,形成可以被虚拟机使用的Java类型,称作类加载机制,过程如下

    在这里插入图片描述

    加载、验证、准备、初始化和卸载这五个阶段的开始顺序是确定的,但可交叉运行

    解析阶段在某些情况下可以在初始化阶段之后再开始,目的是为了实现动态绑定

    主动引用

    什么时候开始加载并无强制约束,但在以下情况必须立即对类进行初始化

    • 遇到new、getstatic、putstatic、invokestatic指令时
    • 进行反射调用时
    • 初始化类时,若父类没有初始化,需先触发父类初始化
    • 虚拟机启动时,用户需要指定一个要执行的主类,即包含main()的类,会初始化这个主类
    • MethodHandle实例解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄且对应的类没有过初始化,需先对其初始化
    • 接口有default方法,其实现类初始化前需对接口初始化

    被动引用

    除主动引用外的所有引用类型方式都不会触发初始化

    情况一

    对于如下程序

    public class Test {
    
    	public static void main(String[] args) {
    		System.out.println(SubClass.value);
    	}
    }
    
    class SuperClass{
    	static {
    		System.out.println("SuperClass init");
    	}
    	public static int value = 123;
    }
    
    class SubClass extends SuperClass{
    	static {
    		System.out.println("SubClass init");
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    打印

    SuperClass init
    123
    
    • 1
    • 2

    对于静态字段,只有直接定义这个字段的类才会被初始化,由子类来引用父类中的静态字段,只会触发父类的初始化而不会触发子类的初始化

    情况二

    如下程序无打印

    public class Test {
    
    	public static void main(String[] args) {
    		SuperClass[] sClasses = new SuperClass[10];
    	}
    }
    
    class SuperClass{
    	static {
    		System.out.println("SuperClass init");
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    没有触发SuperClass初始化,但newarray指令触发了[SuperClass初始化,它是由JVM生成、继承于Object的子类,代表一个元素类型为SuperClass的一维数组,其属性(如length)和方法(如clone)都在这个类中实现

    情况三

    对于如下程序

    public class Test {
    
    	public static void main(String[] args) {
    		System.out.println(ConstClass.HELLO_WORLD);
    	}
    }
    
    class ConstClass{
    	static {
    		System.out.println("ConstClass init");
    	}
    	public static final String HELLO_WORLD = "helloword";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    打印

    helloword
    
    • 1

    不会触发初始化,编译阶段会通过常量传播优化,将"helloword"字符串存储在Test.class,而不必通过ConstClass类去引用

    加载

    通过类全限定名获取其二进制字节流,可通过zip、网络、数据库等方式

    • 非数组类型加载,可用引导类加载器或自定义类加载器去控制字节流的获取
    • 若数组的组件类型(指去掉一个维度的类型)是引用,则按非数组类型加载,数组将被标识在加载该组件类型的类加载器的类名称空间上,与组件类型可访问性一致
    • 若数组的组件类型不是引用,JVM会把数组标记为与引导类加载器关联,可访问性默认为public

    加载结束后,二进制字节流按照虚拟机指定的格式存储在方法区

    堆中实例化一个Class对象,作为程序访问方法区中数据的外部接口

    验证

    确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的约束要求,作为代码运行后不会危害虚拟机自身的安全

    文件格式验证

    验证字节流是否符合Class文件格式规范,且能被当前版本的虚拟机处理

    • 是否以魔数0xCAFEBABE开头
    • 主、次版本号是否在当前JVM接受范围
    • 检查常量池tag是否有不支持的常量类型
    • 指向常量的索引是否有不存在或不符合类型的常量

    上述验证阶段基于二进制字节流进行,通过之后存储到方法区,而后面三个验证阶段则基于方法区上进行,不会再读取、操作字节流

    元数据验证

    对元数据信息(数据类型)进行语义分析,保证符合《Java语言规范》

    • 类是否有父类(除了Object之外都有父类)
    • 是否继承了final类
    • 若不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
    • 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类final字段,或出现不符合规则的方法重载,如方法签名一样但返回值类型不同)

    字节码验证

    通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的

    对类的方法体(Code属性)进行校验分析,保证运行时不会危害虚拟机安全

    • 保证操作数栈的数据类型与指令能配合工作,不会出现“在栈放置了一个int类型的数据,使用时却按long类型来加载到本地变量表”
    • 保证跳转指令不会跳转到方法体外
    • 保证方法体中的类型转换是有效的

    通过字节码验证,也不能保证其一定是安全的,因为用程序去校验程序逻辑是无法做到绝对准确的

    JDK6后新增StackMapTable属性描述方法体所有的基本块(Basic Block,指按照控制流拆分的代码块)开始时本地变量表和操作栈状态

    在字节码验证时,JVM不再需要推导这些状态的合法性,只需要检查StackMapTable属性中的记录是否合法即可,将类型推导转变为类型检查,从而节省校验时间

    符号引用验证

    对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验

    验证该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源

    • 符号引用中全限定名是否能找到对应的类
    • 在指定类中是否存在符合name_index和descriptor_index的方法和字段
    • 符号引用中的类、字段、方法的可访问性是否可被当前类访问

    目的是确保解析行为能正常执行,若验证失败,会抛出IncompatibleClassChangeError的子类异常,如NoSuchMethodError

    可用-Xverify:none关闭大部分的类验证以缩短JVM类加载的时间

    准备

    为类中static变量分配内存并设置初始值

    若为static变量,初始值为各类型零值,如下

    在这里插入图片描述

    若是static final变量,则存在ConstantValue属性,在准备阶段变量值会被初始化所指定的初始值

    解析

    将常量池内的符号引用替换为直接引用

    • 符号引用:定义在Class中,以字面量来描述所引用的目标,只要使用时能无歧义地定位到目标即可
    • 直接引用:可直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄

    《Java虚拟机规范》未规定解析阶段发生的具体时间,只要求了在执行

    • checkcast、instanceof
    • invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual
    • ldc、ldc_w、ldc2_w
    • ane-warray、multianewarray
    • new、putfield、getfield、getstatic、putstatic

    这17个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析

    解析阶段会对方法或字段可访问性进行检查

    JVM可对第一次解析的结果进行缓存,从而避免重复解析,但对于invokedynamic指令,必须等到程序实际运行时,解析动作才能进行

    CONSTANT_Class_info解析

    设当前代码所处的类为D,要把符号引用N解析为一个类或接口C的直接引用

    • 若C非数组,把N的全限定名传递给D的类加载器去加载类C
    • 若C是数组,并且数组元素类型为对象,则按照上面规则加载数据元素类型,再生成一个代表该数组维度和元素的数组对象

    上面解析完后还要验证D是否具备对C的访问权限,JDK9后还要检查模块权限,需具备

    • C是public,与D在同一模块
    • C是public,不与D在同一模块,但C的模块允许被D的模块访问
    • C非public,但与D在同一包内

    CON-STANT_Fieldref_info解析

    解析字段需先解析其所属类或接口(设为C)的符号引用,搜索name_index和descriptor_index都相匹配的字段,并返回其直接引用

    • 先搜索C
    • 递归搜索实现接口
    • 若C不是Object,递归搜索其父类
    • 查找失败抛出NoSuchFieldError

    上面解析能确保获取唯一解析结果,但实际上编译器会更加严格,如不允许继承结构中出现同名字段

    解析完后还要权限验证,无访问权限抛出IllegalAccessError

    CONSTANT_Methodref_info解析

    解析类方法需先解析其所属类或接口(设为C)的符号引用,查找name_index和descriptor_index都匹配的方法

    • 若C是接口,则抛出IncompatibleClassChangeError
    • 先搜索C
    • 递归搜索父类
    • 递归搜索实现父接口,若找到说明C是抽象类,抛出AbstractMethodError
    • 查找失败抛出NoSuchMethodError

    解析完后还要权限验证,无访问权限时抛出IllegalAccessError

    CONSTANT_InterfaceMethodref_info解析

    解析接口方法需先解析其所属类或接口(设为C)的符号引用,查找name_index和descriptor_index都匹配的方法

    • 若C是类,抛出IncompatibleClassChangeError
    • 先搜索C
    • 递归搜索父接口,直到Object类
    • 查找失败抛出NoSuchMethodError

    JDK9后接口方法新增静态私有方法和模块化,无访问权限时抛出IllegalAccessError

    初始化

    初始化阶段就是执行类构造器()方法的过程()由编译器根据代码顺序收集类变量的赋值static{}块中的语句组成

    static{}块只能访问在其之前定义的变量,定义在它之后的变量可在static{}中赋值,但是不能访问

    在这里插入图片描述
    ()不会显式地调用父类构造器,JVM会保证在子类的()执行前,父类的()已经执行,故父类的static{}块优先于子类变量赋值,如下打印2而不是1

    class Parent{
    	public static int A = 1;
    	static {
    		A = 2;
    	}
    }
    
    class Sub extends Parent{
    	public static int B = A;
    	
    	public static void main(String[] args) {
    		System.out.println(Sub.B);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    ()对于类或接口不是必需的,接口的()不需要先执行父接口的(),只有父接口中的变量被使用时才会被初始化,接口的实现类同理

    多线程同时初始化类时,只有一个线程去执行类的()方法,其他线程阻塞等待,直到其执行完,若()中有耗时操作可能造成进程阻塞

    public class Test {
        public static void main(String[] args) {
            Runnable script= new Runnable() {
    
                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    System.out.println(Thread.currentThread() + " start");
                    LoopClass loopClass =new LoopClass();
                    System.out.println(Thread.currentThread() + " run over");
                }
            };
            Thread thread1 = new Thread(script);
            Thread thread2 = new Thread(script);
            thread1.start();
            thread2.start();
        }
    }
    
    class LoopClass{
    	static {
            if(true) {
                System.out.println(Thread.currentThread() + " static{}");
                while(true) {
                	
                }
            }
        }
    }
    
    • 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

    如下为上面程序打印,说明线程2被线程1阻塞

    Thread[Thread-0,5,main] start
    Thread[Thread-1,5,main] start
    Thread[Thread-0,5,main] static{}
    
    • 1
    • 2
    • 3
  • 相关阅读:
    CrySiS勒索病毒最新变种来袭,加密后缀为kharma
    一种轻量分表方案-MyBatis拦截器分表实践
    2023.10.01-winxpsp3绿色安装jdk1.8
    OpenAI ChatGPT API 文档之 Embedding
    使用AI编写测试用例——详细教程
    快排&超详细,Leetcode排序数组题目带你升华掌握
    SpringBoot对象拷贝
    【Proteus仿真】【STM32单片机】智能饮水机
    YOLOX 学习笔记
    CentOS-7安装Docker并设置开机自启动docker镜像
  • 原文地址:https://blog.csdn.net/qq_35258036/article/details/127428492