GC问题
- 什么是GC? 为什么会有GC?
GC 是一种自动管理内存的机制,用于回收程序运行中不再使用的对象所占的内存,以避免内存泄漏和内存溢出问题。避免内存泄漏和内存溢出、提高开发效率。
- 什么时候会导致GC?
- jvm内存空间不足(堆、新生代、老年代、元空间);
- 主动触发(System.gc(), jvm不一定会立马执行);
- GC 的主要算法?
| 名称 | 特点 | 优点 | 缺点 |
|---|
| 标记-清除 | 从 GC Root 开始,标记所有可达的对象。然后回收不可达对象。 | 实现简单 | 内存碎片 |
| 标记-复制 | 将存活的对象从一个区域复制到另一个区域,清空原区域 | 没有内存碎片 | 需要额外的内存空间,内存使用率降低 |
| 标记-整理 | 标记出所有需要回收的对象后,将存活对象向一端移动,清除掉边界外的内存 | 减少了碎片,适合老年代 | 对象移动会增加额外的开销,影响性能 |
| 分代收集 | 新生代 和 老年代,分别采用不同的 GC 策略。新生代使用复制算法,快速回收生命周期短的对象。老年代使用 标记-清除 或 标记-压缩算法,因为对象存活率较高 | | |
- 垃圾收集器的种类
| 垃圾收集器 | 特点 | 优点 | 缺点 | 适用场景 |
|---|
| Serial GC | 新生代使用标记-复制算法,老年代使用标记-压缩算法。 | 简单、单线程 | 无法利用多核, STW时间长 | 小型应用,单核机器 |
| Parallel GC | 新生代使用标记-复制算法,老年代使用标记-压缩算法。 | 高吞吐,多线程并行 | STW时间长 | 高吞吐量场景,批处理 |
| CMS GC | 并发标记阶段与应用线程同时运行,降低 STW时间。 | STW 短,适合低延迟场景 | 内存碎片化问题、Full GC时间过长 | 响应时间敏感的服务端应用 |
| G1 GC | 内存分区为多个小块(Region),新生代和老年代不再是固定大小。使用标记-整理算法,避免内存碎片。 | 超低延迟,支持超大堆 | 内存开销大 | 延迟敏感的大型内存应用 |
- 什么是三色标记法?
- 颜色分类为:白色、灰色、黑色。分别代表未访问过的对象,代表已经被访问但其引用的子对象尚未被标记的对象,代表已经被访问且其引用的所有子对象也都已被标记。
- 标记过程:
- 初始标记: 从 GC Root 开始,将直接可达的对象标记为灰色。(通常会暂停所有用户线程,STW);
- 灰色对象处理: 将引用子对象标记为灰色;将自身标记为黑色;重复两个步骤直到没有灰色对象;
- 清理阶段:未被标记(仍为白色)的对象即为不可达对象,进行回收。
- 优点:分阶段清理,将标记和清理分成两个阶段,减少停顿时间;
- 缺点:漏标问题,在并发标记过程中,用户线程可能会修改对象的引用,导致未正确标记某些对象。 解决方案:写屏障(Write Barrier),监控对象引用的修改。
- 什么时候会触发新生代GC,老年代GC和Full GC?
- 新生代GC: 当新生代的Eden区满的时候;
- 老年代GC:当老年代空间不足时,比如新生代对象晋升到老年代时,空间分配不足;
- Full GC: 老年代GC失败后,尝试Full GC进一步释放内存;元空间不足;显示调用System.gc(); 垃圾收集器的特殊情况(如 CMS GC 发生内存碎片时,会触发 Full GC 进行整理)。
- System.gc() Runtime.gc()会做什么事情? 能保证 GC 执行吗?
- System.gc() 是对它的简单封装,直接调用即可。不能保证GC一定会执行,只是用于向JVM提示一个进行GC的建议,垃圾回收的最终执行由 JVM 的垃圾回收器决定。有些jvm支持配置参数运行强制执行GC。
- 垃圾回收器可以马上回收内存吗?
- 答案:垃圾回收器 不能保证立即回收内存。垃圾回收的行为和时机由 JVM 内部的垃圾回收策略决定,用户无法直接控制其具体执行时间。即使通过显式调用方法(例如 System.gc() 或 Runtime.gc()),也仅仅是向 JVM 提供一个提示,JVM 可以选择忽略这个请求。
- CMS 收集器 与 G1 收集器的特点与区别?
- CMS收集器的目标是减少停顿时间,而G1收集器目标是提供可预测的停顿时间。
- CMS 在标记阶段与应用线程并发运行。它通过 标记-清除算法 来标记存活对象并清除不再使用的对象,减少停顿时间,也因为如此会产生内存碎片。G1将内存对分多个区域(region),而不是将堆分为新生代、老年代,优先回收垃圾最多的区域,并且支持增量回收,对大内存应用(一般4G以上)比较合适。
- CMS垃圾收集的过程?
- 主要分为5个阶段,每个阶段的目的是分步地进行标记、清理和内存回收。大部分操作是并发执行的,但在某些关键阶段,应用程序线程会停顿。
- 初始标记阶段:标记 GC Roots 可达的对象,即标记那些从根对象(如栈中的局部变量、静态引用、活动线程等)可以直接访问的对象。这个阶段会 Stop-the-World,即暂停应用程序的执行。初始标记阶段的时间通常非常短,因为它只标记直接可达的对象,而不进行深度遍历。
- 并发标记阶段:这个阶段将从初始标记开始,继续扫描整个堆,标记所有存活的对象。应用线程和垃圾回收线程并发执行,不会暂停应用程序。由于是并发标记,可能会出现“在并发标记过程中某些对象被修改”(例如,引用改变)的情况,这通常通过 重新标记(Remark) 阶段解决。
- 重新标记阶段:这个阶段会在并发标记之后,重新标记堆中已经变更的对象,以确保标记的正确性。这个阶段需要扫描那些在并发标记过程中发生变化的对象,确保所有可达对象都被正确标记。这个阶段应用线程会暂停,直到重新标记完成。
- 并发清除阶段:清除那些被标记为不可达的对象,释放内存空间。
- 并发重置阶段:在清除阶段完成之后,CMS 会将当前回收周期的标记信息清理掉,为下一次标记过程做好准备。
- 对象晋升到老年代的条件是什么?
- 对象年龄计数(新生代对象每经历一次young gc就增加1),默认为15,最大也是15,存储于对象头的Mark word中,4位bit最大智能支持15;
- 较大的对象会直接分配到老年代,不经过年轻代
- 动态晋升条件:JVM还会根据新生代空间的使用情况和对象的年龄分布来动态地选择对象进入老年代。具体来说,当Survivor区中年龄从1到n的对象大小之和超过Survivor区的50%时(这个比例也可以通过参数-XX:TargetSurvivorRatio进行调整),新生代中年龄大于等于n的对象将进入老年代。
- 老年代空间分配担保规则:如果年轻代里大量对象存活,然后Survivor区放不下了,必须转移到老年代去。如果老年代也放不下就会触发Full gc;Full gc后还放不下就oom.
- 垃圾回收的最佳做法
- 选择合适的垃圾收集器;
- 配置合适的jvm参数,如配置-Xms和-Xmx相同,-Xss=256k等;
- 优化年轻代与老年代的大小,年轻代中eden和Surver的大小;
- 配置GC日志的打印,包括详情信息,发生时间,触发原因,耗时等;
- 配置堆内存溢出时导出堆栈日志;
- 增加jmx监控,配合prometheus+grafana,分析是否存在内存泄漏;
- 吞吐量优先和响应优先的垃圾收集器选择?
- 吞吐量指的应用程序的执行时间和JVM的GC时间的比值,适合后台处理、批量计算、定时任务等不需要追求和用户不直接进行交互体验的应用。响应优先指的是追求应用程序更快的响应,减少GC的停顿时间,所以适合高并发、低延迟的应用,比如Web服务、电商、高频交易。
- 吞吐量优先的一般是: Paralle GC; 响应优先:CMS, G1收集器。
- 举个实际的场景,选择一个 GC 策略
- 需求分析:假设一个大型的股票交易系统,在交易时间内,每天会处理大量的股票买卖的订单交易, 系统除了要满足吞吐量(每秒请求量)外,还需要快速响应,需要尽可能减少停顿时间。需求:低延迟(响应快速)、高吞吐(能处理大量并发的请求,确保交易的流畅)
- 面临着 高吞吐量 和 低延迟 的双重要求,选择G1 收集器。主要原因如下:
- G1 GC 提供了对最大 GC 停顿时间的精细控制
- 交易系统通常需要较大的内存来同时支持大量的请求,G1 GC 适合大堆内存的应用,它能够将堆划分成多个区域(Region),并且通过对不同区域的回收来控制回收过程,逐步清理内存,减少全堆回收的需要,从而避免长时间的停顿。
- 自动优化: G1 GC 在年轻代和老年代的内存回收方面非常灵活。它会根据应用的内存使用情况自动调整回收策略,减少 Full GC 的发生频率。
- 并行与并发收集: G1 GC 能够在后台并行执行垃圾回收工作,这意味着它可以在多个 CPU 核心上并发执行垃圾回收任务,利用多核服务器的优势。这样可以有效地减少 GC 的时间,并能提高吞吐量,适应大量的并发请求。