前言
java源代码经过编译后会生成 .class字节码文件(包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是class常量池)
运行 .class字节码文件后:jvm将字节码文件数据转化为方法区的运行时数据结构 ;由执行引擎执行这些字节码指令,将其解释/编译为本机对应平台上的机器指令。本文就详细介绍加载类到jvm的过程中,在运行时数据区各个组成部分中存储数据的种类与时期。
上图中的运行时数据区具体结构如下:
目录
第三步:执行引擎
第一步:编译
将源代码编译成字节码文件(.class),.class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是class静态的常量池(constant pool table)(用于存放编译期生成的各种字面量和符号引用)。
字面量包括:1.文本字符串、2.八种基本类型的值、3.被声明为final的常量等
符号引用包括:1.类的全限定名、 2.字段名和属性、3.方法名和属性
第二步:类加载
当虚拟机启动时, 用户需要执行程序入口 main( ) 方法(静态方法),main方法在主类中,因而首先需要对主类进行加载、链接、初始化操作,然后调用主类的main方法:。所以此时为了主类的字节码文件装载到jvm内部的运行时数据区进行以后的运算,需要进行如下操作:类加载、链接、初始化,
需要注意的是,在jdk1.8之后,元空间代替永久代来实现方法区,但是方法区并没有改变,变动的只是方法区中内容的物理存放位置。
即:原来永久代中的类型信息(元数据信息)等其他信息被移动到了元空间中;但是运行时常量池和字符串常量池被移动到了堆中。但是不论它们物理上如何存放,逻辑上还是属于方法区的。JDK1.8中字符串常量池、static变量逻辑上属于方法区,但是实际存放在堆内存中,因此既可以说两者存放在堆中,也可以说两则存在于方法区中,这就是造成误解的地方。
在类加载过程结束后,该主类的数据在运行时数据区各组成中,存放数据类型如下:
2.1、加载--方法区
通过一个主类的全限定名获取定义此类的二进制字节流(.class)(静态存储结构,如),将其转化为方法区的运行时数据结构(class静态常量池转化为运行时常量池),并生成代表这个类的java.lang.Class对象(类对象),作为访问方法区中这个类的各种数据时的访问入口。
2.2、连接
2.2.1、验证
检查类的二进制表示形式,验证生成的主类的.class文件是否有效的过程(文件格式验证、元数据验证、字节码验证、符号引用验证);
2.2.2、准备--堆
为主类的类对象在堆中分配内存,为其static变量分配内存(默认值为0、null),注意:final修饰的变量在编译时就被分配内存了(堆中的对象实例都有一个对象头(存有该对象所属类的类对象):在执行对象中的某方法时,需要根据这个信息找到在该方法在方法区中的jvm指令)
2.2.3、解析
将符号引用替换为直接引用,解析的过程会去查询字符串常量池
2.3、初始化
对静态变量初始化,并且执行静态块(从父类执行到子类)。
第三步:执行引擎
执行引擎开始执行这些字节码:在执行主类时,会启动一个线程(所有线程共享数据区中的堆和方法区中的数据,而本地方法栈、栈、程序计数器是每个线程私有的)。该线程创建时:在栈中开辟一段空间(main栈帧)(栈帧用来存储局部变量表、操作数栈、动态链接、方法出口)(栈帧生命周期也随线程生命周期:线程结束时栈被销毁);
栈帧由四个组成:1、局部变量表:是一组变量值的存储空间,用于存储方法参数和方法内部定义的局部变量; 2、操作数栈:执行运算时(如a+b=c),运算数据暂时的保存区:将a、b入栈(后入先出),然后先后去除a、b,将c入栈,最后c出栈存入到局部变量表; 3、动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了方法调用过程中的动态连接。Class文件中的常量池中存有大量的符号引用,字节码中的方法调用指令就是以常量池里指向方法的符号引用作为参数的,这些符号的一部分会在类加载阶段或者第一次使用的时候被转化为直接引用,这种转化被称为静态解析。另一部分在每一次运行期间转化为直接引用,被称为动态连接(存储每个在该方法中出现过的方法的指令码在方法区中的地址); 4、方法出口:当一个方法开始执行后,只有两种方式退出这个方法。无论何种方式退出,方法退出之后都必须返回最初方法被调用的位置,程序才能继续执行:方法返回时可能需要在栈帧中保存一些信息,用来恢复其上层主调方法的执行状态。
当在main函数中加载一个对象(Math a=new Math())时,Main类对象会被类加载器加载到方法区中,并且在堆中存储该Math实例,将该对象的引用变量a以及该对象的地址放在栈的局部变量表中。当调用a的方法时,根据栈帧中局部变量表的数据找到该变量a指向的堆中实例对象;根据该实例对象的对象头(存有该对象所属类的类对象)到方法区中找到该方法的jvm指令,之后将该方法在方法区中的地址存储到栈帧中的动态链接中。
当main函数调用到一个方法之后,根据栈帧中局部变量表的数据找到该变量a指向的堆中实例对象;根据该实例对象的对象头(存有该对象所属类的类对象)到方法区中找到该方法的jvm指令;并在栈中开辟一块程序计数器区域(多线程之间不共享)来存放要执行Math中的下一条jvm指令码(指令码被加载到方法区)的数字;在方法调用完后将该方法在方法区中的地址存储到栈帧中的动态链接中。
另外,jvm运行时数据区中的五个部分,还有方法本地方法栈(当用到使用C语言封装的方法时,会到C语言函数库中)、程序计数器(存放解释器将要执行的下一条jvm指令的地址)