Java Static关键字详解

Java Static Keyword - Complete Guide

深入理解静态成员与类级别编程

静态变量

静态的内容可以理解为是属于类的,是所有对象共享的。我们使用static关键字来声明。

public class Person {
    String name;
    int age;
    static String info; // 静态变量
}

一个对象改变了静态变量的值,其他对象读取的就是被改变的值。

public static void main(String[] args) {
    Person p1 = new Person();
    Person p2 = new Person();
    p1.info = "info:1";
    System.out.println(p2.info); // 输出: info:1
}

通常,我们通过类名来访问静态成员:

public static void main(String[] args) {
    Person.info = "info:2";
    System.out.println(Person.info);
}

静态方法

静态方法同样属于类,而不是具体的某个对象。

static void test(){
    System.out.println("我是静态方法");
}

在静态方法中,无法直接访问非静态的成员变量或使用this关键字,因为它们都与具体对象相关。但是,静态方法可以访问静态变量。

static String info;

static void test(){
    System.out.println("静态变量的值为:"+info);
}

静态(有static) vs 非静态(无static)对比

理解静态成员和非静态成员的根本区别是掌握Java面向对象编程的关键。

特性
静态成员 (static)
非静态成员 (实例成员)
归属关系
属于类,所有对象共享
属于具体对象实例
内存分配
方法区/元空间,类加载时分配
堆内存,对象创建时分配
访问方式
类名.成员名 (推荐)
对象.成员名 (不推荐)
必须通过对象实例访问
生命周期
类加载到程序结束
对象创建到垃圾回收
初始化时机
类首次使用时
每次创建对象时

对比

public class Student {
    // 静态成员 - 属于类
    static int totalCount = 0;         // 静态变量
    static String schoolName = "浙大城市学院"; // 静态变量
    
    // 非静态成员 - 属于对象实例
    String name;                       // 实例变量
    int age;                           // 实例变量
    
    // 静态方法 - 属于类
    static void printSchoolInfo() {
        System.out.println("学校:" + schoolName);
        System.out.println("总学生数:" + totalCount);
        // 错误:不能直接访问实例变量
        // System.out.println(name); // 编译错误!
    }
    
    // 非静态方法 - 属于对象实例
    void printStudentInfo() {
        System.out.println("姓名:" + name);
        System.out.println("年龄:" + age);
        // 可以访问静态变量
        System.out.println("学校:" + schoolName);
        System.out.println("总学生数:" + totalCount);
    }
    
    // 构造方法
    Student(String name, int age) {
        this.name = name;
        this.age = age;
        totalCount++; // 每创建一个学生,总数+1
    }
}

使用示例

public class TestStudent {
    public static void main(String[] args) {
        // 访问静态成员 - 不需要创建对象
        System.out.println("初始学生总数:" + Student.totalCount); // 输出:0
        Student.printSchoolInfo(); // 直接通过类名调用
        
        // 创建对象实例
        Student s1 = new Student("张三", 20);
        Student s2 = new Student("李四", 21);
        
        // 访问实例成员 - 必须通过对象
        s1.printStudentInfo();
        s2.printStudentInfo();
        
        // 静态成员被所有对象共享
        System.out.println("当前学生总数:" + Student.totalCount); // 输出:2
        System.out.println("通过s1访问:" + s1.totalCount);       // 输出:2 (不推荐)
        System.out.println("通过s2访问:" + s2.totalCount);       // 输出:2 (不推荐)
    }
}

静态变量动画演示

静态变量属于类,被所有对象实例共享。让我们通过动画来理解这个概念:

Student 类
static int count = 0;
0
String name;
int age;
student1
name: "张三"
age: 20
count → 0
student2
name: "李四"
age: 21
count → 0
student3
name: "王五"
age: 19
count → 0

观察要点:

  • 当创建新对象时,static变量count会自动增加
  • 所有对象实例都共享同一个static变量
  • 修改static变量会影响所有对象的访问结果

静态代码块和初始化顺序

静态代码块可用于初始化静态变量,它在类加载时执行。

public class Person {
    String name = test();

    {
        System.out.println("我是普通代码块");
    }

    Person() {
        System.out.println("我是构造方法");
    }

    String test() {
        System.out.println("我是成员变量初始化");
        return "小明";
    }

    static String info = init();

    static {
        System.out.println("我是静态代码块");
    }

    static String init() {
        System.out.println("我是静态变量初始化");
        return "test";
    }
}

当创建一个对象时,执行顺序如下:

  1. 我是静态变量初始化
  2. 我是静态代码块
  3. 我是成员变量初始化
  4. 我是普通代码块
  5. 我是构造方法

如果只访问静态成员(如Person.info),则只会执行静态部分的初始化。

static成员的完整生命周期

从类加载到程序结束,理解static成员在内存中的完整生命历程。

🕐 生命周期时间线

1
类加载
→
2
链接
→
3
初始化
→
4
运行时使用
→
5
类卸载

🔄 1. 类加载阶段

加载(Loading)
  • 将.class文件读入内存
  • 在方法区创建Class对象
  • 为static变量分配内存空间
触发条件: 创建实例 访问static成员 反射调用 子类初始化

🔗 2. 链接阶段

验证 → 准备 → 解析
  • 验证:检查字节码格式和安全性
  • 准备:在方法区/元空间为static变量分配内存并设置默认值
  • 解析:符号引用转为直接引用
内存分配: JDK 8之前:永久代 JDK 8+:元空间

⚡ 3. 初始化阶段

执行static代码
  • 执行static变量的显式初始化
  • 执行static代码块
  • 严格按照代码中的声明顺序执行
  • JVM保证线程安全性
重要:每个类只初始化一次,父类先于子类初始化

🏃 4. 运行时阶段

访问特性
  • 全局唯一:所有实例共享同一份
  • 类级访问:通过类名直接访问
  • 内存位置固定在方法区/元空间
内存管理
  • 不受普通GC直接管理
  • 随类的生命周期存在
  • 与堆内存分离,生命周期独立

🗑️ 5. 类卸载阶段

卸载条件(需同时满足):
  • 该类所有实例都已被回收
  • 加载该类的ClassLoader已被回收
  • 该类的Class对象没有被引用
  • 无法通过反射访问该类
实际情况: 系统类通常不卸载 Web应用重部署时卸载 自定义ClassLoader的类可能卸载

static成员的完整生命周期(续)

通过执行输出分析和类加载触发时机,深入理解static成员的行为特点。

生命周期演示代码

public class LifecycleDemo {
    // 静态变量 - 在类加载时初始化
    static String staticVar = initStaticVar();
    
    // 静态代码块 - 在类加载时执行
    static {
        System.out.println("3. 静态代码块执行");
    }
    
    // 实例变量 - 在对象创建时初始化
    String instanceVar = initInstanceVar();
    
    // 实例代码块 - 在对象创建时执行
    {
        System.out.println("6. 实例代码块执行");
    }
    
    // 静态方法 - 用于初始化静态变量
    static String initStaticVar() {
        System.out.println("2. 静态变量初始化");
        return "静态变量值";
    }
    
    // 实例方法 - 用于初始化实例变量
    String initInstanceVar() {
        System.out.println("5. 实例变量初始化");
        return "实例变量值";
    }
    
    // 构造方法
    public LifecycleDemo() {
        System.out.println("7. 构造方法执行");
    }
    
    public static void main(String[] args) {
        System.out.println("1. 开始执行main方法");
        
        System.out.println("4. 准备创建第一个对象");
        LifecycleDemo obj1 = new LifecycleDemo();
        
        System.out.println("8. 准备创建第二个对象");
        LifecycleDemo obj2 = new LifecycleDemo();
        
        System.out.println("9. 程序结束");
    }
}

执行输出分析

1. 开始执行main方法
2. 静态变量初始化
3. 静态代码块执行
4. 准备创建第一个对象
5. 实例变量初始化
6. 实例代码块执行
7. 构造方法执行
8. 准备创建第二个对象
5. 实例变量初始化
6. 实例代码块执行
7. 构造方法执行
9. 程序结束

关键观察点:

  • 静态初始化只在类首次使用时执行一次
  • 实例初始化每次创建对象时都会执行
  • 静态成员的生命周期从类加载开始,直到程序结束或类被卸载

类加载触发时机详解

🔥 主动引用(会触发类加载)

// 1. 创建类的实例
MyClass obj = new MyClass();

// 2. 访问或设置类的静态变量(非final常量)
int value = MyClass.staticVar;
MyClass.staticVar = 100;

// 3. 调用类的静态方法
MyClass.staticMethod();

// 4. 使用反射
Class.forName("com.example.MyClass");

// 5. 初始化子类时,ParentClass会先被加载
ChildClass child = new ChildClass(); 

❄️ 被动引用(不会触发类加载)

// 1. 访问静态final常量
String constant = MyClass.STATIC_FINAL_STRING; 

// 2. 通过数组定义引用类
MyClass[] array = new MyClass[10]; 

// 3. 访问父类的静态成员,只加载父类
String parentVar = ChildClass.parentStaticVar; 

类加载时机对比

主动引用特点
  • 立即触发类的初始化
  • 执行静态代码块
  • 初始化静态变量
  • 可能触发父类加载
被动引用特点
  • 不触发类的初始化
  • 可能只加载类结构
  • 编译时常量直接替换
  • 延迟实际的类初始化

JVM内存模型与Static成员存储

从虚拟机角度深入理解static成员与类的关联,以及非静态成员与对象的关联。

JVM内存结构概览

方法区 (Method Area)

<=JDK 7
🏛️
永久代 (PermGen)

存储类的元数据信息

类的字节码
静态变量
静态方法
常量池
方法信息

元空间 (Metaspace)

>=JDK 8
🌌
本地内存

使用操作系统本地内存

类的元数据
方法信息
字段信息
常量池引用
改进:避免了PermGen的内存溢出

堆内存 (Heap)

所有版本
年轻代
Eden
S0
S1
老年代
长期存活的对象
大对象
对象实例
实例变量
数组
静态变量的值(JDK 8+)

注:方法区是Java虚拟机规范中定义的一个逻辑概念,是所有线程共享的一块内存区域。

永久代是HotSpot虚拟机(Oracle JDK用的)在JDK 7及之前,对“方法区”这个规范的在堆内存里的具体实现。

Static变量存储位置的演进

JDK 7及之前

永久代存储

位置:方法区的永久代
特点:
  • 固定大小,容易OutOfMemoryError
  • 垃圾回收效率低
  • 与堆内存分离
JDK 8及之后

堆内存存储

位置:堆内存 + 元空间
静态变量的值:堆内存
静态变量的元数据(类型信息、名称信息等):元空间
优势:
  • 更好的垃圾回收管理
  • 避免永久代内存溢出
  • 动态调整元空间大小

交互式内存分配演示

通过动画直观理解static成员与实例成员在JVM内存中的分配过程。

代码执行

public class AnimationDemo {
    static String staticVar = "静态变量";
    static int counter = 0;
    
    String instanceVar;
    int id;
    
    public AnimationDemo(String name) {
        this.instanceVar = name;
        this.id = ++counter;
    }
    
    public static void main(String[] args) {
        AnimationDemo obj1 = new AnimationDemo("对象1");
        AnimationDemo obj2 = new AnimationDemo("对象2");
    }
}

内存状态

元空间 (Metaspace)

等待类加载...

堆内存 (Heap)

静态变量区
等待静态初始化...
实例对象区
等待对象创建...

执行时间线

类加载
静态初始化
main方法执行
创建对象1
创建对象2

内存统计

静态变量数量
0
对象实例数量
0
总内存使用
0 B
共享内存节省
0 B
1 / X