深入理解Java虚拟机JVM高级特性与最佳实践阅读总结—— 第七章 虚拟机类加载机制

时间:2022-12-27 15:19:27
类加载机制,将描述类的class文件加载到内存,并对数据进行校验、转换接卸、初始化,最终形成能被虚拟机直接使用的Java数据类型 Java中,类型的加载、连接、初始化都是在程序运行期间完成,动态加载和动态连接也是Java动态扩展的实现
类加载时机 一个类从加载进内存到从内存卸载,生命周期包括: 1、加载:加载时机虚拟机确定 2、验证 3、准备 4、解析 5、初始化,遇到如下5中情况时,必须立即初始化,即主动引用;其他情况不会触发初始化,即被动引用           1、遇到new(创建对象)、getstatic(获取静态字段)、putstatic(设置静态字段)、invokestatic(调用静态方法)时,这里静态字段不包括编译器放入常量池的被final修饰的静态字段           2、使用Java.lang.reflect包方法对类进行反射调用的时候           3、初始化一个类时,父类没有初始化的,先初始化父类           4、虚拟机启动时,用户需要指定个执行主类(包含main),虚拟机先初始化该主类           5、使用1.7的动态语言支持。如果个java.lang.invoke.MethodHandle解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且该类对应的类没有初始化,触发该类的初始化 初始化注意点:           1、对于静态字段,只有直接定义该字段的类才会被初始化,因此通过子类引用父类的静态字段,只会触发父类的初始化而不触发子类的,子类是否需要加载、验证取决于虚拟机实现           2、通过数组定义引用类,不会触发类的初始化           3、常量在编译期间会存入调用类的常量池,本质上没有直接引用到定义常量的类,因此不会触发定义类的初始化           4、对于接口的初始化,和类的初始化类似,但对于第三点,接口在初始化时,不要求父类全部完成初始化,只用在使用到父类接口时才会初始化 6、使用 7、卸载 其中2-4合称为连接;加载、验证、准备、初始化、卸载过程是确定的,解析过程为了支持运行时绑定(晚期绑定、动态绑定),也可以在初始化之后开始
类加载过程 1、加载阶段,对于非数组类,既可以使用系统的类加载器完成,也可以自定义类加载器,主要完成三件事           1、通过类的全限定名获取定义类的二进制字节流           2、将字节流中静态存储结构转换为方法区的运行时数据结构           3、在内存中(方法区)生成该类的Java.lang.class对象,作为方法区这个类的各种数据的访问入口 数组类的加载,本身不通过类加载器创建,由虚拟机直接创建,但是数组的元素类型由类加载器创建,数组类型的创建规则如下:           1、数组的组件类型(去掉一个维度的类型)是引用类型,递归采用上述类加载过程加载组件,数组C将在加载该组件类型的类加载器的类名称空间上被标识(类的唯一性由类和类加载器一起决定)           2、如果组件类型不是引用类型,则与引导类加载器关联           3、数组类的可见性与他的组件类型一致,如果组件类型不是引用类型,则可见性默认是public 2、验证阶段,确保class文件的字节流中包含的信息符合当前虚拟机的要求,且不会危害虚拟机的安全,主要完成四个阶段的验证           1、文件格式验证,检查字节流是否符合class文件规范,并能被当前虚拟机理解,保证输入的字节流能正确的解析并存储于方法区内,格式上符合class类型信息的要求,仅该阶段在二进制字节流上操作           2、元数据验证,对字节流的元数据进行语义分析,保证描述的信息符合Java语言规范           3、字节码验证,通过数据流和控制流分析,确定语义合法,对方法体进行验证,保证类的方法在运行时不会危害虚拟机           4、符号引用验证,发生在虚拟机将符号引用转换(解析阶段发生)成直接引用阶段,即常量池各种符号引用的匹配性校验 3、准备阶段,即为类分配内存并设置初始值的阶段,注意这里使用的内存空间为方法区空间,且仅包括static修饰的变量(实例变量在实例化的时候分配),初始值值零值,而如果是final static类型,即常量,则初始值为设置的值 4、解析,将常量池的符号引用转为直接引用           1、符号引用:一组符号描述引用的目标,被引用的对象不一定加载进内存,与内存布局无关,不同虚拟机可接收的符号引用是相同的           2、直接引用:直接指向目标的指针、相对偏移量或者能间接定位到目标的句柄,被引用对象必定加载进内存,与内存布局有关,不同虚拟机的直接引用一般不同           3、解析时间不确定           4、可以对同一个符号引用进行多次解析请求,除invokedynamic指令,但虚拟机可以对第一次解析结果进行缓存(运行时常量池记录直接引用并标记为已解析)从而避免多次解析,并且同一实体中之前成功过,后续也成功,否则都收到异常,invokedynamic用来支持动态语言调用的,对应的引用称为动态调用点限定符(dynamic call site specifier),即到程序实际运行该指令时才会解析           5、解析动作主要针对类或者接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符 5、初始化,即按照实际需要赋初值,即执行类构造器<cinit>()方法的过程           1、<cinit>()方法是虚拟机收集的所有类变量和静态语句块(static{ })中的语句合并而成的,顺序由源文件出现是顺序决定,注意静态语句块只能访问定义在静态语句块之前的变量,对于定义在后后面的变量,前面的静态语句块中可以赋值,但是不能访问           2、<cinit>()和类构造器(<init>())不同,不需要显式调用父类构造器,虚拟机保证父类<cinit>()先于子类的<cinit>()执行  ->  虚拟机中第一个被执行的<cinit>(),肯定是java.lang.Object           3、由于父类的<cinit>()方法先执行,那么父类定义的静态语句块优先于子类的变量的赋值操作           4、<cinit>()对于类和接口不是必须的,如果类中没有静态语句块,也没有赋值操作,则不生成<cinit>()           5、接口不能使用静态语句块,但可以赋值操作,即接口也可以有<cinit>(),但是执行接口的<cinit>()不一定要先执行父接口的<cinit>(),接口的实现类在初始化时也不会执行接口的<cinit>()           6、<cinit>()方法的执行时同步的,有可能因为某个<cinit>()耗时很长而造成进程阻塞,注意,其他线程虽然会被阻塞,但是当执行<cinit>()的线程退出<cinit>()后,其他线程被唤醒也不会再次进入<cinit>(),因为用一个类加载器下,同一个类只会被初始化一次
类加载器,实现类加载工作,类与加载该类的加载器一起,确定类在虚拟机中的唯一性,每个类加载器都有自己的类名称空间,一般有三种,从上到下依次为:           1、引导类加载器(BootStrap ClassLoader),负责<java_home>\lib下类库的加载,引导类加载器无法被程序直接引用           2、扩展类加载器(Extension ClassLoader),主要加载<java_home>\lib\ext目录下的类库,可以直接使用           3、应用程序类加载器(Application ClassLoader),即系统类加载器,负责classpath路径上指定的类库,如果程序没有自定义类加载器,就是程序中默认的类加载器           4、自定义类加载器
类加载器的双亲委派模型,除引导类加载器,其他加载器必须有父类加载器,这样的好处就是Java类随着它的类加载器具有了一种带优先级的层次关系,解决基础类的统一问题,其工作过程为           1、类加载器收到类加载请求,将请求委派给父类加载器,层层上传,最终达到引导类加载器           2、如果父类加载器无法加载,则子类加载器尝试加载
扩展,线程上下文类加载器(Thread Context ClassLoader),主要为了解决基础类调用用户代码的情形,如JNDI服务就需要使用上下文类加载器加载需要的SPI代码(父亲委托子类加载动作),该类加载器通过Thread类的setContextClassLoader()方法设置,未创建则从父类继承,全局范围内未设置则默认应用程序类加载器,Java中所有涉及SPI的加载动作基本采用线程上下文类加载器,如JDBC