一、概述
Class类文件结构:Java虚拟机只与Class文件关联。任何一个Class文件都对应着唯一一个类或接口的定义信息,但类或接口的定义不一定都在文件里,也可以通过类加载器直接生成。即Class文件格式并不一定是以磁盘文件的形式存在。
WinHex查看Class文件:
1.魔数(头四个字节):用来确定这个文件是否是一个能被虚拟机接受的Class文件
2. 5,6字节是次版本号,7,8字节是主版本号。
3. 常量池:入口放容量计数器,计数从1开始。主要存放字面量和符号引用。
字面量接近于java中的常量,如文本字符串、final常量等。
符号引用包括:①类和接口的全限定名。如com/Test.java。②字段的名称和描述符。③方法的名称和描述符
java代码在进行javac编译的时候,并不像C或者C++一样有连接这一步,而是在虚拟机加载Class文件的时候进行动态连接。即,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转化的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再通过解析将符号引用转化为直接引用。
虚拟机类加载机制:虚拟机把的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。
类型的加载、连接和初始化都是在运行期间完成的。java的动态可扩展的特性就是依赖运行期动态加载和动态连接这个特点实现的。用户可以自定义类加载器让一个应用程序可以在运行时从网络或其他地方加载二进制流作为程序代码的一部分。
二、类的加载过程
1.类的生命周期(七个阶段):加载,验证,准备,解析,初始化,使用和卸载。其中验证、准备、解析统称为连接。
其中加载、验证、准备、初始化和卸载顺序是确定的,解析阶段不一定,在某些情况下在初始化之后开始,这是为了支持运行时绑定(动态绑定和晚期绑定)。
2.类的主动引用(必须对类进行初始化的情况):
① 遇到new,getstatic,putstatic,invokestatic时,如果没有进行过初始化,则需要先触发初始化。常见场景:使用new实例化对象时,读取或设置类的静态字段时(被final修饰,但已在编译期把结果放入常量池的静态字段除外),以及调用类的静态方法时。
②反射调用时(java.lang.reflect) 。
③ 当初始化一个类的时候,如果其父类还没有进行过初始化,则需要先触发其父类的初始化。
④ 虚拟机启动时,虚拟机会先初始化主类(包含main方法的类)。
3.被动引用:其他引用类的方法都不会触发初始化。
① 通过子类引用父类的静态字段
② 通过数组定义来引用类,但会触发另一个类的初始化(一个由虚拟机自动生成、直接继承于java.lang.Object的子类,创建动作由字节码指令newarray触发)。
③ 另一个类的常量在编译时已经存入调用类的常量池中,则以后该调用类对常量的引用实际上都会转化为该类对自身常量池的引用。这两个类在编译之后就不存在任何联系了。
三、类加载过程
加载,连接(验证,准备,解析),初始化,
1.加载
① 通过类的全限定名获取此类的二进制字节流。
获取方法:
- 从ZIP包获取。后期成为JAR、EAR、WAR格式的基础。
- 从网络中获取。如Applet。
- 运行时计算生成。如动态代理技术。
- 由其他文件生成。如JSP(JSP会生成对应的Class类)
- 从数据库中读取。比较少见,可以用于完成程序代码在集群间的分发。
② 将此字节流所代表的静态存储结构转化为方法区的运行时数据结构。
③ 在内存中生成这个类的java.lang.Class对象,作为方法区这个类的访问入口(有些虚拟机并不是存储在堆中,而是存储在方法区里)。
开发人员可通过重写类加载器的loadClass()方法去控制字节流的获取方式。
数组类本身不通过类加载器创建,由虚拟机直接创建,但数组类中的元素类型要靠类加载器去创建。
- 如果数据类型是引用类型,则递归加载。
- 如果数组类的元素类型是引用类型,则数组的可见性与其引用类型的可见性一致。如果元素类型不是引用类型,则数据的可见类型默认是public。
2.验证(连接的第一步)
java语言本身是相对安全的,如果java代码访问数组边界以外的数据,将一个对象转型为未实现的类型,跳转到不存在的代码行之类的事情,编译器将拒绝编译。但class文件不一定是java源码编译而来,也可以从十六进制编辑器直接编写产生。所以虚拟机需要检查输入的字节流,否则可能会因为载入有害的字节流而导致崩溃。
① 文件格式验证(主要是验证字节流是否符合Class文件的规范)
② 元数据验证(主要对的是类的元数据信息进行语义校验)
- 检查这个类是否有父类(除了java.lang.Object外都应有)
- 如果不是抽象类,是否实现了其父类或接口中要求的所有方法。
- ...
③ 字节码验证(对类的方法体进行校验分析)
④ 符号引用验证(发生在符号引用转化为直接引用的时候,转化动作发生在连接的第三个阶段——解析阶段)
- 符号引用中的类、字段、方法的权限(private、protect、default、public)是否符合当前类。
- 符号引用中通过字符串描述的全限定名是否能找到对应的类。
3.准备(连接的第二步)
为类变量分配内存并设置类变量的初始值。变量所使用的内存在方法区中进行分配(只包括被static修饰的类变量,不包括实例变量),初始值是默认零值。
public static int value = 123; //准备阶段过后初始值是0而不是123,把value赋值为123的putstatic指令是在程序被编译后,存放于类构造器<clinit>()方法中,所以该动作在初始化阶段执行。
public static final int value = 123;// 编译时value生成ConstValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123
4.解析
虚拟机将常量池中的符号引用替换为直接引用的过程。
符号引用:以一组符号描述所引用的目标,符号可以是任何形式的字面量,且与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
直接引用:可以直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局相关。
5.初始化
初始化阶段是执行类构造器<clinit>()方法的过程。
<clinit>()方法由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,收集顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值但是不能访问。
public class Test{
static {
i = 0; // 赋值可以通过
System.out.println(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
【Note】:
1.<clinit>()方法与类的构造函数不同,不需要显示的调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的一定是java.lang.Object
2.父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
3.如果一个类没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
4.接口中不能有静态语句块,但有变量初始化的赋值操作,因此接口也会生成<clinit>()方法,但是与类不同的是,执行接口中的<clinit>()方法不需要先执行其父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。接口的实现类也一样。
5.虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都要阻塞等待。
网友评论