美文网首页
Java类的初始化时机

Java类的初始化时机

作者: NoahU | 来源:发表于2018-04-10 17:09 被阅读113次

    一个Java对象的创建过程往往包括 类初始化 和 类实例化 两个阶段。本文讨论的是『类初始化』的时机,以及利用这一特点实现单例模式的方法。

    概述

    我们知道,一个.java文件在编译后会形成相应的一个或多个Class文件(若一个类中含有内部类,则编译后会产生多个Class文件),但这些Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能被运行和使用。事实上,虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程就是虚拟机的 类加载机制

    类加载的时机

    Java类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:

    • 加载(Loading)
    • 验证(Verification)
    • 准备(Preparation)
    • 解析(Resolution)
    • 初始化(Initialization)
    • 使用(Using)
    • 卸载(Unloading)

    这七个阶段。其中准备、验证、解析3个部分统称为连接(Linking),如图所示:

    加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。

    那么现在来回答这个问题:虚拟机什么时候才会加载Class文件并初始化类呢?

    什么情况下虚拟机需要开始加载一个类呢?虚拟机规范中并没有对此进行强制约束,这点可以交给虚拟机的具体实现来自由把握。

    而类初始化时机比较复杂,下面我们具体来说。

    类初始化时机

    在虚拟机规范中是有严格规定的,虚拟机规范指明 有且只有 五种情况必须立即对类进行初始化(而这一过程自然发生在加载、验证、准备之后):

    1. 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先对其进行初始化。生成这四条指令的最常见的Java代码场景是:

      • 使用new关键字实例化对象的时候;
      • 读取或设置一个类的静态字段(被final修饰,已在编译器把结果放入常量池的静态字段除外)的时候;
      • 调用一个类的静态方法的时候
    2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

    3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

    4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

    5. 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化

    注意,对于这五种会触发类进行初始化的场景,虚拟机规范中使用了一个很强烈的限定语:“有且只有”,这五种场景中的行为称为对一个类进行 主动引用。除此之外,所有引用类的方式,都不会触发初始化,称为 被动引用。

    主动使用的简易版本说明

    类的初始化时机就是在"在首次主动使用时",那么,哪些情形下才符合首次主动使用的要求呢?首次主动使用的情形:

    • 创建某个类的新实例时--new、反射、克隆或反序列化;
    • 调用某个类的静态方法时;
    • 使用某个类或接口的静态字段或对该字段赋值时(final字段除外);
    • 调用Java的某些反射方法时
    • 初始化某个类的子类时
    • 在虚拟机启动时某个含有main()方法的那个启动类。

    除了以上几种情形以外,所有其它使用JAVA类型的方式都是被动使用的,他们不会导致类的初始化。

    被动引用的几种经典场景

    1. 通过子类引用父类的静态字段,不会导致子类初始化
    2. 通过数组定义来引用类,不会触发此类的初始化:newarray指令触发的只是数组类型本身的初始化,而不会导致其相关类型的初始化,比如,new String[]只会直接触发String[]类的初始化,也就是触发对类[Ljava.lang.String的初始化,而直接不会触发String类的初始化
    3. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

    实现单例模式

    利用类初始化的特点,我们可以实现线程安全的单例模式(不使用synchronized)。

    饿汉式 static final field

    这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。

    public class Singleton{
        //类加载时就初始化
        private static final Singleton instance = new Singleton();
        
        private Singleton(){}
        public static Singleton getInstance(){
            return instance;
        }
    }
    

    这种写法的缺点是它不是一种懒加载模式(lazy initialization),单例会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法。
    饿汉式的创建方式在一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。

    静态内部类 static nested class

    下面这种方法既是线程安全的,又是Lazy加载的。

    public class Singleton {  
        private static class SingletonHolder {  
            private static final Singleton INSTANCE = new Singleton();  
        }  
        private Singleton (){}  
        public static final Singleton getInstance() {  
            return SingletonHolder.INSTANCE; 
        }  
    }
    

    这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。

    参考资料

    相关文章

      网友评论

          本文标题:Java类的初始化时机

          本文链接:https://www.haomeiwen.com/subject/warnhftx.html