堆和栈都是Java中常用的存储结构,都是内存中存放数据的地方。
- 堆:(对象,数组)
- 存放的东西:引用类型的变量,其内存分配在堆上或者常量池(字符串常量、基本数据类型常量),需要通过new等方式来创建,堆内存主要作用是存放运行时创建(new)的对象。
- 一个程序只有一个堆内存:每个Java程序在一个独立的JVM实例上运行,每个JVM实例对应一个堆,同个java程序内的多线程运行在同个JVM实例上,多个线程之间共享堆内存(多线访问堆时,要实现数据的同步)。
- 什么时候销毁:当对象没有引用变量指向它时,会被系统垃圾回收器不定时回收。
- 特点:主要用于存放对象,存取速度慢,可以运行时动态分配内存,生存期不需要提前确定
- 栈:(基本数据类型变量、对象的引用变量、执行程序)
- 存放的东西:基本数据类型的变量及其值(int、short、long、byte、float、double、boolean、char等)以及对象的引用变量,执行程序。
- 什么时候销毁:变量出了作用域就会自动释放。
- 特点:主要用来执行程序,存取速度快,仅次于寄存器,栈数据可以共享。但大小和生存期必须确定,缺乏灵活性,栈的内存管理是通过栈的"后进先出"模式来实现的。
栈以帧为单位保存线程的状态。JVM对栈只进行两种操作:以帧为单位的压栈和出栈操作。某个线程正在执行的方法称为此线程的当前方法.当前方法使用的帧称为当前帧。当线程激活一个Java方法,JVM就会在线程的 Java堆栈里新压入一个帧。这个帧自然成为了当前帧.在此方法执行期间,这个帧将用来保存参数,局部变量,中间计算过程和其他数据.这个帧在这里和编译原理中的活动纪录的概念是差不多的.
栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义:
int a = 3;
int b = 3;
编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,如果没找到,就将3存放进来,然后将a指向3。接着处理int b = 3;在创建完b的引用变量后,因为在栈中已经有3这个值,便将b直接指向3。这样,就出现了a与b同时均指向3的情况。这时,如果再令a=4;那么编译器会重新搜索栈中是否有4值,如果没有,则将4存放进来,并令a指向4;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。要注意这种数据的共享与两个对象的引用同时指向一个对象的这种共享是不同的,因为这种情况a的修改并不会影响到b, 它是由编译器完成的,它有利于节省空间。而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量
- 实例:
- class 鸭子{
- private int age;
- private int weight;
- public 鸭子(int age,int weight){
- this.age=age;
- this.weight=weight;
- }
- }
- public class Test{
- public static void main(String[] args){
- //int i=0; //基本数据类型变量
- 鸭子 duck =new 鸭子(1,1000); //duck是对象的引用变量,存在栈;鸭子(1,1000)是实际的对象,存在堆。
- }
- }
图例如下:
- 三种内存分配策略:静态的,栈式的,和堆式的
- 静态存储分配要求在编译时能知道所有变量的存储要求,所以要求代码中不允许有可变数组结构,或嵌套递归结构。
- 栈式存储分配要求在过程的入口处必须知道所有的存储大小要求,而且是先进后出
- 堆式存储分配则专门负责在编译时或运行时模块入口处都无法确定存储要求的数据结构的内存分配,比如可变长度串和对象实例.堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放.
- 为什么会有栈内存和堆内存之分
当一个方法执行时,每个方法都会建立自己的内存栈,在这个方法定义的变量将会放到这块栈内存里,随着方法的结束,这个方法的内存栈也将自动销毁。(不需要GC回收)
因此,所有在方法中定义的局部变量放在栈内存中;
当我们在程序中创建一个对象时,这个对象会被保存到运行时数据区中,以便反复利用(复用,因为创建对象的成本通常较大),这个运行时数据区就是堆内存。堆内存中的对象不会随着方法的结束而销毁,即使方法结束后,这个对象还可能被另外一个引用变量所引用(在方法的参数传递时很常见),则这个对象依然不会被销毁。只有当一个对象没有任何引用变量去引用它时,系统的垃圾回收器(GC)才会在合适的时候回收它。
为什么上面说创建对象的开销(成本)比较大?
来看看如何创建对象、创建对象的过程:
创建对象的根本途径是构造器,通过new关键字来调用某个类的构造器即可创建这个类的实例。但对象不是完全由构造器来负责创建的,实际上,当程序员调用构造器时,系统会先为该对象分配内存空间,并为这个对象执行默认初始化,这个对象已经产生了---这些是在构造器执行之前就完成的,也就是说,当系统开始执行构造器的执行体之前,系统已经创建了一个对象,只是这对象还不能被外部程序访问,只能在构造器中通过this来引用。当构造器的执行体执行结束后,这个对象作为构造器的返回值被返回,通常还会赋给另外一个引用类型的变量,从而让外部程序可以访问。(当然可以通过设置构造器的访问权限private,阻止其他类创建该类的实例)