一、类加载过程
类的生命周期如下所示
类的生命周期
类加载过程包括加载、验证、准备、解析和初始化五个部分,其中验证、准备和解析统称为连接。
1. 加载
加载阶段,虚拟机主要完成3件事情:
- 通过一个类的 全限定名 来获取定义此类的 二进制字节流 。
- 将这个字节流所代表的 静态 存储结构转化位方法区的 运行时 数据结构。
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口。
注意这里获取二进制字节流
不一定非得要从一个Class文件获取,这里既可以从ZIP包中读取(比如从jar包和war包中读取),或者从网络中获取,也可以在运行时计算生成(动态代理),或由其它文件生成(比如将JSP文件转换成对应的Class类)。
2. 验证
这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
3. 准备
准备阶段是正式为 类变量(static) 分配内存并设置类变量 初始值(0值) 的阶段,这些变量所使用的内存都将在方法区中进行分配。
这里的初始值“通常情况”下是数据类型的零值(即int默认位0,Object默认为null),例如:
public static int value = 123
变量value在准备阶段结束后,初始值是0,而不是123,把value赋值为123的putstatic指令是在程序被编译后,存放于类构造器<clinit>()
方法中,在初始化阶段执行。
4. 解析
解析阶段是指虚拟机将常量池中的 符号引用 替换为 直接引用 的过程。
符号引用(Symbolic References)
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,相当于用人的身份证号码来无歧义地定位一个人。符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中,各个虚拟机能接受的符号引用必须都是一致的。
直接引用(Direct References)
直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。
5. 初始化
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。
初始化阶段是执行类构造器<clinit>方法
的过程。
-
<clinit>方法
是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。 - 与实例构造器
<init>方法
不同,它不需要 显示 地调用父类构造器,而是由虚拟机保证。由于父类的<clinit>方法
先执行,所以父类中的静态语句块要优先子类的类变量赋值操作。 - 虚拟机会保证一个类的
<clinit>方法
在多线程环境中被正确地加锁、同步(想想饿汉式单例模式),多线程中,只有一个线程会去执行<clinit>方法
,其它会阻塞。 - 如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成
<clinit>()
方法。
每个类或接口C
,都有一个唯一的初始化锁LC
。如何实现从C
到LC
的映射可由Java虚拟机实现自行决定。例如,LC
可以是C
的Class
对象,或者是与Class
对象相关的Monitor
。初始化C的过程如下(省略异常阶段)。
- 同步
C
的初始化锁LC
。这个操作会导致当前线程一直等待直到可以获得LC
锁。 - 如果
C
的Class
对象显示当前C
的初始化是由其它线程正在进行,那么当前线程释放LC
并进入阻塞(BLOCKED
)状态,直到它知道初始化工作已经由其它线程完成,那么当前线程在此重试此步骤。 - 如果
C
的Class
对象显示C
的初始化正由当前线程在做,这就是对初始化的递归请求。释放LC
并正常返回。 - 如果
C
的Class
对象显示Class
已经被初始化完成,那么什么也不做。释放LC
并正常返回。 - 记录下当前线程正在初始化
C
的Class
对象,随后释放LC
。根据属性出现在ClassFile
的顺序,利用常量池中的ConstantValue
属性来初始化C
中的各个final static
字段。 - 如果
C
是类而不是接口,而且它的父类SC
还没有被初始化过,那就对于SC
进行完整(即父类的clinit方法
先执行)的初始化过程。 - 通过查询
C
的定义加载器来决定是否为C
开启断言机制。不懂。。。待学习 - 执行
C
的类或接口初始化方法(clinit方法
)。 - 如果正常地执行了类或接口的初始化方法,之后就请求获取
LC
,标记C
的Class
对象已经被完全初始化,通知所有正在等待的线程,接着释放LC
,正常地退出整个过程。
二、类加载的时机
Java虚拟机规范中并没有进行强制约束,但是 初始化阶段 ,虚拟机规范严格规定了五种情况必须立即对类进行"初始化"(加载、验证、准备需要在此之前,而解析阶段在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的动态绑定),即对一个类的 主动引用。
- 使用 new、getstatic、putstatic 或 invokestatic 四条指令。即:使用
new
实例化对象(new),读取(getstatic)或设置(putstatic)类的静态变量(final修饰、已在编译期把结果放入常量池的静态字段的除外),以及调用类的静态方法(invokestatic)的时候。 - 对类进行 反射 调用的时候。
- 初始化一个类,发现其父类还未初始化,则需先触发父类初始化。
- 包含
main()方法
的类 - 使用JDK1.7动态语言支持时。(暂时不清楚,待学习)
被动引用 不会发生初始化。
1.通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
public class SuperClass {
public static int value = 123;
}
public class SubClass extends SuperClass{
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
如代码所示,SubClass.value只会触发父类(SuperClass)的初始化。只有直接定义这个字段的类才会被初始化。
2.定义对象数组,不会触发该类的初始化。
public class NotInitialization {
public static void main(String[] args) {
SuperClass[] sca = new SuperClass[10];
}
}
3.常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
public class SuperClass {
public static final String HELLO_WORLD = "hello,world!";//关键在final
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SuperClass.HELLO_WORLD);//SuperClass不会初始化
}
}
网友评论