一、类加载机制
类加载不过就是JVM虚拟机为类分配了几块内存空间,说的具体一点,就是jvm虚拟机将类的.class文件加载到内存,并将它放到运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构
1、加载
将class字节码文件加载到内存中,并将这些数据转换成方法区中的运行时数据(静态变量、静态代码块、常量池等),在堆中生成一个Class类对象代表这个类(反射原理),作为方法区类数据的访问入口。
2、链接
将Java类的二进制代码合并到JVM的运行状态之中。
- 验证
确保加载的类信息符合JVM规范,没有安全方面的问题,验证的信息有:文件格式、元数据验证、字节码验证、符号验证
- 准备
正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。注意此时的设置初始值为默认值,具体赋值在初始化阶段完成。
这里要注意final修饰:
private static int aa = 10;
private static final int bb = 10;
static int aa = 10在准备阶段中不是10,而是初始值0;
static final int bb= 10会是10,因为final修饰的静态变量不会导致类的初始化,可以直接计算出结果。
- 解析
虚拟机常量池内的符号引用替换为直接引用(地址引用)的过程。
3、初始化
初始化阶段是执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先初始化其父类。
- 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。
Java程序初始化顺序:
1、父类的静态变量
2、父类的静态代码块
3、子类的静态变量
4、子类的静态代码块
5、父类的非静态变量
6、父类的非静态代码块
7、父类的构造方法
8、子类的非静态变量
9、子类的非静态代码块
10、子类的构造方法
二、什么时候去加载一个类
所有的Java虚拟机实现必须在每个类或接口被Java程序“ 首次主动使用” 时才初始化他们
Java程序对类的使用方式可分为两种
– 主动使用
– 被动使用
java对类的主动使用有六种情况
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(如Class.forName(“com.shengsiyuan.Test”))
- 初始化一个类的子类(先初始化所有的父类,最后初始化本身,接口除外,类初始化的时候,它所实现的接口不会初始化,就算字接口初始化,父接口也不会初始化,只有当程序调用接口的静态变量的时候才会导致接口的初始化)
- Java虚拟机启动时被标明为启动类的类(简单说就是拥有main方法的类)
除了上述六种情况以外,其他情况都属于类的被动调用,主动调用和被动调用的区别可以看下面代码
class Test2{
public static final int n = 2;
static{
System.out.println("test");
}
}
public class Test1 {
public static void main(String[] args) {
System.out.println(Test2.n);
}
}
正确答案是2 。 Test2中的静态代码块是没有运行的,因为Test2是被动调用并没有初始化
注意,这里的final关键字不可缺少,我们知道变量被关键字fianl修饰之后就不可修改,即此变量相当于编译期常量,常量在java编译期已经确定,不需要初始化,但是把fianal去掉,或者把 final int n = 2 改为 final int n = new Random(),运行的结果将变为 test ,2 ,因为n的值为变量或者n值在编译期不能确定,就必须经过初始化才能使用n的值