当前位置: 首页 > news >正文

JVM对象创建与内存分配机制深度解析

一、对象的完整创建过程(六大核心步骤)

Java 中new Object()看似一行代码,底层经过 JVM 六大严格流程,缺一不可。完整顺序:类加载检查 → 分配内存 → 内存初始化零值 → 设置对象头 → 执行构造方法 → 指针压缩优化

1.1 类加载检查(对象创建前置条件)

JVM 执行 new 指令时,不会直接创建对象,首先做类加载合法性校验

  1. 检查当前类的符号引用是否存在于常量池;

  2. 判断该类是否已经完成:加载、验证、准备、解析、初始化;

  3. 如果未加载,触发双亲委派机制完成类加载;

  4. 类加载失败直接抛出编译/运行时异常,终止对象创建。

核心结论先有类、后有对象,类加载是对象实例化的唯一前置依赖。

1.2 分配内存(对象空间划分核心)

类加载完成后,JVM 精准计算对象占用内存大小,从堆内存中划分对应空间。

这个步骤有两个问题:

1.如何划分内存。

2.在并发情况下可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。

1.2.1 划分内存的两种核心方法
1、指针碰撞法(Bump The Pointer)

适用场景:堆内存规整、无内存碎片(使用 Serial、Parallel 回收器)。

原理:如果Java堆中内存是绝对规整的 ,所有用过的内存都放在一边 ,空闲的内存放在另一边 ,中间放着一个指针作为分界点的指示器 ,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。

优点:速度极快、无内存碎片、分配效率最高。

2、空闲列表法(Free List)

适用场景:堆内存不规整、存在大量内存碎片(使用 CMS、G1 回收器)。

原理如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录

并更新列表上的记录

缺点:分配速度慢、需要遍历检索。

1.2.2 内存分配常见问题
  • 并发内存分配冲突:多线程同时 new 对象,抢占同一块空闲内存,导致内存覆盖、对象异常。

  • 内存碎片过多:频繁GC后内存碎片化严重,大对象无法找到连续空闲空间。

  • 分配失败触发GC:新生代空间不足,频繁Minor GC,造成服务抖动。

1.2.3 常见问题解决方案
  • CAS 重试机制(compare and swap):JVM 采用 CAS+失败重试 保证并发内存分配线程安全,替代重量级锁,性能更高。

  • TLAB 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB):每个线程预先分配私有内存块,线程内对象优先在TLAB分配,彻底避免并发竞争。通过XX:+/一UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启XX:+UseTLAB) ,_XX:TLABSize 指定TLAB大小。

  • GC内存整理:G1/CMS 定期整理内存碎片,保证内存规整度。

1.3 内存初始化零值

内存分配完成后,JVM 自动将对象所有成员字段初始化默认零值(不包括对象头),如果使用TLAB ,这一工作过程也可以提前至TLAB分配时进行:

  • 基本数据类型:int=0、boolean=false、double=0.0

  • 引用类型:默认 null

关键区别:这是 JVM 底层默认赋值,早于代码构造器赋值,保证对象字段在任何场景下都有初始值,可直接使用,避免空指针异常。

1.4 设置对象头(MarkWord+类元数据)

零值初始化完成后,JVM 为对象设置对象头,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。对象头是 Java 对象的核心元数据,占用对象内存前置空间。

在HotSpot虚拟机中 ,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance Data)和对齐填充(Padding)。 HotSpot虚拟机的对象头包括两部分信息 ,第一部分用于存储对象自身的运行时数据 , 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。对象头的另外一部分是类型指针 ,即对象指向它的类元数据的指针 ,虚拟机通过这个指针来确定这个对象是哪个类的实例。

1.4.1 对象头具体信息(HotSpot JDK8)

对象头由两部分组成:MarkWord(运行时元数据) + KlassPointer(类型指针),数组对象额外包含数组长度。以下为32位时对象头的相关信息图

1、MarkWord(8字节)

存储对象运行时动态信息,动态复用存储空间:

  • 对象哈希码、GC分代年龄

  • 锁状态标记、偏向锁ID、轻量级锁、重量级锁标记

  • 是否被线程持有、是否可重入

2、KlassPointer 类型指针(4/8字节)

指向方法区(元空间)的类元数据,JVM 通过该指针识别对象所属类、调用对应方法。

3、数组长度(仅数组对象独有,4字节)

普通对象无此字段,数组对象存储数组长度信息。

1.5 执行初始化构造方法(<init>)

前面所有步骤是 JVM 底层初始化,最后执行 Java 代码层面的构造方法:

  1. 执行代码中成员变量自定义赋值

  2. 执行代码块初始化

  3. 执行构造方法赋值

核心顺序:JVM默认零值 → 代码自定义初始化 → 构造方法赋值

1.6 指针压缩介绍及作用(JDK8默认开启)

1.6.1 指针压缩原理

64位JVM下,原生对象指针占用8字节,内存占用大、内存利用率低、GC压力高。JDK8 默认开启UseCompressedOops 指针压缩,将64位指针压缩为32位(4字节)。jvm配置参数:UseCompressedOops ,compressed_压缩、oop(ordinary object pointer)_对象指针,启用指针压缩:_XX:+UseCompressedOops(默认开启) ,禁止指针压缩:_XX:_UseCompressedOops

1.6.2 指针压缩生效条件

堆内存 ≤ 32G 自动生效;堆内存 >32G 指针压缩失效,指针恢复8字节。

1.6.3 核心作用
  • 大幅降低对象内存占用,节省堆内存空间

  • 提升内存利用率,减少GC触发频率

  • 提升对象访问速度,优化整体性能


二、对象内存分配完整机制(全场景覆盖)

Java 对象不是统一在一个区域分配,JVM 根据对象大小、生命周期、内存状态,智能分配到:栈、Eden新生代、老年代,同时配套动态年龄判断、空间担保机制。

2.1 对象内存分配完整流程图

【对象创建】 ↓ 逃逸分析 → 未逃逸 → 栈上分配(直接随线程销毁,无GC) ↓ 逃逸分析 → 已逃逸 ↓ 判断是否为大对象 ├─ 是大对象 → 直接进入老年代 └─ 不是大对象 → 优先进入Eden区 ↓ Eden区满 → 触发MinorGC ↓ 存活对象进入Survivor区,年龄+1 ↓ 动态年龄判断 / 年龄达标(15) → 晋升老年代 ↓ 老年代空间不足 → 触发空间担保 / FullGC

2.2 对象栈上分配(最优分配方式)

2.2.1 栈上分配原理

JVM 通过逃逸分析判断:如果对象仅在方法内部使用、不会逃逸到外部方法/线程,直接在虚拟机栈分配内存,不进入堆内存。JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其通过标量替换先分配在栈上(栈上分配)JDK7之后默认开启逃逸分析,如果要关闭使用参数(-XX:-DoEscapeAnalysis)

标量替换:通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时JVM不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配。开启标量替换参数(-XX:+EliminateAllocations)JDK7之后默认开启。

标量与聚合量:标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如intlong等基本数据类型以及reference类型等标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一步分解的聚合量。

优势:方法执行结束,栈帧弹出,对象自动销毁,零GC、零堆内存占用

2.2.2 常见问题分析与解决方案
  • 问题1:逃逸分析失效,本该栈分配的对象进入堆原因:方法内对象被返回、被外部引用、多线程共享,触发逃逸。 解决方案:优化代码,缩小对象作用域,避免外部引用。

  • 问题2:频繁创建局部对象导致堆压力大解决方案:开启JIT逃逸分析、小对象尽量方法内局部使用。

2.3 对象 Eden 区分配(绝大多数对象默认分配)

2.3.1 Eden区简介

新生代分为:1个Eden区 + 2个Survivor区(From/To),比例默认 8:1:1。可以通过设置参数 -XX:SurvivorRatio修改

核心规则90%以上短期临时对象,优先在Eden区分配,符合对象朝生夕灭特性。

2.3.2 常见操作
  • 新对象优先存入Eden

  • Eden满触发Minor GC,清空无效对象

  • 存活对象移入Survivor区,年龄自增

2.3.3 常见问题、原理与解决方案
  • 问题1:Minor GC 过于频繁原理:Eden区内存过小、瞬时创建大量临时对象,快速填满Eden。 解决方案:调大新生代内存、优化短命对象创建逻辑。

  • 问题2:对象提前晋升老年代原理:Eden空间不足,Survivor放不下存活对象,提前进入老年代。 解决方案:调整Survivor比例,优化新生代整体大小。

2.4 大对象直接进入老年代

大对象定义:超过 JVM 参数-XX:PretenureSizeThreshold阈值的对象/数组。这个参数只在SerialParNew两个收集器下有效。

原理:大对象体积大,在新生代分配容易触发GC、造成内存碎片,JVM 直接分配到老年代。

生产问题:大对象过多导致老年代快速占满,频繁Full GC、OOM。

解决方案:合理设置大对象阈值、避免循环创建超大数组、优化批量查询逻辑。

2.5 长期存活对象进入老年代

对象在Survivor区每经历一次Minor GC,年龄+1,默认年龄阈值15岁(CMS收集器默认6岁,不同的垃圾收集器有所不同),达标直接晋升老年代。

参数:-XX:MaxTenuringThreshold=15

2.6 对象动态年龄判断(高阶重点)

JVM 不会死板等待对象涨到15岁,提供动态年龄机制优化晋升效率:

规则:Survivor区中,一批对象的总内存大小(年龄1+年龄2+...年龄N) > Survivor区一半空间

(-XX:TargetSurvivorRatio可以指定),该年龄N及以上对象全部直接晋升老年代。

作用:避免大量中年对象长期滞留新生代,占用新生代空间、频繁触发GC。

2.7 老年代空间分配担保机制

Minor GC 执行前,JVM 必须做老年代担保校验,防止GC后存活对象无法存放导致OOM。流程如下:

年轻代每次minorgc之前JVM都会计算下老年代剩余可用空间

如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象)

就会看一个“-XX:-HandlePromotionFailure(jdk1.8默认就设置了)的参数是否设置了

如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次minorgc后进入老年代的对象的平均大小

如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次Fullgc,对老年代和年轻代一起回收一次垃圾,如果回收完还是没有足够空间存放新的对象就会发生"OOM"

当然,如果minorgc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发fullgcfull gc完之后如果还是没有空间放minorgc之后的存活对象,则也会发生“OOM

2.7.1 担保核心规则
  1. 判断老年代剩余空间,是否大于新生代所有存活对象总大小;

  2. 足够:正常Minor GC;

  3. 不足:判断是否开启担保;

  4. 担保失败:直接触发Full GC,整理老年代空间。

2.7.2 生产问题

频繁担保失败会导致频繁Full GC、服务严重卡顿,是高并发系统核心性能杀手。

2.7.3 解决方案

调大老年代空间、优化对象晋升频率、减少短期大对象产生。


三、对象内存回收机制(如何判定对象/类可回收)

对象创建分配完成后,生命周期结束后需要被GC回收,下面深度解析JVM垃圾判定全套算法与规则。

3.1 引用计数算法(已淘汰)

原理

每个对象维护一个引用计数器,被引用+1,引用失效-1,计数器为0判定为垃圾。

缺点

无法解决循环引用问题,两个对象互相引用,计数器永远不为0,无法回收,导致内存泄漏。

现状

HotSpot JVM 已彻底废弃,不再使用。

3.2 可达性分析算法(现代JVM核心算法)

核心原理

GC Roots为起始链路,遍历所有引用链,能够遍历到达的对象为非垃圾对象无法遍历到达的对象判定为垃圾对象

GC Roots 核心来源
  • 虚拟机栈中局部变量引用的对象

  • 本地方法栈 Native 引用对象

  • 方法区静态变量、常量引用对象

  • 活跃线程持有对象

优势:完美解决循环引用问题,精准判定垃圾对象。

3.3 四大引用类型(回收规则差异化)

不同引用类型,GC回收策略完全不同,是内存调优基础:

  • 强引用:默认引用,永不回收,直至OOM

  • 软引用:内存不足时回收,适合缓存场景

  • 弱引用:下次GC必回收,适合临时关联对象

  • 虚引用:无实际引用,仅用于回收追踪

3.4 finalize 方法判断对象存活(救赎机制)

对象被判定为不可达垃圾后,不会直接回收,存在一次自救机会:

  1. JVM 判断对象是否重写 finalize 方法;

  2. 未重写直接回收;

  3. 重写且未执行过,放入F-Queue队列执行自救;

  4. 自救成功(重新建立引用链)则存活,失败则彻底回收。

重点:finalize 只会执行一次,自救仅有一次机会,生产禁止依赖该机制。

3.5 如何判断类为无用类(类卸载规则)

很多人只会判断对象回收,不会判断类回收,类卸载需要同时满足三个严格条件

  1. 该类所有实例对象已被全部回收;

  2. 该类的 Class 对象无任何引用;

  3. 加载该类的类加载器已经被回收。

核心作用:满足条件后JVM可卸载类,释放元空间内存,避免元空间溢出。


四、全文总结

  1. 对象创建六步:类加载检查→分配内存→零值初始化→设置对象头→构造方法初始化→指针压缩优化。

  2. 内存分配两种方式:指针碰撞(规整)、空闲列表(碎片多),TLAB解决并发分配问题。

  3. 对象分配四大区域:栈上分配最优、Eden优先分配、大对象直接进老年代、高龄对象晋升老年代。

  4. 动态年龄判断+空间担保机制,是JVM自适应内存优化的核心。

  5. 垃圾判定采用可达性分析,淘汰引用计数,四大引用控制回收策略。

  6. 对象靠可达性判定回收,类靠三重条件判定卸载,彻底释放内存。

http://www.jsqmd.com/news/996275/

相关文章:

  • CANoe高手进阶:如何像搭积木一样管理你的工程文件?.vxp、.tse、.cdd等核心文件实战解析
  • 当InfiniBand网络“大脑”宕机时:深入理解Mellanox SM HA的故障切换机制与业务影响
  • 混合密度网络与条件流匹配:概率建模与风电预测实践
  • 从视频到标签:利用Labelme高效构建视频标注工作流
  • 从手机芯片到显卡:看懂宣传页里的算力(TOPS/FLOPS)到底靠不靠谱
  • 告别103Ω高阻抗!手把手教你用Smith圆图优化不等分Wilkinson功分器设计
  • 汽车ECU诊断会话控制:10服务(0x10)从入门到实战,手把手教你玩转UDS诊断
  • Python+Django实战|线上问卷与投票调研系统:自定义题型、问卷发布、链接分享、答卷收集、数据可视化、报表导出
  • openclaw数字员工解决方案哪个技术强
  • 暗黑破坏神2存档编辑器:三步可视化修改你的游戏角色
  • 2026年广州除甲醛公司哪家效果好?地域化服务对比与避坑指南 - 观域传媒
  • mbedtls RSA签名验签踩坑记:PKCS#1 V1.5和V2.1填充模式到底怎么选?
  • 如何用Arduino打造低成本多功能硬件工具:Flopper Ziro完整指南
  • 别再只盯着BIOS了!聊聊主板上的‘隐形管家’:Embedded Controller (EC) 到底管啥?
  • Nucleus Co-Op完整教程:Windows单机游戏分屏多人本地同乐终极指南
  • 细胞衰老的机制概述
  • 2026年西北地区钢结构加工厂怎么选?从资质、产能到案例的全维度拆解 - 优质品牌商家
  • HarmonyOS6 Flex 垂直布局实战:个人中心分组菜单从零搭建
  • 别再只盯着CD和EMD了!点云补全评估指标F-Score与DCD实战解读(附代码示例)
  • 原神祈愿记录终极导出指南:免费工具让你掌握抽卡全数据
  • Charles:软件能力深度解析 / 跨平台 HTTP/HTTPS 代理调试工具 / 客户端与互联网之间的中间人代理 / 拦截、查看、篡改所有网络流量
  • 从np.zeros到np.ones/np.full:NumPy数组初始化全家桶保姆级指南
  • 深入Transformer内部:手把手拆解Adapter模块结构,看它如何用‘小参数’撬动‘大模型’
  • 从汽车刹车到智能门锁:EEPROM磨损均衡算法实战,让你的产品寿命翻倍
  • 传统云端OCR vs 天若OCR本地版:如何在Windows上实现100%离线文字识别
  • 从RTL到GDS:一个数字IC工程师的DFT实战笔记(含SCAN插入与BIST规划)
  • 降阶拉格朗日神经网络在机器人控制中的应用
  • 2026年更新永康电镐制造商选哪家?实力品牌深度剖析与选择指南 - 品牌鉴赏官2026
  • 视频语言模型的高效编解码原语技术解析
  • 别再死记硬背FOC公式了!用Arduino+ESP32手把手带你理解SVPWM与DQ坐标系