类从虚拟机内存加载到从内存卸载,经历的生命周期是:加载,验证,准备,解析,初始化,使用,卸载这几个阶段, 其中验证,解析,初始化被称为 连接过程(Linking).
(打算这块和类加载原理后再看class文件结构那篇)
除了解析和使用,其他的过程基本顺序就是这样, 解析可以是在初始化完成之后,这是为了运行时动态绑定。
在虚拟机规范中定义了5中情况(有且只有)必须对类进行初始化(之前进行过,加载,验证,准备):
1.碰到new,getstatic,putstatic,invokestatic这4条字节码指令时,如果类没有进行初始化时,
2.在使用反射时,如果类没有初始化。
3.在这个类的父类没有初始化时。
4.虚拟机启动时指定的Main类,即主类。
5.使用动态语言支持时,特定的方法句柄对应的类没有初始化时(REF_getStatic,REF_putStatic,REF_invokeStatic)
类的加载:
1.获取定义一个类的二进制流,二进制流可以是从网络获取,zip包,jar包都可以,也出现在jsp,动态代理(java.lang.reflect.Proxy)等。
2.将二进制流代表的静态存储结构转化为方法区运行时数据结构。
3.内存中生成代表这个类的java.lang.class对象,作为方法区这个类的各种数据访问入口。
非数组类是开发者可控性最强的,它可以有系统提供的classLoader加载,也可以有用户自定义的classLoader加载,而数组类是由虚拟机直接创建,但数组类的类元还是由classLoader来加载。数组类的创建过程遵循一下原则:
1.如果数组的组件类型(类似与 Foo[] fooArray)为引用类型,数组将在该组件类型的类加载器的类名称空间上被标识。
2.如果数组的组件类型不是引用类型(如 int[]),java虚拟机将会把数组标记为与引导类加载器关联。
3.数组类的可见性与它的组件类型的可见性一致
加载完成后,在内存(并没有明确规定是在java堆中,class类对象比较特殊,在HotSpot虚拟机中这块内存指的是方法区内存)中实例化类的java.lang.class对象,这个对象作为程序访问方法区数据类型的外部接口,加载与连接过程是交叉进行的(因为连接过程中包含验证过程),其他连接过程与加载过程依然保持顺序执行。
验证:
java 的class文件并不一定是由java源码生成,它可以由十六进制编译器直接编写来产生,但java虚拟机不会去访问数组边界以外的数据,也不会将对象转换为没有实现的类,执行不存在的代码之类的事情。但这些都需要经过虚拟机的验证过程。也是防止虚拟机遭受恶意代码的攻击,如果要验证的流没有经过class文件格式的规范则抛出java.lang.VerifyError或其子类异常。详细的参考java虚拟机规范 . 大致的验证有以下4个阶段检验动作:
1.文件格式验证:
这个阶段验证字节码是否符合规范及该虚拟是否能处理,包括一下验证点:
是否以魔数0xCAFEBABEK开头。
主,次版本是否在当前虚拟机处理范围内。
常量池是否含有不被支持的常量类型等。
远不止这些,不过这个验证通过后字节码才能进入内存的方法区并转换为运行时的数据结构。
2.元数据验证:
主要是检测元数据是否符合java规范如:
这个类是否有父类。
类是否继承了不允许继承的类。
是否实现了接口的所有方法等。
3.字节码验证(应该可以理解为方法的合法性验证):
整个验证过程中最复杂的一步,主要是通过数据流和控制流分析,程序语法的合法性,确保符合逻辑的,在第二阶段对元数据验证结束后,这个阶段主要是对类的方法体进行验证,保证运行时不会危害虚拟机 ,如:
1.保证操作数栈的数据类型与指令代码序列配合工作且不会出现栈中是int类型,使用时却以long的方式载入到本地变量表,
2.保证跳转指令不会跳转到方法体以外的字节码指令上。
3.保证类型转换是有效的。比如把子类对象赋值给父类对象这是安全的,但把父类对象赋值给子类的,甚至把对象赋值给与之不相干的类,前者是危险的后者是不合法的。
但这个过程之后也并不能完全保证它是安全的。 (关键字:StackMapTable, -XX:-UseSpliteVerifier, -XX:+FailOverToOldVerifier 以后用到的时候在看。先记下来:) )
4.符号引用验证(类,方法,字段是否可解析的验证):
目的是确保解析动作能正常运行,如果无法通过符号验证会抛出,java.lang.incompatibleClassChangeError的子类,如:java.lang.IllegalAccessError,java.lang.NoSuchFieldError,java.lang.NoSuchmethodError等,如果所运行的代码已经被反复使用和验证过,可以考虑使用-Xverify:none参数关闭大部分类验证,以缩短类加载时间。
通常需要校验的内容:
1.符号引用中通过字符串描述的全限定名是否能找到对应的类。
2.在指定的类中是否存在符合方法的字段描述符以及简单名称所描述的方法字段(应该是能否根据字段描述找到方法这个意思)。
3.符号引用中的类,字段,方法是否可被当前类访问(修饰符修饰的访问可能行)。
准备:
准备阶段正式为类的变量分配内存,这里的内存指的是方法区,变量是静态变量,非静态变量和类对象实例都是在java堆中分配内存,如:
static int value=123
在准备阶段value的值为0,这个时候方法还没有执行,只有经历初始化阶段value的值才会是123(putstatic是编译后存放于类构造器<clinit>()方法中),基本数据类型为 数字的都是0, 0f,0l,0d等,char为\u0000,String 不是基本类型,reference是null, 如果以上int 被final修饰过,则准备阶段虚拟机会根据ConstantValue的设置将value设置为123
解析:
解析阶段是把符号引用转换为直接引用的过程。
符号引用:
以一组符号来描述所引用目标(java类并不知道引用类的实际内存地址,因此只能使用符号引用来代替),与虚拟机的内存布局无关,引用的目标不一定已经加载到内存,各种虚拟机实现的内存布局可以不相同,但是他们能接受的符号引用必须相同,符号引用的字面量形式明确定义在java虚拟机规范class文件中。
直接引用:
直接引用可以是指向目标的指针,偏移量,或是能间接定位的目标句柄。与内存布局相关,能直接引用说明对象已经存在与内存中,不同的虚拟机实例上翻译出来的直接引用可能不一样。
虚拟机根据需要判断,是在类加载时对常量池中符号做解析 ,解析字段,方法,接口的过程中会去class_index中索引所属的类 ,并解析类符号,在此过程中如果出错,都会导致以上解析无法继续进行。
1.类或接口解析:
如果代码所处的类为A, 要将为解析过的符号引用N解析为一个类或接口C的直接引用(也就是类A 里有类C的引用)。需要一下步骤。
1.如果这个类C不是数组类型,那虚拟机会把符号引用的全限名传递给类D加载器去加载,可能会触发其他相关类的加载动作,如:该类的父类或实现接口。如果加载过程中出现任何异常,解析会失败。
2.如果C是数组,且元素为对象类型,则会去加载该对象,之后虚拟机生成一个代表此数组纬度和元素的数组对象。
3.上面的步骤完成后要进行符号引用验证,确定D是否有对C的访问全限,如果不具有则抛出java.lang.illgalAcessError。
2.字段解析:
解析一个为被解析过的字段引用,首先会对字段表内class_index中索引的CONSTANT_CLASS_info符号引用做解析,字段所属的类或接口符号引用的过程出现异常,会导致字段符号引用的失败。若成功则有以下步骤:
1.如果一个类本身包含了简单名称和字段描述都与目标字段匹配,则返回这个字段引用,查找结束。
2.否则去查找C是否实现了接口,如果是会往上递归查询,如果存在名称和字段描述都匹配,返回引用查找结束。
3.否则去查找父类当中是否存在,如果存在名称和字段描述都匹配,返回引用查找结束。
4.如果父类,接口都没有查到,抛出java.lang.NoSuchFieldError异常。
若找到了但是没有访问全限,则抛出java.lang.IllegalAccessError.实际上可能会更严格,若字段在父类和接口中多次出现,可能会拒绝编译。
3.方法解析;
方法解析中, 类方法解析和接口方法解析是有区别的,在常量池中这俩符号引用不同。
1.一旦一个类被解析为类而非接口,在类方法表中如果这个方法所属的类是接口,那么直接会抛出java.lang.incompatibleClassChangeError,换句话说是尝试引用一个纯接口而非实现类的方法时,会抛出此异常。
2.如果简单名称与描述符号都与目标匹配的方法,则返回这个方法的引用,查找结束。
3.如果第二部没有找到会往上递归去父类中寻找。
4.如果父类中未找到,则会去实现的接口或接口的父类中去查找,如果找到则会抛出,java.lang.AbstractMethodError,说明该类为抽象类。
5.如果以上都没有找到,则会抛出java.lang.NoSuchMethodError。
查找成功会去做权限验证,如果无访问全限则报:java.lang.IllegalAccessError异常
4:接口解析:
1.第一条与方法解析相反,查找是在接口方发表中查询, 如果这个方法包含的接口是类,则会报java.lang.incompatibleClassChangeError。
2.之后去判断简单名称和描述符号都与目标匹配,如果查到了就返回这个方法的直接引用。
3.否则一直会递归到object类,如果找到则返回直接引用,如果没找到则报java.lang.NoSuchMethodError异常。
在成功的情况下跟类方法解析成功一样都会去做全限检查
初始化:
首先会执行<cinit>方法,cinit方法并不是一定产生 只有当类中有static和变量初始赋值时产生的,在子方法执行<cinit>之前保证执行完父类的<cinit>方法,接口中虽然没有静态变量块但是,仍然会产生<cinit>方法,接口执行cinit方法前不需要执行父类的cinit方法,接口的实现类初始化也不会执行接口的cinit方法,在多线程环境中,一个类的cinit执行会阻塞其他类执行这个类的cinit方法,而且只执行一次。
到此类的加载过程结束,这个时候的类才能真正去使用。