类加载机制:虚拟机把类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
动态加载和动态连接:类型的加载和连接过程在程序运行期完成,会增加性能开销,但是可以高度灵活。
类的生命周期
类的生命周期包括下面几个过程:
仅有上述四种虚拟机会对一个类进行主动引用;其他情况下为被动引用。
- 对于静态字段,只要直接定义这个字段的类才会被初始化,通过子类来引用父类中定义的静态字段,会触发父类的初始化而不会初始化子类的初始化。
- 通过数组定义来引用类,不会触发此类的初始化。
- 常量在编译截断会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
接口和类初始化的区别:在于当一个类初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部完成初始化,只有真正使用到父接口时(如引用接口中定义的常量)才会初始化。
类加载的过程
- 加载
加载阶段虚拟机完成的三件事:- 通过类的全限定名来获取定义此类的二进制字节流;
- 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构;
- 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义,虚拟机规范没有规定此区域的具体数据结构。
加载阶段尚未完成,连接阶段可能已经开始。
2. 验证
目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
3. 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区进行分配。
两个易混淆的地方:该过程进行内存分配的仅包括类变量(static修饰的变量),不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中;这里的初始值“通常情况下”是数据类型的零值。
例如public static int value = 123
类变量value在准备阶段后初始值为0;
而特殊情况字段属性表中存在ConstantValue属性,如public static final int value =123
则会在准备阶段后初始值为123。
4. 解析
虚拟机将常量池内的符号引用替换成直接引用的过程。
符号引用:以一组符号来描述所引用的目标。引用的目标不一定已经加载到内存中。
直接引用:直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。如果有了直接引用,那么引用的目标必定已经在内存中存在。
5. 初始化
真正开始执行类中定义的Java程序代码(字节码)。就是执行类构造器<clinit>()方法的过程。
类构造器<clinit>
-
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,收集顺序为语句在源文件中出现的顺序所决定,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能被访问。
-
<clinit>()与类的构造函数不同,不用显式的调用父类构造器,虚拟机会保证父类<clinit>()在调用子类的<clinit>()方法前已经执行完毕。
-
父类<clinit>()方法先执行,故父类定义的静态代码块要优先与子类的变量赋值操作。
-
<clinit>()方法对类或者接口并不是必须的。
-
接口中不能使用静态语句块,但可以有变量初始化的操作,故也有<clinit>()方法。但接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
-
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁和同步,如果多个线程同时初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动现场执行完毕。如果在一个类的<clinit>()方法中耗时很长,就可能阻塞多个进程。
package demos;
public class ClinitMethodTest {
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);
}
}
结果:
多进程环境中被正确的加锁和同步:
package demos;
public class ClinitMethodStuckTest {
static class DeadLoopClass{
static {
if(true) {
System.out.println(Thread.currentThread()+"init DeadLoopClass");
while(true) {
}
}
}
}
public static void main(String[] args) {
Runnable script = new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread()+"start");
DeadLoopClass dlc = new DeadLoopClass();
System.out.println(Thread.currentThread()+"run over");
}
};
Thread thread1 = new Thread(script);
Thread thread2 = new Thread(script);
thread1.start();
thread2.start();
}
}
结果: