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

本文探讨了静态内部类实现单例模式的优势,解释了其如何实现延迟加载,避免了线程安全问题和不必要的内存占用。通过分析类加载时机、字节码常量池解析,揭示了静态内部类单例在运行时才进行方法调用和符号引用解析,从而实现延迟加载。静态内部类单例是一种兼顾性能和安全的实现方式。

一、单例

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

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

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

静态内部类实现单例,一是解决了懒加载线程安全问题(静态内部类实现单例,单利对象static修饰,所以不需要像懒加载在做null判断的时候进行同步,而是在类加载的第三个步骤初始化时由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实战》

 

附注:

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

评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值