java中栈内存与堆内存(JVM内存模型)
Java中堆内存和栈内存详解1 和 Java中堆内存和栈内存详解2 都粗略讲解了栈内存和堆内存的区别,以及代码中哪些变量存储在堆中、哪些存储在栈中。内存中的堆和栈到底是什么 详细讲述了程序在内存中的模型,从可执行文件(ELF)格式的编译介绍了堆和栈,主要是C/C++语言,讲的比较清楚,借鉴性比较强。
其实,对于java语言,编译后的文件是一个中间字节代码,操作系统不能直接执行,需要jvm解释执行。与C/C++对应起来,理解java的栈内存和堆内存,应该从jvm的内存模型入手(参考深入理解JVM-内存模型(jmm)和GC)。
一、Java内存模型
java程序内存的分配是在JVM虚拟机内存分配机制下完成。
java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
根据java虚拟机规范,java虚拟机管理的内存将分为下面五大区域。
1.程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,也就是说,在同一时刻一个处理器内核只会执行一条线程,处理器切换线程时并不会记录上一个线程执行到哪个位置,所以为了线程切换后依然能恢复到原位,每条线程都需要有各自独立的程序计数器。
特点:
-
线程私有
-
JVM规范中唯一没有规定OutOfMemoryError情况的区域
-
如果正在执行的是Native 方法,则这个计数器值为空
2. java栈(虚拟机栈)(具体参考JVM 系列 - 内存区域 - Java 虚拟机栈(三))
-
Java 虚拟机栈(Java Virtual Machine Stacks)是线程私有的,生命周期随着线程,线程启动而产生,线程结束而消亡。
-
Java 虚拟机栈描述的是 Java 方法执行的内存模型,用于存储栈帧。线程启动时会创建虚拟机栈,每个方法在执行时会在虚拟机栈中创建一个栈帧,用于存储局部变量表、操作数栈、动态连接、方法返回地址、附加信息等信息。每个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中的入栈(压栈)到出栈(弹栈)的过程。
-
Java 虚拟机栈使用的内存不需要保证是连续的。
-
Java 虚拟机规范即允许 Java 虚拟机栈被实现成固定大小(-Xss),也允许通过计算结果动态来扩容和收缩大小。如果采用固定大小的 Java 虚拟机栈,那每个线程的 Java 虚拟机栈容量可以在线程创建的时候就已经确定。
Java 虚拟机栈中的单位元素是栈帧,每个线程中调用同一个方法或者不同的方法,都会创建不同的栈帧。在 Running 的线程,只有当前栈帧有效(Java 虚拟机栈中栈顶的栈帧),与当前栈帧相关联的方法称为当前方法。每调用一个新的方法,被调用方法对应的栈帧就会被放到栈顶(入栈),也就是成为新的当前栈帧。当一个方法执行完成退出的时候,此方法对应的栈帧也相应销毁(出栈)。
每个栈帧中存放局部变量表、操作数栈、动态链接、方法返回地址、附加信息。
3. 本地方法栈
本地方法栈(Native Method Stacks)与 Java 虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。
Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用到 C/C++ 方法。当线程调用 Java 方法时,虚拟机会创建一个栈帧并压入 Java 虚拟机栈。然而当它调用的是 native 方法时,虚拟机会保持 Java 虚拟机栈不变,也不会向 Java 虚拟机栈中压入新的栈帧,虚拟机只是简单地动态连接并直接调用指定的 native 方法。
4.堆(参考JVM 系列 - 内存区域 - Java 堆(五))
Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块,也被称为 "GC堆",是被所有线程共享的一块内存区域,在虚拟机启动时被创建。
唯一目的就是储存对象实例和数组(JDK7 已把字符串常量池和类静态变量移动到 Java 堆),几乎所有的对象实例都会存储在堆中分配。随着 JIT 编译器发展,逃逸分析、栈上分配、标量替换等优化技术导致并不是所有对象都会在堆上分配。
Java 堆是垃圾收集器管理的主要区域。堆内存分为新生代 (Young) 和老年代 (Old) ,新生代 (Young) 又被划分为三个区域:Eden、From Survivor、To Survivor。
根据 Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过 -Xmx 和 -Xms 控制)。
5.方法区(https://www.jianshu.com/p/59f98076b382)
方法区(Method Area)与 Java 堆一样,是所有线程共享的内存区域。
JDK7 之前(永久代)用于存储已被虚拟机加载的类信息、常量、字符串常量、类静态变量、即时编译器编译后的代码等数据。
Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本/字段/方法/接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将类在加载后进入方法区的运行时常量池中存放。运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的是 String.intern() 方法。
java 中基本类型的包装类的大部分都实现了常量池技术,这些类是 Byte、Short、Integer、Long、Character、Boolean,另外 Float 和 Double 类型的包装类则没有实现。另外 Byte、Short、Integer、Long、Character 这5种整型的包装类也只是在对应值在-128到127之间时才可使用对象池。 |
在老版jdk,方法区也被称为永久代(可以通过 -XX:PermSize 和 -XX:MaxPermSize 来进行调节大小),JDK8 彻底将永久代移除出 HotSpot JVM,将其原有的数据迁移至 Java Heap 或 Native Heap(Metaspace),取代它的是另一个内存区域被称为元空间(Metaspace)。
元空间(Metaspace):元空间是方法区的在 HotSpot JVM 中的实现,方法区主要用于存储类信息、常量池、方法数据、方法代码、符号引用等。元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。元空间的大小理论上取决于32位/64位系统内存大小,可以通过 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 配置内存大小。
二、常说的java中的栈内存和堆内存
我们经常说的栈内存和堆内存只是java内存模型中的一部分内容,也就是编程过程中关注比较多的部分。
通常说的栈一般指栈帧中的局部变量表(存放的8种类型: byte、short、int、long、float、double、char、boolean和reference、returnAddress),它是一片连续的内存空间,用来存放方法参数,以及方法内定义的局部变量,存放着编译期间已知的数据类型。局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。
通常说的堆一般指java内存模型中的堆,用于储存对象实例和数组,几乎所有的对象实例都会存储在堆中分配。java堆是java虚拟机管理的内存中最大的一块,也被称为 "GC堆",是垃圾收集器管理的主要区域。
三、栈内存和堆内存的区别(只是便于记忆,并不严谨)
-
存储的数据及生命周期
栈主要用于存储方法参数、局部变量和对象的引用变量,存放的是编译期间已知的数据类型(八大基本类型和对象引用(reference类型),returnAddress类型。每个线程都会有一个独立的栈空间,栈内存的数据生命周期随线程的结束而结束。
所有对象实例及数组都要在堆上分配内存,堆存放的对象是线程共享的,线程结束时,对象实例和数组的生命周期并不一定结束,只有被GC回收后生命周期结束。
-
空间大小及限制
栈的内存大小在编译时确定,是一段连续的空间,运行时不会改变,栈内存随线程的结束自动回收。如果请求的栈的深度大于虚拟机允许的栈深度,JVM会抛出java.lang.StackOverFlowError。
堆内存在程序运行时动态分配,可以是存在物理上不连续的内存空间,线程运行结束后GC进行回收(只有对象或数组不再被引用时才回收)。如果是堆内存没有可用的空间存储生成的对象,JVM会抛出java.lang.OutOfMemoryError。
-
独占或共享
栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。而堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。
-
分配效率
栈由系统自动分配,速度较快。堆由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
-
存取速度(jvm中可能会不同)
由于很多CPU对压栈、出栈操作有硬件(指令)上的支持,所以在栈区分配/归还内存速度极快(相比之下,堆上分配简直是龟速);尤其是函数内部的局部变量,可以轻易与函数调用/返回绑定,因此几乎所有编译型语言都会在利用栈管理局部变量(而且会优先使用空闲的寄存器,所以几乎所有高级语言都是访问局部变量速度最快)。