摘要
经常看到java面试题static,构造函数等混合执行,问会输出什么,这里针对类的加载及类的生命周期进行原理的解析,就能很快明白了。
java类的加载顺序
简单的说,首先要知道Java虚拟机对class文件的加载是运行时加载的,所以对于static修饰(指这个类第一次出现的时候就会先加载)的也是按运行顺序加载的。
先看个简单的例子:
后面会根据这例子进行探索java虚拟机类的生命周期,也可以直接看后面的总结
public class StaticA {
public StaticA(){
System.out.println("construction A");
}
static{
System.out.println("static A");
}
}
public class StaticB {
public StaticB(){
System.out.println("construction B");
}
static {
System.out.println("static B");
}
}
public class Main {
public static void main(String[] args) {
StaticA A = new StaticA();//第一步
StaticB B = new StaticB();//第二步
}
}
//执行结果为:
static A
construction A
static B
construction B
分析
编译完后,所有class文件已经生成
首先加载运行的是Main 类,加载完后并执行完(验证->准备->解析->初始化)后,开始执行main函数,按顺序执行指令
按指令顺序先执行到StaticA A = new StaticA();
这时第一次遇到StaticA class就会开始先进行加载,在初始化阶段首先执行类构造器的
<clint>
方法,在在方法内首先执行static对象的赋值及static{}代码块的执行,所以先输出static A当类构造器
<clint>
方法执行完后,开始执行实例构造器的<init>
方法,这时才会执行先执行{}构造代码块,再执行构造方法,所以输出construction A同上分析,main函数执行到第二步时第一次遇到StaticB,同理先输出static B,再输出construction B
类的生命周期简单分析
基本概念
编译
首先Java在编译期间仅仅是将源码编译为Java虚拟机可以识别的字节码Class类文件Java虚拟机对中Class类文件的加载、连接都在运行时执行
生命周期图
加载
Java虚拟机把Class类文件加载到内存中,并对Class文件中的数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型的过程。在加载阶段,java虚拟机需要完成以下3件事:
通过一个类的全限定名来获取定义此类的二进制字节流。
将定义类的二进制字节流所代表的静态存储结构转换为方法区的运行时数据结构。
在java堆中生成一个代表该类的java.lang.Class对象,作为方法区数据的访问入口。
加载阶段与连接阶段是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,这些夹在加载阶段之中进行的动作仍然属于连接阶段,加载和连接阶段仍然保持着固定的先后顺序。
验证
验证是连接阶段的第一步,其目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的安全,如果验证失败,会抛出java.lang.VerifyError异常。验证阶段的主要工作有:
文件格式验证:验证Class文件魔数、主次版本、常量池、类文件本身等等。
元数据验证:主要是对字节码描述的信息进行语义分析,包括是否有父类、是否是抽象类、是否是接口、是否继承了不允许被继承的类(final类)、是否实现了父类或者接口的方法等等。
字节码验证:是整个验证过程中最复杂的,主要进行数据流和控制流分析,如保证跳转指令不会跳转到方法体之外的字节码指令、数据类型转换安全有效等。
符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候(连接第三阶段-解析阶段进行符号引用转换为直接引用),符号引用验证的目的是确保解析动作能正常执行,如果无法通过符号引用验证,则会抛出java.lang.IncompatibleClassChangeError异常的子类异常,如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。
准备
准备阶段是正式为类变量(静态变量,注意不是实例变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。对于普通非final的类变量,如public static int value = 123;在准备阶段过后的初始值是0(数据类型的零值),而不是123,而把123赋值给value是在初始化阶段才进行的动作。
对于final的类变量,即常量,如public staticfinal int value =123;在准备阶段过程的初始值直接就是123了,不需要准备为零值。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用(SymbolicReference):以一组符号来描述所引用的目标,与虚拟机内存布局无关,引用的目标不一定已经被加载到虚拟机内存中。
直接引用(DirectReference):可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局相关,同一个符号引用在不同虚拟机上翻译处理的直接引用不一定相同,如果有了直接引用,则引用的目标对象必须已经被加载到虚拟机内存中。
解析的动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行解析。
初始化
初始化是类使用前的最后一个阶段,在初始化阶段java虚拟机真正开始执行类中定义的java程序代码。Java虚拟机规范严格规定了有且只有以下四种情况必须立即对类进行初始化:
遇到new、获取静态变量(final常量除外)、为静态变量赋值以及调用静态方法时,如果类没有进行过初始化,则需要先触发其初始化。
使用java.lang.reflect包的方法对类进行反射调用的时候(Class.forName(…)),如果类还没有初始化,需要先触发对其的初始化。
当初始化一个类的时候,如果发现其父类还没有初始化,则需要先触发对其父类的初始化。
.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
初始化的过程其实就是一个执行类构造器
<clint>
方法的过程,类构造器执行的特点和注意事项:类构造器
<clint>
方法是由编译器自动收集类中 所有类变量(静态非final变量)赋值动作和静态初始化块(static{……})中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定。静态初始化块中只能访问到定义在它之前的类变量,定义在它之后的类变量,在前面的静态初始化中可以赋值,但是不能访问。类构造器
<clint>
方法与实例构造器<init>
方法不同,它不需要显式地调用父类构造器方法,虚拟机会保证在调用子类构造器方法之前,父类的构造器<clinit>
方法已经执行完毕。由于父类构造器
<clint>
方法先与子类构造器执行,因此父类中定义的静态初始化块要先于子类的类变量赋值操作。类构造器方法对于类和接口并不是必须的,如果一个类中没有静态初始化块,也没有类变量赋值操作,则编译器可以不为该类生成类构造器
<clint>
方法。接口中不能使用静态初始化块,但可以有类变量赋值操作,因此接口与类一样都可以生成类构造器
<clint>
方法。java虚拟机会保证一个类的
<clint>
方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,只会有一个线程去执行这个类的<clint>
方法,其他线程都需要阻塞等待,直到活动线程执行<clint>
方法完毕。初始化阶段,当执行完类构造器
<clint>
方法之后,才会执行实例构造器的<init>
方法,实例构造方法同样是按照先父类,后子类,先成员变量,后实例构造方法的顺序执行。
使用
当初始化完成之后,java虚拟机就可以执行Class的业务逻辑指令,通过堆中java.lang.Class对象的入口地址,调用方法区的方法逻辑,最后将方法的运算结果通过方法返回地址存放到方法区或堆中。卸载
当对象不再被使用时,java虚拟机的垃圾收集器将会回收堆中的对象,方法区中不再被使用的Class也要被卸载,否则方法区(Sun HotSpot永久代)会内存溢出。
总结
知识点:
java是运行时加载的
先执行类构造器
<clint>
方法针对static 变量的赋值及static{}代码块的执行同样类构造器
<clint>
方法,也得遵循先执行父类的类构造器<clint>
方法,再执行自己本身的类构造器<clint>
方法当类构造器的
<clint>
方法执行完后,接着执行的就是实例构造器的<init>
方法在实例构造器的
<init>
方法先针对执行{}构造代码块再始执行构造方法,也得遵循先执行父类先,子类后的原则
最终可得出以下结论:
(1) 父类静态对象和静态代码块
(2) 子类静态对象和静态代码块
(3) 父类非静态对象和非静态代码块
(4) 父类构造函数
(5) 子类 非静态对象和非静态代码块
(6) 子类构造函数
还有需要注意的是对于一个类,多个此类的对象,其这个类的static对象或static{}代码块只会被执行一次(只要这个类没有被卸载),因为static修饰的对象引用是放在方法区的,只要这个引用的对象有没被GC就会一直存在,简单的说类构造器的<clint>
一般情况下只会执行一次,而类的实例构造器的<init>
方法只要有申请一个对象就会执行。
分析到这里就可以赶快找几个题目练练手吧~