【J2SE】为什么静态内部类的单例可以实现延迟加载

版权声明:本文为原创文章,转载时请注明出处;文章如有错漏,烦请不吝指正,谢谢! https://blog.csdn.net/reliveIT/article/details/52874833

一、单例

    单例是一个常见的设计模式,常见有四种方式来实现,即懒汉式、饿汉式、枚举和静态内部类实现,这个模式的本质是为了控制内存中某个类的实例数量。

    懒汉式采用懒加载,时间换空间,因此需要注意获取实例时的并发安全问题,即便正确并发,每次获取实例的时候还是要浪费一次判断;饿汉式空间换时间,在定义单例对象时就完成实例化,因为JVM在初始化一个类的时候(即调用类构造函数<clinit>())会自动同步,因此不用关心线程安全问题,但是一旦完成类加载过程,无论是否使用该单例,该单例都已经实际占用内存;枚举可以做天然的单例,枚举的思想本质就是该类的实例可以穷举,像季节、性别这种实例可以穷举的类型,然而枚举和饿汉式有一样的缺点,只要加载无论是否使用单例,都会占用内存,但是枚举的构造函数通过反射获取到以后再newInstance是非法的(见例一),因此枚举实现的单例相较之懒汉式和饿汉式,无需在私有的构造函数中再进行单例的判断从而控制构造函数被非法反射调用,即在私有构造函数中省略了if(instance != null){抛异常}。

    至于单例类是否需要对反序列化进行控制的问题,一般单例类都是作为工具类来使用,不需要序列化,因此不需要实现java.io.Serializable接口;特殊情况下,如果单例类实现了序列化接口,只需要再readResolve方法中返回单例即可。

    静态内部类实现单例,一是解决了懒加载线程安全问题(类加载的三个步骤加载、链接、初始化,即调用类构造函数<clinit>()初始化时JVM会自动同步)和获取单例时的判断问题;二是解决了饿汉式和枚举在加载时无论是否使用就分配内存的问题;三是可以和懒加载、饿汉式一样通过在私有构造中判断单例是否为null来进行非法构造方法反射的控制。

    因此,静态内部类来实现单例,是相对较好的一种方式。需要提醒的是,本文是想深入讨论为什么性能好,在实际写项目的时候,大可不必吹毛求疵的追逐性能。

例一 枚举构造函数反射获取后调用newInstance非法

package cn.okc.demo;

public enum Gender {
	MALE, FEMALE;
}
package cn.okc.demo;

import java.lang.reflect.Constructor;

public class TestGender {
	public static void main(String[] args) throws Exception {
		Class<Gender> clazz = Gender.class;
		@SuppressWarnings("unchecked")
		Constructor<Gender>[] constructors = (Constructor<Gender>[]) clazz.getDeclaredConstructors();
		for (Constructor<Gender> c : constructors)
			System.out.println(c);

		Constructor<Gender> constructor = clazz.getDeclaredConstructor(String.class, int.class);
		constructor.setAccessible(true);
		Gender gender = constructor.newInstance("MALE", 0);
		System.out.println(gender);
	}
}
 
 
private cn.okc.demo.Gender(java.lang.String,int)
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at cn.wxy.demo.TestGender.main(TestGender.java:15)
 
 

二、静态内部类实现单例及延迟加载验证测试

例二 静态内部类实现单例示例代码

package cn.okc.demo;

public class Singleton {
	// 静态内部类实现单例
	private static class Inner {
		// 单例对象
		private static Singleton singleton = new Singleton();
		// 类加载分为加载、链接、初始化三大步骤
		// 其中链接又分为验证、准备和解析三小个步骤
		// 类中静态的内容在编译阶段都会被编译到类构造函数<clinit>()中,在初始化步骤调用
		// 因此这个代码块的调用标志着内部类被初始化了
		static {
			System.out.println("内部类被解析了");
		}
	}

	// 私有化构造函数
	private Singleton() {
		// 判断单例对象是否已经存在,用于控制非法反射单例类的构造函数
		if (Inner.singleton != null)
			try {
				throw new IllegalAccessException("单例对象已经被实例化,请不要非法反射构造函数");
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			}
	}

	// 合法获取单例对象的途径
	public static Singleton getInstance() {
		return Inner.singleton;
	}
}

例三 延迟加载测试(HotSpot)


-----------------------------------------------------------------------------------------------------------------------------


    如例三所示,外部类被成功加载并初始化,此时并未导致内部类也跟着被初始化,如果内部类被初始化,则内部类的静态块会被执行并输出。


三、详解

    为什么静态内部类单例可以实现延迟加载?实际上是外部类被加载时内部类并不需要立即加载内部类,内部类不被加载则不需要进行类初始化,因此单例对象在外部类被加载了以后不占用内存。

    实际上,无论是外部类还是静态内部类,对JVM而言,他们是平等的两个InstanceClass对象,只存在访问修饰符限制访问权限的问题,不存在谁包含谁的问题。

    从字节码来窥探静态内部类单例延迟加载,需要从类加载的时机和字节码常量池解析的时机两个方面来得到答案。

1. 窥探字节码

例四 外部类字节码

Classfile /D:/dev_code/workspace_neon/jdbc/target/classes/cn/okc/demo/Singleton.class
  Last modified 2016-10-20; size 856 bytes
  MD5 checksum 845fd5779231adacc4cebe26f2515a66
  Compiled from "Singleton.java"
public class cn.wxy.demo.Singleton
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Class              #2             // cn/wxy/demo/Singleton
   #2 = Utf8               cn/wxy/demo/Singleton
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Methodref          #3.#9          // java/lang/Object."<init>":()V
   #9 = NameAndType        #5:#6          // "<init>":()V
  #10 = Methodref          #11.#13        // cn/wxy/demo/Singleton$Inner.access$0:()Lcn/wxy/demo/Singleton;
  #11 = Class              #12            // cn/wxy/demo/Singleton$Inner
  #12 = Utf8               cn/wxy/demo/Singleton$Inner
  #13 = NameAndType        #14:#15        // access$0:()Lcn/wxy/demo/Singleton;
  #14 = Utf8               access$0
  #15 = Utf8               ()Lcn/wxy/demo/Singleton;
  #16 = Class              #17            // java/lang/IllegalAccessException
  #17 = Utf8               java/lang/IllegalAccessException
  #18 = String             #19            // 单例对象已经被实例化,请不要非法反射构造函数
  #19 = Utf8               单例对象已经被实例化,请不要非法反射构造函数
  #20 = Methodref          #16.#21        // java/lang/IllegalAccessException."<init>":(Ljava/lang/String;)V
  #21 = NameAndType        #5:#22         // "<init>":(Ljava/lang/String;)V
  #22 = Utf8               (Ljava/lang/String;)V
  #23 = Methodref          #16.#24        // java/lang/IllegalAccessException.printStackTrace:()V
  #24 = NameAndType        #25:#6         // printStackTrace:()V
  #25 = Utf8               printStackTrace
  #26 = Utf8               LineNumberTable
  #27 = Utf8               LocalVariableTable
  #28 = Utf8               this
  #29 = Utf8               Lcn/wxy/demo/Singleton;
  #30 = Utf8               e
  #31 = Utf8               Ljava/lang/IllegalAccessException;
  #32 = Utf8               StackMapTable
  #33 = Utf8               getInstance
  #34 = Utf8               (Lcn/wxy/demo/Singleton;)V
  #35 = Methodref          #1.#9          // cn/wxy/demo/Singleton."<init>":()V
  #36 = Utf8               SourceFile
  #37 = Utf8               Singleton.java
  #38 = Utf8               InnerClasses
  #39 = Utf8               Inner
{
  public static cn.wxy.demo.Singleton getInstance();
    descriptor: ()Lcn/wxy/demo/Singleton;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: invokestatic  #10                 // Method cn/wxy/demo/Singleton$Inner.access$0:()Lcn/wxy/demo/Singleton;
         3: areturn
      LineNumberTable:
        line 30: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature

  cn.wxy.demo.Singleton(cn.wxy.demo.Singleton);
    descriptor: (Lcn/wxy/demo/Singleton;)V
    flags: ACC_SYNTHETIC
    Code:
      stack=1, locals=2, args_size=2
         0: aload_0
         1: invokespecial #35                 // Method "<init>":()V
         4: return
      LineNumberTable:
        line 18: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
}
SourceFile: "Singleton.java"

例五 静态内部类字节码

Classfile /D:/dev_code/workspace_neon/jdbc/target/classes/cn/wxy/demo/Singleton$Inner.class
  Last modified 2016-10-20; size 772 bytes
  MD5 checksum 87afaa7bf2981e8a99d143ee3e01054f
  Compiled from "Singleton.java"
class cn.wxy.demo.Singleton$Inner
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Class              #2             // cn/wxy/demo/Singleton$Inner
   #2 = Utf8               cn/wxy/demo/Singleton$Inner
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               singleton
   #6 = Utf8               Lcn/wxy/demo/Singleton;
   #7 = Utf8               <clinit>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Class              #11            // cn/wxy/demo/Singleton
  #11 = Utf8               cn/wxy/demo/Singleton
  #12 = Methodref          #10.#13        // cn/wxy/demo/Singleton."<init>":(Lcn/wxy/demo/Singleton;)V
  #13 = NameAndType        #14:#15        // "<init>":(Lcn/wxy/demo/Singleton;)V
  #14 = Utf8               <init>
  #15 = Utf8               (Lcn/wxy/demo/Singleton;)V
  #16 = Fieldref           #1.#17         // cn/wxy/demo/Singleton$Inner.singleton:Lcn/wxy/demo/Singleton;
  #17 = NameAndType        #5:#6          // singleton:Lcn/wxy/demo/Singleton;
  #18 = Fieldref           #19.#21        // java/lang/System.out:Ljava/io/PrintStream;
  #19 = Class              #20            // java/lang/System
  #20 = Utf8               java/lang/System
  #21 = NameAndType        #22:#23        // out:Ljava/io/PrintStream;
  #22 = Utf8               out
  #23 = Utf8               Ljava/io/PrintStream;
  #24 = String             #25            // 被解析了
  #25 = Utf8               被解析了
  #26 = Methodref          #27.#29        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #27 = Class              #28            // java/io/PrintStream
  #28 = Utf8               java/io/PrintStream
  #29 = NameAndType        #30:#31        // println:(Ljava/lang/String;)V
  #30 = Utf8               println
  #31 = Utf8               (Ljava/lang/String;)V
  #32 = Utf8               LineNumberTable
  #33 = Utf8               LocalVariableTable
  #34 = Methodref          #3.#35         // java/lang/Object."<init>":()V
  #35 = NameAndType        #14:#8         // "<init>":()V
  #36 = Utf8               this
  #37 = Utf8               Lcn/wxy/demo/Singleton$Inner;
  #38 = Utf8               access$0
  #39 = Utf8               ()Lcn/wxy/demo/Singleton;
  #40 = Utf8               SourceFile
  #41 = Utf8               Singleton.java
  #42 = Utf8               InnerClasses
  #43 = Utf8               Inner
{
  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=3, locals=0, args_size=0
         0: new           #10                 // class cn/wxy/demo/Singleton
         3: dup
         4: aconst_null
         5: invokespecial #12                 // Method cn/wxy/demo/Singleton."<init>":(Lcn/wxy/demo/Singleton;)V
         8: putstatic     #16                 // Field singleton:Lcn/wxy/demo/Singleton;
        11: getstatic     #18                 // Field java/lang/System.out:Ljava/io/PrintStream;
        14: ldc           #24                 // String 被解析了
        16: invokevirtual #26                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        19: return
      LineNumberTable:
        line 7: 0
        line 13: 11
        line 14: 19
      LocalVariableTable:
        Start  Length  Slot  Name   Signature

  static cn.wxy.demo.Singleton access$0();
    descriptor: ()Lcn/wxy/demo/Singleton;
    flags: ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #16                 // Field singleton:Lcn/wxy/demo/Singleton;
         3: areturn
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
}
SourceFile: "Singleton.java"

2. 类加载的时机和字节码常量池解析的时机

    弄清楚什么时候加载一个类,才能弄清楚什么时候静态内部类会被加载。

    这部分参考《深入理解java虚拟机》第七章和《java虚拟机规范》内容,其实主要关注例四外部类字节码第19行常量池中Methodref从符号引用解析成直接引用的时机,这个时机可以验证实在运行时,而不是在加载过程中;第55行,invokestatic(这个方法会导致类加载)调用内部类自动生成的方法例五75行access$0(见77行访问标识符ACC_SYNTHETIC,这个标识符表示内容不在原文件中,而是由虚拟机生成),但是这里要关注的是方法在运行被调用才会生成方法栈(参看《深入理解java虚拟机》第八章内容)。

    简而言之:加载的时候方法不会被调用,不会触发外部类getInstance方法中invokestatic指令对内部类进行加载;加载的时候字节码常量池会被加入类的运行时常量池——其中类加载的解析步骤又叫常量池解析,主要是将常量池中的符号引用解析成直接引用,但是这个解析过程不一定非得在类加载时完成,可以延迟到运行时进行——这时候和静态内部类有关的Methodref符号解析会延迟到运行时;因此,静态内部类实现单例参会延迟加载。

    后续有空再详细补充。。。。。。


四、参考资料

1. 《java虚拟机规范》

2. 《深入理解java虚拟机》

3. 《研磨设计模式》

4. 《HotSpot实战》


附注:

    本文如有错漏,烦请不吝指正,谢谢!

猜你喜欢

转载自blog.csdn.net/reliveIT/article/details/52874833