java中堆和栈

在函数中定义的一些基本类型的变量和对象的引用变量都在函数的栈内存中分配。 当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。Java自动管理栈和堆,程序员不能直接地设置栈或堆。 

java堆 

Java堆是被所有线程共享的一块内存区域,在虚拟机启动(程序运行)时创建,是虚拟机所管理的内存中最大的一块。此内存区域的唯一目的就是【存放对象实例和数组】,几乎所有的对象实例和数组都在这里分配内存。

如果在堆中没有内存完成实例分配,并且堆上也无法再扩展时,将会抛出OutOfMemoryError异常。

内存泄露 : 指程序中动态分配内存给一些临时对象,但是对象不会被GC所回收,它始终占用内存。即被分配的对象可达但已无用,可用内存越来越少。
内存溢出 : 指程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于老年代或永久代垃圾回收后,仍然无内存空间容纳新的Java对象的情况。
内存泄露是内存溢出的一种诱因,不是唯一因素。

堆是一个"运行时"数据区,类实例化的对象就是从堆上去分配空间的。堆用于存储Java中的对象和数组。

为什么数组也存在堆中。因为我们定义数组可以扩容, 所以数组也需要动态分配内存。因此数组也需要存放到堆中,其实没有必要把数组单独列举出来。因为数组的定义也是new

例如: String[] arg ={"a","b","c"};  也可以改为 String[] arr = new String[] {"a","b","c"};  如果感觉这个没有睡服力 ,那么你看到这个你一定会相信的。

打开 Class 类可以发现这个方法 

/**
* Determines if this <code>Class</code> object represents an array class.
*
* @return <code>true</code> if this object represents an array class;
* <code>false</code> otherwise.
* @since JDK1.1
*/
public native boolean isArray();

从注释上来看 

判断当前类 对象是否表示数组类。 

返回字符true  那么此对象表示数组类;
否则返回字符false 。

是不是一切都很明了了,class类中有这个方法。就说明数组也是类。

当我们new(在堆上分配空间是通过"new"等指令建立的)一个对象或者创建一个数组的时候,就会在堆内存中开辟一段空间给它(实际上就是调用构造方法,就是为了给对象赋值。调用构造函数就是为了赋值即初始化,否则构造函数没有一点意义。要么是在构造方法里传递参数,要是创建对象后给对象set值。这也是为什么很多的实体类里需要有get 和set方法)。但是这个对象在堆内存中的首地址会存储在栈中。(对象的引用存在栈内,这个引用不仅仅是堆的物理地址信息。当然怎么根据这个地址去找到堆中数据,我也不清楚我们也不需要去了解。)

延伸一点***:   那么对象为什么要放在堆中呢,程序在编译时为什么不去给对象分配地址呢?  这就是java的优点,java的多态性。 只有在新建对象时调用对象的构造方法,才能判定出所需要的内存空间大小然后合理的去分配内存空间。所以  堆 跟 对象实例化  是一种非常好的结合方式,所以堆才可以动态的分配内存,对象也存放在堆中。

堆内存的特点是什么?

(1):FIFO队列优先,先进先出(例如,超市排队)。jvm只有一个堆区被所有线程所共享!堆存放在二级缓存中,调用对象的速度相对慢一些,生命周期由虚拟机的垃圾回收机制定。

延伸一点 ****二级缓存的概念

通常来说,处理器的L1级缓存通常都是静态RAM,速度非常的快,但是静态RAM集成度低(存储相同的数据,静态RAM的体积是动态RAM的6倍),而且价格也相对较为昂贵(同容量的静态RAM是动态RAM的四倍)。扩大静态RAM作为缓存是一个不太合算的做法,但是为了提高系统的性能和速度又必须要扩大缓存,这就有了一个折中的方法:在不扩大原来的静态RAM缓存容量的情况下,仅仅增加一些高速动态RAM做为L2级缓存。高速动态RAM速度要比常规动态RAM快,但比原来的静态RAM缓存慢,而且成本也较为适中。一级缓存和二级缓存中的内容都是内存中访问频率高的数据的复制品(映射),它们的存在都是为了减少高速CPU对慢速内存的访问。

而缓存中的数据要经常按照一定的算法来更换,这样才能保证缓存中的数据经常是被访问最频繁的。命中率算法中较常用的“最近最少使用算法”(LRU算法),它是将最近一段时间内最少被访问过的行淘汰出局。因此需要为每行设置一个计数器,LRU算法是把命中行的计数器清零,其他各行计数器加1。当需要替换时淘汰行计数器计数值最大的数据行出局。这是一种高效、科学的算法,其计数器清零过程可以把一些频繁调用后再不需要的数据淘汰出缓存,提高缓存的利用率。

至于说"堆数据放在二级缓存,栈数据放在一级缓存"这种说法
需要结合具体的体系结构来说,我们一般是指的intel的x86体系
L1cache(高速缓存)里面存放了函数调用时候栈里面的一堆寄存器的值,EAX,EBX,ECX,EDX,EBP。。。等等
原因就是因为只有L1是最快的存储,而流水线执行的时候,需要取EAX,EBX等等的值去做内部映射到实际的物理寄存器,
所以为了保证函数调用时候指令的执行速度 也只能把栈放在这里cache。

(2):堆是为动态分配预留的内存空间,可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。

对堆而言,数据项位置没有固定的顺序。你可以以任何顺序插入和删除。

java虚拟机栈

  java虚拟机栈是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭)

栈描述的是Java方法执行的内存模型:每一个方法执行的同时都会创建一个栈帧(Stack Frame),用于存储局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池的引用、 方法返回地址(Return Address)和一些额外的附加信息。

  每一个方法的执行就对应着栈帧在虚拟机栈中的入栈,出栈过程。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。讲到这里,大家就应该会明白为什么 在 使用 递归方法的时候容易导致栈内存溢出的现象了以及为什么栈区的空间不用程序员去管理了,这部分空间的分配和释放都是由系统自动释放的。对于所有的程序设计语言来说,栈这部分空间对程序员来说是不透明的。

栈帧

  栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧用于存储局部变量表、操作数栈、动态链接、方法返回等信息。 每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。  在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。

(1)局部变量表

  就是用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。

1. 局部变量表所需的内存空间在编译期确定,当进入一个方法时,方法在栈帧中所需要分配的局部变量控件是完全确定的,不可动态改变大小。

2. 异常:线程请求的栈帧深度大于虚拟机所允许的深度---StackOverFlowError,如果虚拟机栈可以动态扩展(大部分虚拟机允许动态扩展,也可以设置固定大小的虚拟机栈),但是无法申请到足够的内存---OutOfMemorError。

(2)操作数栈

  每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的操作数栈,也可以称之为表达式栈(Expression Stack)。操作数栈和局部变量表在访问方式上存在着较大差异,操作数栈并非采用访问索引的方式来进行数据访问的,而是通过标准的入栈和出栈操作来完成一次数据访问。每一个操作数栈都会拥有一个明确的栈深度用于存储数值,一个32bit的数值可以用一个单位的栈深度来存储,而2个单位的栈深度则可以保存一个64bit的数值,当然操作数栈所需的容量大小在编译期就可以被完全确定下来,并保存在方法的Code属性中

  1. 后进先出LIFO,最大深度由编译期确定。栈帧刚建立时,操作数栈为空,执行方法操作时,操作数栈用于存放JVM从局部变量表复制的常量或者变量,提供提取,及结果入栈,也用于存放调用方法需要的参数及接受方法返回的结果。

  2. 操作数栈可以存放一个jvm中定义的任意数据类型的值。

  3. 在任意时刻,操作数栈都一个固定的栈深度,基本类型除了long、double占用两个深度,其它占用一个深度

当在main方法中调用别的方法时,就会有另一个方法的栈帧入虚拟机栈,当该方法调用完了之后,弹栈,然后main方法处于栈顶,就继续执行,直到结束,然后main方法栈帧也弹栈,程序就结束了。总之虚拟机栈中就是有很多个栈帧的入栈出栈,栈帧中存放的都是一些变量名等东西,所以我们平常说栈中存放的是一些局部变量,因为局部变量就是在方法中。

(3)动态连接

   每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(如final、static域等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。

(4)方法返回地址

  当一个方法被执行后,有两种方式退出该方法:执行引擎遇到了任意一个方法返回的字节码指令或遇到了异常,并且该异常没有在方法体内得到处理。无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能保存了这个计数器值,而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。

  方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,如果有返回值,则把它压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。

栈内存的特点

(1):FILO 先进后出(例如,放在木桶里的东西,先放进去的后取出来,跟子弹弹夹)。暂存数据的地方。每个线程都包含一个栈区!栈存放在一级缓存中,存取速度较快,“栈是限定仅在表头进行插入和删除操作的线性表”。

(2):存取速度比堆要快,仅次于寄存器,栈数据可以共享,但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。

在栈上创建变量的时候会扩展,并且会自动回收。对栈而言,栈中的新加数据项放在其他数据的顶部,移除时你也只能移除最顶部的数据(不能越位获取)。

栈的数据共享  例如在类中定义  int a=1  int b=1 首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为1的地址,没找到,就开辟一个存放1这个字面值的地址,然后将a指向1的地址。接着处理int b = 1;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向1的地址。

测试 堆栈的一个小demo

public class demo1 {
public static void main(String[] args) {
String gener = "男";
System.out.println("变化前为gener"+gener);
changeSex(gener);
System.out.println("变化后为gener"+gener);

Apple a = new Apple(2,"二");
System.out.println("变化前a的值为"+a.getA()+",i的值为"+a.getI());
changeA(a);
System.out.println("变化后a的值为"+a.getA()+",i的值为"+a.getI());

}
public static void changeSex(String gener){
gener="女";
System.out.println("变化时为gener"+gener);
}

public static void changeA(Apple a){
a.setI(1);
a.setA("一");
System.out.println("变化时a的值为"+a.getA()+",i的值为"+a.getI());
}
}
class Apple{
private int i;
private String a;
public Apple(int i,String a){
this.i=i;
this.a=a;
}
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
public String getA() {
return a;
}
public void setA(String a) {
this.a = a;
}

}

运行后输出为:

变化前为gener男
变化时为gener女
变化后为gener男
变化前a的值为二,i的值为2
变化时a的值为一,i的值为1
变化后a的值为一,i的值为1

当然 gener有点偷换概念的意思,changeSex 里的参数 gener 跟main里面不一样  不过指向的内存地址一样 然后 changeSex  里gener又重新指向了另一个单main方法里的并没有变

因为常量的作用域有限  在 changeSex方法结束后这个方法里的gener已经消失

其实如果 改一下main方法中代码  gener =changeSex(gener)  和

public static String changeSex(String gener){
gener="女";
System.out.println("变化时为gener"+gener);
return gener;
}

  那么输入的结果就变了

变化前为gener男
变化时为gener女
变化后为gener女
变化前a的值为二,i的值为2
变化时a的值为一,i的值为1
变化后a的值为一,i的值为1

这就看出堆栈的差异性,

方法区 

它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。

在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。

运行时常量池

  (1)用于存放用来存储编译期间生成的字面量和符号引用,这部分内容(也可以称为 .Class文件中的静态常量池)将在类加载后进入方法区的运行时常量池中存放。

字面量 : 比较接近Java语言层面的常量概念,如文本字符串、声明为final的常量值等。(final修饰的 成员变量和类变量!「类变量即静态(成员)变量)」,也就是除final修饰的局部变量

符号引用 : 属于编译原理方面的概念,包括 
1.类和接口的全限定名(即路径,包名+类名)。 
2.字段的名称和描述符。 
3.方法的名称和描述符。 
当虚拟机运行时,需要从常量池获得对应的符号引号,再在类创建或运行时解析、翻译到具体的内存地址之中(直接引用)。

(2) 除了保存Class文件中描述的符号引用外,还会把编译出来的直接引用也存储在运行时常量池中。

(3)Java语言并不要求常量一定只有编译期才能生成,也就是并非置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String 类的intern()方法。(后面会分析String类。)

静态变量

在编译时就能确定每个数据目标在运行时刻的存储空间需求,因而在编译时就可以给他们分配固定的内存空间.这种分配策略要求程序代码中不允许有可变数据结构(比如可变数组)的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间需求.。(如果在定义时没有赋值,在编译时会默认给静态变量赋值。所以没有初始化也可以调用静态变量,而实例变量则不行。如图 f可以直接使用,a则不行必须初始化。一般都是static关键字修饰的  例如  private static String a="ABC";)

静态变量是随着类的加载而加载的,当类加载进内存时,静态变量就已经伴随着类的加载而初始化进内存了,并且静态变量只在类加载时加载一次,存放在方法区中的静态区中。

 静态存储分配要求在编译时能知道所有变量的存储要求,栈式存储分配要求在过程的入口处必须知道所有的存储要求,而堆式存储分配则专门负责在编译时或运行时模块入口处都无法确定存储要求的数据结构的内存分配,比如可变长度串和对象实例.堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放

 参考文档

https://www.cnblogs.com/dolphin0520/p/3613043.html

https://www.cnblogs.com/whgk/p/6138522.html

https://blog.csdn.net/qian520ao/article/details/78952895

https://www.cnblogs.com/niejunlei/p/5987611.html

原文地址:https://www.cnblogs.com/zjf6666/p/9264498.html