Author : Cyan_RA9
Source : 【卡码笔记】网站
Question : Java中的静态变量(static)和非静态变量有什么区别?
【简要回答】
静态变量和非静态变量的定义
- 静态变量:
- 使用 static关键字 修饰,属于类本身,本类所有对象共享同一份数据。
- 在类加载时初始化,生命周期与类一致。
- 通常使用"类名.静态变量名"的形式来访问。
- 非静态变量(实例变量):
- 未使用 static关键字 修饰,属于类的实例(对象)。
- 每个对象独立拥有一份数据,生命周期与对象一致。
- 使用"对象.实例变量名"的形式来访问。
静态变量和非静态变量的区别
| 对比维度 | 静态变量 | 非静态变量 |
|---|---|---|
| 所属范围 | 类级别(所有对象共享) | 对象级别(每个对象独立) |
| 内存分配 | 元数据位于永久代(JDK 8前) / 元空间(JDK 8后),真正的值位于堆内存的Class对象中(JDK7.0后) | 堆内存(对象实例中) |
| 访问方式 | 通过类名访问(eg: Objects.equals(..)) | 通过对象实例访问(eg: stu.study(..)) |
| 初始化时机 | 类加载时初始化(早于对象创建) | 对象创建时初始化(new操作后) |
| 线程安全性 | 需显式同步(多线程共享易冲突) | 默认线程私有(除非显式共享) |
| 使用场景 | 全局配置、工具类常量、共享资源(如计数器) | 对象特有属性(如学生姓名、账户余额) |
【详细回答】
静态变量和非静态变量的定义
- 静态变量:
- 使用 static关键字 声明,属于类本身而非任何实例。
- 生命周期与类绑定,类被加载时创建,类被卸载时销毁(通常发生在 JVM 关闭时)。
- 所有对象共享同一份静态变量,修改静态变量会影响所有实例。
- 非静态变量(实例变量):
- 未使用 static关键字 修饰,属于类的实例(对象)。
- 在对象创建时(new 操作)分配内存并初始化。每个对象拥有独立副本,不同对象的非静态变量相互独立,修改一个对象的非静态变量,不会影响到其他对象。
- 生命周期与对象绑定,对象创建时初始化,对象被垃圾回收时销毁。
静态变量和非静态变量的区别
- 所属范围:
- 静态变量属于类本身,类的所有对象共享同一份数据。
- 非静态变量属于对象实例,类的每个对象拥有独立的副本。
- 内存分配:
- 静态变量的元数据位于永久代(JDK 8前) / 元空间(JDK 8后)中,静态变量真正的值位于永久代(JDK7.0前)/ 堆内存的Class对象中(JDK7.0后)。需要注意的是,虽然元空间由 JVM 自动管理,但静态变量常驻内存,可能导致内存泄漏。
- 非静态变量存储在堆内存中的对象实例内。对象不再被引用时,由垃圾回收器回收。
- 访问方式:
- 静态变量一般通过类名直接访问(推荐方式),例如
Math.PI。 - 非静态变量必须通过对象实例访问,例如
stu.name。
- 静态变量一般通过类名直接访问(推荐方式),例如
- 初始化时机:
- 静态变量在类加载时初始化,早于对象创建。
- 非静态变量在对象创建时(new 操作)初始化。
- 线程安全性:
- 静态变量在多线程环境下需显式同步,否则可能引发线程安全问题。
- 非静态变量默认线程私有(每个线程操作独立对象),无需同步(除非显式共享)。
- 使用场景:
- 静态变量适用于全局配置(如数据库连接参数)、工具类常量(如
Math.PI)、共享资源(如计数器)。 - 非静态变量用于描述对象特有属性(如用户姓名、订单金额等等)。
- 静态变量适用于全局配置(如数据库连接参数)、工具类常量(如
【知识拓展】
类的 加载阶段 与 静态变量初始化 的关系
- 加载(loading) : 将类的字节码文件加载到内存中的方法区(逻辑上的说法),并在堆空间中生成字节码文件对应的Class对象(字节码文件对象)。
- 验证(verification) : 确认字节码文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全;具体检验内容包括文件格式验证(是否以魔数oxcafebabe开头),元数据验证,字节码验证和符号引用验证。PS : 可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,缩短虚拟机类加载的时间。
- 准备(preparation) : 为静态变量分配内存并进行默认初始化,从JDK8.0开始,static修饰的成员变量位于堆空间中;非静态变量(实例变量)在此阶段不作处理。而静态常量则会直接被赋值为指定的值。
- 解析(resolution) : 将运行时常量池内的符号引用替换为直接引用的过程。
- 初始化(initialization) : 真正意义上地开始执行类中定义的java源代码,初始化阶段其实就是执行clinit()方法的过程。clinit()方法是由编译器按照静态语句在源文件中出现时的顺序,依次自动收集类中所有的静态变量的赋值动作和静态代码块中的语句,并进行合并 (eg : 若先定义的静态变量有显式赋值操作,而后定义的静态代码块中仍有对同一静态变量的赋值操作,合并后,以后定义的为准)。PS : 虚拟机会保证一个类的clinit()方法在多线程环境中被正确的加锁,同步;如果多个线程同时去初始化一个类,那么就会只有一个线程去执行这个类的clinit()方法,其他线程都需要阻塞等待,直到活动线程执行clinit()方法完毕(此举保证了Class对象的唯一性)。