JVM内存分配:堆、栈和方法区
摘要:基本类型的变量、对象的引用和函数调用的现场等存储在栈中,通过new关键字和构造器创建的对象存储在堆中,字面量如100、”hello”和常量等存储在静态区。
概述
我们首先介绍一个本文要经常提及的术语字面量(literal)。在计算机科学中,字面量是用于表达源代码中一个固定值的表示法(notation)。几乎所有计算机编程语言都具有对基本类型变量值的字面量表示,诸如整数、浮点数以及字符串,其中常见的字符串字面量是指双引号引起来的一系列字符,双引号中可以没有字符,可以只有一个字符,也可以有很多字符。
int a = 100; // 100为int类型字面量
String bStr = "Hello world" // Hello world 为字符串字面量
字面量作为一种通用的,跨平台的数据交换格式,在程序界是公认的事实。
言归正传,下面聊聊本文的主角。JVM中内存分配一共有三个区:堆区(heap)、栈区(stack)和方法区(静态区),了解java的这3大区域非常有必要,尤其是工作中需要对jvm性能调优,更应该深度掌握下它们的概念和作用。
堆区
堆是一个运行时数据区,专门用来保存类(class)的实例(instance)并分配空间,不存放基本类型和对象引用,例如new 创建的实例和数组,实际上只是保存实例的字面量、类型和类型标记等,然而并不保存实例的方法(方法是指令,保存在下一节介绍的栈中)。
jvm只有一个堆区,它被所有线程共享。堆的大小是由垃圾收集器来负责的,优势是可以动态地分配内存大小,生命周期也不必事先告诉编译器,因为它是在运行时动态分配内存的,垃圾收集器会自动收走不再使用的数据。缺点是由于要在运行时动态分配内存,所以,存取速度较慢。
栈区
每个线程包含一个栈区,栈中只保存基础数据类型变量的字面量和自定义对象的引用(不是对象),对象都存放在堆区中。
每个栈中的数据(基本类型和对象引用)都是私有的,其它栈不能访问。栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
方法区
方法区又叫静态区,跟堆一样,被所有的线程共享。它存储的都是在整个程序中永远唯一的元素,如class和static变量。全局变量和静态成员变量的存储是放在一块的,但是,初始化和未初始化的分别存储在相邻的两块区域。
虚拟机的体系结构包括堆、方法区、本地方法栈和pc寄存器。而方法区保存的就是一个类的模板,堆是放类的实例的,一般来用于函数计算,它里面的数据在函数执行完时是不会存储的,直接丢弃。这就是为什么局部变量每一次都是一样的,即便修改了它后,下次执行函数的时候还是原来的值。
如果栈内存或者堆内存不足都会抛出异常:
-
栈空间不足:java.lang.StackOverFlowError。
-
堆空间不足:java.lang.OutOfMemoryError。
栈的空间大小远远小于堆的。栈空间操作起来最快但是空间很小,通常大量的对象都是放在堆空间,理论上整个内存没有被其它进程使用的空间甚至硬盘上的虚拟内存都可以被当成堆空间来使用。
为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?
第一,从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据。这样分开,使得处理逻辑更为清晰,体现了分而治之的思想,这种隔离、模块化的思想在软件设计的方方面面都有体现。
第二,堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这种共享有很多益处。一方面,它提供了一种有效的数据交互方式(如:共享内存),另一方面,堆中的共享常量和缓存可以被所有栈访问,节约空间。
第三,栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制栈存储内容的能力。而堆不同,堆中的对象可以根据需要动态调整,因此栈和堆的拆分,使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可。
第四,面向对象就是堆和栈的完美结合。其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是,面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于自然方式的思考。当我们把对象拆开时就会发现对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是算法(运行逻辑),放在栈中。我们在编写对象的时候,其实即编写了数据结构,也编写的处理数据的算法。不得不承认,面向对象的设计确实极具曲线美。
知行合一
下面分析一下基础类型变量内存分配过程。
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,它是由编译器完成的,目标是节省空间;而一个对象引用变量修改了堆中对象的内部状态时,会影响其它共同引用了堆中对象的变量。
String bStr = "Hello world";
上面的语句中变量bStr放在栈上,用new创建出来的字符串对象放在堆上,而“Hello world”这个字面量放在静态区。
Reference

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南