虚拟机类加载机制
1、类加载时机
类生命周期
加载(loading)、验证(Verification)、准备(preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)
其中,验证、准备、解析统称为链接(linking)
加载时机
1、遇到new、getstatic、pustatic、invokestatic这4个字节码指令,常见场景:new实例、读取或设置一个类的静态字段、以及调用一个类的静态方法;
2、使用java.lang.reflect包的方法对类进行反射调用;
3、初始化一个类,如果其父类还没有初始化时;
4、虚拟机启动,用户需要指定一个要执行的主类(包含main()方法的类);
5、使用JDK1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果时REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个句柄所对应的类没有进行过初始化时,需要先触发其初始化.
被动引用
上述情况被称为主动引用,除此之外,所有引用类的方式都不会触发初始化称为被动引用。
书中列举三种情况:
1、通过子类引用父类的静态字段,不会导致子类初始化;
2、通过数组定义来引用类,不会触发此类的初始化;
3、常量在编译阶段会存入调用类的常量池,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
2、类加载过程
加载
流程,完成三件事:
- 过类的全限定名获取定义此类的二进制字节流(多种方式:ZIP包、网络、动态代理、文件、数据库)
- 这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 内存中生成代表这个类的java.lang.Class对象,作为方法去这个类的各种数据的访问入口
验证
1、文件格式验证:
- 是否以魔数0xCAFEBABE开头
- 主次版本号是否在当前虚拟机处理范围之内
- 常量池种的常量中是否有不被支持的常量类型(检查常量tag标志)
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
- CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据
- Class文件中各部分机文件本身是否有被删除或附加的其他信息
......
2、元数据验证
- 这个类是否有父类
- 父类是否继承了不允许被继承的类(被final修饰的类)
- 如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法
- 类中的字段、方法、是否与父类产生矛盾(如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一样,返回值类型却不同等)
......
3、字节码验证
对类的方法体进行校验分析,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。如:
- 保证任意时刻操作栈的数据类型与指令代码序列都能配合工作,例如不会出现这样一种情况:在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表
- 保证跳转指令不会跳到方法体以外的字节指令码上
- 保证方法体中的类型转换是有效的,例如子类赋值给父类数据类型
......
4、符号引用验证
虚拟机将符号引用转化为直接引用时,对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验:
- 符号引用中通过字符串描述的全限定名是否能找到对应的类
- 指定的类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
- 符号引用中的类、字段、方法的访问性(private、public、protected、default)是否可以被当前类访问
......
准备
正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区进行分配。仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。其次,都是初始化数据类型的零值。
解析
解析是虚拟机将常量池内的符号引用替换为直接引用的过程。
解析主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。
- 符号引用:任何形式字面量,引用的目标不一定已加载到内存中
- 直接引用:直接指向目标的指针、相对偏移量或一个能直接定位到目标的句柄,指向已加载到内存中的目标
初始化
通过程序指定的主观计划去初始化变量和其他资源,从另一个角度讲:初始化是执行类构造器<clinit>()方法的过程
- <clinit>()是编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的
- 与构造函数不同,不需要显示地调用父类构造器,虚拟机保证子类地<clinit>()执行前父类的<clinit>()已执行完
- 由于父类的<clinit>()先执行,意味着父类中定义的静态语句块要优先于子类的变量赋值操作
- <clinit>()非必需,如果没有静态语句块,就没有对变量的赋值操作,那么可以不为这个类生成<clinit>()方法
- 接口不能使用静态语句块,但仍然有变量初始化的赋值操作,一样会生成<clinit>()方法
- 虚拟机保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,可能会造成进程阻塞,往往很隐蔽。
3、类加载器
虚拟机外部实现类加载阶段地“通过一个类的全限定名来获取描述此类的二进制字节流”的代码模块,称为类加载器。
类与类加载器
判断两个类是否“相等”,只有在这两个类由同一个类加载器加载的前提下才有意义。这里的相等包括equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,及instanceof关键字做对象所属关系判断等情况。
双亲委派模型
类加载器(前三种为系统提供的类加载器):
- 启动类加载器(Bootstrap ClassLoader),虚拟机唯一识别的类加载器,加载<JAVA_HOME>\lib文件夹中的类,或被-Xbootclasspath指定的路径中的类
- 扩展类加载器(Extension ClassLoader),加载<JAVA_HOME>\lib\ext中,或被系统变量java.ext.dirs指定的路径中的类
- 应用程序加载器(Application ClassLoader),由ClassLoader的getSystemClassLoader()方法的返回值,亦称为系统类加载器,负责加载用户类路径(ClassPath)上指定的类库,如果程序没有自定义类加载器,一般是程序默认的类加载器
- 自定义类加载器
双亲委派模型
类加载器收到类加载请求,先委派给父类加载器完成,层层网上,如果顶层父类加载器无法加载,子类再尝试加载。保证如java.lang.Object等基础类唯一性,使java类与类加载器一起具备带有优先级的层次关系。
破坏双亲委派模型
- 双亲委派模型出现之初,为了向下兼容,JDK1.2再java.lang.ClassLoader提供findClass()方法重写类加载逻辑
- 由于模型自身缺陷,基础类并不总是作为被调用的API,也有可能调回用户代码(如JNDI),引用线程上下文类加载器,父类请求子类加载器去完成加载动作
- 用户对程序动态性的追求(OSGI),每一个模块都有自己的类加载器,在平级的类中加载类