1.实例:参考部分博客和<java虚拟机>,用案例来说明java虚拟机的类加载过程。
class Sing{ private static Sing sg=new Sing(); public static int cou1; public static int cou2=0; public static int cou3=5; public int cou4; public int cou5=0; static { cou1++; cou2++; cou3++; } private Sing(){ cou1++; cou2++; cou3++; cou4++; cou5++; } public static Sing getSg(){ return sg; } } public class Main { public static void main(String[] args) { System.out.println(Sing.cou1); System.out.println(Sing.cou2); System.out.println(Sing.cou3); System.out.println(Sing.getSg().cou4); System.out.println(Sing.getSg().cou5); } }
结果:2,1,6,1,1。有了结果接下来分析虚拟机如何加载一个类。
2.虚拟机类加载过程
2.1类的加载过程和生命周期
(图转载)
类的生命周期指类从加载进内存开始,到卸载出内存为止。包括,加载,验证,准备,解析,初始化,使用,卸载。其中加载,验证,准备,初始化,卸载这5个过程的顺序是连续的,类的加载必须按照这个顺序,解析则不一定。
2.2加载
加载就是将程序加载进内存中。在加载阶段,虚拟机需要完成以下操作,
①获取定义此类的二进制字节流。
②将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构。
③在内存(堆)中生成一个代表这个类的对象,用来封装类在方法区中的数据结构,作为方法区中这个类的数据的访问接口。
我们可以通过class来获取类的各种数据。
2.3验证
这一步的目的是为了保证class文件的字节流中包含的信息符合虚拟机的要求。
保证class文件是由java文件编译产生的。
2.4准备
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段。
①类变量:static 修饰的变量。
②初始值:不同数据类型对应的“零值”,初始化并不赋值。
准备阶段是为静态变量分配内存空间,并且对其初始化“零值”。不包括静态代码块和实例变量,静态代码块在下面的初始化阶段执行,实例对象会在对象初始化的时候一起分配到堆中。
2.5解析
解析是虚拟机将常量池内的符号引用替换为直接引用的过程。
①符号引用,用无歧义符号定位目标,与虚拟机实现的内存布局无关,引用目标不一定加载到内存中。
②直接引用,直接指向目标的指针,与虚拟机实现的内存布局相关,引用的目标必然在内存中存在。
2.6初始化
初始化阶段,才真正开始执行类中定义的java程序代码。
在准备阶段,静态变量已经被赋值过“零值”(系统零值),在初始化阶段,为类的静态变量赋予程序中指定的初始值,还有执行静态代码块。
初始化阶段实际是执行类构造器方法(<cinit>())方法,并不是类构造方法,它由编译器自动收集类中所有的静态变量的赋值动作和静态代码块中的语句。编译器的收集顺序是语句在程序中出现的顺序所决定的,静态代码块只能访问在它之前出现的静态变量,定义在静态代码块之后的变量,可以赋值,但不能访问。
<cinit>()
方法与类的构造函数不同,它不需要显示的调用父类的构造器,虚拟机会保证在子类的<cinit>()
方法执行前,父类的<cinit>()
方法已经执行完毕,所以在虚拟机中第一个被执行的<cinit>()
方法的类肯定是java.lang.Object。
由于父类的<cinit>()
方法先执行,也就意味着父类中定义的静态代码块要优先于子类的静态变量赋值操作。
类加载的最终产品是位于堆中的Class对象,封装了类在方法区内的数据结构,并向java程序员提供了访问方法区内数据结构的接口。
类加载完毕,到目前只能使用类的静态变量和静态方法,类对象需要被实例化,有了对象才能访问普通数据和变量。