任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(类和接口也可以用反射的方式通过类加载器直接生成)
Class文件时一组以8位字节为基础单位的二进制流,各个数据都严格按照顺序紧凑排列在Class文件中,没有任何分隔符。
Class文件格式采用一种类似C语言结构体的伪结构存储数据,这种结构中只包含无符号数和表两种类型。
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
编辑器用16进制打开类文件
0 1 2 3 4 5 6 7 8 9 A B C D E F
ca fe ba be 00 00 00 34 00 3f 0a 00 0a 00 2b 08
00 2c 09 00 0d 00 2d 06 40 59 00 00 00 00 00 00
09 00 0d 00 2e 09 00 2f 00 30 08 00 31 0a 00 32....
类文件第一个数据为u4,我们查看16进制文件前4个字符是cafebabe
,它用来确定这个文件是否为一个能被虚拟机接受的Class文件。
u4后的两个u2,即00 00 00 34用来代表jdk的主次版本。
常量池是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据,也是Class文件中第一个出现的表类型数据项目。
存放类型包含:
类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括以下 7 个阶段:加载、验证、准备、解析、初始化、使用、卸载
加载、验证、准备、初始化和卸载这 5 个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始(注意是“开始”,而不是“进行”或“完成”),而解析阶段则不一定:它在某些情况下可以在初始化后再开始,这是为了支持 Java 语言的运行时绑定(多态)。
虚拟机规范严格规定了有且只有5钟情况必须立即对类进行初始化:
这 5 种场景称为对一个类进行主动引用(有且只有这五种才可以触发类的初始化),除此之外,其它所有引用类的方式都不会触发初始化,称为被动引用。
/**
* 被动引用 Demo1:
* 通过子类引用父类的静态字段,不会导致子类初始化。
*/
class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
// SuperClass init!
}
}
对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
/**
* 被动引用 Demo2:
* 通过数组定义来引用类,不会触发此类的初始化。
*/
public class NotInitialization {
public static void main(String[] args) {
SuperClass[] superClasses = new SuperClass[10];
}
}
new数组对象时并不会触发SuperClass
类的初始化,而是在这段代码里触发一个名为Lorg.fenixsoft.classloading.SuperClass
的类初始化,他直接继承自Object
类,由虚拟机来产生和触发。
/**
* 被动引用 Demo3:
* 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
*/
class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String HELLO_ZHIYIN = "Hello ZhiYin";
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(ConstClass.HELLO_ZHIYIN);
}
}
JVM在编译期进行了传播优化,将ConstClass类中的常量放入了NotInitialization的常量池中,事实上这个常量已经和ConstClass类没有了联系,不会触发初始化。
类加载过程包括 5 个阶段:加载、验证、准备、解析和初始化。
“加载”是“类加载”过程的第一步,在加载阶段,虚拟机需要完成以下三件事情:
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口
对于 Class 文件,虚拟机没有指明要从哪里获取、怎样获取。除了直接从编译好的 .class 文件中读取,还有以下几种方式:
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
0xCAFEBABE
开头 符号引用验证如果没有通过,会抛出一个java.lang.IncompatibleClassChangeError
异常的子类,如常见的java.lang.NoSuchFieldError、java.lang.NoSuchMethodError
等
对于虚拟机的类加载机制而言,验证是一个很重要的、但不是必须的(因为对程序运行期无影响)一个阶段,如果运行的全部代码(包括自己编写的以及第三方包中的代码)都已经被反复使用和验证过,那么在实施阶段就可以使用-Xverify:none
来关闭大部分类的验证过程,以缩短虚拟机类加载的时间
准备阶段是正式为类变量(被 static修饰的变量)分配内存并设置类变量初始值(通常为零值,引用类型为null)的阶段,这些变量所使用的内存将在方法去区中进行分配。如下语句中:
public static int value = 666;
value
变量在准备阶段之后初始值变为0而不是666,变为666的过程是在初始化阶段进行。
上面说到通常情况下是零值,特殊情况为该变量同时被final修饰,是常量。
public static final int value = 666;
编译时value就会生成ConstantValue属性(定义为常量),在准备阶段虚拟机就会依据ConstantValue的设置将value赋值为666.
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
class二进制字节流中的引用关系都是符号引用没有真正的意义,解析之后将会变成直接指向目标的指针。
类初始化阶段是类加载过程的最后一步,是执行类构造器方法的过程。
类构造器方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static {} 块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。
静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但不能访问。如下方代码所示:
public class Test {
static {
i = 0; // 给变量赋值可以正常编译通过
System.out.println(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
类构造器方法不需要显式调用父类构造器,虚拟机会保证在子类的类构造器方法执行之前,父类的类构造器方法方法已经执行完毕。
由于父类的类构造器方法方法先执行,意味着父类中定义的静态语句块要优先于子类的变量赋值操作。如下方代码所示:
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B); // 输出 2
}
类构造器方法不是必需的,如果一个类没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成它。
接口中不能使用静态代码块,但接口也需要通过类构造器方法为接口中定义的静态成员变量显式初始化。但接口与类不同,接口的类构造方法不需要先执行父类的类构造方法方法,只有当父接口中定义的变量使用时,父接口才会初始化。
虚拟机会保证一个类的类构造方法在多线程环境中被正确加锁、同步。如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造方法。
参考《深入理解Java虚拟机》、Jvm官方规范