JVM内存模型是逻辑分区与物理分配分离的调度策略:堆区虚拟连续、物理页按需分配;元空间用本地内存,由NMT跟踪已提交物理页;线程栈和程序计数器私有,物理页写时分配;对象布局对齐缓存行以提升硬件效率。

JVM内存模型不是物理内存的直接镜像,而是运行时对物理资源的逻辑组织与调度策略。它通过操作系统内核、CPU分页机制和JVM自身参数协同完成映射,关键在于“逻辑分区”与“物理分配”分离。
堆区:虚拟地址连续,物理页按需落地
堆是JVM最大内存区域,由-Xms/-Xmx指定大小,但初始只申请虚拟地址空间(如mmap匿名映射),不立即占用真实物理内存。真正分配物理页发生在对象首次写入时(缺页中断触发)。可用pmap -x <pid>查看进程虚拟内存布局,其中[ anon ]段即为堆映射;RSS列才反映实际占用的物理内存页数。
- 小对象分配走TLAB(线程本地分配缓冲),减少锁竞争,也影响物理页访问局部性
- 大对象(>-XX:PretenureSizeThreshold)直接进入老年代,可能跨多个物理页,增加TLB压力
- 启用-XX:+UseTransparentHugePages后,内核会尽量用2MB大页承载堆内存,降低页表查询开销
元空间:直接使用本地内存,绕过JVM堆管理
Java 8+废弃永久代,改用元空间存放类元数据(Klass、常量池、方法字节码等),其内存来自本地堆(malloc/new),不受-Xmx限制,但受系统虚拟内存总量制约。元空间物理内存不经过JVM GC管理,而是由NativeMemoryTracking(NMT)跟踪:
- 启动时加-XX:NativeMemoryTracking=detail
- 运行中执行jcmd <pid> VM.native_memory summary scale=MB,观察metaspace行的committed值——这才是已落地的物理内存
- 元空间增长会触发类卸载,但卸载失败(如ClassLoader被引用)会导致物理内存持续累积
栈与程序计数器:线程私有,物理页写时分配
每个Java线程对应一个OS线程,其栈由操作系统在创建线程时分配,默认-Xss1M(64位Linux下)。注意:这个1MB只是虚拟地址预留,实际物理内存仅在栈帧压入并写入数据时才分配页。程序计数器更轻量,通常位于线程控制块(TCB)中,几乎不占额外物理内存。
- 高并发场景若线程数达数千,即使未压满栈,虚拟地址空间也可能耗尽(尤其32位环境)
- 栈溢出(StackOverflowError)是虚拟地址越界,而线程创建失败(OutOfMemoryError: unable to create new native thread)多因物理内存或ulimit限制
- CPU缓存友好性:栈天然具备空间局部性,连续访问利于L1/L2缓存命中
对象布局与硬件对齐:从字节到缓存行的真实映射
一个普通对象(如new Object())在物理内存中并非紧凑排列。启用压缩指针(-XX:+UseCompressedOops,默认开启≤32GB堆)时,对象头含8B Mark Word + 4B Klass Pointer,再加填充字节(padding)确保整体大小为8B整数倍——这是为了对齐CPU缓存行(通常64B),避免伪共享(false sharing)。
- 字段重排序:JVM可能调整实例字段顺序,把long/double放前面,提升内存填充效率
- 数组对象额外含4B length字段,且元素从8B偏移开始(对象头之后),影响批量读取的缓存预取效果
- 用jol-cli工具可精确输出对象在内存中的起始偏移、各字段位置及总大小,验证是否跨缓存行

















