news 2026/7/24 21:20:28

JVM虚拟延迟内存分配机制源码剖析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JVM虚拟延迟内存分配机制源码剖析

VM虚拟延迟内存分配机制源码剖析

  • 前言
  • JVM虚拟延迟内存分配机制源码剖析
    • 架构演进全景
    • 第一阶段:Reserved 状态的诞生(分配 VMA 线性区)
      • 1. JVM 层的调用轨迹
      • 2. Linux 平台的具体实现
      • 3. Linux 内核视角
    • 第二阶段:Committed 状态的转换(修改 VMA 权限)
      • 1. JVM 层的调用轨迹
      • 2. Linux 平台的具体实现
      • 3. Linux 内核视角与 Overcommit 审计
    • 第三阶段:物理内存的真正换入(触发缺页中断)
      • 1. 触发时机与硬件捕获
      • 2. Linux 内核的缺页中断处理流程
    • 关键参数对转换行为的干预
      • 1. `-Xms` 与 `-Xmx` 对齐
      • 2. `-XX:+AlwaysPreTouch` 的降维打击

前言

本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。

JVM虚拟延迟内存分配机制源码剖析

作为软件开发工程师,分析 JVM(以 OpenJDK8为例)的虚拟内存管理,本质上是解构 HotSpot 运行时环境(Runtime)与 Linux 内核虚拟内存子系统(VMS)的协同演进过程。

在 Linux 进程模型中,JVM 视角下的Reserved(预留)Committed(提交),分别对应了操作系统层面的分配 VMA 线性区改变页面访问权限/触发物理内存承诺。而真正的物理内存换入,则是通过延迟分配(Lazy Allocation)机制机制下的缺页中断(Page Fault)实现的。


架构演进全景

在深入源码前,需明确 JVM 虚拟内存的三阶段演进状态:

  1. Reserved(预留状态):JVM 向内核申请一段连续的虚拟地址空间。内核仅在进程的mm_struct中新建或划分一段vm_area_struct(VMA),其页表项(PTE)尚未建立,且访问权限通常设为PROT_NONE。此时不占用任何物理内存与 Swap 空间
  2. Committed(提交状态):JVM 声明将要使用某段已预留的地址空间。通过内核系统调用修改 VMA 的权限为PROT_READ | PROT_WRITE。此时内核会进行Overcommit 审计,并在逻辑上承诺这段内存可用,但依然没有分配物理内存页帧(Page Frame)
  3. Physical Mapped(物理就绪状态):当 JVM 线程首次向该内存区域执行写指令时,CPU 的 MMU 发现页表项无效,触发缺页中断(Page Fault)。内核接管中断后,从伙伴系统申请物理页(Page Frame),将其填入页表并建立映射。此时才真正消耗物理内存

第一阶段:Reserved 状态的诞生(分配 VMA 线性区)

在 JVM 启动时(例如初始化 Java 堆),ReservedSpace类负责向操作系统预留整块连续的虚拟地址空间(大小由-Xmx等参数决定)。

1. JVM 层的调用轨迹

源码位于hotspot/src/share/vm/runtime/virtualspace.cpp

// ReservedSpace 的核心初始化逻辑voidReservedSpace::initialize(size_t size,size_t alignment,boollarge,char*requested_address,constsize_t noaccess_prefix,boolexecutable){// ... 省略对齐与大页(Large Pages)的条件判断 ...// 调用平台相关的操作系统接口预留内存char*base=os::reserve_memory(size,requested_address,alignment);// ... 省略异常检查与 Native Memory Tracking (NMT) 的注册 ...// 此时 NMT 会将这块内存标记为 Tracking Level: Reserved_base=base;_size=size;}

2. Linux 平台的具体实现

os::reserve_memory最终路由到hotspot/src/os/linux/vm/os_linux.cpp中的os::anon_mmap函数:

char*os::anon_mmap(char*fixed_addr,size_t size,size_t alignment,booltransferable){// 设定标准匿名映射标记// MAP_PRIVATE: 创建一个私有的写时复制(COW)映射// MAP_ANONYMOUS: 匿名映射,不映射任何文件,内容初始化为 0intflags=MAP_PRIVATE|MAP_ANONYMOUS;if(fixed_addr!=NULL){flags|=MAP_FIXED;// 如果指定了目标地址(如压缩指针基址优化),则强制覆盖该地址}// 【核心系统调用】// prot 传入 PROT_NONE(无权限:不可读、不可写、不可执行)// 这意味着进程此时如果访问该段虚拟地址,会被 CPU 直接拦截并抛出 SIGSEGVchar*base=(char*)mmap(fixed_addr,size,PROT_NONE,flags,-1,0);if(base==MAP_FAILED){returnNULL;// 预留失败,可能触发 Native OOM}returnbase;}

3. Linux 内核视角

mmap(..., PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, ...)执行时:

  • 内核在进程的全局内存描述符mm_struct中查找一块空闲的虚拟地址。
  • 创建一个新的红黑树节点——vm_area_struct(VMA 线性区),记录vm_startvm_end
  • 将该 VMA 的vm_flags设为VM_NONE(对应PROT_NONE)。
  • 结果:内核仅在内核空间里记录了“该进程占有了这段虚拟地址”,页表(Page Table)中没有任何对应的实体映射

第二阶段:Committed 状态的转换(修改 VMA 权限)

当 JVM 需要实际使用内存时(例如根据-Xms初始化堆大小,或运行时堆动态扩容),VirtualSpace会驱动内存从 Reserved 走向 Committed。

1. JVM 层的调用轨迹

源码位于hotspot/src/share/vm/runtime/virtualspace.cpp

boolVirtualSpace::expand_by(size_t bytes){// ... 边界与对齐检查 ...// 计算当前 committed 边界(high)及需要新提交的大小// 调用 os::commit_memory 转换为 Committed 状态if(!os::commit_memory(high(),bytes,alignment_hint,_executable)){returnfalse;// 提交失败,抛出 OutOfMemoryError}// 成功后移动高位指针,扩大可用边界_high+=bytes;returntrue;}

2. Linux 平台的具体实现

hotspot/src/os/linux/vm/os_linux.cpp中,os::commit_memory最终交付给os::linux::commit_memory_impl

boolos::linux::commit_memory_impl(char*addr,size_t size,intexec){// 决定转换后的权限:如果是代码缓存(CodeCache)则需要可执行权限,堆内存则为可读写intprot=exec?(PROT_READ|PROT_WRITE|PROT_EXEC):(PROT_READ|PROT_WRITE);// 【核心系统调用】// 利用 mprotect 改变该段已有 VMA 的访问权限// 从原先的 PROT_NONE 改变为 PROT_READ | PROT_WRITEif(mprotect(addr,size,prot)==0){returntrue;}// 兜底策略:如果 mprotect 因为特殊的 VMA 拆分限制失败,// 则利用 MAP_FIXED 显式重新进行底层覆盖映射intflags=MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED;char*res=(char*)mmap(addr,size,prot,flags,-1,0);returnres!=MAP_FAILED;}

3. Linux 内核视角与 Overcommit 审计

mprotect(..., PROT_READ | PROT_WRITE)被调用时,内核会执行以下动作:

  • 找到对应的vm_area_struct,将其vm_flags修改为VM_READ | VM_WRITE

  • 根据系统的/proc/sys/vm/overcommit_memory配置进行内存记账审核:

  • 如果设置为2(不允许超发),内核会计算当前系统已 Commit 的总内存是否超过了(RAM * 比例 + Swap)。若超过,mprotect直接返回-ENOMEM(即 JVM 报出著名的errno=12 Cannot allocate memory错误)。

  • 结果:此时内核在逻辑上通过了记账,认可了这笔内存申请,但依旧没有为该进程分配任何物理内存页帧(Page Frame),页表项(PTE)内依然是空的或无效的。


第三阶段:物理内存的真正换入(触发缺页中断)

从 JVM 的 Native Memory Tracking(NMT)指标来看,第二阶段完成后内存已经是Committed了。但在 OS 层面,真正的物理内存换入发生在JVM 首次对该地址进行读写操作时

1. 触发时机与硬件捕获

假设 JVM 开始在刚刚 Commit 的堆内存上分配对象,或者垃圾回收器进行内存置零初始化:

; 伪汇编代码:JVM 线程尝试向 Committed 区域写入数据 MOV [rax], rbx ; rax 存储的是刚被 commit 但从未读写的虚拟地址
  1. MMU 拦截:CPU 的内存管理单元(MMU)拦截到这条写入指令,尝试通过多级页表(PGD -> PUD -> PMD -> PTE)将rax中的虚拟地址翻译为物理地址。
  2. 触发异常:MMU 发现该虚拟地址对应的页表项(PTE)的Present标志位为0(表示该页不在物理内存中)。
  3. 硬件中断:CPU 挂起当前用户态指令,向内核抛出一个Page Fault(缺页中断,中断向量号 14),并将触发异常的虚拟地址存入CR2寄存器。

2. Linux 内核的缺页中断处理流程

内核捕获中断后,进入异常处理程序(X86 架构下为do_page_fault):

// 简化版的 Linux 内核缺页处理伪代码逻辑__visiblevoid__kprobesdo_page_fault(structpt_regs*regs,unsignedlongerror_code){unsignedlongaddress=read_cr2();// 从 CR2 寄存器读取引发缺页的虚拟地址structtask_struct*tsk=current;structmm_struct*mm=tsk->mm;// 1. 在进程的全局红黑树中查找该地址所属的 VMAstructvm_area_struct*vma=find_vma(mm,address);// 2. 权限校验// 此时因为第二阶段执行了 mprotect,vma->vm_flags 已经是 VM_WRITE// 校验通过,否则会报 SIGSEGV(段错误)if(error_code&X86_PF_WRITE){if(!(vma->vm_flags&VM_WRITE))gotobad_area;}// 3. 调用核心分配机制handle_mm_fault(vma,address,flags);}

深入到handle_mm_fault->__handle_mm_fault->handle_pte_fault。由于这是由MAP_ANONYMOUS创建的匿名映射,内核最终会调用do_anonymous_page

staticintdo_anonymous_page(structmm_struct*mm,structvm_area_struct*vma,unsignedlongaddress,pte_t*page_table,pmd_t*pmd,unsignedintflags){structpage*page;// 如果是读缺页,为了节省内存,内核会直接将其映射到一个全局只读的“零页”(Zero Page)if(!(flags&FAULT_FLAG_WRITE)){entry=pte_mkspecial(pfn_to_pte(my_zero_pfn(address)));// ... 填入页表项 ...return0;}// JVM 执行的是写操作,必须分配真实的物理页帧// 从伙伴系统(Buddy System)的高端或普通内存区申请一个全零的物理页框(Page Frame)page=alloc_zeroed_user_highpage_movable(vma,address);if(!page)returnVM_FAULT_OOM;// 系统物理内存耗尽,触发 OS 级别的 OOM Killer// 将物理页的页框号(PFN)和 VMA 的 RW 权限结合,生成标准的页表项(PTE)pte_tentry=mk_pte(page,vma->vm_page_prot);entry=pte_mkdirty(pte_mkwrite(entry));// 标记为脏页、可写// 【核心动作】将生成的 PTE 写入底层硬件页表set_pte_at(mm,address,page_table,entry);// 递增进程的 RSS(Resident Set Size,常驻内存集)计数inc_mm_counter(mm,MM_ANONPAGES);return0;}

do_anonymous_page返回后,内核将 CPU 切回用户态,重新执行刚才失败的MOV [rax], rbx指令。此时 MMU 能够顺着页表完美查找到物理内存页帧,转换宣告完成。


关键参数对转换行为的干预

系统工程师在调优 JVM 时,有两个参数深刻改变了上述的动态转换链路:

1.-Xms-Xmx对齐

  • 不相等(如-Xms1g -Xmx4g
    启动时,JVM 会调用anon_mmap(PROT_NONE)预留4 GB 4\text{GB}4GBReserved空间。接着,对其中1 GB 1\text{GB}1GB的空间调用mprotect(PROT_READ|PROT_WRITE)转换为Committed。随着运行期对象增多,触发堆扩容,动态对剩余的3 GB 3\text{GB}3GB空间按需分批执行mprotect(Committed),并伴随高频的缺页中断换入物理内存。
  • 相等(如-Xms4g -Xmx4g
    启动时,直接一步到位将4 GB 4\text{GB}4GB全部转为Committed状态,避免了运行期由于扩容引发mprotect系统调用的上下文切换开销。

2.-XX:+AlwaysPreTouch的降维打击

即便是-Xms4g -Xmx4g,启动后通过top命令查看进程的RES(物理常驻内存)依然可能只有几百兆,因为 OS 的Lazy Allocation机制仍在生效,缺页中断被延迟到了运行期。

为了彻底消除运行期物理页换入导致的瞬时抖动(STW 期间或对象分配分配时的微小延迟),可以使用-XX:+AlwaysPreTouch。其源码位于hotspot/src/os/linux/vm/os_linux.cpp

voidos::pretouch_memory(char*start,char*end,size_t page_size){for(char*p=start;p<end;p+=page_size){// 强行对每一个内存页的第一个字节写入一个 0// 该赋值操作在原子层面是一条写指令,直接在强行触发操作系统的 do_page_fault*p=0;}}

后果:如果在启动参数中开启该参数,JVM 会在启动阶段的VirtualSpace::expand_by内部同步循环遍历所有 Committed 内存页并写入0。这意味着在 JVM 启动完成的那一刻,所有的虚拟内存就已经完成了从 Reserved -> Committed -> 物理页映射的完整闭环,运行期的物理内存开销一步到位,彻底抹平了后续的缺页中断隐患。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/24 21:19:13

如何用Wand-Enhancer彻底改造你的WeMod游戏体验?完整指南来了!

如何用Wand-Enhancer彻底改造你的WeMod游戏体验&#xff1f;完整指南来了&#xff01; 【免费下载链接】Wand-Enhancer Advanced UX and interoperability extension for Wand (WeMod) app 项目地址: https://gitcode.com/GitHub_Trending/we/Wand-Enhancer 如果你是一名…

作者头像 李华
网站建设 2026/7/24 21:18:34

Chrome全屏截图插件:解决网页截图不完整的终极方案

Chrome全屏截图插件&#xff1a;解决网页截图不完整的终极方案 【免费下载链接】full-page-screen-capture-chrome-extension One-click full page screen captures in Google Chrome 项目地址: https://gitcode.com/gh_mirrors/fu/full-page-screen-capture-chrome-extensio…

作者头像 李华
网站建设 2026/7/24 21:18:15

系统架构师(计算机网络)

1. 奈奎斯特定律 奈奎斯特定律&#xff1a; &#xff08;1&#xff09;理想低通、无噪声信道下&#xff0c;仅由信道带宽决定最大码元传输速率&#xff08;最高码元速率&#xff09;&#xff08;波特率&#xff09;&#xff0c;和信噪比无关。 Rmax​2W (单位&#xff1a;波…

作者头像 李华
网站建设 2026/7/24 21:17:54

琅玡王氏史

文章目录一、琅玡王氏与太原王氏的关系&#xff1a;同出一脉&#xff0c;王翦后裔二、王离被项羽所俘&#xff1a;其子王元、王威&#xff0c;分家避祸三、琅玡王氏四、太原王氏五、诗中的琅琊王氏一、琅玡王氏与太原王氏的关系&#xff1a;同出一脉&#xff0c;王翦后裔 历史…

作者头像 李华
网站建设 2026/7/24 21:17:47

OWASP 移动应用安全之授权(MASTG-DEMO-0090)

https://mas.owasp.org/MASTG 该网站主要围绕以下三大支柱展开&#xff0c;为开发者和安全审计人员提供指导&#xff1a;MASVS (移动应用安全验证标准): 定义了移动 App 的核心安全要求&#xff0c;涵盖了数据存储 (Storage)、密码学 (Crypto)、身份验证 (Auth)、网络通信 (Ne…

作者头像 李华