类加载子系统
类的加载阶段可以分为三个阶段,这三个阶段为加载阶段、链接阶段、初始化阶段。
阶段一(加载阶段 Loading):
- 通过一个类的获取定义此类的二进制字节流
- 转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为该类的访问入口
阶段二(链接阶段 Linking):
- 验证:目的在于确保Class文件的字节流包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全
- 准备:为类变量分配内存并且设置该类变量的默认初始值,即零值。
注:在链接阶段如果变量被final修饰,则直接被赋值
private static int a = 1; private static final int b = 2;
小编用代码和字节码文件来说明一下:
查看字节码文件,由于<clinit>是初始化阶段发生的(也就是在阶段三),只能看到a被赋值为1,说明b是在链接阶段被赋值的(在阶段三中看不到b被赋值),所以证明final是在链接阶段发生的
(这里在IDEA中装上jclasslib插件就可以看到字节码文件了,View-Show Bytecode With Jclasslib就可以看到当前类的字节码文件啦)
阶段三(初始化阶段 Initialization):
- 初始化阶段就是执行类构造器方法 <clinit>()的过程。该方法体是编译器自动收集类中的所有类变量(静态成员)的赋值动作和静态代码块中的语句合并而来(简单的说:<clinit>()为static的动作的合并语句),<clinit>() 执行前,父类的<clinit>() 已经执行完毕。<clinit>() 方法在多线程下被同步加锁。
例:说明一下<clinit>()
在<clinit>()里可以看到,num -->1-->2,number -->20 -->10,按照顺序执行,除此之外,我们还发现声明可以放在后面,但是调用变量必须在声明后调用
为了说明<clinit>是被同步加锁的,来看下面的代码的执行结果:
public class ClinitThreadTest { public static void main(String[] args) { Thread t1 = new Thread("线程1"){ @Override public void run() { System.out.println(Thread.currentThread().getName() + "开始"); C c = new C(); System.out.println(Thread.currentThread().getName() + "结束"); } }; Thread t2 = new Thread("线程2"){ @Override public void run() { System.out.println(Thread.currentThread().getName() + "开始"); C c = new C(); System.out.println(Thread.currentThread().getName() + "结束"); } }; t1.start(); t2.start(); } } class C{ static { System.out.println(Thread.currentThread().getName() + "<clinit>()初始化类C"); try { Thread.currentThread().sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } }
执行结果为:
线程1开始 线程2开始 线程2<clinit>()初始化类C 线程2结束 线程1结束
解释一下代码:
创建了两个线程1、2,线程2进入到类C的static代码块时也就是执行<clinit>()方法,执行输出sysout语句,线程进入睡眠,sleep()不会释放锁,我们看到线程1没有进入<clinit>方法,说明<clinit>()方法被加锁了,而且等线程2将<clinit>()执行完后,线程1没有执行,说明线程1已经知道C的<clinit>方法被执行过了,由此也说明<clinit>不但被加锁,而且是被同步加锁的。(到这里就更加理解了:wow原来静态成员和代码块只能被加载一次是这么回事)
经过了类加载子系统后,将它们加载进了运行时数据区
小编来配一张图,更直观: