在谈堆和栈之前,首先我们先要了解一下Java对内存的分配结构。作为Java程序员大家应该都知道Java的程序都是运行在Java虚拟机上也就是JVM上,程序中所有的变量、实例、方法等都是由JVM在内存上分配空间的。
那么让我们来初步的了解一下Java程序在运行时都会存在哪些内存区域:
1.寄存器:JVM内部虚拟的寄存器跟CPU有关,程序无法控制。
2.栈:用来存放基本数据类型的变量和引用数据类型的实例(实例:某一个类模型new出实体的引用变量)。
3.堆:用来存放程序动态产生的数据,eg:根据类模型new出的实体,需要注意的是我们通过new关键字创建出来的对象在堆内存中只有该对象的成 员变量,而不包括成员方法。因为每一个类的对象都有各自的成员变量存储在各自的堆内存中,但是它们共享该类的成员方法,所以并不是每一次new 都会创建成员方法。
4.常量池:JVM对每个已经加载的类型都会维护一个常量池,常量池就是一个该类型用到的常量的一个有序集合。常量池存在于堆内存区域中。
5.代码段:用来存放冲存储设备中读取的代码片段。
6.数据段:用来存放静态成员,包括静态变量、静态常量、静态方法、静态类。
7.方法区:用来存放所有的函数。
上图简单的描述了Java程序在JVM中运行时的内存分布。接下来我们将了解一下堆和栈的特点。
堆: 1.new出来的对象都有指定的地址值。
2.每个变量都有默认值。eg:int 类型的数组、char类型的数组、Class类型的集合等。
3.当使用完毕后,GC并不会立即对该内存进行清理,GC会在空闲的时候进行扫描该内存是否仍存在引用,若不存在则立即进行回收,若存在则等待下一次的扫描。
栈:通常栈内存存放的是类的实例和基础数据类型(int byte char long double 等),当变量处于该变量的作用于外的时候GC会立即对该内存进行回收。
对于栈内存和堆内存的缺点也比较明显,栈内存在分配空间的时候是固定的不可改变的,而堆内存在分配空间的时候是动态分配空间的。堆内存由于GC并不是立即对废弃的内存进行回收并且GC会采用树的方式进行引用检索,这样就导致了废弃的内存并不会立即回收无端的占用了内存空间。
我想大家应该都看到过这样一个面试题,问"=="和"equals"的区别,那我们通过这个问题来说明堆和栈之间的联系。
String str1 = "abc"; //定义字符串变量str1
String str2 = "abc"; //定义字符串变量str2
String str3 = new String("abc"); //以new的方式定义字符串变量str3
String str4 = new String("abc");//以new的方式定义字符串变量str4
/**
* 那么问题来了
* str1 == str2 ? true:false;
* str2 == str3 ? true:false;
* str3 == str4 ? true:false;
*/
System.out.println("str1 == str2 ? :" + (str1 == str2)); //true
System.out.println("str2 == str3 ? :" + (str2 == str3)); //false
System.out.println("str3 == str4 ? :" + (str3 == str4)); //false
System.out.println("str3.equals(str4) ? :" + str3.equals(str4)); //true
对于str1==str2的值为true和str2 == str3的值为false,我想大家都比较容易,但是str3 == str4 和 str3.equals(str4)的值可能和你的结果就不一定一样了吧。我来说明一下这个代码片段:
1.str1 == str2 这个表达式的比较过程,"=="是一个逻辑运算符,对于基础数据类型比较的是两个值是否相等,对于引用数据类型则比较的是两个引用变量所指向在堆内存中的地址值是否一致。String类型是引用数据类型,但是String是一个特殊的引用数据类型,我们在定义一个String类型的变量的时候,在赋值的过程中,Java虚拟机首先会到字符串常量池中检索所要赋的值是否在字符串常量池中存在,如果存在则将该值所在的字符串常量池中的地址赋予该引用变量,如果不存在则在字符串常量池中分配一个内存来存储所赋的值然后将该值所在的地址赋予引用变量。所以在定义str1变量的时候,JVM在字符串常量池中分配了内存给"abc"这个值空间然后将这个值的地址给予str1这个引用变量,而str2在定义的时候JVM检索字符串常量池中存在了"abc"这个值同样将该值的地址给予了str2,所以str1 == str2 的值为true。
2.str2 == str3 这个表达式的比较过程,上面我们说了"=="逻辑运算符,比较String类型的变量比较的是地址,那么有人会问,不是说"abc"这个值已经在字符串常量池中存在了么,可是为什么结果会是false呢?那么我们要说明一下对于String类型变量在定义的时候的方式了,对于String name = "" 这种方式也是最常用的定义方式,该方式的定义过程就是在字符串常量池中分配内存空间并返回值的地址,而String name = new String();这种定义方式则完全不同,使用new关键字定义的对象所存储的空间是在堆内存中而非在常量池中,它的过程是,首先Java虚拟机会在字符串常量池中检索是否存在该值得内存空间,若不存在则先分配空间给该值,若存在则将该值(不是内存地址)复制一份到堆内存中,然后将该值在堆内存中的地址赋予引用变量str3。这样就会导致str2 == str3的值为false。
3.str3 == str4 的值为false,通过2的解释我想大家可以明白str3所引用的对象和str4所引用的对象在堆内存中的地址是不一致的,所有是false。
4.str3.equals(str4)的值为true,也许有的人会问了,equals()这个方法比较的不也是地址么?我的回答很简单,大家可以看一下Object源码一切就会明白了,
public boolean equals(Object obj) {
return (this == obj);
}
很简单的代码段,要注意用红色框注的部分,这个方法返回的表达式用的是"==",那么上面已经说过了,"=="逻辑运算符在引用变量中比较的是内存中的地址,这样回答可能又会问了,可是答案是true啊?那么我回答你的是,我们比较的是String类型的变量而非是Object类型的变量。Object是Java中所有类(包括自定义类)的超类,所有的类都继承了Object类的元素String类也不例外,但是不同的时候String类对equals()方法进行了重写
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = count;
if (n == anotherString.count) {
char v1[] = value;
char v2[] = anotherString.value;
int i = offset;
int j = anotherString.offset;
while (n-- != 0) {
if (v1[i++] != v2[j++])
return false;
}
return true;
}
}
return false;
}
通过上面的代码段,很容易就能得知str3.equals(str4)的值为什么是true了,最终比较的是两个引用变量的值。
这是我对于堆和栈的理解,可能很浅显所以希望大家可以多多讨论互相帮助!