Java高手的30k之路|面试宝典|精通JVM

垃圾回收

堆内存结构

JVM 堆内存的结构是垃圾收集和内存管理的核心部分,主要分为以下几个区域:新生代(Young Generation)、老年代(Old Generation)和元空间(Metaspace)。

  1. 新生代(Young Generation)

    • Eden 空间:所有新创建的对象首先分配在 Eden 空间。当 Eden 空间满时,进行一次小的垃圾收集(Minor GC),将仍然存活的对象移到 Survivor 空间。
    • Survivor 空间:分为两个部分,通常称为 S0(Survivor 0)和 S1(Survivor 1)。每次 Minor GC 之后,从 Eden 空间和一个 Survivor 空间复制存活对象到另一个 Survivor 空间,并清空源空间。两个 Survivor 空间轮流使用。
  2. 老年代(Old Generation)

    • 老年代:存放生命周期较长的对象。当对象在新生代经历了多次 Minor GC 后仍然存活,或者 Survivor 空间无法容纳时,会被移到老年代。当老年代满时,进行一次大的垃圾收集(Major GC 或 Full GC)。
  3. 元空间(Metaspace)

    • 元空间:存放类的元数据(类定义、方法等)。从 JDK 8 开始,元空间取代了永久代(PermGen)。元空间在本地内存中分配,而不是堆内存中。

堆内存布局图示

+-------------------------------------------+
|                  堆内存                    |
+------------------------+------------------+
|       新生代            |     老年代        |
|  +------+------------+ |                  |
|  | Eden | Survivor S0| |                  |
|  |      |   Survivor S1|                  |
+------------------------+------------------+
+-------------------------------------------+
|                元空间                      |
+-------------------------------------------+

详细说明

  1. 新生代(Young Generation)

    • Eden 空间:这是对象最初分配的地方。Eden 空间相对较小,垃圾收集频繁但速度较快。
    • Survivor 空间:有两个 Survivor 区域,每次 Minor GC 时,一个区域用于保存从 Eden 和另一个 Survivor 区域复制过来的存活对象。
  2. 老年代(Old Generation)

    • 老年代空间较大,存放生命周期较长的对象。由于对象存活时间较长,因此垃圾收集不如新生代频繁,但每次 GC 的耗时较长。
  3. 元空间(Metaspace)

    • 存放类的元数据,包括类定义、方法信息等。与永久代不同,元空间使用的是本地内存,容量由系统内存大小决定,通过 -XX:MaxMetaspaceSize 参数控制。

垃圾收集过程

  1. Minor GC(新生代垃圾收集)

    • 触发时机:Eden 空间满时触发。
    • 过程:扫描 Eden 空间和一个 Survivor 空间,将存活对象复制到另一个 Survivor 空间。清空 Eden 空间和源 Survivor 空间。
  2. Major GC(老年代垃圾收集)

    • 触发时机:老年代满时触发。
    • 过程:扫描整个老年代,清理无用对象,整理碎片。由于对象存活时间较长,GC 过程耗时较长。
  3. Full GC(全堆垃圾收集)

    • 触发时机:堆内存不足、元空间满等情况。
    • 过程:扫描新生代和老年代,以及元空间,清理所有无用对象,整理内存碎片。通常耗时最长。

最佳实践

  1. 堆内存大小调整:根据应用需求调整堆大小,避免频繁 GC 或内存不足。

    • -Xms 设置堆内存初始大小
    • -Xmx 设置堆内存最大大小
  2. 新生代和老年代比例调整:根据应用对象生命周期特点调整新生代和老年代的比例。

    • -XX:NewRatio 设置新生代与老年代的比例
    • -XX:SurvivorRatio 设置 Eden 与 Survivor 的比例
  3. 元空间大小调整:控制元空间大小,避免内存泄漏和 Full GC 过于频繁。

    • -XX:MetaspaceSize 设置元空间初始大小
    • -XX:MaxMetaspaceSize 设置元空间最大大小
  4. 选择合适的垃圾收集器:根据应用特点选择合适的垃圾收集器,如 G1、CMS、Parallel GC 等。

垃圾回收算法

不同的垃圾收集算法有不同的策略和适用场景。以下是主要的垃圾收集算法:

标记-清除算法(Mark-Sweep)

过程:

  1. 标记阶段:遍历所有可达对象并进行标记。
  2. 清除阶段:遍历堆,清除未标记的对象。

优点:

  • 简单易实现。
  • 不需要额外的内存来存储对象的副本。

缺点:

  • 内存碎片化严重。未标记的对象随机分布在内存的不同位置,清除之后可再用的区域也随机分布在不同位置,形成一堆碎片。
  • 标记和清除过程都需要遍历整个堆,效率较低。

标记-压缩算法(Mark-Compact)

过程:

  1. 标记阶段:遍历所有可达对象并进行标记。
  2. 压缩阶段:移动所有存活对象,整理内存,使得所有存活对象集中在堆的一端,清除端以外的内存。

优点:

  • 消除了内存碎片问题。
  • 改善了大对象的分配问题。

缺点:

  • 移动对象需要额外的开销。

复制算法(Copying)

过程:

  1. 将堆分为两个相等的半区,每次只使用其中一个半区。
  2. 当活动半区满时,将存活对象复制到另一个半区,然后清空当前半区。

优点:

  • 分配内存时仅需移动指针,速度快。
  • 没有内存碎片。

缺点:

  • 可用内存减半。
  • 在对象存活率高的情况下效率低。

分代收集算法(Generational Collecting)

分代收集算法将堆内存分为几代,根据对象的存活时间来进行管理。主要分为新生代和老年代。

  1. 新生代(Young Generation):存放新创建的对象,包括 Eden 区和两个 Survivor 区。使用复制算法。
  2. 老年代(Old Generation):存放生命周期较长的对象。使用标记-清除或标记-压缩算法。

优点:

  • 根据对象的生命周期特点进行优化,提高 GC 效率。
  • 新生代 GC 频繁但速度快,老年代 GC 不频繁但能处理大量长生命周期对象。

缺点:

  • 复杂度较高,需要调优参数。

常见垃圾收集器

  1. Serial GC

    • 单线程垃圾收集器,适用于小型应用和单处理器机器。
    • 使用复制算法进行新生代 GC,标记-压缩算法进行老年代 GC。
  2. Parallel GC

    • 多线程垃圾收集器,适用于多处理器机器和高吞吐量应用。
    • 新生代和老年代都使用多线程并行 GC。
  3. CMS(Concurrent Mark-Sweep)

    • 并发标记-清除垃圾收集器,适用于低延迟应用。
    • 老年代 GC 时,大部分工作与应用线程并发进行。
  4. G1(Garbage-First)

    • 面向服务端应用的垃圾收集器,适用于大堆内存和低延迟需求。
    • 将堆划分为多个区域,使用标记-压缩算法,优先回收垃圾最多的区域。

总结

算法优点缺点适用场景
标记-清除简单易实现,不需额外内存内存碎片化严重,效率低通用,但不适合高性能要求的场景
标记-压缩无内存碎片,改善大对象分配移动对象开销大对内存碎片敏感的场景
复制分配速度快,无内存碎片可用内存减半,效率低对象存活率低的场景
分代收集根据生命周期优化,GC 效率高复杂度高,需要调优大多数应用,特别是大型和长期运行的应用

垃圾收集器

Java 中的垃圾回收器随着版本的升级不断改进,以提高性能和适应不同的应用需求。以下是主要垃圾回收器的详细介绍,以及它们在不同 Java 版本中的变化和演进。

垃圾回收器及其在 Java 版本中的演进

  1. Serial GC

    • Java 1.3 及更早版本:最早的垃圾收集器,适用于单处理器机器和小型应用。新生代使用复制算法,老年代使用标记-压缩算法。
    • 参数-XX:+UseSerialGC
  2. Parallel GC(也称为 Throughput GC)

    • Java 1.4:引入 Parallel GC,适用于多处理器机器,主要目标是高吞吐量。
    • Java 8:成为默认垃圾收集器。
    • 特点:新生代和老年代都使用多线程并行 GC。
    • 参数-XX:+UseParallelGC
  3. Parallel Old GC

    • Java 1.6:Parallel GC 的改进版本,对老年代进行并行垃圾收集,提高了整体性能。
    • 参数-XX:+UseParallelOldGC
  4. CMS(Concurrent Mark-Sweep)

    • Java 1.4.1:引入 CMS 垃圾收集器,适用于低延迟应用。主要特点是并发标记和清除,减少了应用停顿时间。
    • Java 9:开始被标记为废弃。
    • 参数-XX:+UseConcMarkSweepGC
    • 注意:CMS 会引起内存碎片问题,需要进行压缩。
  5. G1(Garbage-First)

    • Java 7:引入 G1 垃圾收集器,目标是替代 CMS,适用于大堆内存和低延迟需求。
    • Java 9:成为默认垃圾收集器。
    • 特点:将堆划分为多个区域,使用标记-压缩算法,优先回收垃圾最多的区域。
    • 参数-XX:+UseG1GC
  6. ZGC(Z Garbage Collector)

    • Java 11:引入 ZGC,适用于极低延迟要求的大内存应用,能处理数 TB 级内存。
    • 特点:并发收集,停顿时间非常短(通常在几毫秒以内)。
    • 参数-XX:+UseZGC
  7. Shenandoah

    • Java 12:引入 Shenandoah 垃圾收集器,适用于低延迟应用。
    • 特点:并发压缩垃圾收集,停顿时间短。
    • 参数-XX:+UseShenandoahGC
  8. Epsilon(No-Op GC)

    • Java 11:引入 Epsilon 垃圾收集器,主要用于测试和调试,不进行实际的垃圾收集。
    • 特点:不执行垃圾收集,只进行内存分配。
    • 参数-XX:+UseEpsilonGC

总结

Java 中的垃圾收集器不断演进,以满足不同应用场景的需求。以下是主要垃圾收集器在不同 Java 版本中的支持情况及其特点:

垃圾收集器引入版本特点适用场景参数
Serial GC1.3单线程,适用于小型应用和单处理器机器小型应用-XX:+UseSerialGC
Parallel GC1.4多线程并行垃圾收集,注重高吞吐量高吞吐量应用-XX:+UseParallelGC
Parallel Old GC1.6Parallel GC 的改进版本,老年代并行收集高吞吐量应用-XX:+UseParallelOldGC
CMS1.4.1并发标记-清除,适用于低延迟应用低延迟应用-XX:+UseConcMarkSweepGC
G17分区垃圾收集,适用于大堆内存和低延迟需求大堆内存,低延迟应用-XX:+UseG1GC
ZGC11并发收集,极低停顿时间,适用于大内存应用极低延迟应用,大内存处理-XX:+UseZGC
Shenandoah12并发压缩垃圾收集,低停顿时间低延迟应用-XX:+UseShenandoahGC
Epsilon11不进行垃圾收集,主要用于测试和调试测试和调试-XX:+UseEpsilonGC

垃圾回收调优参数

在 JVM 中,合理设置堆内存大小、新生代大小、元空间大小以及选择合适的垃圾回收器对于提升应用性能至关重要。此外,启用 GC 日志能够帮助开发者分析垃圾收集行为,进一步优化 JVM 参数。

堆内存大小设置

  • 初始堆大小 (-Xms)

    • 通过 -Xms 参数设置 JVM 启动时堆内存的初始大小。
    • 例如:-Xms512m 设置初始堆内存为 512 MB。
  • 最大堆大小 (-Xmx)

    • 通过 -Xmx 参数设置堆内存的最大大小,JVM 可以动态扩展堆内存直到该值。
    • 例如:-Xmx2048m 设置最大堆内存为 2048 MB。

新生代大小设置

  • 新生代大小 (-Xmn)
    • 通过 -Xmn 参数设置新生代的大小。新生代包括 Eden 空间和两个 Survivor 空间。
    • 例如:-Xmn256m 设置新生代大小为 256 MB。

垃圾回收器选择

  • Serial GC

    • 通过 -XX:+UseSerialGC 选择 Serial GC,适用于小型应用和单处理器机器。
    • 例如:-XX:+UseSerialGC
  • Parallel GC

    • 通过 -XX:+UseParallelGC 选择 Parallel GC,适用于多处理器机器和高吞吐量应用。
    • 例如:-XX:+UseParallelGC
  • CMS (Concurrent Mark-Sweep) GC

    • 通过 -XX:+UseConcMarkSweepGC 选择 CMS GC,适用于低延迟应用。
    • 例如:-XX:+UseConcMarkSweepGC
  • G1 (Garbage-First) GC

    • 通过 -XX:+UseG1GC 选择 G1 GC,适用于大堆内存和低延迟需求。
    • 例如:-XX:+UseG1GC

元空间大小设置

  • 元空间初始大小 (-XX:MetaspaceSize)

    • 通过 -XX:MetaspaceSize 设置元空间的初始大小。
    • 例如:-XX:MetaspaceSize=128m 设置元空间初始大小为 128 MB。
  • 元空间最大大小 (-XX:MaxMetaspaceSize)

    • 通过 -XX:MaxMetaspaceSize 设置元空间的最大大小。
    • 例如:-XX:MaxMetaspaceSize=256m 设置元空间最大大小为 256 MB。

启用 GC 日志

  • 启用 GC 日志 (-Xlog:gc*-XX:+PrintGCDetails)

    • 通过 -Xlog:gc* 启用 GC 日志记录所有 GC 活动(适用于 Java 9 及以上版本)。
    • 例如:-Xlog:gc*
    • 通过 -XX:+PrintGCDetails 启用详细的 GC 日志(适用于 Java 8 及以下版本)。
    • 例如:-XX:+PrintGCDetails
  • GC 日志输出到文件

    • 通过 -Xlog:gc*:file=<file_path> 将 GC 日志输出到指定文件(适用于 Java 9 及以上版本)。
    • 例如:-Xlog:gc*:file=gc.log
    • 通过 -Xloggc:<file_path> 将 GC 日志输出到指定文件(适用于 Java 8 及以下版本)。
    • 例如:-Xloggc:gc.log

示例 JVM 参数配置

以下是一个示例配置,适用于一个需要高吞吐量且有较大堆内存需求的 Java 应用:

java -Xms1024m -Xmx4096m -Xmn512m -XX:+UseG1GC -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -Xlog:gc*:file=gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dumps MyApplication

解释

  • -Xms1024m:初始堆内存为 1024 MB。
  • -Xmx4096m:最大堆内存为 4096 MB。
  • -Xmn512m:新生代大小为 512 MB。
  • -XX:+UseG1GC:选择 G1 垃圾收集器。
  • -XX:MetaspaceSize=128m:元空间初始大小为 128 MB。
  • -XX:MaxMetaspaceSize=256m:元空间最大大小为 256 MB。
  • -Xlog:gc*:file=gc.log:启用 GC 日志并输出到 gc.log 文件。
  • -XX:+HeapDumpOnOutOfMemoryError:在 OOM 时生成堆转储。
  • -XX:HeapDumpPath=/path/to/dumps:指定堆转储路径。

常见调优策略

调优新生代和老年代的比例

问题:如何调优新生代和老年代的大小比例?

回答:
调优新生代和老年代的比例,主要是根据应用程序中对象的生命周期特性来进行调整。通常,新生代(Young Generation)用于存放短生命周期的对象,而老年代(Old Generation)用于存放生命周期较长的对象。

  • 分析对象生命周期:首先,通过分析应用程序的对象分配和回收情况,了解对象生命周期的分布。
  • 设置初始比例:使用 -XX:NewRatio 参数设置新生代与老年代的比例。例如,-XX:NewRatio=3 表示新生代与老年代的比例为 1:3。
  • 监控和调整:通过 GC 日志和监控工具观察 GC 频率和内存使用情况,根据应用负载调整比例。例如,若 Minor GC 过于频繁,可能需要增大新生代;若 Full GC 频繁,则可能需要增大老年代。
java -XX:NewRatio=3 -jar MyApplication.jar

调整 Survivor 空间

问题:如何调整 Eden 区和 Survivor 区的比例?

回答:
Eden 区和 Survivor 区的比例可以通过 -XX:SurvivorRatio 参数来调整。该参数控制 Eden 区与两个 Survivor 区的比例。

  • 默认比例:通常默认值为 8,即 Eden 区占新生代的 8/10,两个 Survivor 区各占 1/10。
  • 调整参数:根据对象存活率和 Minor GC 的表现,调整 -XX:SurvivorRatio。例如,-XX:SurvivorRatio=6 表示 Eden 区占新生代的 6/8,两个 Survivor 区各占 1/8。
java -XX:SurvivorRatio=6 -jar MyApplication.jar

优化 Full GC 频率

问题:如何减少 Full GC 的频率?

回答:
减少 Full GC 的频率可以通过调整老年代大小和对象晋升阈值等参数来实现。

  • 调整老年代大小:通过增加老年代的大小,减少老年代的 GC 频率。使用 -Xmx 参数设置最大堆内存,通过 -XX:NewRatio 调整老年代大小。
  • 调整对象晋升阈值:通过 -XX:MaxTenuringThreshold 参数调整对象从新生代晋升到老年代的年龄阈值。默认值通常是 15,可以根据对象存活率进行调整。
java -Xmx4096m -XX:MaxTenuringThreshold=10 -jar MyApplication.jar

控制 GC 停顿时间

问题:如何控制 GC 停顿时间?

回答:
控制 GC 停顿时间需要根据应用需求选择合适的垃圾回收器,并调整相关参数,平衡停顿时间和吞吐量。

  • 选择合适的垃圾回收器

    • 对于低停顿时间的需求,可以选择 G1 GC 或 ZGC。
    • 使用 G1 GC:-XX:+UseG1GC
    • 使用 ZGC:-XX:+UseZGC
  • 调整 GC 参数:对于 G1 GC,可以通过设置目标停顿时间和其他调优参数来控制停顿时间。

    • 目标停顿时间:-XX:MaxGCPauseMillis
    • 年轻代和老年代的初始和最大大小:-XX:G1NewSizePercent-XX:G1MaxNewSizePercent
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar MyApplication.jar

观察和分析 GC 日志

问题:如何通过 GC 日志进行调优?

回答:
通过启用和分析 GC 日志,可以了解垃圾回收的频率、停顿时间和内存使用情况,进行针对性调优。

  • 启用 GC 日志
    • 对于 Java 8 及以下版本:-XX:+PrintGCDetails -Xloggc:gc.log
    • 对于 Java 9 及以上版本:-Xlog:gc*:file=gc.log
java -Xlog:gc*:file=gc.log -jar MyApplication.jar
  • 分析 GC 日志:使用 GC 日志分析工具(如 GCViewer 或 GCeasy)分析日志,关注以下指标:

    • GC 频率:Minor GC 和 Full GC 的频率。
    • 停顿时间:每次 GC 的停顿时间,特别是 Full GC 的停顿。
    • 内存使用情况:堆内存和各代内存的使用情况和回收情况。
  • 调优策略:根据分析结果,调整相关参数。例如,如果 Full GC 频繁且停顿时间长,可以考虑增加老年代大小或调整对象晋升阈值;如果 Minor GC 过于频繁,可以增加新生代大小或调整 Survivor 区比例。

高级调优选项

对象晋升阈值

问题:如何通过对象晋升阈值调优 GC?

回答:
对象晋升阈值决定了对象从新生代(Young Generation)晋升到老年代(Old Generation)的年龄(即在 Survivor 区经历的 Minor GC 次数)。通过调整这个阈值,可以控制老年代的对象数量,进而影响 Full GC 的频率。

  • 参数-XX:MaxTenuringThreshold
  • 默认值:通常是 15,表示对象在新生代经历 15 次 Minor GC 后晋升到老年代。
  • 调优策略
    • 如果对象存活率较低,可以降低阈值,减少 Survivor 区压力。
    • 如果对象存活率较高,可以增加阈值,延迟对象晋升,减少老年代的压力。
java -XX:MaxTenuringThreshold=10 -jar MyApplication.jar

并发线程数

问题:如何设置并行 GC 线程数?

回答:
并行 GC 线程数决定了垃圾收集时使用的并行线程数量。适当调整并行 GC 线程数可以优化 GC 性能。

  • 参数-XX:ParallelGCThreads
  • 默认值:通常为 CPU 核心数。
  • 调优策略
    • 根据 CPU 核心数量和应用负载,调整并行 GC 线程数。
    • 对于多核机器,可以增加线程数以提高 GC 效率。
    • 对于高负载应用,适当减少线程数,避免 GC 线程争夺 CPU 资源。
java -XX:ParallelGCThreads=8 -jar MyApplication.jar

暂停时间目标

问题:如何设置 GC 的最大暂停时间目标?

回答:
设置 GC 的最大暂停时间目标,可以让垃圾收集器尽量在指定时间内完成垃圾收集,从而控制应用停顿时间。

  • 参数-XX:MaxGCPauseMillis
  • 默认值:未设置具体值。
  • 调优策略
    • 设定合理的暂停时间目标,确保应用响应时间符合要求。
    • 根据应用的延迟需求,适当调整该值。例如,对于实时应用,可以设定较短的暂停时间目标。
java -XX:MaxGCPauseMillis=200 -jar MyApplication.jar

G1 GC 特定参数

问题:如何通过 G1 GC 特定参数优化 GC 行为?

回答:
G1 GC 提供了一些特定参数来优化其行为。这些参数可以控制 G1 GC 的内存占用、并发线程数量等。

  1. -XX:InitiatingHeapOccupancyPercent
    • 描述:设置在启动并发 GC 周期之前堆内存使用率的阈值。
    • 默认值:45%
    • 调优策略:降低该值可以更早地启动并发 GC,避免过多的 Full GC。
java -XX:InitiatingHeapOccupancyPercent=35 -jar MyApplication.jar
  1. -XX:ConcGCThreads
    • 描述:设置并发 GC 线程的数量。
    • 默认值:通常为 CPU 核心数的 1/4。
    • 调优策略:根据系统的并发能力和应用负载调整该值,确保 GC 线程足够但不会与应用线程争夺资源。
java -XX:ConcGCThreads=4 -jar MyApplication.jar
  1. -XX:G1ReservePercent
    • 描述:设置堆内存的保留百分比,用于在垃圾收集期间防止内存碎片。
    • 默认值:10%
    • 调优策略:适当增加该值可以减少由于内存碎片导致的 Full GC。
java -XX:G1ReservePercent=15 -jar MyApplication.jar
  1. -XX:G1HeapRegionSize
    • 描述:设置 G1 GC 堆区域的大小。
    • 默认值:通常根据堆大小动态计算。
    • 调优策略:调整堆区域大小,使其适应应用对象大小分布。
java -XX:G1HeapRegionSize=8m -jar MyApplication.jar
  • 14
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值