欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 房产 > 家装 > 【JVMGC垃圾回收场景总结】

【JVMGC垃圾回收场景总结】

2025/6/20 7:34:30 来源:https://blog.csdn.net/weixin_47068446/article/details/148635507  浏览:    关键词:【JVMGC垃圾回收场景总结】

文章目录

  • CMS在并发标记阶段,已经被标记的对象,又被新生代跨带引用,这时JVM会怎么处理?
  • 为什么 Minor GC 会发生 STW?
  • 有哪些对象是在栈上分配的?
  • 对象在 JVM 中的内存结构
  • 为什么需要对齐填充?
  • JVM 对象分配空间机制
  • JVM多线程并发分配对象如何解决堆抢占?
  • JVM容量分配实战案例分析
  • JVM吞吐量和响应时间?

CMS在并发标记阶段,已经被标记的对象,又被新生代跨带引用,这时JVM会怎么处理?

CMS 垃圾回收器 中使用 “记忆集(Remembered Set)”+“写屏障(Write Barrier)”机制 来解决并发标记期间 跨代引用变更 问题。

为什么需要“记忆集” + “写屏障”?
在 CMS 的 并发标记阶段:
GC 与应用线程并发执行;
这期间应用线程可能让年轻代对象指向老年代对象;
但 CMS 默认只标记老年代;
年轻代未被扫描,容易 遗漏跨代引用;
所以必须 追踪引用变更的位置 —— 这就是“写屏障 + 记忆集”组合机制的作用。

写屏障:屏障是一种插入到对象引用写操作前后的特殊代码逻辑,用来记录引用的变化,在 CMS 里主要用于:

  1. 记录跨代引用的变化(新生代指向老年代的引用);
  2. 追踪老年代中对象引用的新增或变化,保证并发标记不遗漏
    什么是记忆集?
    记忆集是一种 记录哪些区域可能包含跨代引用 的辅助数据结构。
    在 CMS 中使用的是:Card Table + Dirty Card Tracking

写屏障的实现:

Card Table 工作机制:
老年代内存被划分为很多小块,每块称为一个 Card(比如 512 Bytes 一块);
JVM 为每个 Card 维护一个字节标记(位于 Card Table 中);
这个字节初始值为 clean(0);
一旦写屏障检测到某个 Card 中的对象引用发生变化,就将对应字节标记为 dirty(1)。

记忆集:
记忆集的本质是一个辅助数据结构,用来记录老年代对象中引用了新生代对象的“引用源地址”。
分代垃圾回收中,Minor GC 只扫描新生代,但可能有老年代对象引用了新生代;
为了避免扫描整个老年代,JVM用记忆集来记录那些引用过新生代的老年代对象;
在 Minor GC 时,只需要扫描记忆集中的那部分老年代对象

为什么 Minor GC 会发生 STW?

  1. 避免对象引用混乱
    Eden 和 Survivor 区中的对象需要移动到 To 区或老年代;
    在对象“复制”过程中,如果程序继续运行,会有引用被修改、丢失或读取脏数据;
    所以,必须暂停所有线程,保证引用关系不变、对象移动安全。
  2. 根可达性分析需要一致的对象图
    GC 需要做 可达性分析(Root Tracing),从 GC Roots 开始向下遍历;
    如果线程在动,分析得到的对象图会是错误的。

有哪些对象是在栈上分配的?

在 JVM 中,“对象在栈上分配” 并不是默认行为,而是一种通过逃逸分析(Escape Analysis)+ 栈上分配优化才有可能实现的高级性能优化机制。默认情况下,Java 中的所有对象都是在堆上分配的,只有局部变量的引用在栈帧中分配。
在栈上分配的条件:

  1. 对象不会逃逸出当前方法,也就是对象只在方法内被使用,不会被返回、不会赋给其他线程可访问的变量。
  2. JVM 启用了逃逸分析优化

栈上分配的优点:
3. 分配速度极快 栈是连续内存,分配只需移动指针(非常快);
4. 自动回收 随方法调用结束,局部变量出栈即自动回收;
5. 不会触发 GC 不在堆上,就不会进入 GC 管理范围;
6. 减少内存碎片 不参与堆整理,降低 Full GC 频率;
7. 标量替换优化可能性 JVM 可能将对象拆分为基本类型变量,进一步提升性能;

对象在 JVM 中的内存结构

  1. 对象头(Header)
    包含两部分信息:
    (1)Mark Word(标记字段) - 占 8 字节(32位JVM)或 12 字节(64位JVM 非压缩指针)
    存储内容因对象状态而异,如:
    HashCode(如果调用过 hashCode())
    GC 分代年龄
    锁信息(轻量级锁、重量级锁、偏向锁)
    标记位(是否为偏向锁/是否为垃圾对象等)
    (2)Class Pointer(类型指针) - 指向类的元数据
    指向方法区中该对象所属类的 class 元信息,用于支持虚方法调用等。
  2. 实例数据(Instance Data)
    真正存放类的 字段(成员变量)值 的地方,包括:
    父类继承下来的字段
    自身定义的字段
    按照 字段声明顺序、类型大小对齐 安排内存布局
  3. 对齐填充(Padding)
    为了满足 8 字节对齐(HotSpot 默认对象起始地址对齐规则),可能会在对象末尾填充一些字节
    不参与逻辑数据存储,只是内存对齐需要

为什么需要对齐填充?

  1. CPU 访问效率
    CPU 访问内存时,更快的访问方式是**按固定字节边界对齐(alignment)**访问,比如 4 字节对齐、8 字节对齐。
    如果数据没有对齐,CPU 需要分多次访问内存才能读取完整数据,导致性能下降。
    对齐保证数据起始地址是特定边界的整数倍,能让 CPU 一次性高效读取。
  2. 硬件平台的限制
    一些处理器架构要求特定类型数据必须对齐访问,不对齐访问会引发硬件异常(bus error),或者不得不做额外处理,降低性能。
    例如,64位系统一般要求 8 字节对齐,32位系统一般要求 4 字节对齐。
  3. 简化内存地址计算
    对齐后,硬件和编译器可以更简单更快地计算内存地址和偏移,方便高效的指令执行。

JVM 对象分配空间机制

  1. 堆内存分配
    Java 对象主要在 **堆(Heap)**上分配。
    堆又分为 新生代(Young Gen)和 老年代(Old Gen)。
    新生代通常使用 Eden 区 + 两个 Survivor 区。
  2. 对象分配流程
    新生代 Eden 区是对象分配的主要场所。
    JVM 默认采用 指针碰撞(Pointer Bump) 或 空闲列表(Free List) 分配策略:
    指针碰撞:Eden 是连续空间,有个指针指向下一分配位置,分配对象时指针往后移,速度快。
    空闲列表:如果 Eden 有碎片,可能采用空闲列表分配。
    大对象(如大数组、字符串)可能直接进入老年代,避免在新生代频繁复制。
  3. 分配失败与垃圾回收
    Eden 空间不足时触发 Minor GC,回收无用对象。
    Minor GC 后仍空间不足时,可能触发 Full GC 或对象晋升到老年代。

JVM多线程并发分配对象如何解决堆抢占?

  1. 多线程分配对象时,如果都操作同一内存区域,会产生同步开销,降低性能。
  2. JVM 采用 线程本地分配缓存(TLAB,Thread Local Allocation Buffer) 来缓解:
    每个线程分配一个小的 Eden 子区域(TLAB)。
    线程先从自己 TLAB 分配,避免和其他线程竞争堆主区域锁。
    只有当 TLAB 空了,才去堆中申请新的 TLAB。
  3. TLAB 分配流程示意
    线程启动时,JVM 为它分配一个初始的 TLAB。
    分配新对象时,线程检查 TLAB 剩余空间是否足够。
    足够,直接在 TLAB 内分配,调整指针。
    不足,从堆中申请新的 TLAB,再分配。
    对象生命周期结束后,垃圾回收释放对象,回收空间。

JVM容量分配实战案例分析

每天100w次登陆请求, 8G 内存该如何设置JVM参数,大概可以分为以下几个步骤。

  1. 任何新的业务在上线之前我们都需要预估其占用的内存大小,而我们分配空间的大小主要来根据以下步骤来判断?
  2. 计算业务系统每秒钟创建的对象会占用多大的内存空间,然后计算集群下的每个系统每秒的内存占用空间(对象创建速度)
  3. 设置一个机器配置,估算新生代的空间,比较不同新生代大小之下,多久触发一次MinorGC。
    为了避免频繁GC,就可以重新估算需要多少机器配置,部署多少台机器,给JVM多大内存空间,新生代多大空间。
  4. 根据这套配置,基本可以推算出整个系统的运行模型,每秒创建多少对象,1s以后成为垃圾,系统运行多久新生代会触发一次GC,频率多高。

具体的案例分析:
新增计费业务,预计每天1000万次请求,高峰时期,每秒处理2000笔的并发,一共5台机器内存大小8G,怎么算每台机器分配多大内存能撑住并发?
1.每秒2000笔,分到每台机器上的请求为400笔,加入每个请求所产生的对象大小为300字节,每个请求大概需要10个对象处理,我们暂估3KB,如果算上RPC和DB、写库、写缓存一顿操作下来6KB的数据,每秒大概产生1~2M的数据,如果高峰期,我们分配的内存为6G,分配给新生代的大小,大概是2G,我们算出大概1000秒才会进行一次MinorGC。

JVM吞吐量和响应时间?

吞吐量是指程序用于处理业务的时间与总运行时间的比值。

吞吐量 = (总运行时间 - GC 时间) / 总运行时间

吞吐量越高,说明更多时间用于业务处理,GC 开销更小。
高吞吐通常意味着:
较少 GC 次数
较长 GC 停顿时间
适合批处理、后台计算任务。

响应时间是指系统对单个请求的响应速度,包括平均响应时间和最大响应时间(99%、99.9% 等分位)。
对交互式系统、低延迟系统(如支付系统、API 网关)非常关键。
低响应时间通常意味着:
更频繁的 GC(避免长时间停顿)
GC 停顿更短
适合前台服务、实时交互系统

堆内存大小 和GC 频率和停顿时间之间的关系:
堆大 ⇒ GC 少发生 ⇒ 每次 GC 回收更多对象 ⇒ 吞吐量大
但:每次 GC 停顿时间长 ⇒ 阻塞线程多 ⇒ 响应延迟上升
堆小 ⇒ GC 更频繁发生 ⇒ 每次 GC 停顿短
但:每次处理对象少 ⇒ 更频繁打断业务执行 ⇒ 吞吐下降 ⇒ CPU 被 GC 占用比例升高

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词