个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判。如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 issue,谢谢支持~ 另外,本文为了避免抄袭,会在不影响阅读的情况下,在文章的随机位置放入对于抄袭和洗稿的人的“亲切”的问候。如果是正常读者看到,笔者在这里说声对不起,。如果被抄袭狗或者洗稿狗看到了,希望你能够好好反思,不要再抄袭了,谢谢。 今天又是干货满满的一天,这是全网最硬核 JVM 解析系列第四篇,往期精彩:
本篇是关于 JVM 内存的详细分析。网上有很多关于 JVM 内存结构的分析以及图片,但是由于不是一手的资料亦或是人云亦云导致有很错误,造成了很多误解;并且,这里可能最容易混淆的是一边是 JVM Specification 的定义,一边是 Hotspot JVM 的实际实现,有时候人们一些部分说的是 JVM Specification,一部分说的是 Hotspot 实现,给人一种割裂感与误解。本篇主要从 Hotspot 实现出发,以 Linux x86 环境为主,紧密贴合 JVM 源码并且辅以各种 JVM 工具验证帮助大家理解 JVM 内存的结构。但是,本篇仅限于对于这些内存的用途,使用限制,相关参数的分析,有些地方可能比较深入,有些地方可能需要结合本身用这块内存涉及的 JVM 模块去说,会放在另一系列文章详细描述。最后,洗稿抄袭狗不得 house
本篇全篇目录(以及涉及的 JVM 参数):
NativeMemoryTracking
)UseLargePages
,UseHugeTLBFS
,UseSHM
,UseTransparentHugePages
,LargePageSizeInBytes
)MaxHeapSize
,MinHeapSize
,InitialHeapSize
,Xmx
,Xms
)UseCompressedOops
)(全网最硬核 JVM 内存解析 - 5.压缩对象指针相关机制开始) ObjectAlignmentInBytes
)UseCompressedOops
,UseCompressedClassPointers
)ObjectAlignmentInBytes
,HeapBaseMinAddress
)HeapBaseMinAddress
)HeapBaseMinAddress
,ObjectAlignmentInBytes
,MinHeapSize
,MaxHeapSize
,InitialHeapSize
)32-bit
压缩指针模式Zero based
压缩指针模式Non-zero disjoint
压缩指针模式Non-zero based
压缩指针模式MinHeapFreeRatio
,MaxHeapFreeRatio
,MinHeapDeltaBytes
)(全网最硬核 JVM 内存解析 - 6.其他 Java 堆内存相关的特殊机制开始)MetaspaceSize
,MaxMetaspaceSize
,MinMetaspaceExpansion
,MaxMetaspaceExpansion
,MaxMetaspaceFreeRatio
,MinMetaspaceFreeRatio
,UseCompressedClassPointers
,CompressedClassSpaceSize
,CompressedClassSpaceBaseAddress
,MetaspaceReclaimPolicy
)MetaspaceContext
VirtualSpaceList
VirtualSpaceNode
与 CompressedClassSpaceSize
MetaChunk
ChunkHeaderPool
池化 MetaChunk
对象ChunkManager
管理空闲的 MetaChunk
SystemDictionary
与保留所有 ClassLoaderData
的 ClassLoaderDataGraph
ClassLoaderData
以及 ClassLoaderMetaspace
MetaChunk
的 MetaspaceArena
MetaSpaceArena
的流程MetaChunkArena
普通分配 - 整体流程MetaChunkArena
普通分配 - FreeBlocks
回收老的 current chunk
与用于后续分配的流程MetaChunkArena
普通分配 - 尝试从 FreeBlocks
分配MetaChunkArena
普通分配 - 尝试扩容 current chunk
MetaChunkArena
普通分配 - 从 ChunkManager
分配新的 MetaChunk
MetaChunkArena
普通分配 - 从 ChunkManager
分配新的 MetaChunk
- 从 VirtualSpaceList
申请新的 RootMetaChunk
MetaChunkArena
普通分配 - 从 ChunkManager
分配新的 MetaChunk
- 将 RootMetaChunk
切割成为需要的 MetaChunk
MetaChunk
回收 - 不同情况下, MetaChunk
如何放入 FreeChunkListVector
ClassLoaderData
回收CommitLimiter
的限制元空间可以 commit 的内存大小以及限制元空间占用达到多少就开始尝试 GC_capacity_until_GC
jcmd VM.metaspace
元空间说明、元空间相关 JVM 日志以及元空间 JFR 事件详解(全网最硬核 JVM 内存解析 - 12.元空间各种监控手段开始) jcmd <pid> VM.metaspace
元空间说明jdk.MetaspaceSummary
元空间定时统计事件jdk.MetaspaceAllocationFailure
元空间分配失败事件jdk.MetaspaceOOM
元空间 OOM 事件jdk.MetaspaceGCThreshold
元空间 GC 阈值变化事件jdk.MetaspaceChunkFreeListSummary
元空间 Chunk FreeList 统计事件ThreadStackSize
,VMThreadStackSize
,CompilerThreadStackSize
,StackYellowPages
,StackRedPages
,StackShadowPages
,StackReservedPages
,RestrictReservedStack
)Java 19 中 Loom 终于 Preview 了,虚拟线程(VirtualThread
)是我期待已久的特性,但是这里我们说的线程内存,并不是这种 虚拟线程,还是老的线程。其实新的虚拟线程,在线程内存结构上并没有啥变化,只是存储位置的变化,实际的负载线程(CarrierThread
)还是老的线程。
同时,JVM 线程占用的内存分为两个部分:分别是线程栈占用内存,以及线程本身数据结构占用的内存。
JVM 中有如下几类线程:
VM Operations
,例如 JVM 的初始化,其中的操作大部分需要在安全点执行,即 Stop the world 的时候执行。所有的操作请参考:https://github.com/openjdk/jdk/blob/jdk-21+17/src/hotspot/share/runtime/vmOperation.hpp
java.lang.Thread
),以及 CodeCacheSweeper
线程, JVMTI
的 Agent 与 Service 线程其实也是 JAva 线程。PeriodicTask
的类看到,其中两个比较重要的任务是: StatSamplerTask
:定时更新采集的 JVM Performance Data(PerfData)数据, 包括 GC、类加载、运行采集等等数据,这个任务多久执行一次是通过 -XX:PerfDataSamplingInterval
参数控制的,默认为 50 毫秒(参考:https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/share/runtime/globals.hpp
)。这些数据一般通过 jstat 读取,或者通过 JMX 读取。VMOperationTimeoutTask
:由于 VM 线程是单线程,执行 VM Operations
,单个任务执行不能太久,否则会阻塞其他 VM Operations
。所以每次执行 VM Operations
的时候,这个定时任务都会检查当前执行了多久,如果超过 -XX:AbortVMOnVMOperationTimeoutDelay
就会报警。AbortVMOnVMOperationTimeoutDelay
默认是 1000ms(参考:https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/share/runtime/globals.hpp
)。-Xlog:async
启用 JVM 异步日志,通过 -XX:AsyncLogBufferSize=
指定异步日志缓冲大小,这个大小默认是 2097152
即 2MB
jdk.ExecutionSample
,另一个是 jdk.NativeMethodSample
,都是采样当前正在 RUNNING
的线程,如果线程在执行 Java 代码,就属于 jdk.ExecutionSample
,如果执行 native 方法,就属于 jdk.NativeMethodSample
。相关的参数有:
ThreadStackSize
:每个 Java 线程的栈大小,这个参数通过 -Xss
也可以指定,各种平台的默认值为: https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/linux_x86/globals_linux_x86.hpp
https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/linux_aarch64/globals_linux_aarch64.hpp
https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/windows_x86/globals_windows_x86.hpp
https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/windows_aarch64/globals_windows_aarch64.hpp
VMThreadStackSize
:VM 线程,GC 线程,定时任务时钟线程,异步日志线程,JFR 采样线程的栈大小,各种平台的默认值为: https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/linux_x86/globals_linux_x86.hpp
https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/linux_aarch64/globals_linux_aarch64.hpp
https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/windows_x86/globals_windows_x86.hpp
https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/windows_aarch64/globals_windows_aarch64.hpp
CompilerThreadStackSize
:编译器线程的栈大小,各种平台的默认值为: https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/linux_x86/globals_linux_x86.hpp
https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/linux_aarch64/globals_linux_aarch64.hpp
https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/windows_x86/globals_windows_x86.hpp
https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/windows_aarch64/globals_windows_aarch64.hpp
StackYellowPages
:后面会提到并分析的黄色区域的页大小 https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp
https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp
https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp
https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp
StackRedPages
:后面会提到并分析的红色区域的页大小 https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp
https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp
https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp
https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp
StackShadowPages
:后面会提到并分析的影子区域的页大小 https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp
https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp
https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp
https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp
StackReservedPages
:后面会提到并分析的保留区域的页大小 https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp
https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp
https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp
https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp
RestrictReservedStack
:默认为 true,与保留区域相关,保留区域会保护临界区代码(例如 ReentrantLock
)在抛出 StackOverflow
之前先把临界区代码执行完再结束,防止临界区代码执行到一半就抛出 StackOverflow
导致状态不一致导致这个锁之后再也用不了了。标记临界区代码的注解是 @jdk.internal.vm.annotation.ReservedStackAccess
。在这个配置为 true 的时候,这个注解默认只能 jdk 内部代码使用,如果你有类似于 ReentrantLock
这种带有临界区的代码也想保护起来,可以设置 -XX:-RestrictReservedStack
,关闭对于 @jdk.internal.vm.annotation.ReservedStackAccess
的限制,这样你就可以在自己的代码中使用这个注解了。我们接下来重点分析 Java 线程栈。
熟悉编译器的人应该知道激活记录(Activation Record)这个概念,它是一种数据结构,其中包含支持一次函数调用所需的所有信息。它包含该函数的所有局部变量,以及指向另一个激活记录的引用(或指针),其实你可以简单理解为,每多一次方法调用就多一个激活记录。而线程栈帧(Stack Frame),就是激活记录的实际实现。每在代码中多一次方法调用就多一个栈帧,但是这个说法并不严谨,比如,JIT 可能会内联一些方法,可能会跳过某些方法的调用等等。Java 线程的栈帧有哪几种呢,其实根据 Java 线程执行的方法有 Java 方法以及原生方法(Native)就能推测出有两种:
在最早的时候,Linux 还没有线程的概念,Java 自己做了一种叫做 Green Thread
的东西即用户态线程(与现在的虚拟线程设计差异很大,不是一个概念了),但是调度有诸多问题,所以在 Linux 有线程之后,Java 也舍弃了 Green Thread
。Java 线程其实底层就是通过操作系统线程实现,是一一对应的关系。不过现在,虚拟线程也快要 release 了,但是这个并不是今天的重点。并且,在最早的时候,Java 线程栈与 Native 线程栈也是分开的,虽然可能都是一个线程执行的。后来,发现这样做对于 JIT 优化,以及线程栈大小限制,以及实现高效的 StackOverflow 检查都不利,所以就把 Java 线程栈与 Native 线程栈合并了,这样就只有一个线程栈了。
JVM 中对于线程栈可以使用的空间是限制死的。对于 Java 线程来说,这个限制是由 -Xss
或者 -XX:ThreadStackSize
来控制的,-Xss
或者 -XX:ThreadStackSize
基本等价, 一般来说,-Xss
或者 -XX:ThreadStackSize
是用来设置每个线程的栈大小的,但是更严谨的说法是,它是设置每个线程栈最大使用的内存大小,并且实际可用的大小由于保护页的存在还要小于这个值,并且设置这个值不能小于保护页需要的大小,否则没有意义。根据前面对于 JVM 其他区域的分析我们可以推测出,对于每个线程,都会先 Reserve
出 -Xss
或者 -XX:ThreadStackSize
大小的内存,之后随着线程占用内存升高而不断 Commit
内存。
同时我们还知道,对于一段 Java 代码,分为编译器执行,C1 执行,C2 执行三种情况,因此,一个 Java 线程的栈内存结构可能如下图所示:
这个图片我们展示了一个比较极端的情况,线程先解释执行方法 1,之后调用并解释执行方法 2,然后调用一个可能比较热点的方法 3,方法 3 已经被 C1 优化编译,这里执行的是编译后的代码,之后调用可能更热点的方法 4,方法 4 已经被 C2 优化编译,这里执行的是编译后的代码。最后方法 4 还需要调用一个 native 方法 5。
JVM 线程内存还有一些特殊的内存区域,结构如下:
NullPointerException
优化方式类似,即抛出 SIGSEGV
被 JVM 捕获,再抛出 StackOverflowError
。保护区包括以下三种: -XX:StackYellowPages
参数决定。如果栈扩展到了黄色区域,则发生 SIGSEGV
,并且信号处理程序抛出 StackOverflowError
并继续执行当前线程。同时,这时候黄色页面会被映射分配内存,以提供一些额外的栈空间给异常抛出的代码使用,抛出异常结束后,黄色页面会重新去掉映射,变成保护区。-XX:StackRedPages
参数决定。正常的代码只会可能到黄色区域,只有 JVM 出一些 bug 的时候会到红色区域,这个相当于最后一层保证。保留这个区域是为了出这种 bug 的时候,能有空间可以将错误信息写入 hs_err_pid.log
文件用于定位。-XX:StackReservedPages
参数决定。在 Java 9 引入(JEP 270: Reserved Stack Areas for Critical Sections)(洗稿狗的区域是细狗区),主要是为了解决 JDK 内部的临界区代码(例如ReentrantLock
)导致 StackOverflowError
的时候保证内部数据结构不会处于不一致的状态导致锁无法释放或者被获取。如果没有这个区域,在 ReentrantLock.lock()
方法内部调用某个内部方法的时候可能会进入黄色区域,导致 StackOverflowError
,这时候可能 ReentrantLock
内部的一些数据可能已经修改,抛出异常导致这些数据无法回滚让锁处于当初设计的时候没有设计的不一致状态。为了避免这个情况,引入保留区域。在执行临界区方法的时候(被 @jdk.internal.vm.annotation.ReservedStackAccess
注解修饰的方法),如果进入保留区域,那么保留区域会被映射内存,用于执行完临界区方法,执行完临界区方法之后,再抛出 StackOverflowError
,并解除保留区域的映射。另外,前面我们提到过,@jdk.internal.vm.annotation.ReservedStackAccess
这个注解默认只能 jdk 内部代码使用,如果你有类似于 ReentrantLock
这种带有临界区的代码也想保护起来,可以设置 -XX:-RestrictReservedStack
,关闭对于 @jdk.internal.vm.annotation.ReservedStackAccess
的限制,这样你就可以在自己的代码中使用这个注解了。-XX:StackShadowPages
参数决定。影子区域只是抽象概念,跟在当前栈占用的顶部栈帧后面,随着顶部栈帧变化而变化。这个区域用于保证 Native 调用不会导致 StackOverflowError
。在后面的分析我们会看到,每次调用方法前需要估算方法栈帧的占用大小,但是对于 Native 调用我们无法估算,所以我们就假设 Native 大小最大不会超过影子区域大小,在发生 Native
调用前,会查看当前栈帧位置加上影子区域大小是否会达到保留区域,如果达到了保留区域,那么会抛出 StackOverflowError
,如果没有达到保留区域,那么会继续执行。这里我们可以看出,JVM 假设 Native 调用占用空间不会超过影子区域大小,JDK 中自带的 native 调用也确实是这样。如果你自己实现了 Native 方法并且会占用大量栈内存,那么你需要调整 StackShadowPages
。我们看下源码中如何体现的这些区域,参考源码:https://github.com/openjdk/jdk/blob/jdk-21%2B18/src/hotspot/share/runtime/stackOverflow.hpp
size_t StackOverflow::_stack_red_zone_size = 0;
size_t StackOverflow::_stack_yellow_zone_size = 0;
size_t StackOverflow::_stack_reserved_zone_size = 0;
size_t StackOverflow::_stack_shadow_zone_size = 0;
void StackOverflow::initialize_stack_zone_sizes() {
//读取虚拟机页大小,第二章我们分析过
size_t page_size = os::vm_page_size();
//目前各个平台最小页大小基本都是 4K
size_t unit = 4*K;
//使用 StackRedPages 乘以 4K 然后对虚拟机页大小进行对齐作为红色区域大小
assert(_stack_red_zone_size == 0, "This should be called only once.");
_stack_red_zone_size = align_up(StackRedPages * unit, page_size);
//使用 StackYellowPages 乘以 4K 然后对虚拟机页大小进行对齐作为黄色区域大小
assert(_stack_yellow_zone_size == 0, "This should be called only once.");
_stack_yellow_zone_size = align_up(StackYellowPages * unit, page_size);
//使用 StackReservedPages 乘以 4K 然后对虚拟机页大小进行对齐作为保留区域大小
assert(_stack_reserved_zone_size == 0, "This should be called only once.");
_stack_reserved_zone_size = align_up(StackReservedPages * unit, page_size);
//使用 StackShadowPages 乘以 4K 然后对虚拟机页大小进行对齐作为保留区域大小
assert(_stack_shadow_zone_size == 0, "This should be called only once.");
_stack_shadow_zone_size = align_up(StackShadowPages * unit, page_size);
}
我们继续针对 Java 线程进行讨论。在前面我们已经知道,Java 线程栈的大小是有限制的,如果线程栈使用的内存超过了限制,那么就会抛出 StackOverflowError
。但是,JVM 如何知道什么时候该抛出呢?
首先,对于解释执行,一般没有任何优化,就是在调用方法前检查。不同的环境下的实现会有些差别,我们以 x86 cpu 为例:
void TemplateInterpreterGenerator::generate_stack_overflow_check(void) {
//计算栈帧的一些元数据存储的消耗
const int entry_size = frame::interpreter_frame_monitor_size() * wordSize;
const int overhead_size =
-(frame::interpreter_frame_initial_sp_offset * wordSize) + entry_size;
//读取虚拟机页大小,第二章我们分析过
const int page_size = os::vm_page_size();
//比较当前要调用的方法的元素个数,判断与除去元数据以外一页能容纳的元素个数谁大谁小
Label after_frame_check;
__ cmpl(rdx, (page_size - overhead_size) / Interpreter::stackElementSize);
__ jcc(Assembler::belowEqual, after_frame_check);
//大于的才会进行后续的判断,因为小于一页的话,绝对可以被黄色区域限制住,因为黄色区域要与页大小对齐,因此至少一页
//小于一页的栈帧不会导致跳过黄色区域,只有大于的须有后续仔细判断
Label after_frame_check_pop;
//读取线程的 stack_overflow_limit_offset
//_stack_overflow_limit = stack_end() + MAX2(stack_guard_zone_size(), stack_shadow_zone_size());
//即栈尾 加上 保护区域 或者 阴影区域 的最大值,即有效栈尾地址
//其实就是当前线程栈容量顶部减去 保护区域 或者 阴影区域 的最大值的地址,即当前线程栈只能增长到这个地址
const Address stack_limit(thread, JavaThread::stack_overflow_limit_offset());
//将前面计算的栈帧元素个数大小保存在 rax
__ mov(rax, rdx);
//将栈帧的元素个数转换为字节大小,然后加上栈帧的元数据消耗
__ shlptr(rax, Interpreter::logStackElementSize);
__ addptr(rax, overhead_size);
//加上前面计算的有效栈尾地址
__ addptr(rax, stack_limit);
//与当前栈顶地址比较,如果当前栈顶地址大于 rax 当前值,证明没有溢出
__ cmpptr(rsp, rax);
__ jcc(Assembler::above, after_frame_check_pop);
//否则抛出 StackOverflowError 异常
__ jump(ExternalAddress(StubRoutines::throw_StackOverflowError_entry()));
__ bind(after_frame_check_pop);
__ bind(after_frame_check);
}
代码的步骤大概是(plagiarism和洗稿是恶意抄袭他人劳动成果的行为,是对劳动价值的漠视和践踏! ):
SIGSEGV
抛出 StackOverflowError
。因为每个保护区域如前面源代码所示,都是对虚拟机页大小进行对齐的,因此至少一页。StackOverflowError
异常。为什么是保护区域与阴影区域的最大值?阴影区域其实是我们假设的最大帧大小,最后至少要有这么多空间才一定不会导致溢出栈顶污染其他内存(当然,如之前所述,如果你自己实现一个 Native 调用并且栈帧很大,则需要修改阴影区域大小)。如果本身保护区域就比阴影区域大,那么就用保护区域的大小,就也能保证这一点。可以看出,编译执行,虽然做了一定的优化,但是还是很复杂,就算大部分栈帧应该都小于一页,但是刚开始的判断指令还是有不小的消耗。我们看看 JIT 编译后的代码,还是以 x86 cpu 为例:
https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/share/asm/assembler.cpp
void AbstractAssembler::generate_stack_overflow_check(int frame_size_in_bytes) {
//读取虚拟机页大小,第二章我们分析过
const int page_size = os::vm_page_size();
//读取影子区大小
int bang_end = (int)StackOverflow::stack_shadow_zone_size();
//如果栈帧大小大于一页,那么需要将 bang_end 加上栈帧大小,之后检查每一页是否处于保护区域
const int bang_end_safe = bang_end;
if (frame_size_in_bytes > page_size) {
bang_end += frame_size_in_bytes;
}
//检查每一页是否处于保护区域
int bang_offset = bang_end_safe;
while (bang_offset <= bang_end) {
bang_stack_with_offset(bang_offset);
bang_offset += page_size;
}
}
https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/macroAssembler_x86.cpp
//检查是否处于保留区域,其实就是将 rsp - offset 的地址上的值写入 rax 上,
//如果 rsp - offset 保护区域,那么就会触发 SIGSEGV
void bang_stack_with_offset(int offset) {
movl(Address(rsp, (-offset)), rax);
}
编译后执行的指令就简单多了:
StackOverflow
即可。检查当前占用位置加上影子区域大小,之后判断是否会进入保护区域即可,不用考虑当前方法栈帧占用大小,因为肯定小于一页。验证是否进入保护区域也和之前讨论过的 NullPointeException
的处理是类似的,就是将 rsp - offset 的地址上的值写入 rax 上,如果 rsp - offset 处于保护区域,那么就会触发 SIGSEGV
。这个和平台是相关的,我们以 linux x86 为例子,假设没有大页分配,一页就是 4K,一个线程至少要保留如下的空间:
这些加在一起是 24 页,也就是 96K。
同时,在 JVM 代码中也限制了,除了这些空间,每种线程的最小大小:
https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/os_cpu/linux_x86/os_linux_x86.cpp
size_t os::_compiler_thread_min_stack_allowed = 48 * K;
size_t os::_java_thread_min_stack_allowed = 40 * K;
size_t os::_vm_internal_thread_min_stack_allowed = 64 * K;
所以,对于 Java 线程,至少需要 40 + 96 = 136K
的空间。我们试一下:
bash-4.2$ java -Xss1k
The Java thread stack size specified is too small. Specify at least 136k
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.