虚拟机将类的数据从Class文件加载到内存,并对数据进行校验,转换解析,初始化,最终形成可以被虚拟机使用的Java类型,称为JVM的类加载机制。
7大过程 :
虚拟机需要完成以下 3 件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
但从整体上看,验证阶段大致上会完成下面 4 个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证
是正式为类变量(static)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
这个阶段中有两个容易产生混淆的概念需要强调一下:
1.首先,这时候进行内存分配的仅包括类变量(被 static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java堆中。
2.其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:public static int value=123;
那变量 value 在准备阶段过后的初始值为 0 而不是 123,因为这时候尚未开始执行任何 Java 方法,而把 value 赋值为 123 的 putstatic 指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把 value 赋值为 123 的动作将在初始化阶段才会执行。
假设上面类变量 value 的定义变为:public static final int
value=123;编译时 Javac 将会为 value 生成 ConstantValue 属性,在准备阶段虚拟机就会根据 ConstantValue 的设置将 value 赋值为 123。
解析是虚拟机将常量池内的符号引用替换为直接引用的过程。
解析动作主要针对类或接口丶字段丶类方法丶接口方法丶方法类型丶方法句柄和调用点限定符7类符号引用,分别对应于常量池的CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info,
CONSTANT_InterfaceMethodref_info,CONSTANT_MethodType_info,
CONSTANT_MethodHandle_info, CONSTANT_InvokeDynamic_info7种常量类型。
4.1类或接口的解析
4.2字段解析
4.3类方法解析
4.4接口方法解析
至于剩下三种则与JDK7新增的动态语言支持息息相关。
方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时不涉及方法内部的具体运行过程。Class文件的编译过程中不包含传统编译的连接步骤,一切方法调用在class文件中存储的都只是符号引用,而不是方法在实际运行内存布局中的入口地址即直接引用。
因此,这个特性给java带来强大扩展性,有些方法调用需要在类加载期间,甚至到运行期间才确定方法的直接引用。
a:解析
调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析。
在Java中符合编译期可知,运行期不改变要求的方法:主要包括 静态方法和私有方法 两大类,前者与类型(class对象)直接关联,后者在外部不可被访问,这两种方法各自的特点决定了它们都不可能通过继承或别的方式重写其他版本,因此它们都适合在类加载阶段进行解析。
b.分派
Java是一门面向对象的语言,具备面向对象的三个基本特征:继承丶封装丶多态。分派调用过程将揭示多态的一些最基本提现,如重载和重写在JVM中是如何确定正确的目标方法的。
静态分派
结果是输出2个hello,guy。
“Human”称为变量的静态类型(Static Type),或者叫做的外观类型(Apparent Type),后面的“Man”则称为变量的实际类型(Actual Type),静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。代码中定义了两个静态类型相同但实际类型不同的变量,但虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据
的。并且静态类型是编译期可知的,因此,在编译阶段,Javac 编译器会根据参数的静态类型决定使用哪个重载版本,所以选择了 sayHello(Human)作为调用目标。
总结:所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。
静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定
静态分派的动作实际上不是由虚拟机来执行的。
动态分派
看如上代码,静态类型同样都是 Human 的两个变量 man 和 woman 在调用 sayHello()方法时执行了不同的行为,并且变量 man 在两次调用中执行了不同的方法。导致这个现象的原因很明显,是这两个变量的实际类型不同。
在实现上,最常用的手段就是为类在方法区中建立一个虚方法表。虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。图中,Son 重写了来自 Father 的全部方法,因此 Son 的方法表没有指向 Father 类型数据的箭头。但是 Son 和 Father都没有重写来自 Object 的方法,所以它们的方法表中所有从 Object 继承来的方法都指向了 Object 的数据类型。
初始化阶段,虚拟机规范则是严格规定了有且只有 5 种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
1)遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这 4 条指令的最常见的Java 代码场景是:使用 new 关键字实例化对象的时候、读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
2)使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含 main()方法的那个类),虚拟机会先初始化这个主类。
5)当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
最后就是使用以及卸载阶段了。
后续补充