static 进阶知识
我们在了解了 static 关键字的用法之后,来看一下 static 深入的用法,也就是由浅入深,慢慢来,前戏要够~
关于 static 的所属类
static 所修饰的属性和方法都属于类的,不会属于任何对象;它们的调用方式都是 类名.属性名/方法名
,而实例变量和局部变量都是属于具体的对象实例。
static 修饰变量的存储位置
首先,先来认识一下 JVM 的不同存储区域。
虚拟机栈
: Java 虚拟机栈是线程私有的数据区,Java 虚拟机栈的生命周期与线程相同,虚拟机栈也是局部变量的存储位置。方法在执行过程中,会在虚拟机栈中创建一个栈帧(stack frame)
。本地方法栈
: 本地方法栈也是线程私有的数据区,本地方法栈存储的区域主要是 Java 中使用native
关键字修饰的方法所存储的区域程序计数器
:程序计数器也是线程私有的数据区,这部分区域用于存储线程的指令地址,用于判断线程的分支、循环、跳转、异常、线程切换和恢复等功能,这些都通过程序计数器来完成。方法区
:方法区是各个线程共享的内存区域,它用于存储虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码等数据,也就是说,static 修饰的变量存储在方法区中堆
:堆是线程共享的数据区,堆是 JVM 中最大的一块存储区域,所有的对象实例,包括实例变量都在堆上进行相应的分配。
static 变量的生命周期
static 变量的生命周期与类的生命周期相同,随类的加载而创建,随类的销毁而销毁;普通成员变量和其所属的生命周期相同。
static 序列化
我们知道,序列化的目的就是为了 把 Java 对象转换为字节序列。对象转换为有序字节流,以便其能够在网络上传输或者保存在本地文件中。
声明为 static 和 transient 类型的变量不能被序列化,因为 static 修饰的变量保存在方法区中,只有堆内存才会被序列化。而 transient
关键字的作用就是防止对象进行序列化操作。
类加载顺序
我们前面提到了类加载顺序这么一个概念,static 修饰的变量和静态代码块在使用前已经被初始化好了,类的初始化顺序依次是
加载父类的静态字段 -> 父类的静态代码块 -> 子类静态字段 -> 子类静态代码块 -> 父类成员变量(非静态字段)
-> 父类非静态代码块 -> 父类构造器 -> 子类成员变量 -> 子类非静态代码块 -> 子类构造器
static 经常用作日志打印
我们在开发过程中,经常会使用 static
关键字作为日志打印,下面这行代码你应该经常看到
private static final Logger LOGGER = LogFactory.getLoggger(StaticTest.class);
然而把 static 和 final 去掉都可以打印日志
private final Logger LOGGER = LogFactory.getLoggger(StaticTest.class); private Logger LOGGER = LogFactory.getLoggger(StaticTest.class);
但是这种打印日志的方式存在问题
对于每个 StaticTest 的实例化对象都会拥有一个 LOGGER,如果创建了1000个 StaticTest 对象,则会多出1000个Logger 对象,造成资源的浪费,因此通常会将 Logger 对象声明为 static 变量,这样一来,能够减少对内存资源的占用。
static 经常用作单例模式
由于单例模式指的就是对于不同的类来说,它的副本只有一个,因此 static 可以和单例模式完全匹配。
下面是一个经典的双重校验锁实现单例模式的场景
public class Singleton { private static volatile Singleton singleton; private Singleton() {} public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
来对上面代码做一个简单的描述
使用 static
保证 singleton 变量是静态的,使用 volatile
保证 singleton 变量的可见性,使用私有构造器确保 Singleton 不能被 new 实例化。
使用 Singleton.getInstance()
获取 singleton 对象,首先会进行判断,如果 singleton 为空,会锁住 Singletion 类对象,这里有一些小伙伴们可能不知道为什么需要两次判断,这里来解释下
如果线程 t1 执行到 singleton == null 后,判断对象为 null,此时线程把执行权交给了 t2,t2 判断对象为 null,锁住 Singleton 类对象,进行下面的判断和实例化过程。如果不进行第二次判断的话,那么 t1 在进行第一次判空后,也会进行实例化过程,此时仍然会创建多个对象。
类的构造器是否是 static 的
这个问题我相信大部分小伙伴都没有考虑过,在 Java 编程思想中有这么一句话 类的构造器虽然没有用 static 修饰,但是实际上是 static 方法,但是并没有给出实际的解释,但是这个问题可以从下面几个方面来回答
- static 最简单、最方便记忆的规则就是没有 this 引用。而在类的构造器中,是有隐含的 this 绑定的,因为构造方法是和类绑定的,从这个角度来看,构造器不是静态的。
- 从类的方法这个角度来看,因为
类.方法名
不需要新创建对象就能够访问,所以从这个角度来看,构造器也不是静态的 - 从 JVM 指令角度去看,我们来看一个例子
public class StaticTest { public StaticTest(){} public static void test(){ } public static void main(String[] args) { StaticTest.test(); StaticTest staticTest = new StaticTest(); } }
我们使用 javap -c 生成 StaticTest 的字节码看一下
public class test.StaticTest { public test.StaticTest(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void test(); Code: 0: return public static void main(java.lang.String[]); Code: 0: invokestatic #2 // Method test:()V 3: new #3 // class test/StaticTest 6: dup 7: invokespecial #4 // Method "<init>":()V 10: astore_1 11: return }
我们发现,在调用 static 方法时是使用的 invokestatic
指令,new 对象调用的是 invokespecial
指令,而且在 JVM 规范中 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.invokestatic 说到
从这个角度来讲,invokestatic
指令是专门用来执行 static 方法的指令;invokespecial
是专门用来执行实例方法的指令;从这个角度来讲,构造器也不是静态的。