Java中的堆和栈存放的数据类型

前言

因为面试中经常被问到关于Java中堆和栈的数据存放问题,今天我们就来简单了解下堆和栈。

这篇文章仅仅是Java堆和栈的一个入门,我们结合实际例子来了解下堆和栈的关系及区别。

正文

我们常说堆栈堆栈,其实堆和栈是两个不同的地方,那么什么样的数据会放在堆中,什么样的数据会放在栈中呢?

这儿先说结论:

  • Java 栈(Stack)中存放如下类型数据:基本数据类型、引用类型变量、方法函数。
  • Java 堆(Heap)中存放如下类型数据:实例对象。

如下表:

栈(Stack)存放的数据堆(Heap)存放的数据
基本类型变量实例对象
引用类型变量
方法函数

栈的优点:存取速度比堆快,仅次于寄存器,栈数据可以共享。

栈的缺点:存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。

在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈Stack内存中分配。当在一段代码块中定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用。

堆Heap内存用于存放由新创建的对象和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量相当于为数组或者对象起的一个别名,或者代号。

引用变量是普通变量,定义时在栈中分配内存,引用变量在程序运行到作用域外释放。而数组&对象本身在堆中分配,即使程序运行到使用new产生数组和对象的语句所在地代码块之外,数组和对象本身占用的堆内存也不会被释放,数组和对象在没有引用变量指向它的时候,才变成垃圾,不能再被使用,但是仍然占着内存,在随后的一个不确定的时间被垃圾回收器释放掉。这个也是java比较占内存的主要原因,实际上,栈中的变量指向堆内存中的变量,这就是 Java 中的指针。

我们来看下一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class LearnHeap {
public static void main(String args[]){

int a=10;
Person person = new Person();
person.age =20;

change(a,person);
System.out.println("a="+ a+",and person.age = "+person.age);

}

static void change(int a, Person person){
a = 11;
person.age= 21;
System.out.println("a="+ a+",and person.age = "+person.age);

}
}
class Person {
int age;
}

上述代码的输出结果是什么呢?如下:

我们如何理解上述输出?

我们画图来看下:

我们对上图加以分析:

  1. 开始时,main方法是入口,JVM执行,首先将main方法压入栈,在栈内存中开辟空间,用来存放int类型变量a,同时在堆内存中开辟空间,用来创建并存放对象Person,这块内存会有自己的内存地址,我们假设是001,然后赋值成员变量age=20,完成后,将Person对象的内存地址传给main方法中的person
  2. 而后执行change函数,在栈内存中又会开辟一个新的空间,用于存放int类型变量a还有person,这儿就是我们常说的地址传递和值传递,这一步在change方法区,a变量开始会被赋值为main方法传进来的10,person变量被赋值为堆内存中的地址(此时它age=20);
  3. change方法执行,a变量被赋值为11,但是它是无法影响到main方法中的a变量的,因为不在同一片栈内存,person.age被赋值为21,此时会更改堆内存中Person对象的age为21;
  4. change方法结束后,其栈空间释放,main方法空间仍然存在,此时输出a=10,age=21

也就是我们经常说到的:

基本数据类型是值传递,引用数据类型是地址传递

我们上面说到的基本数据类型有8种,我们测试一下即可。

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
public class LearnHeap1 {
public static void main(String[] args) {
byte b = 1;
int i=1;
short s=1;
long l = 1L;
char c = '1';
float f = 1.0f;
double d = 1.0d;
boolean bo = true;
change(b,i,s,l,c,f,d,bo);
System.out.println(b+","+i+","+s+","+l+","+c+","+f+","+d+","+bo);
}
static void change(byte b,int i,short s,long l,char c,float f,double d,boolean bo){
b = 2;
i=2;
s=2;
l = 2L;
c = '2';
f = 2.0f;
d = 2.0d;
bo = false;
System.out.println(b+","+i+","+s+","+l+","+c+","+f+","+d+","+bo);
}
}

可以看到输出如下:

说明基本数据类型都是值传递。

那它们的包装类呢?

我们对Integer做下测试即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LearnHeap2 {
public static void main(String[] args) {
Integer i = 1;
Integer i1 = new Integer("1");
Integer i2 = new Integer("300");
change(i,i1,i2);
System.out.println(i+","+i1+","+i2);
}
static void change(Integer i,Integer i1,Integer i2){
i = 2;
i1 = new Integer("2");
i2 = new Integer("301");
System.out.println(i+","+i1+","+i2);
}
}

输出如下:

可以看到包装类型是值传递的表现

另外数组,集合,Map的传递也为引用传递,我们来看下。

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
34
35
36
37
38
39
40
41
42
public class LearnHeap3 {
public static void main(String[] args) {
int[] array = new int[]{1,2,3};
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Map<Integer,Integer> map = new HashMap<>();
map.put(1,1);
map.put(2,2);
map.put(3,3);
change(array,list,map);
sout(array,list,map);
}
static void change(int[] array,List<Integer> list,Map<Integer,Integer> map){
array[0]=4;
array[1]=5;
array[2]=6;
list.set(0,4);
list.set(1,5);
list.set(2,6);
map.put(1,4);
map.put(2,5);
map.put(3,6);
sout(array,list,map);
}

static void sout(int[] array,List<Integer> list,Map<Integer,Integer> map){
for (int i : array) {
System.out.print(i+",");
}
System.out.println();
for (Integer integer : list) {
System.out.print(integer+",");
}
System.out.println();
map.forEach((k,v)->{
System.out.print(k+":"+v+",");
});
System.out.println();
}
}

输出结果:

我们再来看下String,看看它遵循怎样的规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LearnHeap4 {
public static void main(String[] args) {

String a = "A";
String a1 = new String("A");
System.out.println(a==a1);
change(a,a1);
System.out.println(a+","+a1);
System.out.println(a==a1);
}
static void change(String a,String a1){
a = "B";
a1 = new String("B");
System.out.println(a+","+a1);
}
}

输出结果如下:

可以看到String也是值传递的表现,但是两种创建String的地址比较后,是不相同的。

  • String a ="A":此种创建方式,先在栈中创建一个对String类的对象引用变量a,然后查找栈中有没有存放”A”,如果没有,则将”A”存放进栈,并令其指向a,如果已经有 ”A” ,则直接令a指向”A”;
  • String a1 = new String("A"):此种创建方式,是在堆内存中新建一个”A”对象,同时栈中存放其地址引用a1,每调用一次,就会新建一个对象。

注意:

这儿需要有些注意的地方,可以看到上面我说到包装类型和String测试结果表现为值传递,我们的结论是值传递的表现

其实对于包装类或者String,new这种创建方式,会在堆内存上开辟空间存放对象,在堆内存上开辟空间,底层肯定就是将堆内存的地址传递过去

但由于包装类或者String其类型为final,赋值后就不可更改,我们更改相当于再创建一个对象,将当前变量指向那个地址。

所以我们看到了值传递的表现。

总结

以上就是我们本文的全部内容。

我们用下表来总结下:

类型参数传递类型
基本数据类型(byte、short、long、int、boolean、float、double、char)值传递
常规对象引用传递
Array、List、Map引用传递
包装类和String值传递表现(底层引用传递)



-------------文章结束啦 ~\(≧▽≦)/~ 感谢您的阅读-------------

您的支持就是我创作的动力!

欢迎关注我的其它发布渠道