引言:
在我们Java中类的加载和连接过程都是在程序运行期间完成的。
一般我们的类从它被加载到虚拟机内存,和到被卸载出内存为止:它的生命周期可以细分为:加载 - 验证 - 准备 - 解析 - 初始化 - 使用 - 卸载。
我们统一把验证,准备,和解析阶段统称为连接阶段。一般我们说的类加载完成包括三个阶段,加载 - 连接 - 初始化。下面我们就来了解了解这三个阶段做了哪些事。
1.加载阶段
加载阶段是我们平时所说的类加载完成的第一个阶段,这个阶段主要做了以下三件事:
- 获取此类的二进制字节流。
- 将字节流所代表的存储结构转化为方法区的运行时结构。
- 在Java堆上生成一个代表这个类的Class对象。
加载阶段是开发期可控性最强的阶段,我们可以使用系统的类加载器去完成,也可以使用自定义的加载器去完成。
2.连接阶段
在这一阶段我们我们主要做了下面几件事:
1.验证我们的Class文件中的字节流是否符合我们虚拟机的要求,并且不会危害虚拟机的自身安全。
2.为我们的类变量分配内存并设置类变量的初始值。(类变量是我们static修饰的变量,不要和不是static修饰的成员属性(实例变量)混淆。)
还要注意一点就是设置类变量的初始值,不等同于我们的赋值操作。
比如 static int a = 2;这个阶段完成以后,我们的a的初始值为0,而非2(被final修饰的常量除外)。这个我们后面说到类的初始化的时候再讲。
3.将常量池的符号引用替换为直接引用(符号引用,引用的目标并不一定以及加载到内存,直接用引用,目标一定已经被加载到内存)。
3.初始化阶段
在初始化我们做的主要事情就是初始化我们的类变量和我们在程序中编写的初始化操作。上面我们在连接阶段提到的a被初始化为零值,在我们的初始化阶段我们为我们的类变量进行赋值操作。
虚拟机严格规定了有且只有四种情况必须立即对类进行初始化。
1.遇到new ,getstatic,putstatic,invokestatic这四条字节码指令时。
new就是我们所熟悉的在实例化对象的时候,getstatic,putstatic,invokestatic是什么呢?就是我们在获取和设置一个static变量(final修饰的类变量除外,final修饰的类变量,在编译期已经把结果放入常量池。)时,还有我们在调用我们类的静态方法的时候。如果类没有被初始化,必须立即对类进行初始化。
2.在我们使用反射对类进行发射调用的时候。
3.当我们初始化一个类的时候,发现其父类没有初始化,必须先对其父类进行初始化。
4.当虚拟机启动时,我们的主类会被初始化(包含main()方法的那个类)。
下面我们通过我们的代码来验证以下,类的初始化过程。
除了上面的四种情况,其他任何一种情况,都不会触发类的初始化操作。
下面我们来举个例子验证以下,我们上面所说的几种情况:
我们先定义两个类:一个Person类和Person类的子类Person类。
public class Person {
public static int a = 10;
public static final int b = 10;
static {
System.out.print("person类被初始化了。");
}
}
public class Child extends Person{
static {
System.out.print("Child类被初始化了。");
}
public static int addNum(int a ,int b){
return a+b;
}
}
先运行我们的程序,发现控制台并没有打印任何输出,我们的两个类都没有被初始化。
1.调用new Person()实例化一个Person对象,我们来看看控制台的输出结果:
2.调用Person.a访问Person类的变量a,我们看下控制台输出。
3.调用Person.b访问常量
我们的控制台没有打印。可见访问final 修饰的类变量不会导致我们的类初始化。
4.调用Child.addNum(1,2)访问Child类的静态方法:
可以看到我们虽然没有调用Person类,我们只访问了Child类的静态方法,它的父类优先它进行了初始化操作。
5.调用Class childClass = Class.forName(“类的包名+类名”);控制台打印输出如下:
可以看出结果和我们上面所说情况是一致的。有兴趣的朋友可以自己去验证验证。