Java内存区域详解
Java内存区域详解
JVM内存结构主要包括以下几部分:
- 程序计数器
- Java虚拟机栈
- 本地方法栈
- Java堆
- 方法区(元空间)
- 运行时常量池
- 直接内存
其中:
- 线程私有的:
- 程序计数器
- Java虚拟机栈
- 本地方法栈
- 线程共享的:
- Java堆
- 方法区(元空间)
- 直接内存
JDK1.7及之前,方法区被称为永久代(PermGen),从JDK1.8开始,方法区被移除,取而代之的是元空间(Metaspace)。
一、程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
每个线程都有一个独立的程序计数器,线程切换时,程序计数器可以保存当前线程的执行位置,以便恢复执行。
所以程序计数器主要有两个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制
- 在多线程情况下,程序计数器用于记录当前线程执行位置,从而实现线程切换和恢复
程序计数器的生命周期与线程完全同步:
- 创建:随着线程的创建而创建
- 销毁:随着线程的结束而销毁
注意:程序计数器是JVM规范中唯一没有规定任何OutOfMemoryError的内存区域,因为它的内存需求非常小,且由线程私有,不会出现内存溢出的问题。
二、Java虚拟机栈
Java虚拟机栈也是线程私有的,生命周期与线程相同。
方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈,每一个方法调用结束后,都会有一个栈帧被弹出。
每个栈帧中都拥有:局部变量表、操作数栈、动态链接和方法返回地址等信息。
- 局部变量表:主要存放编译期可知的各种类型(boolean、byte、char、short、int、long、float、double)、对象引用(reference类型)。
- 操作数栈:主要作为方法调用的中转站使用,用于存放方法执行过程中产生的各种中间结果。
- 动态链接:主要用于在运行时解析虚方法的调用点,将其链接到正确的方法版本上。
- 方法返回地址:主要用于存储方法调用结束后,程序计数器应该返回的位置。
程序运行中栈可能出现的两种异常:
StackOverflowError:当线程请求的栈深度超过虚拟机所允许的最大深度时,抛出该异常。OutOfMemoryError:当虚拟机无法为线程分配足够的栈内存时,抛出该异常。
三、本地方法栈
虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现StackOverFlowError和OutOfMemoryError两种错误。
四、Java堆
Java堆是JVM中最大的一块内存区域,是所有线程共享的内存区域,主要用于存放对象实例和数组。
Java堆是垃圾收集器管理的主要区域,因此也被称为GC堆。
JDK1.7及之前,堆一般分为下面三部分:
- 新生代(Young Generation):主要存放新创建的对象,垃圾收集器会频繁地对新生代进行垃圾回收。新生代一般占堆内存的1/3。
- 老年代(Old Generation):主要存放经过多次垃圾回收仍然存活的对象,垃圾收集器对老年代的垃圾回收频率较低。老年代一般占堆内存的2/3。
- 永久代(PermGen):主要存放类的元数据,如类的结构信息、常量池等。
从JDK1.8开始,永久代被移除,取而代之的是元空间(Metaspace),元空间不再使用堆内存,而是使用本地内存。

新生代分为Eden区和两个Survivor区(S0和S1),新创建的对象首先被分配到Eden区,当Eden区满时,垃圾收集器会进行Minor GC,将存活的对象从Eden区复制到Survivor区。
当Survivor中的对象年龄增加到一定程度时(默认是15岁,这是因为记录年龄使用的区域是4为,这四位可以表示的最大二进制数字是1111,即15),就会被晋升到老年区。
老年代主要存放经过多次垃圾回收仍然存活的对象,垃圾收集器对老年代的垃圾回收频率较低,通常在老年代满时才会进行Full GC。
在某些JVM实现中,为大对象分配了专门的区域,称为大对象区。这类对象直接分配在老年代,以避免在新生代进行频繁的垃圾回收。
补充:Survivor为什么要分为S0和S1两个区?
- 主要是为了实现对象的复制算法。在垃圾回收过程中,垃圾收集器会将存活的对象从Eden区复制到一个Survivor区(比如S0),而另一个Survivor区(比如S1)则作为空闲区。当下一次垃圾回收发生时,垃圾收集器会将存活的对象从Eden区复制到另一个Survivor区(S1),同时将之前存活的对象从S0区复制到S1区。这样通过交替使用两个Survivor区,可以有效地管理对象的生命周期和内存使用。
五、方法区(元空间)
方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码等数据。
方法区主要存储以下核心数据:
- 类的元数据
- 方法的字节码
- 运行时常量池
需要注意的是,以下几类数据虽然在逻辑上与类相关,但在HotSpot虚拟机中,他们并不储存在方法区内:
- 静态变量:自JDK7开始,静态变量被移到了Java堆中。
- 字符串常量池:常量池中的字符串常量和基本类型常量也被移到了Java堆中。
- 即时编译器编译后的代码:JIT编译器生成的本地代码被存储在一个独立的名为
Code Cache的内存区域中。
方法区中的方法执行过程:
- 解析方法调用:JVM会根据方法的符号引用找到实际的方法地址。
- 栈帧创建:在调用方法前,JVM会在当前线程的Java虚拟机栈中为该方法分配一个新的栈帧。
- 执行方法:执行方法内部的字节码指令。
- 返回处理:方法执行完毕后,JVM会根据方法的返回类型处理返回值,并清理当前栈帧,回复调用者的执行环境。
JDK1.8及之后,方法区被移除,取而代之的是元空间(Metaspace)。
为什么要将永久代替换为元空间?
- 永久代有一个JVM本身设置的固定大小上限,无法进行调整,而元空间使用的是本地内存,受本机可用内存的限制,具有更大的灵活性。
- 元空间里面存放的是类的元数据,这样加载多少类的元数据就不由
MaxPermSize控制了,而由系统的实际可用空间来控制,这样能加载的类就更多了。 - 永久代会为GC带来不必要的复杂度,而且回收效率偏低。
六、运行时常量池
运行时常量池是方法区的一部分,用于存储编译期生成的各种字面量和符号引用。它是类文件中的常量池在运行时的表现形式。
七、直接内存
直接内存是一种特殊的内存缓冲区,并不在 Java 堆或方法区中分配的,而是通过 JNI 的方式在本地内存上分配的。
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致OutOfMemoryError错误出现。
八、总结
总体结构图
1 | 线程私有: |
参考资料: