上图中,加载、验证、准备、初始化和卸载这个五个阶段的必须是按部就班的,而解析在某些情况下可以在初始化阶段以后再开始。
《Java虚拟机规范》只规范了在什么时候初始化,其他的没规范,但是他又规范了加载、验证、准备是要在初始化之前进行。规定有以下场景才会强制初始化(有且只有):
出现以上六种称为主动引用,除此之外,所有的引用类型都不会触发初始化,称为被动引用。举个例子
public class SuperClass {
static {
System.out.println("Superclass init");
}
public static int value = 123;
}
class SubClass extends SuperClass {
static {
System.out.println("subclass init");
}
}
class NotInitialization{
public static void main(String[] args) {
System.out.println(SuperClass.value);
}
}
输出:Superclass init
123
上面这个就是,非直接引用子类的静态字段,但是直接引用了父类的静态字段,所以只会打印Superclass init和123
public class SuperClass {
static {
System.out.println("Superclass init");
}
public static int value = 123;
}
class SubClass extends SuperClass {
static {
System.out.println("subclass init");
}
}
class NotInitialization{
public static void main(String[] args) {
SuperClass[] sca = new SuperClass[10];
}
}
输出:
上面代码啥也没输出。数组在jvm中不直接引用原类型,字节码指令anewarray会生成一个类,直接继承于java.lang.Object,java语言对数组的操作比c++安全,这个机制影响很大。
public class SuperClass {
static {
System.out.println("Superclass init");
}
public final static int value = 123;
}
class SubClass extends SuperClass {
static {
System.out.println("subclass init");
}
}
class NotInitialization{
public static void main(String[] args) {
System.out.println(SuperClass.value);
}
}
输出:123
当value被finali修饰的时候,就不输出Superclass init,这是因为在编译优化的时候,value已经被放到NotInitialization的常量池中了,调用的时候是对自身常量池的引用,也就是说,NotInitialization的class文件中没有对SuperClass类的符号引号入口,这两个类在编译完成后,就彼此无关了。
加载阶段虚拟机需要完成三件事:
数组不太一样,数组是由java虚拟机直接在内存中动态构造出来的。
给被static修饰的变量分配内存,并且设置初始值的阶段,jdk7之前用永久代来实现方法去,就放在永久代,jdk7以后,和对象一起放在堆中。而被final修饰过的,则会被虚拟机修饰额ConstantValue属性,会在准备阶段就会被赋值
public static int value = 123;在准备阶段值是0;
public static final int value = 123;在准备阶段值是123;
数据类型 | 零值 |
---|---|
int | 0 |
long | 0L |
short | (short)0 |
char | ‘\u0000’ |
byte | (byte)0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
符号引用:由于引用对象可能还未加载到虚拟机了,没有内存地址,只能一组符号表示目标的定位。
直接引用:直接引用是可以直接指向目标指针、相对偏移量或者是一个能间接定位到目标的句柄,如果是直接引用,那么被引用的一定是在虚拟机中真实存在的。
解析就是把符号引用转化为直接引用的过程。同时会对被引用都对象、方法、属性进行权限判断。
初始化是类加载的最后一个步骤,这个时间才开始执行我们自己写的代码。主导权移交给程序本身,即执行< clinit>()方法的过程,给准备阶段赋值为零值的那些属性赋予具体的值。
< clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的 语句合并产生的。
由于静态语句块中是可以互相访问的,所以先赋值后定义是可以的,但是其他调用会报非法的向前引用的错误。
public class Test {
public static void main(String[] args) {
System.out.println(Sub.B);
}
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent{
public static int B = A;
}
}
输出:2
上图表示,父类的< clinit>一定优先于子类的执行,所以java中第一个被执行的< clinit>一定是java.lang.Object
类+类加载器才可以确定唯一性,如果同一个类由两个不同的类加载器加载,则必不相等。
从java虚拟机角度来看,只存在两种类加载器,一种是Bootstrap ClassLoader(启动类加载器)由C++实现,另一种是其他所有的类加载器,都是由java语言实现,全部继承自抽象类java.lang.ClassLoader
双亲委派模型就是类加载时,子加载器调用父类的加载方法loadClass,父类未找到反过来调用子类的loadClass去加载。