前一节我们讲解了JVM类加载的5个阶段,主要是加载、验证、准备、解析和初始化。
加载阶段主要完成3件事情:
1.通过类的全限定名来获取定义此类的二进制字节流
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3.在内存中生成一个代表这个类的java.lang.Class对像,作为方法区这个类的各种数据的访问入口。
需要注意的是,加载与连接阶段的部分额呢绒时交叉进行的,可能加载并未结束,连接可能已经开始了。
验证阶段的主要步骤:
1.文件格式验证 ,这个主要基于字节流操作
2.元数据验证。 针对方法区的存储结构进行操作
3.字节码验证 针对方法区的存储结构进行操作
4.符号引用验证 针对方法区的存储结构进行操作
准备阶段的主要步骤:
准备阶段主要是分配内存并设置类变量的初始值,这些变量所使用的将会在本地方法区中分配。这里的设置初始值指的是类型的0值,针对于一下代码
public static int value=123;
这里的初始值是0而不是123,将value赋予值123,是在类的初始化阶段。
正对于有final修饰的静态变量,在开始指定值得时候,就被设置为代码中指定的值,假设
public static final int value=123;
这里的value将会一开始就设置为123.
解析阶段:
这一阶段是将常量池的符号引用替换为直接应用的过程。
类和接口的解析
字段解析
类方法解析
接口方法的解析
初始化:
静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句快可以复制,但是不能访问
package cn.edu.hust.jvm; public class MainTest { static { i=0; System.out.println(i); } public static int i=1; public static void main(String[] args) { // System.out.println(SubClass.value); // SubClass[] tt=new SubClass[10]; System.out.println(Constant.s); } }
上面的语句,IDE已经开始报错了。
初始化阶段时执行类构造器的<clinit>()fanff的过程,这个方法是编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。
<clinit>的方法与类的构造器不同,它不需要先显示的调用父类构造器,虚拟机会保证子类的<clinit>方法执行之前,父类的方法已经执行完毕。所以在虚拟机中,第一个执行的一定是object类的<clinit>方法。
以下是一个测试的小例子:
package cn.edu.hust.jvm; public class SuperClass { public static int value=123; static { value=10; System.out.println("父类被初始化"); } } package cn.edu.hust.jvm; public class SubClass extends SuperClass{ public static int t=value; static { System.out.println("子类初始化"); } } package cn.edu.hust.jvm; public class MainTest { public static void main(String[] args) { // System.out.println(SubClass.value); // SubClass[] tt=new SubClass[10]; System.out.println(SubClass.t); } }
答应出来的结果是
可以看出父类的<clinit>()方法执行,然后再是子类。
如果一个类没有静态语句块,这个类可以没有<clinit>方法。
接口的<clinit>()不需要先执行父接口的<clinit>方法。
针对于<clinit>()方法,这里可以在多线程环境中被正确的加锁、同步,只有一个线程去初始化一个类。