Java内存的故事
Stack堆栈, 和Heap堆
先科普一下计算机里内存内存的结构:
每当一个程序被执行,系统就要为它开启一个进程,并且为它分配内存。从低址区到高址区,分成几个不同的区域。
低址:存放程序代码本身。
次低址:存放全局变量,无论是初始化的还是未初始化的。
中址:就是堆和堆栈的区域。用来储存进程运行过程中产生的变量。
最高址:为系统额外预留的空间。我们无法操作。
stack(堆栈)
从高址区往下延展。用来存储Scoped Variable(限域变量)。简单说就是已知存储空间以及生命周期的变量。为什么stack效率高呢?因为变量大小确定,都是紧挨着储存的,在堆栈中创建和释放储存空间只要一条汇编语言,分别是将栈顶指针向下和向上移动。而stack本身又是LIFO(Last in First out)的,所以效率极高。
heap(堆)
从较低地址区往上移动。heap是个动态内存池。从下面的图我们看的很清楚,heap不像stack那样是数据是连续的。heap的数据是不连续的,动态随便乱贴的。创建和释放效率都不高。
Java对象存在哪儿?
“引用”存放在”stack”(堆栈)中
Java声称一切都是对象,Java完全采用了动态内存分配方式。这是因为所有创建的对象全都继承自单根基类Object,而这个Object又只能以唯一的方式从heap堆中创建。
对于 Java 应用程序,实际上包含两个池:Java 堆和本机(非 Java)堆。Java 堆的大小由 JVM 的 Java 堆设置控制:-Xms 和 -Xmx 分别设置最小和最大 Java 堆。在按照最大的大小设置分配了 Java 堆之后,剩下的用户空间就是本机堆
本机堆里包含JVM堆。剩下的就是空闲Native堆。上图中显得Java数据好像全在heap里,完全不用stack。这是不准确的,Java用stack!实际上,Java每个对象都有一个指向他的指针,叫”引用“。可以理解为C++的指针。Java不是不用指针,只是泛化他,看下面这个声明
String s;
这里s创建的只是”引用”,并不是”对象”。这时候还没有对象,只有引用。这时候如果要求输出s,系统会返回错误。
对象的引用存放在”stack“(堆栈)中。
“对象”存放在”heap”(堆)中|
new关键字,负责创建对象。对象存放在heap(堆)中。看下面的例子,
String s = new String("hello");
这时候s是reference引用,存在stack堆栈里。String(hello)是对象,存在heap堆中。
当然,我们可以用另一种”奇怪”的方式声明一个String,
String s = "hello";
这里不用new关键字,只不过是Java的一个”特性”,并不是本性。只是说Java用了特殊的方法,形式上允许不用new来创建一个String对象,可以直接赋值。但本质上,Java内部处理以后,这个”hello”还是以对象的方式存在heap区里。
例外
特别强调八种基本类型
因为他们不属于对象,不存放在heap堆区,而是直接存在stack堆栈区。因为他们的所占用的存储空间和生命周期都是已知的。
让我们再瞻仰一下他们伟岸的面容。其实下面有九种,因为加上了一个void空型。
基本类型对应的包装器类是在堆中创建的非基本对象。
一个Java对象里有些啥?
Java一切都是对象的理念很美,但付出的内存的代价也是巨大的。对象的元数据,大小相当惊人,一般都是他们存放的数据本身的好几倍,根据 JVM 的版本和供应的不同,对象元数据的数量也各有不同,但其中通常包括:
类:一个指向类信息的指针,描述了对象类型。举例来说,对于 java.lang.Integer 对象,这是 java.lang.Integer 类的一个指针。
标记:一组标记,描述了对象的状态,包括对象的散列码(如果有),以及对象的形状(也就是说,对象是否是数组)。
锁:对象的同步信息,也就是说,对象目前是否正在同步。
对象元数据后紧跟着对象数据本身,包括对象实例中存储的字段。对于 java.lang.Integer 对象,这就是一个 int。 如果您正在运行一个 32 位 JVM,那么在创建 java.lang.Integer 对象实例时,对象的布局可能如下图所示。也就是说,为了储存一个32位的int数据,java要占用128位内存。
Java的作用域
前面说过了,java把对象的引用存在stack区,把对象存在heap区。看下面这张图
所有存在stack区的内容,还是遵守花括号{}的作用域,比如基本型i=4,y=2,还有对象的引用cls1,出了域的终点–花括号,就都消失了,所以他们的作用域和占用空间是已知的。但在heap区的对象本身还存在,并没有被销毁。只是我们已经找不到他了,因为指向他的引用cls1已经擦除了。
这是Java很好的一个特性,因为只要我们注意传递和复制对象的引用,在后面的程序中我们一直可以调用这个对象。因为只要有一个引用能让我们找到他,他一直在那儿。
另一个好处(对程序员),但也可以说是坏处(对系统),就是一旦一个对象完全失去引用,我们不必像C++这样手动用delete()释放heap区的对象。我们可以完全不去管它。但我们不管他,意味着必须有别人去管它,这就是JVM里的垃圾回收器(Garbage Collection)。但因为处理的对象都是heap区里的家伙,所以开销要大很多,对系统负担也很大。