运行时内存布局
zhuib 2020/12/11 JVM
# 运行时内存布局
# Java 运行时内存详解
# 1. 概述
Java 虚拟机 (JVM) 在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。
JVM 运行时内存区域主要包括以下几个部分:
- 程序计数器(Program Counter Register)
- Java 虚拟机栈(Java Virtual Machine Stacks)
- 本地方法栈(Native Method Stack)
- Java 堆(Java Heap)
- 方法区(Method Area)
其中,程序计数器、Java 虚拟机栈和本地方法栈是线程私有的,Java 堆和方法区是线程共享的。
# 2. 各个内存区域详解
# 2.1 程序计数器(Program Counter Register)
- 作用: 记录当前线程执行的字节码指令的地址。
- 特点:
- 线程私有,每个线程都有一个独立的程序计数器。
- 生命周期与线程相同。
- 是 JVM 规范中唯一没有规定任何
OutOfMemoryError情况的区域。 - 如果线程正在执行 Java 方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地方法,则计数器值为空(Undefined)。
- 用途:
- 多线程环境下,线程切换后能恢复到正确的执行位置。
# 2.2 Java 虚拟机栈(Java Virtual Machine Stacks)
- 作用: 描述 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 特点:
- 线程私有,每个线程都有一个独立的 Java 虚拟机栈。
- 生命周期与线程相同。
- 会出现两种异常:
StackOverflowError(栈深度超过虚拟机允许的最大深度) 和OutOfMemoryError(如果 JVM 允许动态扩展栈的大小,当扩展时无法申请到足够的内存)。
- 栈帧 (Stack Frame):
- 局部变量表: 存储方法参数和方法内部定义的局部变量。 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
- 操作数栈: 用于存放方法执行过程中产生的中间结果。 类似于 CPU 的寄存器,用于执行算术运算、方法调用等操作。
- 动态链接: 指向运行时常量池中该栈帧所属方法的引用,目的是支持动态链接。
- 方法出口: 记录方法正常退出或异常退出时的返回地址。
# 2.3 本地方法栈(Native Method Stack)
- 作用: 与 Java 虚拟机栈的作用相似,区别在于 Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
- 特点:
- 线程私有,每个线程都有一个独立的本地方法栈。
- 生命周期与线程相同。
- 具体实现由不同的 JVM 实现决定。 Sun HotSpot 虚拟机直接将本地方法栈和 Java 虚拟机栈合二为一。
- 也会出现
StackOverflowError和OutOfMemoryError异常。
- Native 方法: 用
native关键字修饰的方法,通常由 C/C++ 编写,用于访问操作系统底层资源。
# 2.4 Java 堆(Java Heap)
- 作用: 用于存放对象实例。 几乎所有的对象实例都在堆中分配内存,是 JVM 管理的内存中最大的一块。
- 特点:
- 线程共享,所有线程都可以访问堆中的对象。
- 是垃圾收集器管理的主要区域,也称为“GC 堆”(Garbage Collected Heap)。
- 可以通过
-Xms(初始堆大小) 和-Xmx(最大堆大小) 参数设置堆的大小。 - 会出现
OutOfMemoryError异常。
- 堆的结构 (HotSpot 虚拟机):
- 新生代 (Young Generation): 存放新创建的对象。 新生代又分为 Eden 区和两个 Survivor 区 (From Survivor 和 To Survivor)。
- 老年代 (Old Generation): 存放经过多次垃圾回收仍然存活的对象。
- (JDK8 以后)元空间 (Metaspace): 存放类的元数据信息、常量池等。 元空间并不在堆中,而是直接使用本地内存。
- 对象分配:
- 新对象通常分配在 Eden 区。
- 当 Eden 区满时,会触发 Minor GC (也称为 Young GC),将 Eden 区和 From Survivor 区中存活的对象复制到 To Survivor 区,并清空 Eden 区和 From Survivor 区。
- 当 To Survivor 区也满时,会将部分对象晋升到老年代。
- 当老年代也满时,会触发 Full GC (也称为 Major GC),清理整个堆空间。
# 2.5 方法区(Method Area)
- 作用: 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 特点:
- 线程共享,所有线程都可以访问方法区中的数据。
- 可以选择不实现垃圾收集,垃圾收集行为在这个区域出现的频率相对较低。
- 可以通过
-XX:MetaspaceSize(初始元空间大小) 和-XX:MaxMetaspaceSize(最大元空间大小) 参数设置方法区的大小。 - 会出现
OutOfMemoryError异常。
- 重要组成部分:
- 运行时常量池 (Runtime Constant Pool): 是类加载后常量池表在 JVM 内存中的表示形式。 运行时常量池相对于 Class 文件常量池具备更动态的特性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的
intern()方法。
- 运行时常量池 (Runtime Constant Pool): 是类加载后常量池表在 JVM 内存中的表示形式。 运行时常量池相对于 Class 文件常量池具备更动态的特性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的
- JDK 8 永久代 (Permanent Generation) 的变化:
- 在 JDK 7 及之前,HotSpot 虚拟机使用永久代来实现方法区。 永久代位于堆中,受堆内存大小的限制,并且容易发生内存溢出。
- 在 JDK 8 中,HotSpot 虚拟机使用元空间(Metaspace)来代替永久代。 元空间并不在堆中,而是直接使用本地内存,从而避免了永久代的问题。
- 字符串常量池从永久代移到堆中。
- 类的元数据信息、常量池等从永久代移到元空间。
# 3. 总结
理解 JVM 运行时内存区域对于理解 JVM 的运行机制、进行性能调优至关重要。 掌握各个区域的作用、特点以及可能出现的异常,有助于我们更好地编写高效、稳定的 Java 程序。