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

JVM对象创建与内存分配

JVM对象创建与内存分配

一、对象创建流程

对象创建需依次执行以下 5 个核心步骤,确保对象从 “指令触发” 到 “可用状态” 的完整生命周期:

  1. 类加载检查

    • 触发条件:虚拟机遇到new 指令(对应语言层面的new关键词、对象克隆、对象序列化)。
    • 检查内容:
      • 定位常量池中该类的符号引用
      • 验证该符号引用对应的类是否已完成加载、解析、初始化
    • 处理逻辑:若类未加载,则先执行完整的类加载流程。
  2. 分配内存

    • 前提:类加载检查通过后,对象所需内存大小已确定(类加载完成后可计算)。

    • 核心问题与解决方案:

      问题类型 解决方案 关键细节
      内存划分方式 1. 指针碰撞:内存规整时,移动分界指针(默认方式);2. 空闲列表:内存交错时,维护可用内存块列表 指针碰撞依赖堆内存的 “规整性”(如 Serial、ParNew 收集器);空闲列表适用于 CMS 等收集器
      并发安全问题 1. CAS + 失败重试:保证内存分配操作的原子性;2. TLAB:线程预分配独立内存块 TLAB 参数:-XX:+UseTLAB(默认开启)、-XX:TLABSize(指定大小)
  3. 初始化零值

    • 操作:将分配的内存空间(除对象头外)全部初始化为零值(如 int=0、String=null)。
    • 优势:保证对象的实例字段在 Java 代码中不赋初始值也可直接使用,程序能访问到字段的默认零值。
    • 特殊情况:若使用 TLAB,零值初始化可提前至 TLAB 分配时执行。
  4. 设置对象头

    • HotSpot 虚拟机中,对象内存布局分为 3 部分:对象头(Header)实例数据(Instance Data)对齐填充(Padding)

    • 核心组成:

      • 对象头(Header):存储对象运行时关键信息,分为三部分:

        1. Mark Word:占 32 位(4 字节)或 64 位(8 字节),存储哈希码、GC 分代年龄、锁状态标志等,不同锁状态下结构不同:

          锁状态 32 位 Mark Word 结构(示例) 2bit 锁标志位 关键说明
          无锁态 25bit 哈希码 + 4bit 分代年龄 + 1bit 无偏向 01 初始状态,无锁保护
          偏向锁 23bit 线程 ID + 2bit Epoch + 4bit 分代年龄 + 1bit 有偏向 01 优化单线程下的锁竞争
          轻量级锁 30bit 指向栈中锁记录的指针 + 2bit 标志 00 多线程轻度竞争,无阻塞
          重量级锁 30bit 指向互斥量的指针 + 2bit 标志 10 重度竞争,依赖操作系统互斥量
          GC 标记 30bit 无意义 + 2bit 标志 11 GC 回收标记阶段
        2. 类型指针:指向类元数据的指针,虚拟机通过该指针确定对象所属类;默认开启指针压缩时占 4 字节,禁用时占 8 字节。

        3. 数组长度(仅数组对象才有):占 4 字节,存储数组的元素个数。

      • 实例数据(Instance Data):存储对象的实例字段(如int idString name),字段顺序受 JVM 优化影响。

      • 对齐填充(Padding):无实际意义,仅用于保证对象大小为8 字节的整数倍(HotSpot 虚拟机要求),根据前两部分大小动态调整。

  5. 执行方法

    • 作用:将对象从 “零值状态” 转换为 “程序员定义的初始化状态”。
    • 具体操作:
      • 执行类的构造方法(<init>())。
      • 为实例字段赋值(区别于 “初始化零值”,此处为程序员显式赋值)。

二、对象大小与指针压缩

1. 对象大小查看工具:jol-core
  • 依赖引入:

    <dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.9</version>
    </dependency>
    
  • 使用示例:通过ClassLayout.parseInstance(Object obj).toPrintable()打印对象内存布局,包含偏移量(OFFSET)、大小(SIZE)、字段描述等。

2. 指针压缩(64 位 JVM 核心优化)
  • 核心概念:通过编码 / 解码方式,让 64 位 JVM 使用 32 位指针访问内存,平衡内存消耗与寻址范围。

  • 关键参数

    参数 作用 默认值
    -XX:+UseCompressedOops 开启指针压缩(OOP=Ordinary Object Pointer) 开启(JDK7+)
    -XX:-UseCompressedOops 禁用指针压缩 不推荐
  • 压缩逻辑

    • 32 位指针默认支持 4G 内存(2³²),压缩后通过 “按 8 字节对齐” 优化,可支持≤32G 堆内存(4G×8)。
    • 堆内存 > 32G 时,压缩失效,强制使用 64 位指针(8 字节),导致内存消耗增加 1.5 倍左右,GC 压力增大(所以有时候不是机器的内存越大越好,视情况而定)。
  • 示例效果

    • 普通对象(如new Object()):开启压缩时对象大小为 16 字节(8 字节 Mark Word + 4 字节类型指针 + 4 字节对齐填充);禁用压缩时为 24 字节(8+8+8)。

三、对象内存分配机制

JVM 根据对象特性(生命周期、大小),将对象分配到不同内存区域,优化 GC 效率:

1. 栈上分配(减少堆 GC 压力)
  • 核心目的:对 “不会逃逸” 的临时对象,在栈帧中分配内存,随栈帧出栈自动销毁,无需 GC 回收。
  • 依赖机制
    • 逃逸分析(Escape Analysis):分析对象动态作用域,判断是否被外部方法引用(是则“逃逸”,否则 “不逃逸”)。
      • 开启参数:-XX:+DoEscapeAnalysis(JDK7 + 默认开启);禁用:-XX:-DoEscapeAnalysis。
    • 标量替换(Scalar Replacement):将 “不逃逸” 的聚合量(对象)分解为标量(如intreference),在栈帧 / 寄存器分配空间。
      • 开启参数:-XX:+EliminateAllocations(JDK7 + 默认开启);禁用:-XX:-EliminateAllocations。
  • 示例验证:调用 1 亿次alloc()创建User对象,开启逃逸分析与标量替换时(-Xmx15m)无 GC,禁用则触发大量 GC。

事实上,真实的业务中逃逸的对象占比很少,调优时可以不用考虑

2. Eden 区分配(新生代默认分配)
  • 分配规则:大多数对象优先在新生代Eden 区分配,Eden 与 Survivor 区(From/To)默认比例为8:1:1
  • GC 触发:当 Eden 区无足够空间时,触发Minor GC(仅回收新生代垃圾,频繁、速度快)。
  • 关键参数
    • -XX:-UseAdaptiveSizePolicy:禁用自适应大小策略,固定 Eden:Survivor=8:1:1;默认开启时 JVM 会动态调整比例。
  • 示例效果:分配 60MB 对象时 Eden 区占满(100% 使用),再分配 8MB 对象触发 Minor GC,若 Eden 存活对象无法放入 Survivor,则提前移入老年代。
3. 老年代分配(长期存活 / 大对象)
  • 1. 大对象直接进老年代
    • 定义:需大量连续内存的对象(如大数组、长字符串)。
    • 触发参数:-XX:PretenureSizeThreshold(单位:字节),对象大小超过该值则直接进入老年代。
    • 限制:仅对SerialParNew收集器有效。
    • 目的:避免大对象在新生代频繁复制(Minor GC 时复制成本高)。
  • 2. 长期存活对象进老年代
    • 年龄计数器:对象在 Eden 出生,经 1 次 Minor GC 存活并进入 Survivor 后,年龄设为 1;每熬过 1 次 Minor GC,年龄 + 1。
    • 晋升阈值:年龄达到默认 15 岁(可通过 - XX:MaxTenuringThreshold 调整)时,晋升到老年代。
  • 3. 动态年龄判断
    • 触发条件:Survivor 区中某批对象的总大小超过该区域内存的50%(比例由 - XX:TargetSurvivorRatio 指定)。
    • 处理逻辑:年龄≥该批对象年龄最大值的所有对象,直接进入老年代(避免 Survivor 区溢出)。
4. 老年代空间分配担保机制
  • 核心逻辑:Minor GC 前,JVM 判断老年代是否有足够空间容纳新生代存活对象,避免 Minor GC 后对象无法存放:
    1. 计算老年代剩余可用空间。
    2. 若可用空间 < 新生代所有对象大小之和:
      • 检查 - XX:-HandlePromotionFailure(JDK1.8 默认开启):
        • 若开启:判断可用空间是否 > 历史 Minor GC 后进入老年代的对象平均大小→是则执行 Minor GC;否则触发 Full GC。
        • 若禁用:直接触发 Full GC。
    3. Minor GC 后,若存活对象大小 > 老年代可用空间→触发 Full GC;Full GC 后仍不足→抛出 OOM。

四、对象内存回收机制

1. 对象死亡判断(核心:可达性分析算法)
  • 1. 引用计数法(淘汰方案)
    • 逻辑:为对象添加引用计数器,引用新增 + 1,引用失效 - 1,计数器 = 0 则标记为垃圾。
    • 缺陷:无法解决对象循环引用问题(如objA.instance=objBobjB.instance=objA,计数器均不为 0,无法回收)。
  • 2. 可达性分析算法(主流方案)
    • 逻辑:以GC Roots为起点,向下搜索引用链,未被搜索到的对象标记为 “可回收垃圾”。
    • GC Roots 包含:
      • 线程栈中的本地变量(如方法参数、局部变量)。
      • 方法区中的静态变量。
      • 本地方法栈中的变量(Native 方法引用的对象)。
2. 四种引用类型(影响对象回收时机)
引用类型 核心特点 使用场景 示例代码
强引用 普通变量引用,GC 永不回收(OOM 也不回收) 常规对象引用(如User user = new User() public static User user = new User();
软引用 GC 后内存不足时回收,用于内存敏感缓存 浏览器后退按钮缓存、临时数据缓存 SoftReference<User> user = new SoftReference<>(new User());
弱引用 GC 触发时直接回收,生命周期极短 临时关联数据(如 WeakHashMap 键) WeakReference<User> user = new WeakReference<>(new User());
虚引用 最弱引用,仅用于跟踪 GC,无法通过引用获取对象 堆外内存回收通知(如 DirectByteBuffer) PhantomReference<User> user = new PhantomReference<>(new User(), referenceQueue);
3. finalize () 方法(对象最后的 “自救机会”)
  • 两次标记流程
    1. 第一次标记:可达性分析中无引用链的对象,筛选是否需执行 finalize ()(未覆盖则直接回收;已覆盖则放入 “F-Queue” 队列)。
    2. 第二次标记:虚拟机执行 F-Queue 中对象的 finalize (),若对象在方法中重新建立引用链(如赋值给类变量),则移除 “可回收” 集合;否则标记为 “最终可回收”。
  • 限制:一个对象的 finalize () 仅执行 1 次,第二次被标记时无法再自救。
4. 无用类判断(方法区回收条件)

类需同时满足以下 3 个条件,才被判定为 “无用的类”,可在方法区中回收:

  1. 该类所有实例已被回收(Java 堆中无该类的任何对象)。
  2. 加载该类的ClassLoader 已被回收
  3. 该类的java.lang.Class对象无任何引用(无法通过反射访问该类的方法)。

关键问题

问题 1:JVM 在为对象分配内存时,面临 “并发下多个线程同时使用同一指针分配内存” 的安全问题,其解决方案有哪些?各方案的核心逻辑与参数是什么?

答案:JVM 通过两种核心方案解决内存分配的并发安全问题:

  1. CAS + 失败重试机制
    • 核心逻辑:利用 CAS(Compare and Swap)操作的原子性,每次分配内存时对比 “预期指针位置” 与 “实际指针位置”,若一致则修改指针(分配成功);若不一致则重试,直到成功。
    • 适用场景:未使用 TLAB 或 TLAB 空间不足时。
  2. TLAB(Thread Local Allocation Buffer,本地线程分配缓冲)
    • 核心逻辑:为每个线程在 Java 堆中预先分配一小块独立内存,线程分配对象时优先使用自身 TLAB,避免与其他线程竞争全局指针,仅当 TLAB 不足时才使用 CAS 补充 TLAB 或直接分配全局内存。
    • 关键参数:
      • -XX:+UseTLAB:开启 TLAB(JVM 默认开启);-XX:-UseTLAB:禁用 TLAB。
      • -XX:TLABSize:指定 TLAB 的初始大小(默认由 JVM 动态计算)。

问题 2:什么是 “对象的栈上分配”?其能减少 GC 压力的核心原因是什么?依赖哪些 JVM 机制才能实现?

答案

  1. 定义:栈上分配是 JVM 对 “不会逃逸的临时对象” 的优化手段 —— 将对象内存分配在线程栈帧中,而非 Java 堆,对象随栈帧出栈(方法执行结束)自动销毁,无需 GC 回收。
  2. 减少 GC 压力的原因:堆中对象需依赖 GC 回收,而栈上对象随栈帧销毁自动释放,避免了 “临时对象大量进入堆导致 GC 频繁触发” 的问题。
  3. 依赖的核心机制
    • 逃逸分析(Escape Analysis):JVM 通过分析对象动态作用域,判断对象是否 “逃逸”(如被外部方法引用、作为返回值),仅 “不逃逸” 的对象才具备栈上分配资格。
      • 控制参数:-XX:+DoEscapeAnalysis(JDK7 + 默认开启)、-XX:-DoEscapeAnalysis(禁用,对象强制分配到堆)。
    • 标量替换(Scalar Replacement):JVM 将 “不逃逸的聚合量(对象)” 分解为多个标量(如int idString name),在栈帧或寄存器中分配标量空间,避免因 “对象需连续内存” 导致的分配限制。
      • 控制参数:-XX:+EliminateAllocations(JDK7 + 默认开启)、-XX:-EliminateAllocations(禁用,无法分解对象,栈上分配失效)。

问题 3:方法区的回收主要针对 “无用的类”,需满足哪些严格条件才能判定一个类为 “无用的类”?为什么这些条件必须同时满足?

答案

  1. 判定 “无用的类” 的 3 个必要条件(需同时满足):
    • 条件 1:该类的所有实例已被回收(Java 堆中不存在任何该类的对象)。
    • 条件 2:加载该类的ClassLoader 已被回收(确保无其他类依赖该 ClassLoader 加载的类)。
    • 条件 3:该类对应的java.lang.Class对象无任何引用(无法通过反射访问该类的方法、字段,确保类无 “潜在使用场景”)。
  2. 条件需同时满足的原因
    • 若仅满足 “无实例”(条件 1),但 ClassLoader 未回收(条件 2),ClassLoader 仍可能重新加载该类生成新实例,类不可回收。
    • 若仅满足前两个条件,但Class对象有引用(条件 3),程序可通过反射(如Class.forName())重新使用该类,类仍处于 “可用状态”,不可回收。
    • 三者缺一不可,确保类完全无 “使用可能”,才允许方法区回收其元数据,避免误回收导致程序异常。
http://www.hskmm.com/?act=detail&tid=17231

相关文章:

  • 目录
  • 交互:在终端中输入用户信息
  • 电脑迁移技巧:适用于 Windows 10/11 的免费磁盘克隆优秀的工具
  • Java学习日记9.18
  • 一种CDN动态加速首次访问加速方法
  • 9.25
  • 字典
  • CF1716题解
  • 使用vosk模型进行语音识别
  • AI Agent如何重塑人力资源管理?易路iBuilder平台实战案例深度解析
  • docker-compose + macvlan + Elasticsearch - 9.1.4 + Kibana - 9.1.4
  • WinForm 计时器 Timer 学习笔记
  • RocketMQ入门:基本概念、安装、本地部署与集群部署 - 详解
  • 【LeetCode】122. 买卖股票的最佳时机 II
  • VSCode 使用技巧笔记
  • 【LeetCode】55. 跳跃游戏
  • Ansible + Docker 部署 Apache Kafka 3.9 集群
  • 【LeetCode】45. 跳跃游戏 II
  • 深入了解一波JVM内存模型
  • 什么是UDFScript用户自定义脚本
  • 【LeetCode】121. 买卖股票的最佳时机
  • CCPC2024-Zhengzhou G Same Sum(线段树)
  • Openwrt-DDNS 配置详解
  • 实用指南:Metal - 2. 3D 模型深度解析
  • 【2025.9.16】关于举办PostgreSQL数据库管理人才研修与评测班的通知
  • Java锁相关问题
  • CDN中使用边缘函数实现自定义编程
  • 第一次课程中的所有动手动脑的问题以及课后实验性的问题
  • 敏捷开发的几个阶段
  • 隐藏在众目睽睽之下:从PEB中解除恶意DLL的链接