docs.oracle.com/javase/spec...
2.5. 运行时数据区
Java虚拟机定义了在程序执行期间使用的各种运行时数据区域。其中一些数据区域在Java虚拟机启动时创建,只有在Java虚拟机退出时才会被销毁。其他数据区域则是每个线程独有的。每个线程的数据区域在线程创建时创建,并在线程退出时销毁。
2.5.1. pc寄存器
Java虚拟机可以同时支持多个执行线程(JLS §17)。每个Java虚拟机线程都有自己的pc(程序计数器)寄存器。在任何时候,每个Java虚拟机线程都在执行单个方法的代码,即该线程的当前方法(§2.6)。如果该方法不是native方法,则pc寄存器包含当前正在执行的Java虚拟机指令的地址。如果线程当前正在执行的方法是native方法,则Java虚拟机的pc寄存器的值是未定义的。Java虚拟机的pc寄存器足够宽,可以在特定平台上容纳返回地址(returnAddress)或本地指针(native pointer)。
2.5.2. Java虚拟机栈
每个Java虚拟机线程都有一个私有的Java虚拟机栈,它与线程同时创建。Java虚拟机栈存储帧(§2.6)。Java虚拟机栈类似于C语言等传统语言的栈:它保存局部变量和部分结果,并在方法调用和返回中发挥作用。由于Java虚拟机栈除了压入和弹出帧之外,从不被直接操作,因此栈帧可以堆分配。Java虚拟机栈的内存不需要是连续的。
在《Java® 虚拟机规范》的第一版中,Java虚拟机栈被称为Java栈。
本规范允许Java虚拟机栈具有固定大小,或者根据计算的需要动态扩展和收缩。如果Java虚拟机栈具有固定大小,则每个Java虚拟机栈的大小可以在创建该栈时独立选择。
Java虚拟机实现可以向程序员或用户提供对Java虚拟机栈初始大小的控制,以及,在Java虚拟机栈动态扩展或收缩的情况下,对最大和最小大小的控制。
以下是与 Java 虚拟机栈相关的异常情况:
- 如果线程中的计算需要的 Java 虚拟机栈大于允许的大小,Java 虚拟机将抛出一个 StackOverflowError。
- 如果 JVM 栈可以动态增大,但增大时没有足够的内存,或者创建新线程时没有足够的内存分配给它的 JVM 栈,就会抛出 OutOfMemoryError 错误。
2.5.3. 堆
Java 虚拟机有一个堆,它在所有 Java 虚拟机线程之间共享。堆是运行时数据区,所有类实例和数组的内存都从中分配。
堆在虚拟机启动时创建。对象在堆中的存储由自动存储管理系统(称为垃圾收集器)回收;对象永远不会被显式释放。Java 虚拟机不假定任何特定类型的自动存储管理系统,并且可以根据实现者的系统要求选择存储管理技术。堆可以是固定大小的,也可以根据计算的需要进行扩展,如果不再需要更大的堆,则可以收缩。堆的内存不需要是连续的。
Java 虚拟机实现可以为程序员或用户提供对堆的初始大小的控制,以及,如果堆可以动态扩展或收缩,对最大和最小堆大小的控制。
以下异常情况与堆相关联:
- 如果计算需要的堆超过了自动存储管理系统可以提供的量,Java 虚拟机将抛出一个 OutOfMemoryError。
2.5.4. 方法区
Java 虚拟机有一个方法区,它在所有 Java 虚拟机线程之间共享。方法区类似于传统语言的编译代码存储区,或者类似于操作系统进程中的"文本"段。它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括用于类和接口初始化以及实例初始化的特殊方法(§2.9)。
方法区在虚拟机启动时创建。虽然方法区在逻辑上是堆的一部分,但简单的实现可以选择不对其进行垃圾收集或压缩。本规范不强制规定方法区的位置或用于管理编译代码的策略。方法区可以是固定大小的,也可以根据计算的需要进行扩展,如果不再需要更大的方法区,则可以收缩。方法区的内存不需要是连续的。
Java 虚拟机实现可以为程序员或用户提供对方法区初始大小的控制,以及,在可变大小方法区的情况下,对最大和最小方法区大小的控制。
以下是与方法区相关的异常情况:
- 如果方法区中的内存无法用于满足分配请求,Java 虚拟机将抛出一个 OutOfMemoryError。
2.5.5. 运行时常量池
运行时常量池是类文件(§4.4)中 constant_pool 表的每个类或每个接口的运行时表示。它包含几种常量,从编译时已知的数字字面量到运行时必须解析的方法和字段引用。运行时常量池的功能类似于传统编程语言的符号表,尽管它包含比典型符号表更广泛的数据。
每个运行时常量池都从 Java 虚拟机的方法区 (§2.5.4) 中分配。类或接口的运行时常量池在 Java 虚拟机创建类或接口 (§5.3) 时构建。
以下异常情况与类或接口的运行时常量池的构建相关联:
- 当创建类或接口时,如果运行时常量池的构建需要的内存超过了 Java 虚拟机方法区可以提供的量,Java 虚拟机将抛出一个 OutOfMemoryError。
关于运行时常量池的构建,请参考第 5 节(加载、链接和初始化)。
2.5.6. 本地方法栈
Java 虚拟机的实现可以使用传统的栈,俗称"C 栈",来支持本地方法(用 Java 编程语言以外的语言编写的方法)。本地方法栈也可以被 Java 虚拟机指令集的解释器实现(例如用 C 语言实现)所使用。不能加载本地方法并且自身不依赖于传统栈的 Java 虚拟机实现不需要提供本地方法栈。如果提供,本地方法栈通常在每个线程创建时按线程分配。
本规范允许本地方法栈可以是固定大小的,也可以根据计算的需要动态扩展和收缩。如果本地方法栈是固定大小的,则可以在创建该栈时独立选择每个本地方法栈的大小。
Java 虚拟机实现可以为程序员或用户提供对本地方法栈初始大小的控制,以及,在可变大小本地方法栈的情况下,对最大和最小方法栈大小的控制。
以下异常情况与本地方法栈相关联:
- 如果线程中的计算需要的本地方法栈大于允许的大小,Java 虚拟机将抛出一个 StackOverflowError。
- 如果本地方法栈可以动态扩展,并且尝试扩展本地方法栈但无法获得足够的内存,或者如果无法获得足够的内存来为新线程创建初始本地方法栈,Java 虚拟机将抛出一个 OutOfMemoryError。
JVM调优
Java虚拟机栈
查看JVM栈默认大小
ruby
java -XX:+PrintFlagsFinal -version | findstr ThreadStackSize
ThreadStackSize=0
表示 JVM 没有显式设置栈大小,而是使用操作系统的默认值
在 Windows 平台上,JVM 会根据操作系统的架构(32位或64位)自动选择默认的栈大小。
默认值的实现
ini
// hotspot/os/windows/os_windows.cpp
// ThreadStackSize是通过 JVM 参数(-Xss)指定的线程栈的大小
size_t stack_commit_size = align_up(ThreadStackSize*K, os::vm_page_size());
tty->print_cr("Stack size: %d KB, Committed stack size: %d KB", ThreadStackSize, stack_commit_size / K);
// default_stack_size()函数是Windows系统特定函数,返回操作系统默认线程栈大小
size_t default_reserve_size = os::win32::default_stack_size();
tty->print_cr("Default stack size: %d KB", default_reserve_size / K);
size_t actual_reserve_size = stack_commit_size;
if (stack_commit_size < default_reserve_size) {
actual_reserve_size = default_reserve_size;
}
tty->print_cr输出了两个值。(这里使用的JDK是github拉取源码后,自行本地编译过后的)
使用-Xss参数调整栈大小
为什么os::win32::default_stack_size()获取的关于操作系统相关的默认值也变了(我理解是个固定值)?
-Xss
参数影响了 JVM 启动时为每个线程分配的栈大小,间接影响通过 os::current_stack_size()
获取的栈大小。如果设置了 -Xss
,os::current_stack_size()
返回的大小应该会接近指定的值,但最终的栈大小还会受到操作系统的影响。
如何优化
- 根据线程的数量和递归深度设置合适的栈大小。对于递归算法和深层调用,适当增加栈大小;对于不使用深层递归的应用,尽量设置较小的栈大小。
- 避免创建过多的线程,尤其是在栈大小较大的情况下,过多的线程会迅速消耗系统内存。使用线程池等技术来有效管理线程的数量。
- 确保栈大小设置符合操作系统的最大线程栈大小限制。特别是在 32 位系统中,栈的大小可能会受到内存限制。
- 通过实际的性能测试和负载测试,找出合适的栈大小,并观察系统在不同栈大小下的表现,确保系统的稳定性和性能。
堆内存
查看堆内存默认大小
ruby
java -XX:+PrintFlagsFinal -version | findstr "HeapSize"
InitialHeapSize := 260046848
大约 248 MB。表示在 JVM 启动时,初始的堆内存大小。
MaxHeapSize := 4135583744
大约 4 GB。表示 JVM 可以使用的最大堆内存大小。
MinHeapSize := 8388608
大约 8 MB。表示 JVM 最小堆内存大小。
默认值实现
scss
// hotspot/share/runtime/arguments.cpp
void Arguments::set_heap_size() {
// 省略部分代码...
// 如果没有使用-Xmx设置最大堆内存,则将其设置为物理内存大小的一部分,同时遵守堆内存的最大和最小限制。
// 这个判断就表示没有使用-Xmx设置最大堆内存
if (FLAG_IS_DEFAULT(MaxHeapSize)) {
julong reasonable_max = (julong)((phys_mem * MaxRAMPercentage) / 100);
const julong reasonable_min = (julong)((phys_mem * MinRAMPercentage) / 100);
if (reasonable_min < MaxHeapSize) {
// 如果物理内存较小,JVM 会自动调整堆内存的大小,通常将其设置为物理内存的一小部分(例如 1/4 或更小),以避免堆内存占用过多,导致系统资源不足
reasonable_max = reasonable_min;
} else {
// 系统的物理内存较大,JVM 会确保堆内存的大小至少与 MaxHeapSize 一样大
reasonable_max = MAX2(reasonable_max, (julong)MaxHeapSize);
}
if (!FLAG_IS_DEFAULT(ErgoHeapSizeLimit) && ErgoHeapSizeLimit != 0) {
// 将堆内存大小限制为 ErgoHeapSizeLimit
reasonable_max = MIN2(reasonable_max, (julong)ErgoHeapSizeLimit);
}
// 省略部分代码...
reasonable_max = limit_heap_by_allocatable_memory(reasonable_max);
if (!FLAG_IS_DEFAULT(InitialHeapSize)) {
// 如果在命令行中指定了初始堆内存大小,则需要确保最大堆内存大小与其一致。
// 这一步在调用 limit_heap_by_allocatable_memory 之后执行,因为该方法可能会减少分配的内存大小。
reasonable_max = MAX2(reasonable_max, (julong)InitialHeapSize);
} else if (!FLAG_IS_DEFAULT(MinHeapSize)) {
reasonable_max = MAX2(reasonable_max, (julong)MinHeapSize);
}
log_trace(gc, heap)(" Maximum heap size " SIZE_FORMAT, (size_t) reasonable_max);
FLAG_SET_ERGO(MaxHeapSize, (size_t)reasonable_max);
}
// 如果最小堆内存大小或初始堆内存大小未被设置,或者未请求以启发式方式设置,则根据情况设置它们。
if (InitialHeapSize == 0 || MinHeapSize == 0) {
julong reasonable_minimum = (julong)(OldSize + NewSize);
reasonable_minimum = MIN2(reasonable_minimum, (julong)MaxHeapSize);
reasonable_minimum = limit_heap_by_allocatable_memory(reasonable_minimum);
if (InitialHeapSize == 0) {
julong reasonable_initial = (julong)((phys_mem * InitialRAMPercentage) / 100);
reasonable_initial = limit_heap_by_allocatable_memory(reasonable_initial);
reasonable_initial = MAX3(reasonable_initial, reasonable_minimum, (julong)MinHeapSize);
reasonable_initial = MIN2(reasonable_initial, (julong)MaxHeapSize);
FLAG_SET_ERGO(InitialHeapSize, (size_t)reasonable_initial);
log_trace(gc, heap)(" Initial heap size " SIZE_FORMAT, InitialHeapSize);
}
// 如果最小堆内存大小未被设置(通过 -Xms 或 -XX:MinHeapSize),则与 InitialHeapSize 同步,以避免默认值引发的错误。
if (MinHeapSize == 0) {
FLAG_SET_ERGO(MinHeapSize, MIN2((size_t)reasonable_minimum, InitialHeapSize));
log_trace(gc, heap)(" Minimum heap size " SIZE_FORMAT, MinHeapSize);
}
}
}
启用 gc
和 heap
标签的日志,并输出到控制台(stdout
)。
ruby
-Xlog:gc+heap=trace:stdout
使用参数调整堆内存
- -Xmx 最大堆内存
- -Xms 初始堆内存
- -XX:MinHeapSize 最小堆内存
初始堆大小 (-Xms
) :默认值为物理内存的 1/64。
最大堆大小 (-Xmx
) :默认值为物理内存的 1/4。
最小堆大小 (-XX:MinHeapSize
) :通常与初始堆大小 (-Xms
) 同步。
如何优化
- 根据应用程序的对象创建频率、数据量和并发用户数,评估其内存需求。通过监控工具(如 JVisualVM、JConsole)观察堆内存的使用情况,确定合理的最小和最大堆内存大小
- 确保堆内存大小不超过物理内存的 80%,以避免操作系统频繁进行内存交换(Swapping),从而影响性能。
- 堆内存越大,垃圾回收的暂停时间可能越长。选择合适的垃圾回收器(如 G1、CMS 或 ZGC)并根据应用场景调整垃圾回收参数(如
-XX:MaxGCPauseMillis
)以减少停顿时间。(垃圾回收器后续在讨论) - 建议将初始堆大小(
-Xms
)和最大堆大小(-Xmx
)设置为相同值,以避免运行时动态调整堆内存带来的性能开销。 - 定期监控堆内存使用情况和垃圾回收日志,分析 GC 频率和持续时间,进一步优化堆内存设置。
- 合理分配年轻代(Young Generation)和老年代(Old Generation)的大小。例如,对于短生命周期对象较多的应用,可以增加年轻代的大小(通过
-Xmn
参数),以减少老年代的压力。(垃圾回收器后续在讨论)
方法区
查看元空间默认值
bash
java -XX:+PrintFlagsFinal -version | findstr /i "MetaspaceSize MaxMetaspaceSize"
MaxMetaspaceSize的默认值为18446744073709551615
这是一个 64 位无符号整数的最大值,表示 无限制。表示元空间可以扩展到系统的最大可用内存。
默认值实现
scss
// hotspot/share/runtime/globals.hpp
product(size_t, MetaspaceSize, NOT_LP64(16 * M) LP64_ONLY(21 * M), \
"Initial threshold (in bytes) at which a garbage collection " \
"is done to reduce Metaspace usage") \
constraint(MetaspaceSizeConstraintFunc,AfterErgo)
64位系统,默认元空间初始大小为21M。
使用参数调整元空间
- -XX:MetaspaceSize 元空间的初始大小
默认值在 64 位系统中为 21 MB(Windows 平台)
- -XX:MaxMetaspaceSize 元空间的最大大小
默认值为无限制(即系统内存上限)
- -XX:MinMetaspaceFreeRatio 元空间的最小空闲比例
当元空间空闲比例低于此值时,会触发元空间扩容。默认值为 40%
- -XX:MaxMetaspaceFreeRatio 元空间的最大空闲比例
当元空间空闲比例高于此值时,会触发元空间释放空间。默认值为 70%
如何优化
- 设置元空间的初始大小,避免频繁触发垃圾回收。
- 设置元空间的最大大小,防止元空间无限制增长导致内存耗尽。
- 使用工具如
jstat
或JVisualVM
监控元空间的使用情况,及时发现内存泄漏或过度使用的问题。 - 避免频繁加载和卸载类,减少动态生成类的使用(如反射和动态代理);合并公共类,减少重复加载。
- 通过
-XX:MinMetaspaceFreeRatio
和-XX:MaxMetaspaceFreeRatio
调整元空间的扩展和收缩阈值。 - 启用
-XX:+UseCompressedOops
参数,减少元空间的内存占用。