虚拟机类加载机制
1.概述
虚拟机把描述的类从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机使用的java类型,这就是虚拟机的类加载,其中类型的加载连接和初始化都在程序运行中运行,为java提高了高度的灵活性,特性就是以来运行期动态加载和动态连接,例如OSGI.
2.类加载生命周期
类加载到虚拟机整个生命周期包括:加载(Loading),验证(Verification),准备(Preparation),解析(Resolution),初始化(Initialization),使用(Using),卸载(Unloading),其中验证准备解析统称为连接(Linking).虚拟机为“初始化”进行了规范,指定了在规定时期遵守
1.遇到new,get static,putstatic或invoke static字节码指令,如果类没有进行过初始化,则需要触发初始化,指定场景是用new创建实例对象或者设置一个类的静态字段以及调用一个静态方法
2.使用java.lang.reflect方法对类进行反射时,如果类没有初始化,则需要触发初始化.
3.首先触发父类初始化
4.主类首先初始化
5.java.lang.invoke.MethodHandle实例初始化
3.类加载过程
3.1加载
类加载阶段需要完成以下事情:
1.通过一个类的全限定名来获取定义此类的二进制字节流
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3.在内存中生成一个代表类的java.lang.Class对象,作为方法区的数据访问入口
机制使虚拟机的灵活度变大,成为广大应用的基础,如zip包,动态代理技术
对于非数组类的加载阶段可以使用引导类加载器或者用户自定义的加载器,但在一个数组类加载器需要遵循以下原则建立
1.数组组件类型(Component Type)是引用类型需要递归加载组建类型,数组将在加载该类组建类型的类加载器的类名称被标示.
2.数组组件类型不是引用类型,jvm会把数组标记与引导类加载器关联
3.数组类的可见性和组建类型一致,如果组建类型不是引用类型,数组类的默认类型为public‘
在加载完成后在内存中实例化一个java.lang.Class类的对象,加载阶段和连接阶段部分是交叉进行的.
3.2验证
验证确保Class文件的字节流中包含的信息符合虚拟机的要求,并且不会危害虚拟机的安全,在对验证保证约束的情况下,在不符合约束情况下,虚拟机会抛出java.lang.VerifyError异常,在类验证中大致完成了4个检验动作
3.2.1文件格式验证
验证class文件格式的规范,并且能够被当前虚拟机处理,处理的目的是保证输入的字节流能正确的解析并存储与方法之内,这阶段的验证基于二进制字节流进行,只有通过了这个验证,字节流才会进入内存的方法区进行存储,而后面3个阶段是基于方法区的存储结构进行,不会操作字节流
3.2.2元数据验证
对字节码描述的信息进行分析,以保证其描述的信息符合语言规范,主要验证是否有父类(export Object),该父类是否继承了不允许被继承的类,如果不是抽象类,是否实现其父类或接口要求实现的方法.
3.2.3字节码验证
通过数据流和控制流分析,确保程序语义合法符合逻辑,保护虚拟机,
3.2.4符号引用验证
虚拟机将符号引用转化为直接引用,这个转化动作在解析阶段发生,符号引用验证可以被看作是对类自身以外信息匹配校验,需要检验以下内容
1.是否通过全限定名找到对应的类
2.指定类是否存在复合符号方法的字段的字段描述
3.符号引用的类,字段,方法访问性是否可以被当前类访问
在验证阶段不是一个必要的阶段,可以通过-Xverify:none参数来关闭类加载措施,以缩减类加载的时间
3.3准备
准备阶段是正式为类变量分配内存并设置类变量初始化阶段,这些变量所使用的内存都在方法区中分配,在内存所分配的仅包含类变量(static修饰的变量),而不是实例变量.实例变量将在对象实例化随着对象一起分配在java堆,这里的初始化值通常情况下指的是数据类型的"零值",假设一个类变量定义为
public static int value=123
数据类型 | 零值 | 零值 | 数据类型 |
int | 0 | false | boolean |
long | 0L | 0.0f | float |
short | (short)0 | 0.0d | double |
char | '\u0000' | null | reference |
byte | (byte)0 |
在准备阶段过后的初始值为0,因为还没执行任何java方法,在putstatic指令被程序编译后存储于类构造器<client>()方法,所以value赋值为123的动作在初始化才会执行,以下包含了数据类型的零值
3.4解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用(Symbolic References)指引用以一组符号来描述所引用的目标,符号可以适合字面的字面量,引用的目标不一定加载到内存中,但是接受的符号引用必须一致,因为符号引用的字面量形式定义在虚拟机规范的Class文件格式中
直接引用(Direct References)直接引用可以是指向目标的指针,相对偏移定位到目标的句柄,直接引用于虚拟机的内存布局有关,且必须在内存中存在.
对同一个符号引用进行多次解析请求属于常见的事情,除了invokedynamic指令,虚拟机实现可以对第一次解析的结果进行缓存.
对于invokedymadic指令,目的用于动态语言的支持,所对应的引用称为"动态调用点限定符",动态指的是必须等到程序实际运行到这条指令,解析动作才能进行,在针对不同语义的解析中,对特定的动作做出指定的解析
3.4.1类或接口解析
如果类不是数组类型,虚拟机会把全限定名传递给d的类加载c,过程中经过元数据验证字节码验证,会触发加载其他类的操作
如果类是数组类型,并且元素类型为对象,会加载类似java.lang.Integer数组类型
3.4.2字段解析
对于未被解析的字符段符号引用,对字段表中的class_index项中索引的CONSTANT_CLASS_INFO进行解析,也就是符号引用.
3.4.3类方法引用
需要对class_index项中索引进行符号引用,如果成功,以后续方法进行类方法搜索,如果发现class_index索引是个接口,抛出java.lang.IncompatibleClassChangeError异常,如果是抽象类,抛出java.lang.AbstractMethodError异常,
3.4.4接口方法解析
class_index项中索引进行分类接口搜索
3.5初始化
初始化过程通过执行类构造器<client>()执行过程:由编译器自动收集类中的所有变量的赋值操作和静态语句块,中的语句合并产生,编译器收集的顺序是由语句在原文件中出现的顺序决定。
<client>()方法与类的构造函数不同,不需要显式调用父类构造器,虚拟机保证在子类<client>()在执行之前,父类构造器执行完毕,因此虚拟机第一个执行的<client>()方法的类是java.lang.Object
在父类<client>()执行完毕时,父类定义的静态字段要优于子类字段.
<client>()方法对于类和接口并不是必需的,如果类中没有静态语句块,也没有变量赋值操作,那么编译器不会为类生成<client>()方法
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口和类一样会生成<client>()方法,但接口与类不同的是,执行接口的<client>()方法不需要先执行父接口的<client>()方法,只有当父接口中定义变量使用时,父接口才会初始化,借口的实现类在初始化时一样不会执行接口的<client>()方法。
4.类加载器
类加载器最初被Java Applet的需求开发出来,通过获取一个类的全限定名来描述此类的二进制字节流,
4.1类与类加载器
类加载器只用于实现类的加载操作,对于任意一个类,都需要由加载她的类加载器和类本身一同确立java虚拟机的唯一性,每一个类加载器,都有独立的类加载空间,比较两个类相等,需要两个类在同一个类加载器中,相等指的是class对象返回的结果,包括instanceof关键字做对象所属的关系判断,
1 package com.jvm.demo; 2 3 import java.io.IOException; 4 import java.io.InputStream; 5 6 public class ClassLoaderTest { 7 public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException { 8 ClassLoader myLoader =new ClassLoader(){ 9 @Override 10 public Class<?> loadClass(String name){ 11 try { 12 String fileName = name.substring(name.lastIndexOf(".")+1)+".class"; 13 14 InputStream is =getClass().getResourceAsStream(fileName); 15 // 16 if (is == null) { 17 return super.loadClass(name); 18 19 } 20 byte[] b =new byte[is.available()]; 21 is.read(b); 22 return defineClass(name,b,0,b.length); 23 } catch (ClassNotFoundException e) { 24 // TODO Auto-generated catch block 25 e.printStackTrace(); 26 } catch (ClassFormatError e) { 27 // TODO Auto-generated catch block 28 e.printStackTrace(); 29 } catch (IOException e) { 30 // TODO Auto-generated catch block 31 e.printStackTrace(); 32 } 33 return null; 34 } 35 }; 36 37 Object obj = myLoader.loadClass("com.jvm.demo.ClassLoaderTest").newInstance(); 38 39 System.out.println(obj.getClass()); 40 System.out.println(obj instanceof com.jvm.demo.ClassLoaderTest ); 41 } 42 }
运行结果为不相等
4.2双亲委派模型
在jvm中,只存在两种不同的类加载器:启动类加载器(bootstrap ClassLoader),这个类加载器由C++实现,是虚拟机的一部分,另外一个是其他类加载器,这些类加载器全部由java实现,并且继承java.lang.ClassLoader
bootstrap ClassLoader 负责将存放在<java_home>\lib目录中的,或者-Xbootclasspath参数所放的路径,并且是虚拟机识别的类库加载到虚拟机内存中,,启动类加载器无法被程序直接引用,如果用户在编写自定义加载器时,需要记载请求委派给引导类加载器。
Extension ClassLoader,由sum.misc.Launcher$ExtClassLoader实现,负责加载<java_home>\lib\ext目录.
Application ClassLoader: 这个类加载器时ClassLoader中的GetSystemClassLoader()方法中的返回值,也被称为系统类加载器,负责加载用户类路径上指定的类库。
在双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器应当有自己的父类加载器,这里的加载器不会以继承的关系实现,而是使用组合的关系来复用父类加载器,典型的例子是object。
以上是对类加载机制的总结......glhf