Java虚拟机类加载机制——案例分析

原文出处: 朱小厮

在《Java虚拟机类加载机制》一文中详细阐述了类加载的过程,并举了几个例子进行了简要分析,在文章的最后留了一个悬念给各位,这里来揭开这个悬念。建议先看完《Java虚拟机类加载机制》这篇再来看这个,印象会比较深刻,如若不然,也没什么关系~~
下面是程序代码:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

package jvm.classload;

public class StaticTest

{

    public static void main(String[] args)

    {

        staticFunction();

    }

    static StaticTest st = new StaticTest();

    static

    {

        System.out.println("1");

    }

    {

        System.out.println("2");

    }

    StaticTest()

    {

        System.out.println("3");

        System.out.println("a="+a+",b="+b);

    }

    public static void staticFunction(){

        System.out.println("4");

    }

    int a=110;

    static int b =112;

}

问题是:请问这段程序的输出是什么?
这个是我在论坛上看到的一个问题,我觉得比较金典。
一般对于这类问题,小伙伴们脑海中肯定浮现出这样的knowledge:

Java中赋值顺序: 
1. 父类的静态变量赋值 
2. 自身的静态变量赋值 
3. 父类成员变量赋值 
4. 父类块赋值 
5. 父类构造函数赋值 
6. 自身成员变量赋值 
7. 自身块赋值 
8. 自身构造函数赋值

ok,按照这个理论输出是什么呢?答案输出:1 4,这样正确嚒?肯定不正确啦,这里不是说上面的规则不正确,而是说不能简单的套用这个规则。
正确的答案是:


1

2

3

4

5

2

3

a=110,b=0

1

4

是不是有点不可思议?且听我一一道来,这里主要的点之一:实例初始化不一定要在类初始化结束之后才开始初始化。
类的生命周期是:加载->验证->准备->解析->初始化->使用->卸载,只有在准备阶段和初始化阶段才会涉及类变量的初始化和赋值,因此只针对这两个阶段进行分析;
类的准备阶段需要做是为类变量分配内存并设置默认值,因此类变量st为null、b为0;(需要注意的是如果类变量是final,编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将变量设置为指定的值,如果这里这么定义:static final int b=112,那么在准备阶段b的值就是112,而不再是0了。)
类的初始化阶段需要做是执行类构造器(类构造器是编译器收集所有静态语句块和类变量的赋值语句按语句在源码中的顺序合并生成类构造器,对象的构造方法是<init>(),类的构造方法是<clinit>(),可以在堆栈信息中看到),因此先执行第一条静态变量的赋值语句即st = new StaticTest (),此时会进行对象的初始化,对象的初始化是先初始化成员变量再执行构造方法,因此打印2->设置a为110->执行构造方法(打印3,此时a已经赋值为110,但是b只是设置了默认值0,并未完成赋值动作),等对象的初始化完成后继续执行之前的类构造器的语句,接下来就不详细说了,按照语句在源码中的顺序执行即可。
这里面还牵涉到一个冷知识,就是在嵌套初始化时有一个特别的逻辑。特别是内嵌的这个变量恰好是个静态成员,而且是本类的实例。
这会导致一个有趣的现象:“实例初始化竟然出现在静态初始化之前”。
其实并没有提前,你要知道java记录初始化与否的时机。
看一个简化的代码,把关键问题解释清楚:


1

2

3

4

5

6

7

public class Test {

    public static void main(String[] args) {

        func();

    }

    static Test st = new Test();

    static void func(){}

}

根据上面的代码,有以下步骤:

  1. 首先在执行此段代码时,首先由main方法的调用触发静态初始化。
  2. 在初始化Test 类的静态部分时,遇到st这个成员。
  3. 但凑巧这个变量引用的是本类的实例。
  4. 那么问题来了,此时静态初始化过程还没完成就要初始化实例部分了。是这样么?
  5. 从人的角度是的。但从java的角度,一旦开始初始化静态部分,无论是否完成,后续都不会再重新触发静态初始化流程了。
  6. 因此在实例化st变量时,实际上是把实例初始化嵌入到了静态初始化流程中,并且在楼主的问题中,嵌入到了静态初始化的起始位置。这就导致了实例初始化完全至于静态初始化之前。这也是导致a有值b没值的原因。
  7. 最后再考虑到文本顺序,结果就显而易见了。

详细看到这里,心中大概有个结论了吧,如果对于类的加载机制比较模糊的话,可以参考开篇推荐的博文~ 有问题欢迎留言。

时间: 2024-10-07 02:17:37

Java虚拟机类加载机制——案例分析的相关文章

[转]Java虚拟机类加载机制浅谈

Java语言是一种编译后再经过解释器执行的过程, 解释器主要就是如何处理解释Class文件的二进制字节流.JVM主要包含三大核心部分:运行时数据区,类加载器和执行引擎. 虚拟机将描述类的数据从Class文件加载到内存,并对数据进行校验.准备.解析和初始化,最终就会形成可以被虚拟机使用的Java类型,这就是一个虚拟机的类加载机制.Java中的类是动态加载的,只有在运行期间使用到该类的时候,才会将该类加载到内存中,Java依赖于运行期动态加载和动态链接来实现类的动态使用. 一个类的整个生命周期如下:

学习java虚拟机 - 类加载机制

学习java虚拟机 - 类加载机制  一.是什么 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 在Java语言里面,类型的加载.链接.初始化过程都是在程序运行期间完成的,Java里天生可以动态扩展的语言特性就是依赖运行期间动态加载和动态连接这个特点实现的.例如,如果编写一个面向接口的应用程序,可以等到运行时在制定实际的实现类:用户可以通过Java预定义的和自定义类加载器,让一个本地的应用程序

java虚拟机类加载机制和双亲委派模型

java虚拟机类加载机制:虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的java类型. 类的生命周期是从类被加载到虚拟机内存中,到卸载出内存为止: 类的生命周期: 加载 loading . 验证 verification. 准备 preparation. 解析 resolution. 初始化 initialization. 使用 using. 卸载 unloading 类加载器的层次结构: 双亲委派模型过程: 某个特定的类加载器

Java虚拟机类加载机制

原文出处: 朱小厮 看到这个题目,很多人会觉得我写我的java代码,至于类,JVM爱怎么加载就怎么加载,博主有很长一段时间也是这么认为的.随着编程经验的日积月累,越来越感觉到了解虚拟机相关要领的重要性.闲话不多说,老规矩,先来一段代码吊吊胃口. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public cla

Java虚拟机 - 类加载机制

[深入Java虚拟机]之四:类加载机制 类加载过程     类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载.验证.准备.解析.初始化.使用和卸载七个阶段.它们开始的顺序如下图所示: 其中类加载的过程包括了加载.验证.准备.解析.初始化五个阶段.在这五个阶段中,加载.验证.准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定).另外注意这里的几个阶段是按顺序开

JAVA 虚拟机类加载机制和字节码执行引擎

引言 我们知道java代码编译后生成的是字节码,那虚拟机是如何加载这些class字节码文件的呢?加载之后又是如何进行方法调用的呢? 一 类文件结构 无关性基石 java有一个口号叫做一次编写,到处运行.实现这个口号的就是可以运行在不同平台上的虚拟机和与平台无关的字节码.这里要注意的是,虚拟机也是中立的,只要是符合规范的字节码,都可以被虚拟机接受,例如Groovy,JRuby等语言,都会生成符合规范的字节码,然后被虚拟机所运行,虚拟机不关心字节码由哪种语言生成. 类文件结构 class类文件是一组

Java虚拟机——类加载机制

转自:http://blog.csdn.net/ns_code/article/details/17881581 类加载过程 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载.验证.准备.解析.初始化.使用和卸载七个阶段.它们开始的顺序如下图所示: 其中类加载的过程包括了加载.验证.准备.解析.初始化五个阶段.在这五个阶段中,加载.验证.准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时

深入了解java虚拟机---类加载机制----加载

加载是类加载的一个阶段.不要弄混淆了 这一阶段主要做了下面三件事.1.通过一个类的全限定名获取此类的二进制流 2.将这个二进制流代表的静态存储结构转化为方法区的运行时数据结构 3.在内存中生成java.lang.Class对象,作为访问入口 .通过一个类的全限定名获取此类的二进制流:这一步说的不明确,要怎样获取呢.java虚拟机没有指明.所以就产生了很多种获取方法 1.从zip,jar,war包获取 2.从网络中获取 3.运行时生成,如动态代理 4.有其他文件生成 如jsp 5.从数据库中获取,

深入了解java虚拟机---类加载机制----初始化阶段

准备阶段是给变量赋系统的初始值.而初始化阶段就是给变量赋程序员自己设的值 初始化阶段是<clinit>()方法的执行过程.过程如下 1.编辑器收集所有的赋值动作和静态块合并形成<clinit>方法.收集是按照代码出现的顺序决定的.并且静态初始化块只能访问定义在之前的变量,对于在之后的变量可以赋值,但是不能访问 2.先执行父类的<clinit>方法,再执行本类的<clinit>方法.而且不需要显式调用,虚拟机会保证父类<clinit>方法先执行.虚