概况
内存结构如图所示

JVM的内存结构大概分为:
- 堆(Heap):线程共享。所有的对象实例以及数组都要在堆上分配。回收器主要管理的对象。从结构上来分,可以分为新生代和老年代。而新生代又可以分为Eden 空间、From Survivor 空间(s0)、To Survivor 空间(s1)。 所有新生成的对象首先都是放在新生代的。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来的对象,和从前一个Survivor复制过来的对象,而复制到老年代的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的。
- 方法区(Method Area):线程共享。存储类信息、常量、静态变量、即时编译器编译后的代码。它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。。
tip:有些地方认为方法区即永久代,Java8发布后去除了该内存区域转而使用元空间(Metaspace)
- 程序计数器(Program Counter Register):线程私有。有些文章也翻译成PC寄存器(PC Register),同一个东西。它可以看作是当前线程所执行的字节码的行号指示器。指向下一条要执行的指令。
- 虚拟机栈(线程栈):线程私有。存储局部变量表、操作栈、动态链接、方法出口,对象指针。
- 本地方法栈(Native Method Stack):线程私有。为虚拟机使用到的Native方法服务。如Java使用c或者c++编写的接口服务时,代码在此区运行。
- 直接内存:直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。
tip: 线程私有部分内存生命周期与线程生命周期一致
堆空间
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。堆结构如下图所示:

上图所示的 Eden 区、两个 Survivor 区都属于新生代(为了区分,这两个 Survivor 区域按照顺序被命名为 from 和 to),右边红色区块属于老年代。
Java对象优先分配在Eden区域,在一次新生代垃圾回收(Minor GC)后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
对象进入老年代的条件有三种:
- 大对象:可通过参数 -XX:PretenureSizeThreshold 设置,超过该阈值所设大小的对象直接进入老年代,但部分垃圾回收算法不支持该参数,比如:Parallel Scavenge
- 长期存活对象:在堆中分配内存的对象,其内存布局的对象头中(Header)包含了 GC 分代年龄标记信息。当分代年龄超过了 -XX:MaxTenuringThreshold 设置年龄阀值就会进入老年代
- Survivor 空间不足?:当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值, 超过阈值进入老年代,代码如下:
uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
//survivor_capacity是survivor空间的大小
size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);
size_t total = 0;
uint age = 1;
while (age < table_size) {
total += sizes[age];//sizes数组是每个年龄段对象大小
if (total > desired_survivor_size) break;
age++;
}
uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
...
}
常用设置参数:
- -Xms:初始堆大小
- -Xmx:最大堆大小
线上业务一般这两个值配置一样
- -Xmn(-XX:MaxNewSize):新生代最大内存
- -XX:NewSize:启动时分配的新生代内存
- -XX:SurvivorRatio:设置新生代中1个Eden区与1个Survivor区的大小比值,默认为8,表示 eden:s0:s1 -> 8:1:1
- -XX:NewRatio:指定老年代/新生代的堆内存比例。如配置为4,表示 老年代:新生代 -> 4:1
- -XX:OldSize:启动分配的老年代内存大小
方法区(元空间)
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
方法区也被称为永久代。
tip: 相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了
Java8的时候,方法区(HotSpot 的永久代)被彻底移除了,取而代之是元空间,元空间使用的是直接内存, 如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。
Java8以前设置参数:
- -XX:PermSize:方法区 (永久代) 初始大小
- -XX:MaxPermSize:方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
Java8以后设置参数:
- -XX:MetaspaceSize:设置 Metaspace 的初始(和最小大小)
- -XX:MaxMetaspaceSize:设置 Metaspace 的最大大小,默认值为不限制
程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。可认为是如下代码中指令前面的序号
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: getstatic #2 // Field math:LApplication$Math;
7: iload_1
8: iload_2
9: invokevirtual #3 // Method Application$Math.add:(II)I
12: istore_3
13: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
16: iload_3
17: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
20: return
字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
程序计数器主要有以下两个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
tip: 程序计数器是唯一一块不会产生OOM的内存区域
虚拟机栈(线程栈)
与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。
虚拟机栈主要由一个个栈帧组成,结构大致如下:

栈帧
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存 储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。结构如下图所示:

局部变量表
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位,Java虚拟机规范并没有定义一个槽所应该占用内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型。
tip: 在Java程序编译为Class文件时,就在方法的Code属性中的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。(最大Slot数量)
一个局部变量可以保存一个类型为boolean、byte、char、short、int、float、reference和returnAddress类型的数据。reference类型表示对一个对象实例的引用。
操作数栈
操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO)。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到方法的Code属性的max_stacks数据项中。
操作数栈的每一个元素可以是任意Java数据类型,32位的数据类型占一个栈容量,64位的数据类型占2个栈容量,且在方法执行的任意时刻,操作数栈的深度都不会超过max_stacks中设置的最大值。
当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。
动态链接
在一个class文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池。
Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态连接(Dynamic Linking)。
这些符号引用一部分会在类加载阶段或者第一次使用时就直接转化为直接引用,这类转化称为静态解析。另一部分将在每次运行期间转化为直接引用,这类转化称为动态连接。
返回地址
当一个方法开始执行时,可能有两种方式退出该方法:
- 正常完成出口
- 异常完成出口
正常完成出口是指方法正常完成并退出,没有抛出任何异常(包括Java虚拟机异常以及执行时通过throw语句显示抛出的异常)。如果当前方法正常完成,则根据当前方法返回的字节码指令,这时有可能会有返回值传递给方法调用者(调用它的方法),或者无返回值。具体是否有返回值以及返回值的数据类型将根据该方法返回的字节码指令确定。
异常完成出口是指方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退出。
tip: 无论是Java虚拟机抛出的异常还是代码中使用athrow指令产生的异常,只要在本方法的异常表中没有搜索到相应的异常处理器(catch),就会导致方法退出。
方法退出过程实际上就等同于把当前栈帧出栈,因此退出可以执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压如调用者的操作数栈中,调整PC计数器的值以指向方法调用指令后的下一条指令。
一般来说,方法正常退出时,调用者的PC计数值可以作为返回地址,栈帧中可能保存此计数值。而方法异常退出时,返回地址是通过异常处理器表确定的,栈帧中一般不会保存此部分信息。
执行过程示例
例如线程需要执行如下代码,左边为java源码,右边为字节码

指令解释(参考Java虚拟机规范 Java SE7版):
- iconst_1:将int类型1推送入栈顶
- istore_1:将栈顶int类型数值存入第1个局部变量表
- iload_1: 将局部变量表中第一个int数值推送入栈顶
- i_add: 将栈顶两个int数值出栈计算后把结果推入栈顶
- ireturn: 返回栈顶int数值
执行流程如图:

本地方法栈
和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。
JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。