ref: https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/jvm/类加载器.md
-
类加载过程步骤
-
什么时候在加载验证、准备之后需要初始化:
- 遇到new、getstatic、putstatic或invokestatic字节码指令;
即遇到new一个对象、**读取或设置【并不是定义就会初始化】**一个类的静态字段的时候(被final修饰的除外->会被放入常量池中); - 对类进行反射调用的时候(java.lang.refect);
- 先出发父类的初始化;(接口不会初始化父类,用到时才初始化)
- 虚拟机启动时初始化main所在的主类;
- 当使用HDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法的句柄所对应的类没有进行过初始化,则需要先触发其初始化;
- 遇到new、getstatic、putstatic或invokestatic字节码指令;
-
加载
- 加载分为三部:
- 通过全类名获取定义此类的二进制字节流;
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构;
- 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口;
- 程序员可以通过各种方式对字节流进行读取:
- zip、网络、数据库。。。
- 相对于类加载过程的其他阶段,一个非数组类的加载阶段(准确地说,是加载阶段中获取类的二进制字节流的动作)是开发人员可控性最强的,因为加载阶段既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式(即重写一个类加载器的loadClass()方法)。
- 加载分为三部:
-
验证
- 文件格式验证: 是否符合class文件格式的规范.以保证可以正确的使数据存储在方法区之类;
- 元数据验证: 对类的元数据信息进行语义校验,如检验是否其父类继承了不允许被继承的类;
- 字节码验证: 通过对数据流和控制流的分析,确保程序语义时合法的,如保证跳转指令不会跳到方法体之外的字节码之中;
- 在符号引用转化为直接引用时(解析阶段)进行检验,确保解析动作能正常进行;
-
准备
- 为类变量分配内存并设置初始值;
- 内存分配只有类变量(static)_,实例变量会在对象初始化的时候和对象一起分配在堆上;
- 本阶段的初始值均为0,并不会赋予实际的值(未执行java方法),但final修饰的static变量会;
-
解析
- 将常量池中分符号引用替换为直接引用;
- 符号引用: 以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中(就是用来表明程序中有这个东西…)。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。
- 直接引用: 与虚拟机的内存布局相关
(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
(3)一个能间接定位到目标的句柄
常量池分为两种,静态常量池和运行时常量池:
静态常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:
1. 类和接口的全限定名
2. 字段名称和描述符
3. 方法名称和描述符
运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。 -
初始化
- 初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行类构造器 clinit ()方法的过程。
- clinit()方法: 编译器会自动收集类中所有类变量的赋值动作和静态语句块,把他们合到一起;
- 静态语句块中只能访问定义在静态语句块之前的内容;
- 虚拟机会保证子类执行clinit()的时候,父类的clinit都已经执行过了;
- 对于clinit() 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为clinit() 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起死锁,并且这种死锁很难被发现。
- 类加载器
- 类加载器和类本身一同确定其在虚拟机中的唯一性,每一个类加载器,都有一个独立的名称空间–>比较两个类是否"相等",只有在这个类是由同一个加载器加载的前提下才有意义;
- 分类:
- 启动类加载器:将\lib目录下或别指定的路径中的被虚拟机识别的类库加载到虚拟机之中;
- 扩展类加载器:加载<JAVA_HOME>\lib\ext下或别指定的路径中的类库,开发者可以使用;
- 引用程序加载器(系统类加载器):加载用户路径中的类库,开发者可以直接使用;
- 双亲委派模型:
- 好处: 双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。