澳门新浦京娱乐游戏两道面试题带你解析 Java 类加载机制

澳门新浦京娱乐游戏 6

在许多Java面试中,我们经常会看到关于Java类加载机制的考察,例如下面这道题:

前言

YY:Kitty,我最近在看Thinking in Java
这本书Kitty:喔?是么,你不是一直觉得那本书又厚又乏味,代码还非常不用户友好,难以阅读,而总是停留在第一章么,这次不会还是停留在第一章吧YY:好啦,这不人家感觉Java基础还是需要打打扎实么,所以就只能硬着头皮看这本被誉为Java届圣经的神书咯!好不容易下定决心一定要认认真真看,你就别拿人家打趣儿了Kitty:好样的嘛,那祝你收获大大的咯!YY:嘿嘿!昨天看了第五章–初始化和清理,才发现写了那么久的Java,都还没认真研究过从点击“运行”那一刻开始,我们的代码都是以怎样的规则和顺序一条一条被JVM执行的呢,你有想过这个问题么?Kitty:呃呃。。。这个嘛,还真没考虑过,那么你说说是怎么回事呗!YY:好呀,正好我昨天写了一个小Demo帮助我理解,那么我就用这个小Demo和你讲讲,顺便巩固一下我的知识吧,要是有讲错的或者讲得不好的地方,你可得帮我指出来哈,不然我还糊里糊涂的以为自己都理解对了呢!Kitty:好的好的,木有问题(亲爱的读者朋友们,你们要是发现文中有不足之处,一定一定要给小编我指出来哈,在此先行谢过了_)YY:先上小Demo

<a name=”demo”>小Demo</a>

// 父类public class Father { // 非静态变量 C fa = new C; // 静态变量 static C fb = new C; // 静态语句块 static { OutUtil.print("Static blocks 1 in Father! "); } // 静态语句块 static { OutUtil.print("Static blocks 2 in Father! "); } // 静态常量 static final int T = 28; // 构造方法 public Father() { super(); OutUtil.print("Construct method in Father! "); } // 带参构造器 public Father(String name) { OutUtil.print("Construct method in Father! " + "Name = " + name); } // 非静态代码块 { OutUtil.print("Common blocks in Father! "); } // 静态方法 static void staticShow() { OutUtil.print("Static method in Father! "); } // 非静态方法 void show() { OutUtil.print("Common method in Father! "); }}

// 子类public class Child extends Father { // 非静态变量 C ca = new C; // 静态常量 static final int T = 28; // 构造方法 public Child() { super(); OutUtil.print("Construct method in Child! "); } // 带参构造器 public Child(String name) { OutUtil.print("Construct method in Child! " + "Name = " + name); } // 非静态代码块 { OutUtil.print("Common blocks in Child! "); } // 静态方法 static void staticShow() { OutUtil.print("Static method in Child! "); } // 非静态方法 void show() { OutUtil.print("Common method in Child! "); } // 静态变量 static C cb = new C; // 静态语句块 static { OutUtil.print("Static blocks 1 in Child! "); } // 静态语句块 static { OutUtil.print("Static blocks 2 in Child! "); }}

// 辅助类public class C { public static final String A = "A in C"; public static String showC() { return "showC method in C!"; } public C(){ OutUtil.print("Construct method in C!"); } public C(String msg){ OutUtil.print("Construct method in C! " + msg); }}

// 入口程序所在类public class Main { C ma = new C; // 打印结果显示ma并未进行初始化 static C mb = new C; public Main(){ OutUtil.print("I am Main!"); } static{ OutUtil.print(mb.getClass().getCanonicalName; } public static void main(String[] args) { OutUtil.print; Child child = new Child(); child.show(); OutUtil.print; OutUtil.print); } static Child mc = new Child;}

// 打印输出的工具类public class OutUtil { public static void print { System.out.println; }}

// 程序运行输出结果Construct method in C! mbclassloadertest.CConstruct method in C! fbStatic blocks 1 in Father! Static blocks 2 in Father! Construct method in C! cbStatic blocks 1 in Child! Static blocks 2 in Child! Construct method in C! faCommon blocks in Father! Construct method in Father! Construct method in C! caCommon blocks in Child! Construct method in Child! Name = cbMainConstruct method in C! faCommon blocks in Father! Construct method in Father! Construct method in C! caCommon blocks in Child! Construct method in Child! Common method in Child! A in CshowC method in C!

Kitty:控制台输出了好多语句呀,看得我眼睛都花了,你给我指点指点撒YY:OK,先给你画个图吧!

澳门新浦京娱乐游戏 1结果分析.PNG

YY:看了上面的图应该可以大致看出哪些语句被执行了,哪些语句没有被执行,下面再给你分析分析。

这篇文章主要讲解Java在创建对象的时候,初始化的顺序。主要从以下几个例子中讲解:

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);  //入口
    }
}

Demo分析

上面的小Demo中含有四个类:Father、Child、C和Main,其中Child类继承自Father类;C类是一个辅助类,主要用于打印输出,便于分析程序的运行流程;Main类是Java程序入口方法main方法所在的类,也是Java程序运行时加载的第一个类。

下面根据上图分析该Demo的运行流程。从图中可以看出,程序首先执行了Main类中的静态域和静态代码块(静态域和静态代码块的具体执行顺序依代码中定义的顺序而定),然后执行main方法中的代码,根据main方法中的代码需要,再加载所需要的类并进行初始化。

注:

  1. 整个程序执行过程中并没有执行Main类中的非静态域,也没有执行Main类的初始化方法,由此可知,并没有实例化Main类;
  2. 图中第一个大矩形框执行的是Main类最后一行语句,即初始化静态域mc。从矩形框中可以看出创建一个类实例的流程:a.
    首先执行父类中的静态域和静态代码块(静态域和静态代码块的具体执行顺序依代码中定义的顺序而定);b.
    然后执行子类中的静态域和静态代码块;c.
    然后执行父类中的非静态域和非静态代码块;d.
    接着执行父类的初始化方法(如果子类在构造函数中明确指明了调用父类的哪一个构造函数,则调用相应的构造函数,否则调用父类的无参构造函数)e.
    然后执行子类中的非静态域和非静态代码块;d.
    最后执行子类的初始化方法(即被调用的那一个构造函数)。
  3. static Child mc = new Child和Child child = new
    Child()这两句都是实例化一个Child对象,从下图可以看出,类中的静态域只进行了一次初始化操作,而非静态域则进行了两次初始化,由此可知,一个类中的静态域和静态代码块只会在类第一次加载时进行初始化,而非静态域和非静态代码块则会在每一次实例化时均执行。

澳门新浦京娱乐游戏 2对象实例化

Kitty:那也可能是因为上面的代码中第一次是初始化一个静态实例,第二次只是初始化一个非静态实例呀?YY:问得很好,要解开这个疑惑很简单,我们变一下程序流程就好了。

public class Main { C ma = new C; static C mb = new C; public Main(){ OutUtil.print("I am CallbackMain!"); } static{ OutUtil.print(mb.getClass().getName; } public static void main(String[] args) { OutUtil.print; Child mc = new Child; Child child = new Child(); child.show(); OutUtil.print; OutUtil.print); } // static Child mc = new Child;}

澳门新浦京娱乐游戏 3对象实例化

YY:上面的代码和图显示了同时在main方法中实例化两个Child对象的执行流程,可以看到上面的第3点观察结论是正确的–类中的静态域只会在类第一次加载时执行,而非静态域则会在每一次实例化时均执行。Kitty:确实是这样的呀,看来!

YY:下面用一个流程图简易表示Java中对象的初始化顺序以加深记忆吧!Java中,没有显式使用extends继承的类都默认继承自Object类,也就是说,除Object类以外,每个类都会有一个显式或隐式的父类,并且任何对象的初始化都会自Object类开始。

澳门新浦京娱乐游戏 4对象初始化顺序

Kitty:好啦,通过你的小Demo演示以及你的观察结论,我已经大致清楚了Java程序的执行流程以及在Java中,创建一个对象会经历哪些过程了。可是。。。YY:可是什么???Kitty:你不说还好,被你这么一说吧,我的脑子里冒出了一堆???YY:哈哈,有问号说明你还在思考,说说都有哪些疑问,我们一起把它们变成!!!呗Kitty:我现在主要有一下几个困惑:

  1. 为什么一个对象的初始化过程是这样的呢?
  2. 上面的Demo显示Father类和Child类会执行非静态域、非静态初始化块和构造方法,但是Main类中的非静态域与初始化方法却并未执行,这又是怎么一回事呢?
  3. 那么,Java中一个类的初始化过程又是怎样的呢,和对象的初始化过程又有什么区别和联系?
  4. static关键字好神奇的样子,可是该怎么用呢?

YY:问题已经抛出来了,接下来就开始解决它们。

  • 继承关系中初始化顺序
  • 初始化块与构造器的顺序
  • 已经加载过的类的初始化顺序
  • 澳门新浦京娱乐游戏,加载父类,会不会加载子类
  • 创建子类对象会不会创建父类对象

请写出最后的输出字符串。

预备知识

YY:看样子你的Java基础也不咋地呀,那么先给你介绍几个基本概念作为预备知识热热身吧,不然后面你又该冒???了

  • 普通代码块:在方法或语句中出现的 {}
    就称为普通代码块,普通代码块和一般的语句执行顺序由他们在代码中出现的次序决定–“先出现先执行”

  • 构造块:直接在类中定义且没有加static关键字的代码块 {}
    称为构造代码块。用来初始化每一个对象的非静态变量,构造块在每次创建
    **对象 **时均会被调用,并且 **构造块的执行次序优先于类构造函数
    **;

  • 静态代码块: 在 Java 中使用 static 关键字声明的代码块 {}称为静态代码块。静态块用于初始化类,为类的属性初始化。每个静态代码块只会在Class对象首次加载时执行一次。由于JVM在加载类时会执行静态代码块,所以静态代码块先于主方法执行。如果类中包含多个静态代码块,那么将按照
    **”先出现先执行” **的顺序执行。

    • 静态代码块不能存在于任何方法体内
    • 静态代码块不能直接访问非静态实例变量和实例方法,需要通过类的实例对象来访问
    • 即便没有显式使用使用static关键字,构造器实质上也是静态方法(出自Thinking
      in Java)
  • JVM中的内存区域: Java 程序执行时需要先被 JVM
    加载进内存,为了提高程序运算效率, JVM
    会将不同类型的数据加载进内存中的不同区域,因为每一片区域均有不同的内存管理方式和数据处理方式!JVM
    中几个比较重要的区域为:

    • 程序计数器:每个线程拥有一个PC寄存器,在线程创建时创建,指向下一条指令的地址,执行本地方法时,PC的值为undefined
    • 方法区:保存装载的类信息,如类型的常量池、字段、方法信息、方法字节码等,通常和永久区关联在一起
    • 堆区:用于存放类的对象实例,为所有线程所共享
    • 栈区:
      也叫java虚拟机栈,是由一个一个的栈帧组成的后进先出的栈式结构,栈桢中存放方法运行时产生的局部变量、方法出口等信息。当调用一个方法时,虚拟机栈中就会创建一个栈帧存放这些数据,当方法调用完成时,栈帧消失,如果方法中调用了其他方法,则继续在栈顶创建新的栈桢。栈区是
      线程私有 的,生命周期和线程相同

YY:热身活动完成,进入正题!

例子1——继承关系中初始化顺序

先看简单的情况,看下面的例子:

public class Father {

    public String fatherVar = "父类构造块初始化";
    public static int fatherStaticVar;
    public int i;
    static {
        int i = 100;
        System.out.println("父类静态块初始化,i的值为" + i);
        System.out.println("父类静态变量初始化,fatherStaticVar的值为" + fatherStaticVar);
    }

    {
        System.out.println(fatherVar);
    }

    public Father(){
        System.out.println("父类构造函数的初始化,i的值" + i);
    }
}

public class Son extends Father {

    public String sonVar = "子类构造块初始化";
    public static int sonStaticVar;
    public int i;
    static {
        int i = 101;
        System.out.println("子类静态块初始化,i的值为" + i);
        System.out.println("子类静态变量初始化,sonStaticVar的值为" + sonStaticVar);
    }

    {
        System.out.println(sonVar);
    }

    public Son(){
        super();
        System.out.println("子类构造函数的初始化,i的值" + i);
    }

    public static void main(String[] args) {
        new Son();
    }
}

其执行的结果如下:

父类静态块初始化,i的值为100
父类静态变量初始化,fatherStaticVar的值为0
子类静态块初始化,i的值为101
子类静态变量初始化,sonStaticVar的值为0
父类构造块初始化
父类构造函数的初始化,i的值0
子类构造块初始化
子类构造函数的初始化,i的值0

按照结果,我们可以知道在有继承的时候,虽然是创建一个Son对象,但是JVM发现Son对象的类还没有装载,而Son类又继承自Father类,只有加载了Father类,才能加载Son类。于是加载Father类的时候,就会初始化一切静态变量和静态块。所以上文结果中第一行和第二行是父类静态变量和静态块初始化的结果,然后加载完Father类之后,又会加载Son类,同样是初始化Son类的静态块和静态变量,出现上文中第三行和第四行的结果。等这个2个类都加载完了,才开始创建Son对象,因为Son对象,显示调用了Father类的构造器,所以先执行Father类的构造器,出现第五行和第六行的结果,等Father类构造器执行完了,才执行后续Son构造器的内容,所以最后出现了第七行和第八行的结果。

正确答案是:

Java类的生命周期

澳门新浦京娱乐游戏 5Java类生命周期

当我们编写一个java的源文件后,经过编译会生成一个后缀名为 **.class
**的文件,这种文件叫做字节码文件,只有这种字节码文件才能够在 Java
虚拟机中运行, **Java
类的生命周期就是指一个class文件从加载到卸载的全过程 **,如上图所示。

  1. 装载:在装载阶段,JVM会通过一个类的全限定名获取描述此类的.class文件,然后通过这个.class文件将类的信息加载到
    JVM
    的方法区,然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类的信息的入口。虚拟机设计团队把加载动作放到
    JVM
    外部实现,以便让应用程序决定如何获取所需的类,实现这个动作的代码被称为
    “类加载器” 。至于何时加载一个类, JVM
    并没有一个统一的规范,所以不同的虚拟机可能采取不同的加载策略,有些虚拟机会选择
    在执行前就预先加载类 ,而另一些虚拟机则会在
    真正需要使用到一个类的时候才会加载 。但无论如何,一个类总是会在
    JVM
    “预期”到即将会使用之前被加载。常用的hotspot虚拟机采取的是懒加载原则,即等到真正需要使用到一个类时才加载这个类;
  2. 连接: JVM 将已读入的二进制文件合并到 JVM
    的运行时状态的过程,这个过程由 验证、准备和解析 三个子步骤构成

  1. 验证:确认该类型符合 Java 语言的语义,并且该类型不会危及 JVM
    的完整性,主要包括
    格式验证、元数据验证、字节码验证和符号引用验证 等;
  2. 准备:在准备阶段,JVM 为 类变量 (所谓类变量就是被 static
    关键字修饰的变量)分配内存,设置默认的初始值,(默认值的设置过程是通过将此片内存区清零实现的,即通过将对象内存设为二进制零值而一举生成)此时默认值设置如下,而非在代码中赋予的值(在准备阶段并不会执行
    Java 代码):
  • 基本类型(int、long、short、char、byte、boolean、float、double):
    默认值为0;
  • 引用类型: 默认值为 null;
  • 常量: 默认值为程序中设定的值,比如我们在程序中定义final static int a
    = 8,则准备阶段中a的初值就是8;

private static int a = 8; // 上面这句在准备阶段只会将 a 初始化为0,需要等到后面的初始化阶段,才会将 a 赋值为8private static final int A = 8;// 这句例外,因为上面这句表明 A 是一个编译期常量// 所以在编译阶段会为 A 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属性将 A 赋值为8
  1. 解析:
    在类的常量池中寻找类、接口、方法和字段的符号引用,将符号引用替换为直接引用的过程,实质上,在符号引用被程序首次使用以前,这个过程都是可选的。

    • 符号引用:使用一组符号来描述所引用的目标,可以是任何形式的字面常量,定义在Class文件格式中
    • 直接引用:可以是直接指向目标的指针、相对偏移量或能间接定位到目标的句柄

  1. 初始化:即为 **类变量 **赋予 **“正确”
    **的初始值的过程,(“正确”的初始值是指代码中希望这个类变量拥有的初始值)也就是上面的小Demo中的执行静态域和静态代码块的过程(后面详述类变量初始化过程);

  2. 对象生命:这个就很好理解了,一旦一个类完成了装载、连接和初始化这三个过程,这个类就随时可以被使用了,包括调用类变量、类方法以及实例化类等。每次对类进行实例化操作时都会创建该类的一个新的对象,开启该对象的生命周期。对象的生命周期包含三个阶段:

1).*** 对象实例化:***
即对象的初始化阶段,在本阶段完成对象初始化工作,回看上面的Demo,即执行类中的非静态域和非静态代码块部分,然后执行类的构造函数中的代码。具体流程为:当通过显式或隐式的方式创建一个类的实例时,JVM
会首先为该类及其所有超类中的实例变量在堆中分配内存,然后 JVM
会将该块内存空间清零,从而将实例变量初始化为 默认的初始值
(数字、字符和布尔型变量初始化为0,引用类型变量初始化为null),然后根据我们在代码中书写的内容,为实例变量赋予正确的初始值;完成对象实例化过程后,就可以通过该对象的引用使用对象了,如调用对象的方法、获取对象中某个域的值等;

2). *** 垃圾收集:*** 当一个对象不再被引用的时候,JVM
就可以将这个对象所占据的内存回收,从而使得该部分内存可以被再次使用,垃圾回收时机、策略等是一个非常复杂的过程,具体可以参见深入Java虚拟机一书;

3).
对象终结:当一个对象被垃圾收集器收集后,该对象就不复存在,也就是说该对象的生命周期结束;

  1. 类卸载:类卸载是类生命周期的最后一个过程,当程序不再引用某一类型时,那么这个类型就无法再对未来的计算过程产生影响,从而该类就可以被
    JVM 垃圾回收。

YY:上面大致描述了 Java
中一个类的生命周期流程,本文并不会对每个过程都进行深入细致的分析,那样的话会陷入到细节陷阱中无法自拔,如果你对哪个部分有疑惑或者感兴趣的话,可以去查阅相关资料详加了解,下面详细讲解一下
Java
类的初始化过程,毕竟这才是本文的重点嘛_Kitty:好的,好的!我现在正兴趣浓厚,快开始讲吧!YY:瞧你那猴急猴急的样,平时也没见你这么认真学习了!那我开始咯,你好好听哈。

例子2——初始化块与构造器的顺序

在上面的例子中,有2个语句块叫初始化块。在上文的结果中是初始化块的执行是先于构造器的,现在看一下把初始化块的内容放到构造器下面,会是什么的结果

public class InitBlock {

    public InitBlock(){
        System.out.println("构造器在执行......");
    }

    {
        System.out.println("初始化块1在执行......");
    }

    {
        System.out.println("初始化块2在执行......");
    }

    public static void main(String[] args) {
        new InitBlock();
    }
}

结果如下:

初始化块1在执行......
初始化块2在执行......
构造器在执行......

很显然,无论初始化块写在哪个地方,都是先于构造器执行的,但是初始化块之间的顺序是前面的先初始化,后面在初始化。

爷爷在静态代码块
爸爸在静态代码块
爸爸的岁数:25

Java类初始化

为了让一个类/接口被首次主动使用,在加载、连接过程完成后,JVM
会执行类初始化过程。前面已经简要介绍过,类初始化时会执行类变量初始化语句和静态语句块,将准备阶段赋予类变量的默认初始值替换为“正确”的初始化值。

Kitty:** 首次 **我知道,不就是第一次么,可是 主动使用
是个什么鬼?YY:别急撒,接下来就为你揭开它的神秘面纱

  • 主动使用:在 Java 中只有如下几种活动被视为主动使用
    • 创建类的新实例;
    • 调用类中声明的静态方法;
    • 操作类/接口中声明的 非常量 静态域;
    • 调用 Java 中的反射方法;
    • 初始化一个类的子类;
    • 指定一个类作为 JVM 启动时的初始化类,即 main 函数所在的类。
  • 被动使用:不属于上述六种情况的活动均被视之为被动使用

** 注:被动使用一个类时并不会触发类初始化过程**, 如

  • 非首次主动使用一个类时不会触发类的初始化过程,也就是所第二次主动使用一个类也不再会触发类的初始化
  • 使用一个类的非常量静态字段时,只有当该字段确实是由当前类/接口声明的时才可以称之为主动使用,否则是被动使用。比如说,当通过子类调用父类的
    public
    的非常量静态域时,对于子类来说这是被动使用,对于父类才是主动使用,所以会触发父类的初始化,而不会触发子类的初始化
  • 如果一个变量被 static 和 final
    同时修饰,并且使用一个编译期常量表达式进行初始化,那么对这样的字段的使用就不是对声明该字段的类的主动使用,因为
    Java编译器会把这样的字段解析成对常量的本地拷贝(该常量存在于引用者的字节码流中或者常量池中,或二者均有)
  • 定义一个类的对象数组时并不会触发该类的初始化

Kitty:哎呀,一会儿是主动使用,一会儿又不是主动使用的,这些抽象的概念一点都不好理解,我的脑子里现在就像小燕子说的了–全是浆糊了YY:抽象的东西确实不太好理解和记忆,下面沿用上面小
Demo 中出现的类写几个小例子,你就会有所理解了。

  • 非首次主动使用一个类时也不会触发类的初始化过程

public class InitTest { public static void main(String[] args) { Father.staticShow(); //首次主动使用 Father.staticShow(); //非首次主动使用 }}

程序输出为:

Construct method in C! fbStatic blocks 1 in Father! Static blocks 2 in Father! Static method in Father! Static method in Father! 

由输出可知,虽然在代码中先后两次执行了Father中的静态方法staticShow(),但是Father类的初始化过程只执行了一次。

  • 通过子类调用父类的静态域

public class InitTest { public static void main(String[] args) { OutUtil.print(Child.str); // str是Father类中的静态字符串,初始值为“str in Father” }}

程序输出为:

Construct method in C! fbStatic blocks 1 in Father! Static blocks 2 in Father! str in Father

输出显示,只执行了Father类的初始化代码,而未执行Child类的初始化代码。

  • 调用编译期常量不会触发类初始化

public class InitTest { public static void main(String[] args) { OutUtil.print; }}

程序输出为:

28

由此可见并未触发类Father的初始化操作。

  • 定义一个类的对象数组时并不会触发该类的初始化

public class InitTest { public static void main(String[] args) { Father[] cArray = new Father[8]; }}

执行上面这段代码后,控制台并未产生输出,这就说明并未初始化Father类。可以通过查看这段代码产生的字节码文件加以验证。

澳门新浦京娱乐游戏 6类字节码

由上图可以看到,首先执行了Object类的初始化方法,然后执行InitTest类中的main方法,其中anewarray指令为新数组分配空间,但并未触发类Father的初始化。

Kitty:喔!!!原来主动使用和被动使用是这样的呀,Java
加载类,对类进行初始化的时机为首次主动使用的时候,可是你还是没有讲 JVM
执行类初始化操作的具体流程呀。YY:OK,接下来就是了。

在 Java 类和接口的 class 文件中有一个只能够被 JVM
调用的<clinit>()方法,这个方法会将类/接口的所有类变量初始化语句和静态初始化块均收集起来,然后在需要执行类初始化操作时,JVM
便调用该方法为类变量赋予“正确”的初始值。具体由以下两个步骤构成:

  1. 如果类存在直接超类,并且直接超类还未被初始化,则先初始化超类;
  2. 如果类存在类初始化方法,则执行该初始化方法;在执行超类的初始化时也是通过这两个步骤完成,因此,程序中第一个被初始化的类永远是Object类,其次是被主动使用的类继承层次树上的所有类,超类总是先于子类被初始化。
  • 初始化接口时并不需要先初始化其父接口,只有使用父接口中定义的变量时,才会执行父接口的初始化动作
  • <clinit>()方法的代码并不会显式调用超类的<clinit>()方法,JVM
    在调用类的<clinit>()方法时会先确认超类的<clinit>()方法已经被正确调用
  • 为了防止多次执行<clinit>,JVM
    会确保<clinit>()方法在多线程环境下被正确的加锁同步执行。当有多个线程需要对一个类执行初始化操作时,只能由一个线程来执行,其它线程均处于等待状态,当活动线程执行完成后,必须通知其它线程
  • 并非所有的类均会在class文件中拥有<clinit>()方法,只有那些的确需要执行
    Java
    代码来赋予类变量正确的初始值的类才会有<clinit>()方法。下面几种情况下,类的class文件中不会包含<clinit>()方法:

    • 类中没有声明任何类变量,也没有包含静态初始化块;

public class Test1 { int a = 8; int add(){return ++a;}}
  • 虽然类声明了类变量,但是并没有明确使用类变量初始化语句或静态初始化语句对它们进行初始化;

public class Test2 { static int c;}
  • 类中仅包含static
    final变量的初始化语句,并且初始化语句是编译期常量表达式

public class Test3 { static final int A = 8; static final int B = A * 8;}

上面代码中,A和B均是编译时常量,JVM 在加载 test
类时,不会将A、B作为类变量保存到方法区,而是会被当做常量,被 Java
编译器特殊处理。因此,不需要<clinit>()方法来对它们进行初始化。

YY:OK,到此 Java
类的初始化部分就结束啦,下面由Kitty你来说说你的理解,然后再回顾一下上面的小
Demo呗。Kitty:好的,正好回顾一下,不然知识都是零零散散的,一下就忘了。

例子3——已经加载过的类的初始化顺序

更改一下例子1中的main方法,改成如下:

public static void main(String[] args) {
        new Father();
        System.out.println("=============");
        new Son();
    }

结果如下:

父类静态块初始化,i的值为100
父类静态变量初始化,fatherStaticVar的值为0
子类静态块初始化,i的值为101
子类静态变量初始化,sonStaticVar的值为0
父类构造块初始化
父类构造函数的初始化,i的值0
=============
父类构造块初始化
父类构造函数的初始化,i的值0
子类构造块初始化
子类构造函数的初始化,i的值0

结果很有意思,创建父类对象的时候,加载Father类,出现第一行和第二行的结果,但是这个竟然会还把子类的静态变量和静态块初始化?这个原因,例子4在说。
最后执行父类的构造器创建父类对象。当再创建子类的时候,发现父类和子类已经加载过了,所以不会再加载Father和Son类,只会调用父类的构造器,再执行后续子类构造器的内容,创建子类。

我相信很多同学看到这个题目之后,表情是崩溃的,完全不知道从何入手。有的甚至遇到了几次,仍然无法找到正确的解答思路。

Demo回顾

下面将 Java 程序入口类 Main 的代码提出来了,其余代码见小Demo

// 入口程序所在类public class Main { C ma = new C; // 打印结果显示ma并未进行初始化 static C mb = new C; public Main(){ OutUtil.print("I am Main!"); } static{ OutUtil.print(mb.getClass().getCanonicalName; } public static void main(String[] args) { OutUtil.print; Child child = new Child(); child.show(); OutUtil.print; OutUtil.print); } static Child mc = new Child;}

当前程序运行流程:

  1. main 方法所在的类为 Main, 所以 JVM 会先加载 Main类;
  2. 完成 Main 类的连接步骤,将 Main 类中的静态域 mb 和 mc 初始化为null;
  3. 初始化 Main 类,执行 Main 类中的静态块,此时会执行如下几句:

static C mb = new C;static{ OutUtil.print(mb.getClass().getName; }static Child mc = new Child;
  1. 执行 main 方法

static Child mc = new Child一句可以更加详细地说明类初始化流程。

  1. 首先,JVM 会加载该句所需要的类,因为 Child 类是 Father
    类的子类,所以首先加载 Father 类;
  2. 连接 Father 类;
  3. 初始化 Father 类(即会执行 Father 类中的静态块和静态域初始化语句);
  4. 加载 Child 类;
  5. 连接 Child 类;
  6. 初始化 Child 类;
  7. 执行 Father 类的非静态域初始化语句和构造块;
  8. 执行 Father 类的构造方法;
  9. 执行 Child 类的非静态域初始化语句和构造块;
  10. 执行 Child 类的构造方法。

例子4——加载父类,会不会加载子类

用一个崭新的例子来看看上面,创建父类的时候,为什么会打印出子类静态初始化执行的结果。

public class StaticFather {
    static{
        System.out.println("父类静态初始化块");
    }
}

public class StaticSon extends StaticFather{
    static {
        System.out.println("子类静态初始化块");
    }
}

public class Test {

    public static void main(String[] args) {
        new StaticFather();
    }
}

结果如下:

父类静态初始化块

这次就不会创建父类的时候,加载子类。例子3之所以出现这个原因
是因为main函数在子类中写的,要执行main函数必须要加载子类。只会加载子类之前要先加载父类,因为不加载父类,只加载子类,怎么让子类调用父类的方法和变量。但是加载父类不会加载子类,反正父类也调用不了子类的方法。

其实这种面试题考察的就是你对Java类加载机制的理解。

总结

  1. 只有在应用程序 首次主动使用 一个类时,JVM
    才会对这个类进行初始化;
  2. 类的生命周期主要有如下几个阶段:加载–连接–初始化–[对象生命]–卸载,其中对象生命阶段是可选的,也就是说,一旦完成类的加载、连接和初始化工作,就可以使用类了,当程序中不再有该类的引用时,就可以被
    JVM 回收,至此类生命周期结束;
  3. 对象的生命周期为:初始化–使用–回收–终结,对象的生命周期依赖于类的生命周期,只有当完成了类的加载、连接和初始化工作后,才会创建对象;
  4. Java
    类在进行初始化时,会先执行父类的初始化步骤,再执行子类的初始化,所以所有的类初始化工作均起始于
    Object 类

YY:Well done!Kitty:嘿嘿,疑惑消除,可以愉快地玩耍了~~~

例子5——创建子类对象会不会创建父类对象

做个实验,看一下创建子类对象的时候,到底会不会创建一个父类对象,先说结论:不会。从道理上讲,如果创建任何一个对象都要创建出一个他的父类对象的话,那么整个JVM虚拟机都是Object对象。看下面的实验:

public class ObjectFather {

    public void getInfo(){
        System.out.println(getClass().toString());
    }
}

public class ObjectSon extends ObjectFather{

    public ObjectSon(){
        super();
        super.getInfo();
    }

    public static void main(String[] args) {
        new ObjectSon();
    }
}

结果如下:

class com.byhieg.init.ObjectSon

可以看出来,创建子类对象时那个父类的Class还是子类的,也就是说创建子类对象并没有创建一个父类的对象,只是说调用了父类的构造器,对父类的属性进行初始化,并且给子类提供了一个super指示器去调用父类中那些变量和方法。
更详细的说,new一个对象实际上是通过一个new指令开辟一个空间,来存放对象。在new ObjectSon()的时候,就只有一个new指令,只会开辟一个空间,所谓初始化父类等等,都是在这个空间中有一个特殊的区域来存放这些数据,而super关键字就是提供了访问这个特殊区域的方法,通过super去访问这个特殊区域。
还可以比较super和this的hashcode来判断,结果必然是两者的hashcode是一致的。

如果你对Java加载机制不理解,那么你是无法解答这道题目的。

总结

至此,Java初始化的讲解到结束了,基本了覆盖了绝大多数情况中的初始化。

所以这篇文章,我先带大家学习Java类加载的基础知识,然后再实战分析几道题目让大家掌握思路。

下面我们先来学习下Java类加载机制的七个阶段。

Java类加载机制的七个阶段

当我们的Java代码编译完成后,会生成对应的 class
文件。接着我们运行java Demo命令的时候,我们其实是启动了JVM 虚拟机执行
class 字节码文件的内容。而 JVM 虚拟机执行 class
字节码的过程可以分为七个阶段:加载、验证、准备、解析、初始化、使用、卸载。

加载

下面是对于加载过程最为官方的描述。

加载阶段是类加载过程的第一个阶段。在这个阶段,JVM
的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在
JVM 的方法区创建一个对应的 Class 对象,这个 Class
对象就是这个类各种数据的访问入口。

其实加载阶段用一句话来说就是:把代码数据加载到内存中。这个过程对于我们解答这道问题没有直接的关系,但这是类加载机制的一个过程,所以必须要提一下。

验证

当 JVM 加载完 Class 字节码文件并在方法区创建对应的 Class 对象之后,JVM
便会启动对该字节码流的校验,只有符合 JVM 字节码规范的文件才能被 JVM
正确执行。这个校验过程大致可以分为下面几个类型:

  • JVM规范校验。JVM 会对字节流进行文件格式校验,判断其是否符合 JVM
    规范,是否能被当前版本的虚拟机处理。例如:文件是否是以 0x cafe bene开头,主次版本号是否在当前虚拟机处理范围之内等。
  • 代码逻辑校验。JVM 会对代码组成的数据流和控制流进行校验,确保 JVM
    运行该字节码文件后不会出现致命错误。例如一个方法要求传入 int
    类型的参数,但是使用它的时候却传入了一个 String
    类型的参数。一个方法要求返回 String
    类型的结果,但是最后却没有返回结果。代码中引用了一个名为 Apple
    的类,但是你实际上却没有定义 Apple 类。

当代码数据被加载到内存中后,虚拟机就会对代码数据进行校验,看看这份代码是不是真的按照JVM规范去写的。这个过程对于我们解答问题也没有直接的关系,但是了解类加载机制必须要知道有这个过程。

准备(重点)

当完成字节码文件的校验之后,JVM
便会开始为类变量分配内存并初始化。这里需要注意两个关键点,即内存分配的对象以及初始化的类型。

  • 内存分配的对象。Java
    中的变量有「类变量」和「类成员变量」两种类型,「类变量」指的是被
    static
    修饰的变量,而其他所有类型的变量都属于「类成员变量」。在准备阶段,JVM
    只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。

例如下面的代码在准备阶段,只会为 factor 属性分配内存,而不会为 website
属性分配内存。

public static int factor = 3;
public String website = "www.cnblogs.com/chanshuyi";
  • 初始化的类型。在准备阶段,JVM
    会为类变量分配内存,并为其初始化。但是这里的初始化指的是为变量赋予
    Java 语言中该数据类型的零值,而不是用户代码里初始化的值。

例如下面的代码在准备阶段之后,sector 的值将是 0,而不是 3。

public static int sector = 3;

但如果一个变量是常量(被 static final
修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。例如下面的代码在准备阶段之后,number
的值将是 3,而不是 0。

public static final int number = 3;

之所以 static final 会直接被复制,而 static
变量会被赋予零值。其实我们稍微思考一下就能想明白了。

两个语句的区别是一个有 final 关键字修饰,另外一个没有。而 final 关键字在
Java 中代表不可改变的意思,意思就是说 number
的值一旦赋值就不会在改变了。既然一旦赋值就不会再改变,那么就必须一开始就给其赋予用户想要的值,因此被
final 修饰的类变量在准备阶段就会被赋予想要的值。而没有被 final
修饰的类变量,其可能在初始化阶段或者运行阶段发生变化,所以就没有必要在准备阶段对它赋予用户想要的值。

解析

当通过准备阶段之后,JVM
针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7
类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用。

其实这个阶段对于我们来说也是几乎透明的,了解一下就好。

初始化(重点)

到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM
会根据语句执行顺序对类对象进行初始化,一般来说当 JVM 遇到下面 5
种情况的时候会触发初始化:

  • 遇到 new、getstatic、putstatic、invokestatic
    这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  • 使用 java.lang.reflect
    包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 当使用 JDK1.7 动态语言支持时,如果一个
    java.lang.invoke.MethodHandle实例最后的解析结果
    REF_getstatic,REF_putstatic,REF_invokeStatic
    的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

看到上面几个条件你可能会晕了,但是不要紧,不需要背,知道一下就好,后面用到的时候回到找一下就可以了。

使用

当 JVM 完成初始化阶段之后,JVM
便开始从入口方法开始执行用户的程序代码。这个阶段也只是了解一下就可以。

卸载

当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class
对象,最后负责运行的 JVM 也退出内存。这个阶段也只是了解一下就可以。

看完了Java的类加载机智之后,是不是有点懵呢。不怕,我们先通过一个小例子来醒醒神。

public class Book {
    public static void main(String[] args)
    {
        System.out.println("Hello ShuYi.");
    }

    Book()
    {
        System.out.println("书的构造方法");
        System.out.println("price=" + price +",amount=" + amount);
    }

    {
        System.out.println("书的普通代码块");
    }

    int price = 110;

    static
    {
        System.out.println("书的静态代码块");
    }

    static int amount = 112;
}

思考一下上面这段代码输出什么?

给你5分钟思考,5分钟后交卷,哈哈。

怎么样,想好了吗,公布答案了。

书的静态代码块
Hello ShuYi.

怎么样,你答对了吗?是不是和你想得有点不一样呢。

下面我们来简单分析一下,首先根据上面说到的触发初始化的5种情况的第4种(当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类),我们会进行类的初始化。

那么类的初始化顺序到底是怎么样的呢?

重点来了!

重点来了!

重点来了!

在我们代码中,我们只知道有一个构造方法,但实际上Java代码编译成字节码之后,是没有构造方法的概念的,只有类初始化方法 和
对象初始化方法 。

那么这两个方法是怎么来的呢?

  • 类初始化方法。编译器会按照其出现顺序,收集类变量的赋值语句、静态代码块,最终组成类初始化方法。类初始化方法一般在类初始化的时候执行。

上面的这个例子,其类初始化方法就是下面这段代码了:

    static
    {
        System.out.println("书的静态代码块");
    }
    static int amount = 112;
  • 对象初始化方法。编译器会按照其出现顺序,收集成员变量的赋值语句、普通代码块,最后收集构造函数的代码,最终组成对象初始化方法。对象初始化方法一般在实例化类对象的时候执行。

上面这个例子,其对象初始化方法就是下面这段代码了:

    {
        System.out.println("书的普通代码块");
    }
    int price = 110;
    System.out.println("书的构造方法");
    System.out.println("price=" + price +",amount=" + amount);

类初始化方法 和
对象初始化方法 之后,我们再来看这个例子,我们就不难得出上面的答案了。

但细心的朋友一定会发现,其实上面的这个例子其实没有执行对象初始化方法。

因为我们确实没有进行 Book 类对象的实例化。如果你在 main 方法中增加 new
Book() 语句,你会发现对象的初始化方法执行了!

感兴趣的朋友可以自己动手试一下,我这里就不执行了。

通过了上面的理论和简单例子,我们下面进入更加复杂的实战分析吧!

实战分析

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

也许会有人问为什么没有输出「儿子在静态代码块」这个字符串?

这是因为对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块)。因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

对面上面的这个例子,我们可以从入口开始分析一路分析下去:

  • 首先程序到 main 方法这里,使用标准化输出 Son 类中的 factor
    类成员变量,但是 Son
    类中并没有定义这个类成员变量。于是往父类去找,我们在 Father
    类中找到了对应的类成员变量,于是触发了 Father 的初始化。
  • 但根据我们上面说到的初始化的 5 种情况中的第 3
    种(当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化)。我们需要先初始化
    Father 类的父类,也就是先初始化 Grandpa 类再初始化 Father
    类。于是我们先初始化 Grandpa 类输出:「爷爷在静态代码块」,再初始化
    Father 类输出:「爸爸在静态代码块」。
  • 最后,所有父类都初始化完成之后,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
书的静态代码块
书的静态方法

下面我们一步步来分析一下代码的整个执行流程。

在上面两个例子中,因为 main 方法所在类并没有多余的代码,我们都直接忽略了
main 方法所在类的初始化。

但在这个例子中,main 方法所在类有许多代码,我们就并不能直接忽略了。

  • 当 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();
}

即输出:「书的静态方法」。

方法论

从上面几个例子可以看出,分析一个类的执行顺序大概可以按照如下步骤:

  • 确定类变量的初始值。在类加载的准备阶段,JVM
    会为类变量初始化零值,这时候类变量会有一个初始的零值。如果是被 final
    修饰的类变量,则直接会被初始成用户想要的值。
  • 初始化入口方法。当进入类加载的初始化阶段后,JVM 会寻找整个 main
    方法入口,从而初始化 main
    方法所在的整个类。当需要对一个类进行初始化时,会首先初始化类构造器(),之后初始化对象构造器()。
  • 初始化类构造器。JVM
    会按顺序收集类变量的赋值语句、静态代码块,最终组成类构造器由 JVM
    执行。
  • 初始化对象构造器。JVM
    会按照收集成员变量的赋值语句、普通代码块,最后收集构造方法,将它们组成对象构造器,最终由
    JVM 执行。

如果在初始化 main
方法所在类的时候遇到了其他类的初始化,那么就先加载对应的类,加载完成之后返回。如此反复循环,最终返回
main 方法所在类。

树义有话说

看完了上面的解析之后,再去看看开头那道题是不是觉得简单多了呢。很多东西就是这样,掌握了一定的方法和知识之后,原本困难的东西也变得简单许多了。

一时没有看懂也不要灰心,毕竟我也是用了不少的时间才弄懂的。不懂的话可以多看几遍。

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图