1. static的含义
口诀:static在天边,实现数据共享- static用来修饰成员变量,成员常量,方法,和代码块等。static不能修饰方法内部的局部变量和局部常量,也不能修饰方法内部的普通代码块。
- 理解static修饰符的前提需要掌握JVM类装载流程和JVM内存分配的基础知识。
- 结合多线程知识,能更深刻理解static的含义。static是在多线程的资源共享级别之上的共享。换言之,static一旦修饰了某个目标类的成员变量,则在线程中,不论以哪种方式管理线程子类,该static资源一定是被共享的。
- 实际上只需要记住,static修饰的成员变量,一定被目标类的所有实例对象所共享。
- static这种共享特征的根因:
从JVM装在流程可知,static修饰的成员随着目标类的装载,在“加载、链接(验证,准备,解析)、初始化”的链接阶段的准备阶段就会被初始化,并且只会初始化一次,因为目标类只会被JVM装载一次。
从JVM内存可知,static成员被初始化后保存在JVM的静态区内存中。
因此,static成员是JVM生命周期中最早被初始化保存在JVM静态区的,且整个JVM生命周期中只会初始化一次。 - 类装在流程中,“静、静、非、构、构、初始化方法”中,第一个初始化的就是static静态成员。
2. 详解static
2.1 是种修饰符
Java中的static关键字是种修饰符,用来修饰类中的成员变量、成员方法、静态代码块,使其成为共享成员。
所谓共享,是指该静态成员被当前类的所有实例对象共享,所有实例化对象指向的都是方法区内存里面的静态区内存地址。
2.2 只随着目标类装载一次
static修饰的成员变量和成员方法表示:该成员变量和成员方法是属于类本身的,而不属于该类的任何一个对象,即成员方法和成员变量是静态的。
所谓静态:即static成员和方法不随着目标类对象的创建而创建,即不随着任何一个new操作而创建。因为属于类本身,因此是随着JVM装载目标类而装载的。并且目标类在整个JVM生命周期中只装载一次,因此static成员也只会装载一次。
2.3 装载时机
JVM装载目标类的流程:“加载、链接(验证,准备,解析)、初始化”。
static成员是在*.class文件被加载进JVM中,在JVM的链接阶段的准备阶段,即开辟方法区内存的静态区内存用来存储初始化的static成员值和static方法链接符。
2.4 保存在方法区内存的静态区
因为static成员和方法链接符是在方法区内存的静态区内存进行保存,而JVM的方法区内存是共享区域,是可以被任何实例化对象进行访问的,因此,static成员和方法是共享成员。
2.5 方法区内存的静态区永远被类链接符指向
一个类的*.class文件被加载进JVM后,这个类会在JVM的方法区内存中缓存一个全局唯一的权限定地址,这个权限定地址被成为类链接符,和方法链接符类似。
同时,一个类被加载进JVM后,会在堆内存中为该类首先创建一个Class实体对象,这个Class对象保存着当前类的结构。
方法区内存中的类链接符只会指向一个地方—>方法区内存的静态区。
而方法区内存的静态区存储着当前类的Class对象的地址,和当前类所有的static成员的地址;其中Class对象的地址指向Class实体,这个实体就是存储在堆内存中目标类的结构。
如下对静态成员name和class对象的调用:
Person.name;
Person.class;
其中这个Person就是一个地址,表示类链接符,保存在方法区中,指向方法区内存的静态区;name是存储在方法区内存的静态区的静态成员的地址,class是存储在方法区内存的静态区的Class对象的地址,指向堆内存中的Class实例。
2.6 只随着目标类装载一次,但可以初始化多次
static成员随着目标类的装载只被JVM装载一次,然后保存在方法区内存的静态区中,基本类型保存的是值,引用类型保存的是地址;且该静态区内存始终被*.class类链接符所指向。
因为目标类只会被JVM在整个生命周期中装载一次,因此static成员也只会被JVM装载一次。
但是,尽管static成员只被JVM装载一次,但是它可以被初始化多次。
加载目标类往往伴随着初始化操作(如果不显示指定初始化值,则加载往往将变量初始化为默认值);但是否显式的继续进行初始化操作则取决于后续是否对变量进行重置。
详解:static成员默认和目标类一样在整个JVM生命周期中只被JVM装载一次,但是如果后续手动重置了该成员的话,则它后续会被初始化或者实例化多次;这是饿汉式单例的核心原理。
// 此场景,随着目标类的装载就实例化成员per,同时将实例化后的per引用地址赋值给变量per进行初始化操作;
static Person per = new Person();
// 此场景,随着目标类的装载加载该成员per,但不进行实例化,只是初始化为默认值null。
static Person per;
2.7 static成员本身永远不会被gc回收
不论是 static Person per = new Person();还是 static Person per;只要是static成员一旦被JVM装载,便永远不会被gc回收。
根因:static成员随着目标类装载完后,其方法区的静态区地址将在整个JVM生命周期中被.class类链接符永远指向。
结论:static成员本身永远不会被gc回收。
理解结论:描述的是static成员本身,这个本身可能保存的是基本类型的值,也可能是引用类型的地址。即并不是说static成员指向的对象实体不会被回收。
举例:static Person per = new Person();此场景中,per引用地址永远被.class类链接符指向,永远不会被gc回收,但是new Person()实体便不一定了。一旦后续动作频发的重置了per,则旧的new Person()堆内存就会被gc回收。
比如,重置了per变量的指向,即重置了static成员,让per指向了新的堆内存,那之前的堆内存就会被gc回收。
即 per = new Person(“1”);后,则之前的new Person()堆内存将等着被gc回收。
2.8 static成员可以是基本类型,也可以是引用类型
static只能修饰成员,该成员可以是基本类型,比如int;也可以是引用类型,比如Person。
当是基本类型时,保存在方法区内存静态区的就是基本类型的值;当是引用类型时,般存在方法区内存静态区的就是引用类型的引用地址。这一点和非静态成员是一致的,即 基本类型保存值,引用类型保存地址(指向真正的堆内存目标实体)。
解释:
// 此场景,per本身的引用地址保存在方法区内存的静态区.
// 而被共享的也是per本身的引用地址;至于new Person()则是per指向的堆内存中的实体对象,是在堆内存中开辟的。
static Person per = new Person();
// 10本身被保存在方法区内存的静态区。
static int i = 10;
2.9 static成员和单例
static成员在整个JVM生命周期中只和目标类一样被JVM装载一次,但并不是只初始化一次;它首先在JVM的“加载、链接(验证,准备和解析)、初始化”阶段的准备阶段被初始化一次,后续是否被重置取决于代码实现。
然而一个良好的单例,应该确保,该单例在整个JVM生命周期中只会初始化一次。饿汉式单例只会初始化一次,这就是饿汉式单例必须是static的原因,只有是static才能保证其在这个生命周期中只加载一次。
至于懒汉式单例,则需要采取其他措施保证单例的“单实例”。
2.10 static成员和static块
static成员和static块,都随着目标类的装载而装载一次。
区别:
static成员虽然只装载一次,但后续可能初始化多次,即同一个static成员可能被重新赋值执行多次!
static块无法在后续被重置,就不能多次初始化,因此,static块在整个JVM生命周期中和目标类完全绑定,只会装载一次!
2.11 static方法和static块中不能使用this或者super
static方法和static代码块中不能直接使用this和super,如果要等价使用this或者super,则必须实例化自身对象或者直接父类对象。
原因:static成员是类成员,随着目标类的装载而被初始化,并不随着目标类的new实例化而初始化。即static成员是最早被JVM初始化的,属于静态初始化,而不是new操作进行的动态初始化。
因此,如果强制在static中使用this或者super的话,此时堆内存并没有开辟空间保存当前类的实例化对象或者父类的实例化对象,那this和super根本就不存在指向的内存空间,故而无法直接使用。
static成员只要类一经加载就会在方法区内存的静态区分配空间保存;而非静态成员不进行对象的实例化操作便不会分配空间保存。
所以,在一个类的静态成员中去访问非静态成员之所以会出错是因为在类的非静态成员不存在的时候静态成员就已经存在了,访问一个内存中不存在的东西当然会出错。
3. static修饰后的区别
3.1 加载时机
从加载时机来说,静态成员变量和静态成员方法随着目标类的加载而创建,在JVM “加载、链接、初始化”的准备阶段,即在方法区内存的静态区开辟内存空间保存并初始化为默认值,并不需要new一个目标类的对象才会创建static成员。
static成员在一个目标类被加载过程中是最早被加载并初始化的,存在在方法区内存的静态区,且永远被*.class类链接符永远指定。
总结:
static成员最早初始化,且永远不会被gc回收。
而非static成员和方法,只有当目标类被new之后才会在堆内存空间开辟空间进行初始化,它永远被自己的应用对象所指向。
3.2 内存存储
从内存存储角度来讲:
静态成员和静态方法保存在内存的独立区域,这块独立区域叫做方法区内存的静态区;该内存被目标类所有实例化对象共享,是独立于堆内存的。
非静态成员和非静态方法只会随着目标类的new操作实例化而在对应的堆内存中开辟空间保存成员,此时的成员变量和成员方法只是属于当前new出来的这个对象,而不是目标类的所有对象。
总结:
static修饰的成员和方法是属于当前类的,是静态的,是随着当前类被JVM装载便开始初始化的,且独立存放在方法区内存的静态区的,是被当前类所有实例对象共享的,是不随着new操作而加载的。
而非static成员和方法只会随着目标对象的new操作才会被初始化的,且只是存放在当前对象的堆内存空间里的,当前类的其他对象是不能共享的。
3.3 调用方式
从调用方式来讲:
不论是在同一个类还是多个类之间,只需要采用“目标类名.静态成员”和“目标类名.静态方法”的形式便可以调用目标类的static成员和static方法。
而要调用非static成员和非static方法,需要首先实例化一个目标类的对象,然后才能通过该对象去调用,即需要new一个实例化对象,才能通过该“实例化对象.实例成员”和“实例化对象.实例方法”去调用。
总结:
静态成员被类链接符指向,因此通过类链接符即类名即可调用;
非静态成员被实例化对象指向,因此通过实例化对象即可调用。
3.4 依赖类而不依赖对象
上面已经反复说过:
static成员随着目标类被JVM装载而装载,属于静态初始化,它不需要动态new当前对象才能触发加载,且整个JVM生命周期只装载一次,存放在方法区内存的静态区。
而并不随着目标类的实例化对象而装载。
因此:
被static关键字修饰的方法或者变量不需要依赖于实例化对象来进行访问,只要类被加载了,就可以通过类名去进行访问。
扩展:
为什么工具类是不能交给Spring进行IOC呢?
正是因为工具类中全部是Util方法,即全部是static方法和static成员,因此任何一个工具类是在JVM一开始加载当前类,就已经进行初始化操作了。
而spring的IOC只是将目标对象的new操作进行了管理,因此spring无法管理工具类的加载!
即便你尝试将一个Util工具类交给spring管理,spring只会创建一个该工具类的实例对象,而这个对象对于我们来说是没有任何作用的,因为我们不需要对工具类进行对象的创建,所以,强制将工具类交给spring进行实例化,没有必要而且浪费内存。
既然对于工具类的调用无法直接依赖spring容器,那么,工具类中就无法直接注入spring管理的bean对象了。
解决方案:
当我们定义了工具类,目的是为了直接通过类名调用静态方法,方便使用。然而,有时候工具类中不得不依赖spring的IOC容器管理的某些bean来进行操作,如下:
(1) 原始的工具类:
public class Test {
public static int add(int a, int b) {
return a + b;
}
}
使用时,直接通过类名调用:
int result = Test.add(1, 1);
(2)但有的时候,我们的工具类可能会需要使用Spring的bean容器中定义的一些单例bean的方法来辅助计算,比如:
public class Test {
// 注意,这是spring中的一个单例bean,是一个由spring创建的实例化对象
@Autowired
private XXService xxService;
public static int add(int a, int b) {
// 这里会收到报错:Cannot make a static reference to the non-static field aservice
// 报错意思很清楚:无法在一个静态区域中直接使用实例对象;如果要使用,比如手动new一个局部的实例对象。
int c = xxService.doSomething(a);
return c + b;
}
}
(3)将工具类改造成交给spring管理的单例,这样便可以注入spring管理的其他bean实例了。
第一步,使用注解@Component将Test交给Spring容器来管理,这样才能让Test在初始化时注入XXService的实例
第二步,在Test类中创建一个自己类型的静态变量,用于后续的调用
第三步,使用注解@PostConstruct(这是在构造函数执行后执行的初始化动作)的方法,来将Test对象自己的实例赋给自己类型的静态变量。
第四步,把原来Test的静态add方法,变为成员方法
完整代码如下:
@Component // 第一步
public class Test {
public static Test INSTANCE; // 第二步
@PostConstruct // 第三步
public void init() {
INSTANCE = this;
}
@Autowired
private AService aservice;
public int add(int a, int b) { // 第四步,去掉static
aservice.service();
return a + b;
}
}
后面的调用就简单了,改为:
Test.INSTANCE.add(1, 1);
文档信息
- 本文作者:Marshall