本文参考自https://blog.csdn.net/YXXXYX/article/details/120126269

java变量分类

image-20230821155934814

JVM中的主要内存空间

image-20230827150105919

栈区

  • 存放各种方法:静态方法、实例方法、构造方法等
  • 存放局部变量

堆区

存放new出来的对象(对象的实例)

方法区

  • 存放各个类
  • 静态变量在此初始化

三大变量内存分配情况

image-20230827150147796

成员变量与局部变量

  • 成员变量是在中定义的变量
  • 局部变量是在方法里定义的变量

成员变量

成员变量包括静态变量实例变量

  • 静态变量:变量有static修饰,在类的准备阶段就存在了,生命周期一直到系统销毁这个类结束,静态变量的作用域与这个类的生存范围相同
  • 实例变量:没有static修饰,它从该类的实例被创建时就存在,直到系统销毁这个实例,实例变量的作用域与对应实例的生存范围相同
  • 静态变量访问方法:通过类名访问,不需要创建实例;
  • 实例变量访问方法:通过实例访问,需要先new一个实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MemoryShow {
public static void main(String[] args) {
Person p1 = new Person();
Person p2 = new Person();
Person.name = "李四";
System.out.println("姓名:" + Person.name);
System.out.println("p1年龄:" + p1.age);
System.out.println("p2年龄:" + p2.age);
}
}
class Person {
//静态变量
static String name = "张三";
//实例变量
int age;
}

当程序第一次执行Person类时,系统先加载这个类,并初始化这个类,在类的准备阶段,系统就为该类的类变量分配内存空间并指定默认初始值(静态变量name也是在这个阶段完成了初始化);

然后接下来系统就在堆内存中为Person类分配了一块内存区,且为age默认赋值为0;然后生成了Person对象,并通过引用变量p1指向该对象;

当再次执行Person类时,已经不需要再为Person类初始化了,所以直接生成了Person对象,通过p2指向它;

局部变量

局部变量中的三种不同形式:

  • 形参: 在定义方法签名时定义的变量,形参的作用域在整个方法内有效
  • 方法局部变量:方法体中定义的变量,作用域从定义该变量的地方生效,到该方法结束时失效
  • 代码块局部变量: 在代码块中定义的变量,作用域从定义该变量的地方生效,到该代码块结束时失效

和成员变量不同的一点是:局部变量除了形参外,都需要显式初始化,就是指定一个初始值,否则无法访问;

局部变量在内存中的运行机制:

  • 因为局部变量需要显式初始化,所以系统不会对它进行初始化,即系统并没有给局部变量分配内存空间,只有它赋值后,系统才会分配内存将该值放入其中;
  • 因为局部变量不属于任何对象或者类,所以它存放在栈内存中,且栈内存的变量不需要系统垃圾回收,因为它们会随着方法或者代码块运行结束而结束;所以局部变量只保存基本类型或者对象的引用(引用变量),所以局部变量占用内存比较小;

java语法允许局部变量和成员变量重名,但是如果在一个方法里,局部变量会覆盖成员变量;如果想要在该方法里访问成员变量,就需要通过this引用(针对实例变量)或者类名(针对静态变量)作为调用者来限定访问成员;如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class RepeatTest {
// 静态变量(类变量)
static String name = "张三";
// 实例变量
int age = 18;

public static void main(String[] args) {
// 局部变量 和静态变量name = "张三"重名
String name = "李四";

// 因为静态变量name被这里的局部变量覆盖,所以输出为“李四”
System.out.println("姓名:" + name);

// 这时候如果想要调用静态变量可以使用类来调用
//不能用this调用的原因是:main方法是静态方法
System.out.println("姓名:" + RepeatTest.name);

// 调用test方法
new RepeatTest().test();
}
public void test() {
// 局部变量 和实例变量age = 18重名
int age = 666;
// 因为实例变量name被这里的局部变量覆盖,所以输出为“666”
System.out.println("年龄:" + age);
// 这时候如果想要调用实例变量可以使用this调用
System.out.println("年龄:" + this.age);
}
}

变量使用

关于成员变量与局部变量的使用,以下面代码为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ScopeTest01 {
//静态变量(类成员变量)
//再次强调一下这里为什么定义静态变量而不是实例变量
//因为main方法是静态方法,不需要对象只通过类调用,而实例变量必须有对象才能调用
static int i;
public static void main(String[] args) {
for (i = 0; i < 5; ++i) {
System.out.println("Hello World!!!");
}
}
}


// 局部变量 生命周期与方法生命周期一致
public class ScopeTest02 {
public static void main(String[] args) {
//局部变量
int i;
for (i = 0; i < 5; ++i) {
System.out.println("Hello World!!!");
}
}
}

// 局部变量 生命周期为这个for循环代码块
public class ScopeTest03 {
public static void main(String[] args) {
// 代码块局部变量
for (int i = 0; i < 5; ++i) {
System.out.println("Hello World!!!");
}
}
}

这三个代码的运行结果都是一样的,而它们分别用了成员变量和局部变量,结果都一样,不同点在于三种方式定义变量的方式不同。

ScopeTest01 使用的是成员变量,我们都知道成员变量存在于堆内存中,且只有类销毁时或者实例销毁时它才销毁,这就将作用域扩大到类存在范围或者实例存在范围,作用域的扩大有两个坏处

  • 增加了变量的生存时间,会导致更大的内存开销
  • 扩大了变量的作用域,不利于提高程序的内聚性

ScopeTest02和ScopeTest03也可以同理比较,通过比较得出ScopeTest03最符合规范。

所以定义变量的时候要尽可能的保证作用范围最小,这样可以很好的提高程序的性能,包括局部变量;

考虑使用成员变量有四种情况:

  1. 如果定义的变量需要描述对象的信息,且每个对象实例都有可能不同,那么用成员变量中的实例变量;
  2. 如果定义的变量所描述的信息对这个类的所有对象都相同,那么类相关的信息就定义为成员变量中的静态变量;
  3. 如果某个类中需要一个变量保存该类或者实例运行时的状态信息,该变量定义为成员变量;
  4. 如果某个信息需要在类的多个方法之间共享,则该信息使用成员变量