当java虚拟机将java源码编译为字节码之后,虚拟机便可以将字节码读取进内存,从而进行解析,运行等整个过程,在这个过程中我们叫:java虚拟机的类加载机制。JVM虚拟机执行class字节码的过程可以分为七个阶段:加载,验证,准备,解析,初始化,使用,卸载。

在开始之前,先给大家看一道面试题。
class Grandpa{
static{
System.out.println("爷爷在静态代码块");
}
}
class Father extends Grandpa{
static{
System.out.println("爸爸在静态代码块");
}
public static int factor = 25;
public Father(){
System.out.println("我是爸爸~");
}
}
class Son extends Father{
static {
System.out.println("儿子在静态代码块");
}
public Son(){
System.out.println("我是儿子~");
}
}
public class InitializationDemo{
public static void main(String[] args){
System.out.println("爸爸的岁数:" + Son.factor); //入口
}
}
请写出最后的输出字符串。
正确答案是:
爷爷在静态代码块
爸爸在静态代码块
爸爸的岁数:25
我相信很多同学看到这个答案之后,是很疑惑的。也许会有人问为什么没有输出「儿子在静态代码块」这个字符串?
其实这种面试题考察的就是你对Java类加载机制的理解。
加载
把代码数据加载到内存中。JVM的主要目的是将字节码从各个位置转化为二进制字节码流加载到内存中,接着会为这个类在JVM的方法区创建一个对应的Class对象,这个Class对象就是这个类各种数据的访问入口。
验证
验证的主要作用就是确保被加载的类的正确性。也是连接阶段的第一步。说白了也就是我们加载好的.class文件不能对我们的虚拟机有危害,所以先检测验证一下。
准备
当完成字节码文件的校验之后,JVM便会为类变量分配内存并初始化。这里需要主要两个关键点,即内存分配的对象以及初始化的类型。
1.内存分配的对象。java中的变量有类变量和类成员变量两种类型,类变量指的是被static修饰的变量,而其他所有类型的变量都属于类成员变量。在准备阶段,jvm只会为类变量分配内存,而不会为类成员变量分配内存。类成员变量的内存分配需要等到初始化阶段才开始。
解析
这个阶段对于我们来说也是几乎透明的,了解一下就好.
初始化
到了初始化阶段,用户定义的java程序代码才真正开始执行。在这个阶段,JVM会根据语句执行顺序对类进行初始化,一般来说当JVM遇到下面5中情况的时候会触发初始化:
1.遇到new,getStatic,putStatic,invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这四条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段的时候,以及调用一个类的静态方法的时候
2.使用reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3.当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化
4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类)虚拟机会先初始化这个主类
使用
当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。这个阶段也只是了解一下就可以
卸载
当用户程序代码执行完毕后,jvm便开始销毁创建的class对象,最后负责运行的jvm也退出内存。
现在我们回到刚才开头面试题,为何没有输出儿子在静态代码块这个字符串?
这是因为对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块)。因此通过此类来引用父类中的静态字段,只会触发父类的初始化而不会触发子类的初始化。
下面我们分析下:
1.首先程序到 main 方法这里,使用标准化输出 Son 类中的 factor 类成员变量,但是 Son 类中并没有定义这个类成员变量。于是往父类去找,我们在 Father 类中找到了对应的类成员变量,于是触发了 Father 的初始化。
2.但根据我们上面说到的初始化(当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化)。我们需要先初始化 Father 类的父类,也就是先初始化 Grandpa 类再初始化 Father 类。于是我们先初始化 Grandpa 类输出:「爷爷在静态代码块」,再初始化 Father 类输出:「爸爸在静态代码块」。
3.最后,所有父类都初始化完成之后,Son 类才能调用父类的静态变量,从而输出:「爸爸的岁数:25」
我们在来看一个稍微复杂点的例子,看看输出结果是啥。
class Grandpa{
static{
System.out.println("爷爷在静态代码块");
}
public Grandpa() {
System.out.println("我是爷爷~");
}
}
class Father extends Grandpa{
static{
System.out.println("爸爸在静态代码块");
}
public Father(){
System.out.println("我是爸爸~");
}
}
class Son extends Father{
static {
System.out.println("儿子在静态代码块");
}
public Son(){
System.out.println("我是儿子~");
}
}
public class InitializationDemo{
public static void main(String[] args) {
new Son(); //入口
}
}
输出结果是
爷爷在静态代码块
爸爸在静态代码块
儿子在静态代码块
我是爷爷~
我是爸爸~
我是儿子~
让我们仔细来分析一下上面代码的执行流程:
首先在入口这里我们实例化一个 Son 对象,因此会触发 Son 类的初始化,而 Son 类的初始化又会带动 Father 、Grandpa 类的初始化,从而执行对应类中的静态代码块。因此会输出:「爷爷在静态代码块」、「爸爸在静态代码块」、「儿子在静态代码块」。
当 Son 类完成初始化之后,便会调用 Son 类的构造方法,而 Son 类构造方法的调用同样会带动 Father、Grandpa 类构造方法的调用,最后会输出:「我是爷爷」、「我是爸爸」、「我是儿子~」。
看完了两个例子之后,相信大家都胸有成足了吧。
下面给大家看一个特殊点的例子,有点难哦!
public class Book {
public static void main(String[] args) {
staticFunction();
}
static Book book = new Book();
static{
System.out.println("书的静态代码块");
}
{
System.out.println("书的普通代码块");
}
Book(){
System.out.println("书的构造方法");
System.out.println("price=" + price +",amount=" + amount);
}
public static void staticFunction(){
System.out.println("书的静态方法");
}
int price = 110;
static int amount = 112;
}
上面这个例子的输出结果是
书的普通代码块
书的构造方法
price=110,amount=0
书的静态代码块
书的静态方法
下面我们一步步来分析一下代码的整个执行流程。
当 JVM 在准备阶段的时候,便会为类变量分配内存和进行初始化。此时,我们的 book 实例变量被初始化为 null,amount 变量被初始化为 0。
当进入初始化阶段后,因为 Book 方法是程序的入口,根据我们上面说到的类初始化的五种情况的第四种(当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类)。所以JVM 会初始化 Book 类,即执行类构造器 。
JVM 对 Book 类进行初始化首先是执行类构造器(按顺序收集类中所有静态代码块和类变量赋值语句就组成了类构造器 ),后执行对象的构造器(按顺序收集成员变量赋值和普通代码块,最后收集对象构造器,最终组成对象构造器 )。
对于 Book 类,其类构造方法()可以简单表示如下:
static Book book = new Book();
static{
System.out.println("书的静态代码块");
}
static int amount = 112;
于是首先执行static Book book = new Book();这一条语句,这条语句又触发了类的实例化。于是 JVM 执行对象构造器 ,收集后的对象构造器 代码:
{
System.out.println("书的普通代码块");
}
int price = 110;
Book(){
System.out.println("书的构造方法");
System.out.println("price=" + price +", amount=" + amount);
}
于是此时 price 赋予 110 的值,输出:「书的普通代码块」、「书的构造方法」。而此时 price 为 110 的值,而 amount 的赋值语句并未执行,所以只有在准备阶段赋予的零值,所以之后输出「price=110,amount=0」。
当类实例化完成之后,JVM 继续进行类构造器的初始化:
static Book book = new Book(); //完成类实例化
static{
System.out.println("书的静态代码块");
}
static int amount = 112;
即输出:「书的静态代码块」,之后对 amount 赋予 112 的值。
到这里,类的初始化已经完成,JVM 执行 main 方法的内容。
public static void main(String[] args){
staticFunction();
}
即输出:「书的静态方法].
注:内部类和静态内部类都是延时加载的,也就是说只有在明确用到内部类时才加载。只使用外部类时不加载。
网友评论