💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## 一些理解 堆是堆(heap),栈是栈(stack),堆栈是栈。嗯我很不喜欢“堆栈”这种叫法,容易让新人掉坑里。JVM规范让每个Java线程拥有自己的独立的JVM栈,也就是Java方法的调用栈。同时JVM规范为了允许native代码可以调用Java代码,以及允许Java代码调用native方法,还规定每个Java线程拥有自己的独立的native方法栈。这俩都是JVM规范所规定的概念上的东西,并不是说具体的JVM实现真的要给每个Java线程开两个独立的栈。以Oracle JDK / OpenJDK的HotSpot VM为例,它使用所谓的“mixed stack”——在同一个调用栈里存放Java方法的栈帧与native方法的栈帧,所以每个Java线程其实只有一个调用栈,融合了JVM规范的JVM栈与native方法栈这俩概念。JVM里的“堆”(heap)特指用于存放Java对象的内存区域。所以根据这个定义,Java对象全部都在堆上。 要注意,这个“堆”并不是数据结构意义上的堆(Heap (data structure),一种有序的树),而是动态内存分配意义上的堆——用于管理动态生命周期的内存区域。JVM的堆被同一个JVM实例中的所有Java线程共享。它通常由某种自动内存管理机制所管理,这种机制通常叫做“垃圾回收”(garbage collection,GC)。JVM规范并不强制要求JVM实现采用哪种GC算法。 ## 堆和栈的区别 * 功能不同 栈内存用来存储局部变量和方法调用。 而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。 * 共享性不同 栈内存是线程私有的。 堆内存是所有线程共有的。 * 异常错误不同 如果栈内存或者堆内存不足都会抛出异常。 栈空间不足:java.lang.StackOverFlowError。 堆空间不足:java.lang.OutOfMemoryError。 * 空间大小 栈的空间大小远远小于堆的。 ## 深入理解栈 栈帧由三部分组成:局部变量区、操作数栈、帧数据区。局部变量区和操作数栈的大小要视对应的方法而定,他们是按字长计算的。但调用一个方法时,它从类型信息中得到此方法局部变量区和操作数栈大小,并据此分配栈内存,然后压入Java栈。 ### 局部变量区 局部变量区被组织为以一个字长为单位、从0开始计数的数组,类型为short、byte和char的值在存入数组前要被转换成int值,而long和double在数组中占据连续的两项,在访问局部变量中的long或double时,只需取出连续两项的第一项的索引值即可,如某个long值在局部变量区中占据的索引时3、4项,取值时,指令只需取索引为3的long值即可。 如下代码以及图所示: ``` public static int runClassMethod(int i,long l,float f,double d,Object o,byte b) { return 0; } public int runInstanceMethod(char c,double d,short s,boolean b) { return 0; } ``` ![](https://box.kancloud.cn/6eb95e3262204f64161112b1e865e0ec_670x300.png) ### 操作数栈 和局部变量区一样,操作数栈也被组织成一个以字长为单位的数组。但和前者不同的是,它不是通过索引来访问的,而是通过入栈和出栈来访问的。可把操作数栈理解为存储计算时,临时数据的存储区域。下面我们通过一段简短的程序片段外加一幅图片来了解下操作数栈的作用。 ``` int a = 100; int b = 98; int c = a+b; ``` ![](https://box.kancloud.cn/85b8f5f423a48241557b25cc04417f24_587x257.png) 从图中可以得出:操作数栈其实就是个临时数据存储区域,它是通过入栈和出栈来进行操作的。 ### 帧数据区 除了局部变量区和操作数栈外,java栈帧还需要一些数据来支持常量池解析、正常方法返回以及异常派发机制。这些数据都保存在java栈帧的帧数据区中。 当JVM执行到需要常量池数据的指令时,它都会通过帧数据区中指向常量池的指针来访问它。 除了处理常量池解析外,帧里的数据还要处理java方法的正常结束和异常终止。如果是通过return正常结束,则当前栈帧从Java栈中弹出,恢复发起调用的方法的栈。如果方法有返回值,JVM会把返回值压入到发起调用方法的操作数栈。 为了处理java方法中的异常情况,帧数据区还必须保存一个对此方法异常引用表的引用。当异常抛出时,JVM给catch块中的代码。如果没发现,方法立即终止,然后JVM用帧区数据的信息恢复发起调用的方法的帧。然后再发起调用方法的上下文重新抛出同样的异常。 ### 栈的整个结构 栈是由栈帧组成,每当线程调用一个java方法时,JVM就会在该线程对应的栈中压入一个帧,而帧是由局部变量区、操作数栈和帧数据区组成。那在一个代码块中,栈到底是什么形式呢?下面是《深入JVM》中的一个例子: ``` public class Main{ public static void addAndPrint(){ double result = addTwoTypes(1,88.88); System.out.println(result); } public static double addTwoTypes(int i,double d){ return i + d; } } ``` 执行过程中的三个快照: ![](https://box.kancloud.cn/fa384b31d782aa53e3970efd84bbc76a_635x423.png) 上图说明两件事情: 1、只有在调用一个方法时,才为当前栈分配一个帧,然后将该帧压入栈 ; 2、帧中存储了对应方法的局部数据,方法执行完,对应的帧则从栈中弹出,并把返回结果存储在调用 方法的帧的操作数栈中。 ## 从操作系统里进程的内存结构解释 下图是linux 中一个进程的虚拟内存分布: ![](https://box.kancloud.cn/690041547c7a5d73e70786fd4394c868_530x327.png) 图中0号地址在最下边,越往上内存地址越大。以32位地址操作系统为例,一个进程可拥有的虚拟内存地址范围为0-2^32。分为两部分,一部分留给kernel使用(kernel virtual memory),剩下的是进程本身使用, 即图中的process virtual memory。普通Java 程序使用的就是process virtual memory。上图中最顶端的一部分内存叫做user stack,中间有个 runtime heap, 他们的名字和数据结构里的stack 和 heap 几乎没啥关系。注意在上图中,stack 是向下生长的; heap是向上生长的。当程序进行函数调用时,每个函数都在stack上有一个 call frame。比如对于以下程序, ``` public void foo(){ //do something... println("haha"); // <<<=== 在这儿设置breakpoint 1 } public void bar(){ foo(); } main(){ bar(); println("hahaha"); // <<<=== 在这儿设置 breakpoint 2 } ``` 当程序运行到breakponit1时,user stack 里会有三个frame: ![](https://box.kancloud.cn/6650d9a15b4e14df89663ac218ce669e_426x234.png) 其中 esp 和 ebp 都是寄存器。 esp 指向stack 的顶(因为stack 向下生长,esp会向下走); ebp 指向当前frame的边界。当程序继续执行到brekapoing 2的时候stack 大概是这样的: ![](https://box.kancloud.cn/c3b094f9e4ecb6f1caa9d5eeb38f0b50_417x142.png) 也就是说当一个函数执行结束后,它对应的call frame就被销毁了。(其实就是esp 和 ebp分别移动,但是内存地址中的数据只有在下一次写的时候才被覆盖。) 说了这么多,终于该说什么东西放在stack 上什么东西放在heap 上了。最直白的解释: ``` public void foo(){ int i = 0; // <= i 的值存在stack上,foo()的call frame 里。 Object obj = new Object(); // object 对象本身存在heap 里, foo()的call frame 里存该对象的地址。 } ``` ## 常见误区 ### Java中的基本数据类型一定存储在栈中吗? 不一定。栈内存用来存储局部变量和方法调用。 如果该局部变量是基本数据类型例如 : ``` int a = 1; ``` 那么直接将该值存储在栈中。 如果该局部变量是一个对象如 : ``` int[] array=new int[]{1,2}; ``` 那么,将引用存在栈中,而对象({1,2})存储在堆内。 ### 栈的速度比堆快吗? 一定情况下栈的速度是比堆快的,但是快的并不明显。毕竟都是RAM。所以这算不上堆和栈的一大区别。 ## 结论 基本类型数据如果是局部变量并且非对象,那么JVM中是把值直接存入栈中的,而不是存储一个引用对象然后借由这个对象来找到值。这其实算的上是实际运行时JVM提供的性能优化。因此基本数据类型和引用类型在栈中的存储情况就是不一样的了。 但是这些不一样,对于用户(程序员)来说是透明的。所以如果仅仅从语义的角度把基本类型看成引用类型,虽然不够严谨,但是对于使用者(程序员)来说有利于理解和学习。