【J2SE】為什麼靜態內部類的單例可以實現延遲載入
一、單例
單例是一個常見的設計模式,常見有四種方式來實現,即懶漢式、餓漢式、列舉和靜態內部類實現,這個模式的本質是為了控制記憶體中某個類的例項數量。
懶漢式採用懶載入,時間換空間,因此需要注意獲取例項時的併發安全問題,即便正確併發,每次獲取例項的時候還是要浪費一次判斷;餓漢式空間換時間,在定義單例物件時就完成例項化,因為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實戰》
附註:
本文如有錯漏,煩請不吝指正,謝謝!