单例对象的初始化时机:
上篇博文设计模式之单例模式给出了7种单例模式的实现方法,其中静态代码块与饿汉模式的本质一致,都归为饿汉模式。其中饿汉模式和枚举方式都属于立即加载,懒汉式和静态代码块属于延时加载。如何理解立即加载和延时加载,需要从类加载机制聊一下。
Java虚拟机的类加载过程主要有七个步骤:Loading、verification、preparation、resolution、initialization、using、unloading。翻译中文就是:加载,验证,准备,解析,初始化,使用和卸载。
如果不了解jvm虚拟机的,可以看我之前的文章
深入理解Java虚拟机 之 Java 内存区域
深入理解Java虚拟机 之 垃圾回收
深入理解Java虚拟机之性能监控与故障分析工具
深入理解Java虚拟机之Class文件结构
深入理解Java虚拟机之类加载机制
类什么时候加载:
类的加载是通过类加载器(Classloader)完成的,它既可以是立即加载[eagerly load](只要有其它类引用了它就加载)加载类,也可以是延时[lazy load](等到类初始化发生的时候才加载),由不同的JVM实现有关。
类什么时候初始化:
加载完类后,类的初始化就会发生,意味着它会初始化所有类静态成员,以下情况一个类被初始化:
- 实例通过使用new()关键字创建或者使用class.forName()反射,但它有可能导致ClassNotFoundException。
- 类的静态方法被调用
- 类的静态域被赋值
- 静态域被访问,而且它不是常量
- 在顶层类中执行assert语句
- 反射同样可以使类初始化,比如java.lang.reflect包下面的某些方法。
类是如何被初始化的:
现在我们知道什么时候触发类的初始化了,他精确地写在Java语言规范中。但了解清楚 域(fields,静态的还是非静态的)、块(block静态的还是非静态的)、不同类(子类和超类)和不同的接口(子接口,实现类和超接口)的初始化顺序也很重要类。
下面是类初始化的一些规则:
类从顶至底的顺序初始化,所以声明在顶部的字段的早于底部的字段初始化
- 超类早于子类和衍生类的初始化
- 如果类的初始化是由于访问静态域而触发,那么只有声明静态域的类才被初始化,而不会触发超类的初始化或者子类的初始化即使静态域被子类或子接口或者它的实现类所引用。
- 接口初始化不会导致父接口的初始化。
- 静态域的初始化是在类的静态初始化期间,非静态域的初始化时在类的实例创建期间。这意味这静态域初始化在非静态域之前。
- 非静态域通过构造器初始化,子类在做任何初始化之前构造器会隐含地调用父类的构造器,他保证了非静态或实例变量(父类)初始化早于子类。
单例模式与类加载:
回到单例,饿汉模式属于立即加载模式在类一旦加载就会就会实例化单例对象。不管有没有使用到该单例类,都会导致单例对象在内存中存在,知道程序退出结束。如何理解这句话,看下面的代码:
public class Singleton {
public static int MIN_USER = 10000;
static {
System.out.println("init !!!");
}
//私有构造函数防止外部创建对象
private Singleton() {
System.out.println("Constructor !!!");
}
//静态对象对象初始化
private static Singleton singleton = new Singleton();
//静态工程方法
public static Singleton getInstance() {
return singleton;
}
public void doSomething() {
System.out.println(this.getClass().getName());
}
public static void doSomethingStatic() {
System.out.println("doSomethingStatic ...");
}
}
在这个类中添加了一个静态属性,一个静态方法。
public class SingletonTest {
public static void main(String[] args){
Singleton singleton = null;
System.out.println(singleton == null);
}
}
输出:true
可以看到虽然引用了单例类,却没有生成单例对象。
访问静态属性调用:
public class SingletonTest {
public static void main(String[] args){
int max_User = Singleton.MIN_USER;
System.out.println(max_User);
}
}
输出:
init !!!
Constructor !!!
10000
如果为了像上述一样只为访问其某些静态属性或静态方法,却创建的单例对象。同理如果只调用doSomethingStatic()方法也会生成对象。
静态方法调用:
public class SingletonTest {
public static void main(String[] args){
Singleton.doSomethingStatic();
}
}
输出:
init !!!
Constructor !!!
doSomethingStatic ...
考虑一下final:
被final修饰的静态属性的访问不会触发实例化,被final修饰的静态方法仍然会触发实例化。被final修饰的类属性会被作为编译期常量加入常量池,以后访问对应类的常量池,不会在常量池中保存一个指向类字段的符号引用,不触发类的初始化。
总结:
- 饿汉模式是通过类的静态属性初始化来实现单例模式的实例化 private static Singleton singleton = new Singleton(); 立即加载在类完成初始化时也完成了单例对象的实例化。
- 枚举方式也是同样的道理。
- 懒汉式(延时加载)是在显式调用 getInstance() 方法来完成单例对象的实例化,即类加载的七步中的使用阶段。
静态内部类方式:
public class Singleton2 {
static {
System.out.println("Singleton2 init ...");
}
private Singleton2() {
System.out.println("Singleton2 Constructor ...");
if (InnerObjcet.singleton != null) {
throw new IllegalStateException();
}
}
private static class InnerObjcet {
static String str = "TEST";
private static Singleton2 singleton = new Singleton2();
static {
System.out.println("Singleton2 inner init ...");
}
}
public static Singleton2 getInstance() {
return InnerObjcet.singleton;
}
public static void doSomethingStatic() {
System.out.println(InnerObjcet.str);
}
public static void doSomething() {
System.out.println("doSomething .. ");
}
}
调用静态方法 doSomething:
public class SingletonTest {
public static void main(String[] args){
Singleton2.doSomething();
}
}
输出:
Singleton2 init ...
doSomething ..
调用静态方法 doSomethingStatic :
public class SingletonTest {
public static void main(String[] args){
Singleton2.doSomethingStatic();
}
}
输出:
Singleton2 init ...
Singleton2 Constructor ...
Singleton2 inner init ...
TEST
结论
- 静态内部类能够实现延时加载是由于对于没有使用的类jvm是不会加载,即便其是一个内部类。其通过private修饰只能在外部类中访问。外部类中访问的唯一地方就是在 getInstance() 方法中。
- 静态内部类中尽量不要声名公有的静态变量,因为一旦被外部调用,内部类就会被初始化,又造成了不必要的开销,也就丧失该方式的优势,如果必须要写, 尽量用 final修饰
暂时先写到这吧,想起什么再补充~
网友评论