* 被动引用类字段演示三:

 * 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

 * 结果仅仅输出 hello world ,而不会初始化ConstClass1 init! 静态代码块;

 * 分析:因为虽然在java源码中引用了ConstClass1类中的常量HELLOWORLD,但是在编译阶段将此常量的值存储到了NotInitialization1类的常量池中

 * 实际上最后引用的是自己的常量池中的常量,并不会引入ConstClass1的加载。

 */

class ConstClass1{

    static{

        System.out.println(\"ConstClass1 init!\");

    }

    public static final String HELLOWORLD = \"hello world\";

}

public class NotInitialization1 {

    public static void main(String[] args) {

        System.out.println(ConstClass1.HELLOWORLD);

    }

}

 

4.接口的初始化:接口在初始化时,并不要求其父接口全部完成类初始化,只有在正使用到父接口的时候(如引用接口中定义的常量)才会初始化。(接口中没有static{ } 语句块,但编译器任然会为接口生成<clinit>()类构造器,用于初始化接口中所定义的成员变量)

接口和类的真正区别是接口是只有在正使用到父接口的时候(如引用接口中定义的常量)才会初始化

 

 

非数组类的加载可以通过系统提供的引导类加载器完成,也可由用户自定义类加载器完成,可以通过定义的类加载器去控制字节流的获取方式(即重写一个类加载器的loadClass()方法)

数组类本身不通过类加载器创建,由JVM直接创建的

数组类的创建过程遵循以下规则: 
  1.如果数组的组件类型(指的是数组去掉一个维度的类型)是引用类型,那就递归采用上面的加载过程去加载这个组件类型,数组c将加载该组件类型的类加载器的类名称空间上被标识(一个类必须和类加载器一起确定唯一性)
  2.如果数组的组件类型不是引用类型(如int[]),Java虚拟机将会把数组c标识为与引导类加载器关联 
  3.数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public 
 

 

类加载过程详解:

类的加载指将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构

类加载的过程:类从被加载到虚拟机内存中开始,到卸载出内存为止的过程:

整个生命周期包括:加载、链接、初始化。

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。

 

以下内容以HotSpot为基准。

1.加载(Loading):生成Class对象

获取类的二进制流,转为方法区数据结构,在Java堆中生成对应的java.lang.Class对象。【找到.class文件并把这个文件包含的字节码加载到内存中】,对于数组类来说,并没有对应的字节流,而是由JVM直接生成的。

在加载阶段(可参考java.lang.ClassLoader的loadClass()方法),虚拟机需要完成以下3件事情:

1、   通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可从其他渠道,如:网络(URLClassLoader)、动态生成、数据库等);

2、   将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;

3、  在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;

对象肯定是存放在堆中的,但Class对象比较特殊,对于HotSpot虚拟机而言,Class对象是存放在方法区中的。

 

Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口(反射)

Class类无参构造函数为private。且只有java虚拟机才可以创建class类

 

链接:获取二进制字节流的方式?

1)从zip包中读取,最终成为日后JAR、EAR、WAR格式的基础 
  2)从网络中获取,典型的应用就是  
  3)运行时计算生成,常见的是动态代理计技术。在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流
  4)由其他文件生成,典型场景是JSP应用 。即由Jsp生成class类
  5)从数据库中读取,这种场景相对少一些(中间件服务器)

 

加载和连接阶段( ing)的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。


2.连接( ing)

链接是指将创建成的类合并到JVM中,使之能执行的过程。

1)验证(Verification)

验证(安全的考虑,需要验证)为了保证class流的格式正确

为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

 

大致完成4个阶段的检验动作:文件格式验证,元数据验证,字节码验证,符号引用验证

文件格式验证:

文件格式验证:验证字节流是否符合Class文件格式的规范;

如:

         1.是否以魔数开头:0xCAFEBABE

         2.主、次版本号是否在当前虚拟机的处理范围中

         3.常量池的常量中是否有不被支持的常量类型(检验常量tag标志)

         4.指向常量中的各种索引值中是否有指向不存在的常量或不符合类型的常量

         5.CONSTANT_Utf8_infoz型的常量中是否有不符合UTF8编码的数据

         6.Class文件中各个部分以及文件本身是否有被删除的或附加的其他信息

         …(还有很多)

【这个验证都是基于二进制字节流进行验证,只有通过类这个阶段的验证后,字节流才会进入内存的方法区进行存储,后面的3个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流】

元数据(语义)验证:

元数据(语义)验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;

如:这个类是否有父类。Fianl类型的方法/类是否被覆盖,非抽象实现所有抽象方法。类中字段、方法是否与父类产生矛盾(如覆盖类父类的final字段,或出现不符合规则的方法重载,如方法参数都一致,但返回值类型却不同等) 

         1.是否有父类(除java.lang. 外)

         2.是否继承了不允许被继承的类(被final修饰的类)

         3.如果不是抽象类,是否实现了父类中所有的抽象方法和接口中的所有方法

         4.是否覆盖了父类的final字段或方法重载不符合规则

 

【主要目的是对类元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息】

字节码验证:

字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

如运行检查。栈数据类型和操作码数据参数吻合,跳转指令到正确合理的位置。保证方法体中的类型转换时有效的(避免子类new父类)【对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件】

         1.保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作(比如不会出现在操作数栈防了一个int型的数据却按long类型来加载到本地变量表中)

         2.保证跳转指令不会跳到方法体外的字节码指令上

         3.保证类型转化是有效的

         但即使进行了大量的分析也不能保证字节码就是安全的,涉及到了著名的停机问题

 

由于数据流校验的高复杂性,耗时较大,JDK1.6后,在Javac中引入一项优化方法(可以通过参数-XX:-UseSplitVerifier关闭这种优化):在方法体的Code属性的属性表中增加一项“StackMapTable”属性,该属性描述了方法体中所有基本块开始时本地变量表和操作栈应有的状态,在字节码验证阶段,就不需要根据程序推到这些状态的合法性,只需要检查StackMapTable属性中的记录是否合法即可。从而将字节码验证的类型推导转变为类型检查从而节省一些时间。

JDK1.7后,对于主版本号大于50的class文件,使用类型检查来完成数据分析校验是唯一选择,不允许再退回到类型推倒的校验方式。

 

注:理论上StackMapTable存在错误或被篡改的可能,有可能code属性被修改了,然后StackMapTable也被篡改,欺骗jvm的校验。

符号引用验证:

符号引用(二进制兼容)验证:确保解析动作能正确执行。如work类引用了cat对象并调用其run方法,需判断是否有run方法,没有则抛出NoSuchMehodError异常。如常量池中描述类是否存在,访问的方法或字段是否存在足够的权限

检验内容有:

1.符号引用中通过字符串描述的全限定名是否能找到对应的类;

2.在指定类中是否存在符号方法的字段描述及简单名称所描述的方法和字段;

3.符号引用中的类、字段和方法的访问性(private、protected、public、default)是否可被当前类访问。

 

验证阶段非常重要,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,可考虑用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。


2)准备(Preparation): 为静态变量分配内存并设置默认的初始值

正式为类变量分配内存并设置类变量初始值的阶段,这些变量所用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中(分配空间并进行默认值初始化)。【类变量在方法区。实例变量在堆区】

这里的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:

     
public static int value=123;

变量value在准备阶段过后的初始值为0而不是123.因为这时尚未开始执行任何java方法,把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作在初始化阶段才会执行。
至于“特殊情况”是指:

public static final int value=123,

当类字段的属性是ConstantValue时,会在准备阶段初始化为指定的值,标注为final后,value的值在准备阶段初始化为123而非0.

 

3).解析(Resolution): 将符号引用替换为直接引用

虚拟机将常量池内的符号引用(也就是字符串)替换为直接引用(指针或者地址偏移量)的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。


虚拟机规范中并为规定解析阶段发生的具体时间,只要求了再执行Java 虚拟机指令 anewarray、checkcast、getfield、getstatic、instanceof、nvokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield 和 putstatic 这16个用于操作符号的引用的字节码指令之前,先对他们所使用的符号引用进行解析。也就是执行上述任何一条指令都需要对它的符号引用的进行解析。所以:虚拟机实现可以根据需要来判断到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才取解析它。

 

对同一个符号引用进行多次解析请求是很常见的事情,虚拟机实现可以对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标识为已解析状态)从而避免解析动作重复进行。

对于invokedynamic指令,上面规则则不成立。当碰到某个前面已经由invokedynamic指令触发过解析的符号引用时,并不意味着这个解析结果对其他invokedynamic指令也同样生效。因为invokedynamic指令是JDK1.7新加入的指令,目的用于动态语言支持,它所对应的引用称为“动态调用点限定符”(Dynamic Call Site Specifier),这里“动态”的含义就是必须等到程序实际运行到这条指令的时候,解析动作才能进行。相对的,其余可触发解析的指令都是“静态”的,可以在刚刚完成加载阶段,还没有执行代码时就进行解析。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号进行引用,下面只对前4种引用的解析过程进行介绍,对于后面3种与JDK1.7新增的动态语言支持息息相关

 

 

类或接口的解析

        假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,虚拟机完成整个解析的过程需要以下3个步骤:

1.      如果C不是一个数组类型,虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。在加载过程中,由于元数据验证、字节码验证的需要,又可能触发其他相关类的加载动作,例如加载这个类的父类或实现的接口。一旦这个加载过程出现了任何异常,解析过程就宣告失败。

2.      如果C是一个数组类型,并且数组的元素类型为对象,也就是N的描述符会是类似“[Ljava/lang/Integer”的形式,那将会按照第1点的规则加载数组元素类型。如果N的描述符如前面所假设的形式,需要加载的元素类型就是“java.lang.Integer”,接着由虚拟机生成一个代表此数组维度和元素的数组对象。

3.      如果上面的步骤没有出现任何异常,那么C在虚拟机中实际上已经成为一个有效的类或接口了,但在解析完成之前还要进行符号引用验证,确认D是否具备对C的访问权限。如果发现不具备访问权限,将抛出java.lang.IllegalAccessError异常。

 

字段解析

        要解析一个未被解析过的字段符号引用,首先将会对字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析,也就是字段所属的类或接口的符号引用。如果在解析这个类或接口符号引用的过程中出现了任何异常,都会导致字段符号引用解析的失败。如果解析成功完成,那将这个字段所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续字段的搜索。

1.      如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。

2.      否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和他的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。

3.      否则,如果C不是java.lang. 的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段直接引用,查找失败。

4.      否则,查找失败,抛出java.lang.NoSuchFieldError异常。

     如果查找过程成功返回了引用,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常。

        在实际应用中,虚拟机的编译器实现可能会比上述规范要求的更加严格一些,如果有一个同名字段同时出现在C的接口和父类中,或者同时在自己或父类的多个接口中出现,那编译器将可能拒绝编译。在下面代码示例中,如果注释了Sub类中的“public static int A=4; ”,接口与父类同时存在字段A,那编译器将提示“The field Sub.A is ambiguous”,并且拒绝编译这段代码。

 

public class FieldResolution {
    interface Interface0 {
        int A = 0;
    }
    interface Interface1 extends Interface0 {
        int A = 1;
    }
    interface Interface2 {
        int A = 2;
    }
    static class Parent implements Interface1 {
        public static int A = 3;
    }
    static class Sub extends Parent implements Interface2 {
        public static int A = 4;
    }
    public static void main(String[] args) {
        System.out.println(Sub.A);
    }
}

类方法解析

        类方法解析的第一个步骤与字段解析一样,也需要先解析出类方法表的class_index项中索引的方法所属的类或接口的符号引用,如果解析成功,我们依然用C表示这个类,接下来虚拟机将会按照如下步骤进行后续的类方法搜索。

1.      类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。

2.      如果通过了第1步,在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

3.      否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

4.      否则,在类C实现的接口列表及他们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明类C是一个抽象,这时查找结束,抛出java.lang.AbstractMethodError异常。

5.      否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError。

        最后,如果查找过程成功返回了直接引用,将会对这个方法进行权限验证,如果发现不具备对此方法的访问权限,将抛出java.lang.IllegalAccessError异常。

 

接口方法解析

        接口方法也需要先解析出接口方法表的class_index项中索引的方法所属的类或接口的符号引用,如果解析成功,依然用C表示这个接口,接下来虚拟机将会按照如下步骤进行后续的接口方法搜索。

1.      与类方法解析不同,如果在接口方法表中发现class_index中的索引C是个类而不是接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。

2.      否则,在接口C中查找是否有简单名称和描述符都与
--------------------- 
作者:晋文子上 
来源:CSDN 
原文:https://blog.csdn.net/qq_34190023/article/details/84927582 
版权声明:本文为博主原创文章,转载请附上博文链接!

收藏 打印