http://www.importnew.com/25295.html
加载
加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个Class文件获取,这里既可以从ZIP包中读取(比如从jar包和war包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将JSP文件转换成对应的Class类)。
验证
这一阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。注意这里所说的初始值概念,比如一个类变量定义为:
1
public static int v = 8080;
实际上变量v在准备阶段过后的初始值为0而不是8080,将v赋值为8080的putstatic指令是程序被编译后,存放于类构造器方法之中,这里我们后面会解释。
但是注意如果声明为:
1
public static final int v = 8080;
在编译阶段会为v生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将v赋值为8080。
解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是class文件中的:
CONSTANT_Class_info
CONSTANT_Field_info
CONSTANT_Method_info
等类型的常量。
下面我们解释一下符号引用和直接引用的概念:
符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。
初始化
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。
初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证方法执行之前,父类的方法已经执行完毕。p.s: 如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。
注意以下几种情况不会执行类初始化:
通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
定义对象数组,不会触发该类的初始化。
常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
通过类名获取Class对象,不会触发类的初始化。
通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
通过ClassLoader默认的loadClass方法,也不会触发初始化动作。
JAVA类的加载机制
Java类加载分为5个过程,分别为:加载,连接(验证,准备,解析),初始化,使用,卸载。
加载
加载主要是将.class文件(也可以是zip包)通过二进制字节流读入到JVM中。 在加载阶段,JVM需要完成3件事:
1)通过classloader在classpath中获取XXX.class文件,将其以二进制流的形式读入内存。
2)将字节流所代表的静态存储结构转化为方法区的运行时数据结构;
3)在内存中生成一个该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2.1. 验证
主要确保加载进来的字节流符合JVM规范。验证阶段会完成以下4个阶段的检验动作:
1)文件格式验证
2)元数据验证(是否符合Java语言规范)
3)字节码验证(确定程序语义合法,符合逻辑)
4)符号引用验证(确保下一步的解析能正常执行)
2.2. 准备
准备是连接阶段的第二步,主要为静态变量在方法区分配内存,并设置默认初始值。
2.3. 解析
解析是连接阶段的第三步,是虚拟机将常量池内的符号引用替换为直接引用的过程。
初始化
初始化阶段是类加载过程的最后一步,主要是根据程序中的赋值语句主动为类变量赋值。
当有继承关系时,先初始化父类再初始化子类,所以创建一个子类时其实内存中存在两个对象实例。
注:如果类的继承关系过长,单从类初始化角度考虑,这种设计不太可取。原因我想你已经猜到了。
通常建议的类继承关系最多不超过三层,即父-子-孙。某些特殊的应用场景中可能会加到4层,但就此打住,第4层已经有代码设计上的弊端了。
使用
程序之间的相互调用。
卸载
即销毁一个对象,一般情况下中有JVM垃圾回收器完成。代码层面的销毁只是将引用置为null。
通过上面的整体介绍后,再来看Singleton2.getInstance()的执行分析:
1)类的加载。运行Singleton2.getInstance(),JVM在首次并没有发现Singleton类的相关信息。所以通过classloader将Singleton.class文件加载到内存中。
2)类的验证。略
3)类的准备。将Singleton2中的静态资源转化到方法区。value1,value2,singleton在方法区被声明分别初始为0,0,null。
4)类的解析。略(将常量池内的符号引用替换为直接引用的过程)
5)类的初始化。执行静态属性的赋值操作。按照顺序先是value1 = 5,value2 = 3,接下来是private static Singleton2 singleton2 = new Singleton2();
这是个创建对象操作,根据 结论1 在执行Singleton2的构造方法之前,先去执行static资源和非static资源。但由于value1,value2已经被初始化过,所以接下来执行的是非static的资源,最后是Singleton2的构造方法:value1++;value2++。
所以Singleton2结果是6和4。
以上除了搞清楚执行顺序外,还有一个重点->结论2:静态资源在类的初始化中只会执行一次。不要与第3个步骤混淆。
有了以上的这个结论,再来看Singleton.getInstance()的执行分析:
1)类的加载。将Singleton类加载到内存中。
2)类的验证。略
3)类的准备。将Singleton2的静态资源转化到方法区。
4)类的解析。略(将常量池内的符号引用替换为直接引用的过程)
5)类的初始化。执行静态属性的赋值操作。按照顺序先是private static Singleton singleton = new Singleton(),根据 结论1 和结论2,value1和value2不会在此层执行赋值操作。所以singleton对象中的value1,value2只是在0的基础上进行了++操作。此时singleton对象中的value1=1,value2=1。
然后, public static int value1 = 5; public static int value2 = 3; 这两行代码才是真的执行了赋值操作。所以最后的结果:5和3。
如果执行的是public static int value1; public static int value2;结果又会是多少?结果: 1和1。
注:为什么 Singleton singleton = new Singleton()不会对value1,value2进行赋值操作?因为static变量的赋值在类的初始化中只会做一次。
程序在执行private static Singleton singleton = new Singleton()时,已经是对Singleton类的static变量进行赋值操作了。这里new Singleton()是一个特殊的赋值,类似于递归里层,外层已经是赋值操作了,所以里层会自动过滤static变量的赋值操作。但非static的变量依然会被赋值。
结论3:在结论2的基础上,非静态资源会随对象的创建而执行初始化。每创建一个对象,执行一次初始化。
掌握结论1,2,3基本对java类中程序执行的顺序了如指掌。这还不够,ClassLoader还没有介绍呢。
类加载器
JVM提供了以下3种系统的类加载器:
启动类加载器(Bootstrap ClassLoader):最顶层的类加载器,负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
应用程序类加载器(Application ClassLoader):也叫做系统类加载器,可以通过getSystemClassLoader()获取,负责加载用户路径(classpath)上的类库。如果没有自定义类加载器,一般这个就是默认的类加载器。
这3种类加载器先了解一下是什么即可,由于篇幅和主题限制,类加载器将会在下篇文件详细介绍。
作者:nerowu
链接:https://www.jianshu.com/p/202f6abb229c
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。