代码实现

方式一

1
2
3
4
5
6
7
8
9
10
public class StaticParam {
private static StaticParam ourInstance = new StaticParam();
public static StaticParam getInstance() {
return ourInstance;
}
private StaticParam() {
}
}

方式二

1
2
3
4
5
6
7
8
9
10
11
12
public class SingleHolder {
private SingleHolder() {
}
private static class Holder {
private static SingleHolder instance = new SingleHolder();
}
public SingleHolder getInstance() {
return Holder.instance;
}
}

方式一俗称饿汉模式,即在类加载的时候就完成单例的初始化。该种方式虽然依赖类加载机制可以保证线程安全,但是在类加载之后就直接初始化,会直接占用内存,造成内存浪费。
方式二利用静态内部类的静态变量来保存单例对象,只有通过 getInstance() 方法访问时,才会触发内部类的加载和初始化,从而完成单例的初始化。所以方法二相较于方法一可以实现延迟加载,从而在真正使用单例对象的时候再进行初始化,节省内存。

两种方式都依赖类的加载机制来保证线程安全,那类加载具体过程是怎样的呢?

类加载机制

JVM 加载类分为 7 个阶段:加载,验证,准备,解析,初始化,使用,卸载。

加载

类加载过程即将字节码加载到 JVM 内存中的过程。 Java 源码在编译成字节码之后可以保持在磁盘,Jar 包,网络中,JVM支持从这些源加载字节码到 JVM 中。
类加载到内存中,会在 JVM 的方法区创建一个对象的 Class 对象,通过这个 Class 对象,我们可以获取这个类的所有基本信息,成员变量,方法定义等。

验证

为保证已加载类能被 JVM 正确的识别,需要在加载完成后做验证,验证主要分为:

  1. 文件格式验证,主要验证字节码是否符合规范,当前 JVM 版本是否支持,能否被 JVM 正确识别并存储到方法区等。

  2. 元数据校验,验证类文件中数据类型,语法等是否合法

  3. 字节码校验,分析代码语句及逻辑,防止代码有危害 JVM 的操作

  4. 符号引用校验,分析代码对于类以外内容引用的合法性。例如常量池内对象,其他类文件等。

准备

该阶段会对类变量进行赋初值,内存的分配发生在方法区。类变量仅包含类定义的静态变量,基本类型被赋初始值,应用类型被赋 null。

解析

解析阶段会将符号引用转换为直接引用。符号引用是存在于 Class 文件中的数据,解析阶段会将文件中的接口,类,变量,方法等映射到内存中,将其内存引用缓存起来以供后续使用

初始化并使用

初始化阶段才开始真正执行用户代码逻辑。触发初始化的时机包括 new 关键字创建对象,访问类的静态字段或调用静态方法等。该阶段会按照代码中的逻辑对变量进行初始化,该过程其实是底层 clinit 方法执行的过程。虚拟机会保证 clinit 方法在多线程环境中能被正确的加锁和同步,从而保证仅有一个线程执行类的 clinit 方法。

初始化还有一个阶段是调用 init 方法进行对象的初始化,也即调用类的构造方法按照用户代码逻辑初始化一个对象。

卸载

类的卸载条件:

  1. 类的所有实例均被回收
  2. 类的 ClassLoader 已被回收

由此可以看出,JVM 自身所带的类加载器在 JVM 整个生命周期是不会被卸载的,只有用户定义了 ClassLoader 的类才可能被卸载。

参考:「深入java虚拟机学习 – 类的加载机制」地址