类初始化时机
1.主动引用
虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”:
- 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
- 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
2.被动引用
public class SuperClass {
static{
System.out.println("SuperClass init!");
}
public static int value=123;
}
public class SubClass extends SuperClass {
static{
System.out.println("SubClass init!");
}
}
/**
* 非主动使用类字段演示
**/
public class NotInitialization {
public static void main(String[]args)
{
System.out.println(SubClass.value);
}
}
对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化。
package org.fenixsoft.classloading;
/**
*被动使用类字段演示二:
*通过数组定义来引用类,不会触发此类的初始化
**/
public class NotInitialization{
public static void main(String[]args){
SuperClass[]sca=new SuperClass[10];
}
}
通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
System.out.println(ConstClass.HELLOWORLD);
常量在编译阶段中,已经将常量的值存储到调用类的常量池中,调用类对常量的引用实际转化为对自身常量池的引用。
类加载的过程
加载(加载class文件的信息到内存中,有硬盘到内存的迁移)
在加载阶段,虚拟机需要完成以下3件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
其中二进制字节流可以从以下方式中获取:
- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
- 从网络中获取,最典型的应用是 Applet。
- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。
链接
将加载阶段加载到内存中的二进制数据整合到虚拟机中
验证:为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 文件格式验证
- 元数据验证等等
准备:准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
- 为类静态变量分配内存,并将其初始化为默认值
public static int value=123;
- 给常量分配内存并设置值
public static final int value=123;
解析
- 将常量池的符号引用替换为直接引用的过程。
- 其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
初始化(为类的静态变量赋予正确的初始化值按照从上到下的顺序(<clinit>))
初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
public class Test{
static{
i=0;//给变量赋值可以正常编译通过
System.out.print(i);//这句编译器会提示"非法向前引用"
}
static int i=1;
}
虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作,如在代码清单7-6中,字段B的值将会是2而不是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);
}
<clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。如果在一个类的 <clinit>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。
使用
这个阶段就是调用<init>方法实例化出对象来使用了
卸载
当类使用完了,再也不用到的时候就可以将类下载了(必须保证所有的实例被GC)
示例:
public class SingleTone {
private static SingleTone singleTone = new SingleTone();
public static int count1;
public static int count2 = 0;
public SingleTone(){
count1++;
count2++;
}
public static SingleTone getInstance(){
return singleTone;
}
public static void main(String[] args) {
SingleTone instance = SingleTone.getInstance();
System.out.println("count1 = "+instance.count1);
System.out.println("count2 = "+instance.count2);
}
}
控制台输出:
count1 = 1
count2 = 0
准备:singleTone = null,count1=0,count2 = 0
初始化:singleTone = new SingleTone()==》 count1=1,count2 = 1
count1不变
count2 = 0