虚拟机类加载与运行机制

class类文件的结构

虚拟机类加载与运行机制_第1张图片
根据java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。
Class文件格式包括:
虚拟机类加载与运行机制_第2张图片
虚拟机类加载与运行机制_第3张图片
虚拟机类加载与运行机制_第4张图片

常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时还是在Class文件中第一个出现的表类型数据项目。
每个java文件编译为class文件后,都将产生当前类独有的常量池,我们称之为静态常量池。常量池中主要存放两大类常量:字面量(Literal)和符号引用。字面量例如文本字符串、申明为final的常量值等。而符号引用则包括:a)类和接口的全限定名(Fully Qualified Name);b) 字段的名称和描述符;c) 方法的名称和描述符。在类被jvm装载并第一次使用这些符号引用时,这些符号引用将会解析为直接引用。
分析Class文件字节码的工具javap,例如javap -verbose TestClass
区别与运行时常量池!java中将的常量池,通常指的是运行时常量池,它是方法区的一部分,一个JVM实例只有一个运行常量池,各线程间共享该运行常量池。
为什么需要常量池?JVM在栈帧中进行操作数和方法的动态链接,为了便于链接,jvm使用常量池来保存跟踪当前类中引用的其他类及其成员变量和成员方法。每个栈帧都包含一个运行常量池的引用,这个引用指向当前栈帧需要执行的方法,jvm使用这个引用来进行动态链接。

类加载过程

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。

加载
在加载阶段,虚拟机需要完成:
A 通过一个类的全限定名来获取定义此类的二进制字节流
B 将这个字节流所代表的静态存储结构转化为方法去的运行时数据结构
C 在内存中生成一个代表这个类的java.lang.Class对象(对于HotSpot而言,Class对象是存放在方法区里面),作为方法区这个类的各种数据的访问入口。
加载阶段与连接阶段是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。
验证
验证的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。大致包括4个阶段的验证:文件格式验证、元数据验证、字节码验证、符号引用验证。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个阶段进行内存分配仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配进java堆中。
对于非final静态变量值会赋为0值,对于给定值是在初始化阶段才会被赋值,对于final静态常量会直接赋值为给定值。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
直接引用可以是直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、和调用点限定符7类符号引用进行。
初始化
初始化阶段是执行类构造器()方法的过程,例如静态代码块。
五种情况需要立即对类进行“初始化”:
A. 需要触发初始化的java代码有:使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰,已在编译器把结果放入常量池的静态字段除外)、以及调用一个类的静态方法的时候。
B. 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先出发其父类的初始化。
C. 当虚拟机启动时,用户需要指定一个执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出发其初始化。

方法调用

方法调用并不等于方法执行,方法调用阶段唯一的任务就是确定被调用的是哪一个方法。一切方法调用在Class文件中存储的都是符号引用,而不是方法在实际运行时内存布局中的入口地址,即直接引用。
解析
所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器运行编译时就必须确定下来。这类方法的调用称为解析。例如静态方法、构造方法、私有方法、父类方法和final方法。
静态分派与动态分派
People p = new Man(); //People称为静态类型,Man称为实际类型
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。
动态分派就是指运行期间根据实际类型确定方法执行版本的分派过程。例如子类重写父类的抽象方法。

public class Test {
        Human h1 = new Man(); h1.say();
	    Human h2 = new Woman(); h2.say();
	    h1 = new Woman(); h1.say();
	}
	static class Human {
	     public void say() {
	         System.out.println("Human");
	     }
	}
	static class Man extends Human	{
	    public void say() {
	        System.out.println("man");
	    }
	}
	static class Woman extends Human {
	    public void say() {
	        System.out.println("Woman");
	    }
	}
}
输出结果分别为:man woman woman

你可能感兴趣的:(java虚拟机)