回顾:
java源码的运行会在编译期被编译成.class
字节码文件,然后再运行期由类加载器加载到内存,最终形成虚拟机直接使用的java类
一、Class文件的结构
class文件是一组以8个字节为基础的二进制流,各个数据项目严格按照顺序紧凑排列在文件中,class文件中的存储内容几乎全部是程序运行的必要数据。
1)魔数与Class文件的版本
主要是检验class文件是否可以运行。
魔数: 出现在最开始的4个字节,作用是确定这个文件是否为一个被虚拟机接收的Class文件。类似一个身份识别。
class版本信息: 魔数后的4个字节,5-6是次版本号,7-8是主版本号。高版本的JDK可以兼容低版本的class文件,但低版本的JDK不能运行高版本的class文件。
2)常量池(※)
出现位置紧接着魔数和版本信息,它是Class文件的资源仓库,是Class文件结构中与其他项目关联最多的数据。
主要存放两大类常量——字面量和符号引用
-
字面量: 文本字符串,声明为final的常量值等(可见final类型其实在编译期就已经写入了类中)
-
符号引用: 在编译时,java类并不知道引用类的实际内存地址,因此只能使用符号引用来代替,等到类加载时会从常量池获得对应的符号引用,在类创建或运行时才会解析到具体的内存地址中。符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这样的)。
因为常量池中常量的数值是不固定的,且有很多项目,如下图为常量池的项目类型,所以紧接着的字节码会定义常量池的容量(说明常量池的长度),常量池中的每一项常量都对应了一个表。通过常量的值查表可以的得知当前常量的类型与名称。
3)访问标志
常量池结束后,紧接着2个字节表示访问标志,这个标志用于识别一些类或接口层次的访问信息,比如:这个Class是类还是接口,是否是public的;是否是abstract;如果是类的话是否被声明为final等。它也对应了一张含义对照表。
4)类索引,父类索引接口索引集合
Class文件由这三项数据确定这个类的继承关系。类索引用于确定这个类的全限定类名,父类索引用于确定这个类父类的全限定类名(只有一个,因为java不支持多继承),接口索引集合,用于确定这个类是否实现了接口实现了哪些接口。
5)字段表集合
用于描述接口或类中声明的变量,比如变量的作用域,是否是static的,是否是final的,是否是volatile的,字段名称,数据类型等。修饰符用标志位来表示,字段的名字,被定义成什么类型的数据,只能引用常量池中的常量描述。(常量池中的CONSTANT_Utf8_Info型常量,同理方法也是)
6)方法表集合
与字段表集合相同,包括访问标志,名称索引,描述符索引属性表集合,其中描述符主要是用来描述字段的数据类型、或方法的参数列表和返回值。它也有自己的一套描述符规则。
7)属性表集合
Class文件,字段表,方法表,都可以携带自己的属性表集合,以描述某些场景的专有信息。
重要属性表——Code属性表
java程序方法体的代码经过javac后,会变成字节码指令存储在Code属性内,Code属性出现在方发表的属性集合中,但并非所有方法表都必须存在这个属性(比如接口,抽象类)。结构如下图:
其中code_length和code属性用于储存java编译后生成的字节码指令。
Code属性是用于描述代码,其他所有的数据项目都用于描述元数据。
二、类加载
1. 类加载的时机
Java虚拟机把描述的类的数据从Class文件加载到内存,并对数据进行校验、转换解析、初始化,最终形成可以被虚拟机直接使用的java类型,这个过程称为虚拟机的类加载机制。
一个类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期如下
加载、验证、准备、初始化和卸载这五个阶段的顺序是固定的,类的加载过程必须按照这种顺序执行,但解析阶段不一定。 它在某些情况下可以在初始化阶段之后开始(比如动态绑定)。
2. 类加载的过程
1)加载
在加载阶段java虚拟机完成了以下事情:
在加载结束后,java虚拟机外部的二进制字节流就按照虚拟机设定的格式存储在方法区,然后会在 java堆 内实例化一个java.lang.class类的对象。这个对象是程序访问方法区中的类型数据的外部接口。
2)验证
这一阶段是保证Class文件的字节流中包含的信息符合要求,不会危害虚拟机自身安全。
3)准备
该阶段正式为类中定义的变量(静态变量)分配内存并设置类变量初始值。
注意点:
- 此时进行内存分配的仅包括类变量,不包括实例变量。
- 初始值,指数据类型的0值比如
public static int val = 22;
,变量val在准备阶段过后的初始值是0,不是123,因为此时未执行任何java方法,赋值的putstatic指令是程序编译后,在类的初始化阶段才会执行的。 - 如果某个值是final的,那么在准备阶段,虚拟机就会根据ConstantValue赋值。
4)解析
解析阶段是虚拟机将class常量池内符号引用替换为直接引用的过程。 同时对于字段和方法的访问,也在解析阶段对他们的可访问性进行检查。
①类和接口的解析:
②字段解析:
③方法解析:
④接口方法解析:
5)初始化
初始化阶段是类加载的最后一个过程,从该步开始java虚拟机才真正开始执行类中编写的java程序代码,将主导权交给应用程序。
该阶段就是执行类构造器<clinit>()
方法的过程,这个方法是javac编译器的自动生成物。
<clinit>()
方法:是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生,它与类的构造函数不同,他不需要调用父类构造器,JVM会保证在子类的<clinit>()
执行前,父类的<clinit>()
执行完毕。所以在JVM中第一个被执行的<clinit>()
方法的类型就是java.lang.Object。
因为父类的<clinit>()
先执行,所以父类中定义的静态语句块优先于子类的变量赋值!!!!(原来是这样)
3. 类加载器
1)类与类加载器
2)双亲委派模型
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。