Java运行时数据区

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() 获取的栈大小。如果设置了 -Xssos::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);
         }
     }
 }

启用 gcheap 标签的日志,并输出到控制台(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%

如何优化

  • 设置元空间的初始大小,避免频繁触发垃圾回收。
  • 设置元空间的最大大小,防止元空间无限制增长导致内存耗尽。
  • 使用工具如 jstatJVisualVM 监控元空间的使用情况,及时发现内存泄漏或过度使用的问题。
  • 避免频繁加载和卸载类,减少动态生成类的使用(如反射和动态代理);合并公共类,减少重复加载。
  • 通过 -XX:MinMetaspaceFreeRatio-XX:MaxMetaspaceFreeRatio 调整元空间的扩展和收缩阈值。
  • 启用 -XX:+UseCompressedOops 参数,减少元空间的内存占用。
相关推荐
Y雨何时停T5 分钟前
Spring IoC 详解
java·spring·rpc
&白帝&20 分钟前
Java @PathVariable获取路径参数
java·开发语言·python
Yuanymoon30 分钟前
【由技及道】镜像星门开启:Harbor镜像推送的量子跃迁艺术【人工智障AI2077的开发日志010】
java·docker·jenkins·harbor·devops
木胭脂沾染了灰31 分钟前
策略设计模式-下单
java·前端·设计模式
sevevty-seven1 小时前
Spring Boot 自动装配原理详解
java·spring boot·后端
lamdaxu2 小时前
分布式调用(02)
后端
daiyunchao2 小时前
让Pomelo支持HTTP协议
后端
芒猿君2 小时前
AQS——同步器框架之源
后端
SaebaRyo3 小时前
手把手教你在网站中启用https和http2
后端·nginx·https
Forget the Dream3 小时前
设计模式之迭代器模式
java·c++·设计模式·迭代器模式