Bootstrap

JAVA面试题、八股文学习之JVM篇

1.什么是空闲列表?

空闲列表(Free List) 是一种内存管理技术,主要用于跟踪和管理堆内存中可用的空闲块。它广泛应用于各种内存分配算法中,特别是在 分段分配(Segmented Allocation) 和 链式分配(Linked Allocation) 策略中。下面详细介绍空闲列表的工作原理及其特点。

空闲列表的基本概念

空闲列表是一种数据结构,用于维护堆内存中所有未被使用的内存块的信息。每个空闲块通常包含以下信息:

  • 起始地址:该空闲块在内存中的起始位置。
  • 大小:该空闲块的字节数。
  • 下一个空闲块指针:指向列表中的下一个空闲块。

通过维护这样一个列表,内存分配器可以快速找到合适的空闲块来满足对象的分配请求,并在释放对象时将相应的内存块重新加入到空闲列表中。

工作原理

  • 初始化
    • 当 JVM 启动时,整个堆内存被视为一个大的空闲块,并将其添加到空闲列表中。
  • 对象分配
    • 当需要创建新对象时,内存分配器会遍历空闲列表,查找足够大的空闲块。
    • 如果找到合适的空闲块,则从该块中分配所需的空间,并更新空闲列表。
    • 如果找不到合适的大块,则可能需要触发垃圾回收或调整堆大小。
  • 空闲块分割
    • 为了提高内存利用率,分配器可能会将较大的空闲块分割成两部分:一部分用于分配对象,另一部分保留在空闲列表中作为新的空闲块。
  • 合并相邻空闲块
    • 当释放对象时,相应的内存块会被标记为空闲,并尝试与相邻的空闲块合并,以减少碎片化。
  • 更新空闲列表
    • 分配和释放操作都会导致空闲列表的变化。分配操作会移除或缩小某个空闲块,而释放操作会添加新的空闲块或合并现有块。

示例图解

假设有一个简单的堆内存结构如下:

+-------------------+
|       Free        |  Size: 100 bytes
|                   |
|                   |
+-------------------+
|      Allocated    |  Size: 50 bytes
|                   |
+-------------------+
|       Free        |  Size: 75 bytes
|                   |
+-------------------+

对应的空闲列表可能如下:

+-------------------+
|  Start Address: 0x1000  |
|  Size: 100 bytes     |
|  Next: 0x10080         |
+-------------------+
|  Start Address: 0x100D0 |
|  Size: 75 bytes      |
|  Next: null            |
+-------------------+

分配对象 A (Size: 30 bytes)

  • 查找空闲块:
    • 查找第一个空闲块(Start Address: 0x1000, Size: 100 bytes),其大小足够分配对象 A。
  • 分配空间:
    • 从该空闲块中分配 30 字节的空间给对象 A。
    • 更新空闲块的起始地址和大小。
  • 更新空闲列表:
    • 修改第一个空闲块的起始地址为 0x1001E,大小变为 70 bytes。

结果如下:

+-------------------+
|       Free        |  Size: 70 bytes
|                   |
|                   |
+-------------------+
|      Allocated    |  Size: 30 bytes
|      [A]          |
+-------------------+
|      Allocated    |  Size: 50 bytes
|                   |
+-------------------+
|       Free        |  Size: 75 bytes
|                   |
+-------------------+

对应的空闲列表如下:

+-------------------+
|  Start Address: 0x1001E |
|  Size: 70 bytes       |
|  Next: 0x10080          |
+-------------------+
|  Start Address: 0x10080 |
|  Size: 75 bytes       |
|  Next: null             |
+-------------------+

释放对象 A

  • 标记为空闲:
    • 将对象 A 所占的空间标记为空闲。
  • 合并相邻空闲块:
    • 检查相邻的空闲块,发现前一个空闲块(Start Address: 0x1001E, Size: 70 bytes)和当前释放的块可以合并。
    • 合并这两个空闲块,形成一个新的空闲块。
  • 更新空闲列表:
    • 移除旧的空闲块,并更新合并后的空闲块信息。

结果如下:

+-------------------+
|       Free        |  Size: 100 bytes
|                   |
|                   |
+-------------------+
|      Allocated    |  Size: 50 bytes
|                   |
+-------------------+
|       Free        |  Size: 75 bytes
|                   |
+-------------------+

对应的空闲列表如下:

+-------------------+
|  Start Address: 0x1000  |
|  Size: 100 bytes     |
|  Next: 0x10080         |
+-------------------+
|  Start Address: 0x10080 |
|  Size: 75 bytes      |
|  Next: null            |
+-------------------+

特点和优势

  • 高效性
    • 空闲列表允许快速查找和分配空闲块,适合动态内存分配场景。
  • 灵活性
    • 可以灵活地处理不同大小的对象分配请求。
  • 简单实现
    • 相比于其他复杂的内存管理策略,空闲列表的实现相对简单。
  • 降低碎片化
    • 通过合并相邻的空闲块,可以有效减少内存碎片化。

注意事项

  • 碎片化问题:
    • 即使使用空闲列表,长时间运行的应用程序也可能出现内存碎片化的问题。
    • 定期进行垃圾回收可以帮助减少碎片化。
  • 搜索时间:
    • 在大型堆中,查找合适的空闲块可能会消耗较多的时间。
    • 可以采用不同的搜索策略(如最佳适应、首次适应等)来优化性能。
  • 锁竞争:
    • 在多线程环境中,访问空闲列表可能导致锁竞争。
    • 可以使用细粒度锁或多线程友好的分配策略来缓解这个问题。

总结

空闲列表是一种有效的内存管理技术,广泛应用于各种内存分配算法中。通过维护一个记录所有空闲块的数据结构,它可以快速地分配和释放内存,从而提高内存利用率和系统性能。

2.什么是TLAB ?

TLAB(Thread Local Allocation Buffer) 是 Java 虚拟机(JVM)中的一种优化技术,主要用于提高多线程环境下对象分配的性能。通过将每个线程的对象分配空间独立化,TLAB 减少了线程间竞争和锁的开销,从而提升了整体吞吐量。下面详细介绍 TLAB 的工作原理及其优势。

TLAB 的基本概念

TLAB 是为每个线程预先分配的一小块内存区域,专门用于该线程创建的对象。当一个线程需要创建新对象时,它首先尝试从自己的 TLAB 中分配内存。只有在 TLAB 不足时,才会请求更多的内存或从共享的堆内存中分配。

工作原理

  • 初始化:
    • 当 JVM 启动并创建线程时,会为每个线程分配一个 TLAB。
    • TLAB 的大小可以根据 JVM 参数进行配置,默认情况下由 JVM 自动调整。
  • 对象分配:
    • 线程在创建新对象时,首先检查其 TLAB 是否有足够的空间。
    • 如果有足够空间,则直接在 TLAB 中分配对象,并移动 TLAB 的顶部指针。
    • 如果没有足够空间,则向全局堆请求新的 TLAB 或者直接从堆中分配对象。
  • TLAB 回收:
    • 当 TLAB 被耗尽时,线程会请求一个新的 TLAB。
    • 旧的 TLAB 中的对象会被标记为存活,后续会被垃圾回收器处理。
  • 动态调整:
    • JVM 可以根据实际使用情况动态调整 TLAB 的大小,以平衡分配效率和内存利用率。

示例图解

假设有一个简单的多线程环境,有两个线程 Thread A 和 Thread B,各自拥有一个 TLAB:

+-------------------+
|    Thread A       |
|  +-------------+  |
|  |     TLAB    |  |
|  | [Object 1]  |  |
|  | [Object 2]  |  |
|  +-------------+  |
+-------------------+

+-------------------+
|    Thread B       |
|  +-------------+  |
|  |     TLAB    |  |
|  | [Object 3]  |  |
|  |             |  |
|  +-------------+  |
+-------------------+

分配对象 C (Size: 10 bytes) 到 Thread A

  • 检查 TLAB 空间
    • Thread A 检查其 TLAB 是否有足够的空间来分配对象 C(10 bytes)。
    • 假设 TLAB 还有足够的空间。
  • 分配对象 C
    • 在 TLAB 中分配 10 字节的空间给对象 C。
    • 移动 TLAB 的顶部指针。

结果如下:

+-------------------+
|    Thread A       |
|  +-------------+  |
|  |     TLAB    |  |
|  | [Object 1]  |  |
|  | [Object 2]  |  |
|  | [Object C]  |  |
|  +-------------+  |
+-------------------+

+-------------------+
|    Thread B       |
|  +-------------+  |
|  |     TLAB    |  |
|  | [Object 3]  |  |
|  |             |  |
|  +-------------+  |
+-------------------+

分配对象 D (Size: 50 bytes) 到 Thread B

  • 检查 TLAB 空间
    • Thread B 检查其 TLAB 是否有足够的空间来分配对象 D(50 bytes)。
    • 假设 TLAB 不足以容纳对象 D。
  • 请求新的 TLAB 或直接分配
    • Thread B 请求一个新的 TLAB 或者直接从全局堆中分配对象 D。

结果如下:

+-------------------+
|    Thread A       |
|  +-------------+  |
|  |     TLAB    |  |
|  | [Object 1]  |  |
|  | [Object 2]  |  |
|  | [Object C]  |  |
|  +-------------+  |
+-------------------+

+-------------------+
|    Thread B       |
|  +-------------+  |
|  |     TLAB    |  |
|  | [Object D]  |  |
|  |             |  |
|  +-------------+  |
+-------------------+

特点和优势

  • 减少锁竞争
    • 每个线程都有自己独立的 TLAB,减少了对全局堆锁的竞争。
    • 大多数对象分配都在本地完成,降低了锁的开销。
  • 提高分配速度
    • 使用指针碰撞(Pointer Bumping)技术,在 TLAB 中分配对象非常高效。
    • 避免了频繁的锁获取和释放操作,提高了对象分配的速度。
  • 降低碎片化
    • TLAB 中的对象通常是连续分配的,减少了内存碎片化的问题。
    • 有利于提高缓存命中率,提升整体性能。
  • 动态调整
    • JVM 可以根据实际应用的需求动态调整 TLAB 的大小。
    • 适应不同的应用场景,优化内存分配策略。

配置参数

JVM 提供了一些参数来控制 TLAB 的行为:

  • -XX:+UseTLAB:启用 TLAB(默认启用)。
  • -XX:-UseTLAB:禁用 TLAB。
  • -XX:TLABSize=:设置 TLAB 的初始大小。
  • -XX:TLABRefillWasteFraction=:设置 TLAB 再填充浪费分数。
  • -XX:MinTLABSize=:设置 TLAB 的最小大小。
  • -XX:MaxTLABSize=:设置 TLAB 的最大大小。

注意事项

  • 线程数量:
    • 如果线程数量过多,可能会导致大量的 TLAB 分配,增加管理开销。
    • 需要根据实际情况调整线程池大小和 TLAB 参数。
  • 对象大小:
    • 对于非常大的对象,通常不会分配到 TLAB,而是直接从全局堆中分配。
    • 需要注意大对象的分配策略,避免不必要的性能开销。
  • 垃圾回收:
    • TLAB 中的对象在 TLAB 被耗尽后会被标记为存活,参与垃圾回收过程。
    • 适当的 TLAB 大小可以帮助减少垃圾回收的压力。

总结

TLAB 是一种重要的内存分配优化技术,通过为每个线程分配独立的内存区域,显著提高了多线程环境下对象分配的性能。通过减少锁竞争、提高分配速度和降低碎片化,TLAB 成为现代 JVM 中不可或缺的一部分。

3.对象头具体都包含哪些内容?

在 Java 虚拟机(JVM)中,对象头(Object Header) 是每个对象都必须包含的一个部分,用于存储与对象相关的重要元数据信息。对象头的结构和内容因 JVM 的具体实现而异,但通常包含以下几个关键部分:

对象头的基本结构

Java 对象头主要由两部分组成:

  • Mark Word:用于存储对象的运行时数据。
  • Klass Pointer:指向对象所属类的元数据(在某些情况下可以省略)。

1. Mark Word

Mark Word 是对象头中最重要的一部分,用于存储对象的状态信息、哈希码、GC 状态、锁状态等。具体的结构取决于 JVM 的实现,以下是一些常见的字段:

  • Hashcode:对象的哈希码值,用于 hashCode() 方法。
  • Age:对象的年龄,用于判断对象是否需要晋升到老年代。
  • Biased Locking:偏向锁的信息,用于轻量级锁优化。
  • Lock Status:对象的锁定状态,包括无锁、偏向锁、轻量级锁、重量级锁等。
  • GC Metadata:垃圾回收相关的标志位,如可达性标记等。
  • Thread ID:持有偏向锁的线程 ID。
  • Epoch:偏向锁的时间戳或版本号。

2. Klass Pointer

Klass Pointer 指向对象所属类的元数据(klass 结构),包含了类的详细信息,如父类、方法表、字段信息等。在某些情况下,特别是压缩指针启用的情况下,Klass Pointer 可能被省略或合并到 Mark Word 中。

具体示例

以下是一个典型的 64 位 JVM 中对象头的结构示意图:

+--------------------------------------------------+
|                    Mark Word                     |
|--------------------------------------------------|
| Hashcode | Age | Biased Locking | Lock Status    |
+--------------------------------------------------+
|                 Klass Pointer                    |
+--------------------------------------------------+

Mark Word 的详细结构

在不同的 JVM 实现中,Mark Word 的具体结构可能会有所不同。以下是 HotSpot JVM 中的一种常见结构:

+--------------------------------------------------+
| hashcode:25 | age:4 | biased_lock:1 | lock:2     |
+--------------------------------------------------+
| thread_id:54 | epoch:2                          |
+--------------------------------------------------+
  • hashcode:25:对象的哈希码值。
  • age:4:对象的年龄,用于分代垃圾回收。
  • biased_lock:1:表示是否启用了偏向锁。
  • lock:2:表示对象的锁定状态(0b00 表示无锁,0b01 表示偏向锁,0b10 表示轻量级锁,0b11 表示重量级锁)。
  • thread_id:54:持有偏向锁的线程 ID。
  • epoch:2:偏向锁的时间戳或版本号。

压缩指针

为了节省内存,现代 JVM 通常会启用压缩指针(Compressed Pointers)。压缩指针技术通过使用较小的指针大小来减少内存占用。例如,在 64 位 JVM 上,Klass Pointer 和普通对象引用可以被压缩为 32 位。

启用压缩指针

可以通过以下 JVM 参数启用压缩指针:

  • -XX:+UseCompressedOops:启用压缩对象指针。
  • -XX:+UseCompressedClassPointers:启用压缩类指针。

示例代码

虽然对象头的内容不能直接通过 Java 代码访问,但我们可以通过一些工具和技术来观察和分析对象头的信息。例如,使用 jol-core 库可以查看对象头的布局和大小。

[<title="使用 jol-core 查看对象头信息">]
import org.openjdk.jol.info.ClassLayout;

public class ObjectHeaderExample {
    public static void main(String[] args) {
        MyClass obj = new MyClass();
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
}

class MyClass {
    private int value;
}

运行上述代码,输出可能如下所示:

com.example.ObjectHeaderExample$MyClass object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    12        (object header)                           N/A
     12     4   int MyClass.value                             N/A
     16     4        (loss due to the next object alignment)
Instance size: 20 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

在这个输出中,(object header) 部分代表了对象头的大小和内容。

总结

Java 对象头是每个对象不可或缺的一部分,包含了对象的运行时元数据信息。主要包括 Mark Word 和 Klass Pointer 两个部分。Mark Word 存储了对象的状态信息、哈希码、GC 状态、锁状态等,而 Klass Pointer 指向对象所属类的元数据。通过理解对象头的结构和内容,可以更好地掌握 JVM 内存管理和对象生命周期的相关知识。

4.你知道哪些JVM调优参数?

JVM 调优参数是用于调整 Java 虚拟机的行为和性能的关键设置。通过合理配置这些参数,可以显著提升应用程序的性能、稳定性和资源利用率。以下是一些常用的 JVM 调优参数及其作用:

堆内存调优参数

    1. 初始堆大小 (-Xms)
    • 作用:设置 JVM 启动时分配的初始堆内存大小。

示例:

-Xms512m
    1. 最大堆大小 (-Xmx)
    • 作用:设置 JVM 允许的最大堆内存大小。

示例:

-Xmx2g
  • 3. 新生代大小 (-Xmn)
    • 作用:设置新生代(Young Generation)的大小。

示例:

-Xmn768m
  • 4. 年轻代 Eden 区大小 (-XX:NewRatio)
    • 作用:设置年轻代中 Eden 区与 Survivor 区的比例。默认值为 2,表示 Eden 区占 2/3,每个 Survivor 区占 1/6。

示例:

-XX:NewRatio=3
    1. Survivor 区比例 (-XX:SurvivorRatio)
    • 作用:设置 Eden 区与 Survivor 区的比例。默认值为 8,表示 Eden 区占 8/10,每个 Survivor 区占 1/10。

示例:

-XX:SurvivorRatio=6
  • 6. 压缩指针 (-XX:+UseCompressedOops)
    • 作用:启用压缩对象指针,减少 64 位 JVM 中的对象引用大小。

示例:

-XX:+UseCompressedOops
  • 7. 压缩类指针 (-XX:+UseCompressedClassPointers)
    • 作用:启用压缩类指针,减少 64 位 JVM 中的类引用大小。

示例:

-XX:+UseCompressedClassPointers

垃圾回收调优参数

  • 1. G1 垃圾收集器 (-XX:+UseG1GC)
    • 作用:启用 G1 垃圾收集器。

示例:

-XX:+UseG1GC
  • 2. CMS 垃圾收集器 (-XX:+UseConcMarkSweepGC)
    • 作用:启用 CMS(Concurrent Mark Sweep)垃圾收集器。

示例:

-XX:+UseConcMarkSweepGC
  • 3. Parallel GC (-XX:+UseParallelGC)
    • 作用:启用 Parallel GC(并行垃圾收集器)。

示例:

-XX:+UseParallelGC
  • 4. Parallel Old GC (-XX:+UseParallelOldGC)
    • 作用:启用 Parallel Old GC(并行老年代垃圾收集器)。

示例:

-XX:+UseParallelOldGC
  • 5. Young GC 线程数 (-XX:ParallelGCThreads)
    • 作用:设置 Young GC 使用的线程数。

示例:

-XX:ParallelGCThreads=4
  • 6. Old GC 线程数 (-XX:ConcGCThreads)
    • 作用:设置 Concurrent GC 使用的线程数。

示例:

-XX:ConcGCThreads=2
  • 7. G1 Region 大小 (-XX:G1HeapRegionSize)
    • 作用:设置 G1 垃圾收集器中每个 region 的大小。

示例:

-XX:G1HeapRegionSize=16m
  • 8. G1 并发标记线程数 (-XX:ConcGCThreads)
    • 作用:设置 G1 并发标记阶段使用的线程数。

示例:

-XX:ConcGCThreads=4
  • 9. G1 Initiating Heap Occupancy Percent (-XX:InitiatingHeapOccupancyPercent)
    • 作用:设置触发并发标记周期的堆占用百分比。

示例:

-XX:InitiatingHeapOccupancyPercent=45

内存映射文件调优参数

  • 1. Metaspace 大小 (-XX:MetaspaceSize)
    • 作用:设置 Metaspace 的初始大小。

示例:

-XX:MetaspaceSize=256m
  • 2. 最大 Metaspace 大小 (-XX:MaxMetaspaceSize)
    • 作用:设置 Metaspace 的最大大小。

示例:

-XX:MaxMetaspaceSize=512m
  • 3. PermGen 大小 (-XX:PermSize 和 -XX:MaxPermSize)
    • 作用:设置永久代(PermGen)的初始和最大大小(已弃用,推荐使用 Metaspace)。

示例:

-XX:PermSize=128m
-XX:MaxPermSize=256m

其他常用参数

  • 1. 禁用偏向锁 (-XX:-UseBiasedLocking)
    • 作用:禁用偏向锁。

示例:

-XX:-UseBiasedLocking
  • 2. 启用逃逸分析 (-XX:+DoEscapeAnalysis)
    • 作用:启用逃逸分析优化。

示例:

-XX:+DoEscapeAnalysis
  • 3. 禁用逃逸分析 (-XX:-DoEscapeAnalysis)
    • 作用:禁用逃逸分析优化。

示例:

-XX:-DoEscapeAnalysis
  • 4. 启用 JIT 编译器 (-client 或 -server)
    • 作用:选择客户端或服务器模式的 JIT 编译器。

示例:

-client
-server
  • 5. 日志输出 (-Xloggc:)
    • 作用:将垃圾回收日志输出到指定文件。

示例:

-Xloggc:/path/to/gc.log
  • 6. 输出详细 GC 日志 (-verbose:gc)
    • 作用:输出详细的垃圾回收日志信息。

示例:

-verbose:gc
  • 7. 设置 GC 日志格式 (-XX:+PrintGCDetails)
    • 作用:打印详细的 GC 日志信息。

示例:

-XX:+PrintGCDetails
  • 8. 设置 GC 日志时间戳 (-XX:+PrintGCTimeStamps)
    • 作用:在 GC 日志中添加时间戳。

示例:

-XX:+PrintGCTimeStamps
  • 9. 设置 GC 日志日期戳 (-XX:+PrintGCDateStamps)
    • 作用:在 GC 日志中添加日期时间戳。

示例:

-XX:+PrintGCDateStamps
  • 10. 设置 GC 日志轮转 (-XX:+UseGCLogFileRotation)
    • 作用:启用 GC 日志文件轮转。

示例:

-XX:+UseGCLogFileRotation
  • 11. 设置 GC 日志文件数量 (-XX:NumberOfGCLogFiles)
    • 作用:设置 GC 日志文件的数量。

示例:

-XX:NumberOfGCLogFiles=5
  • 12. 设置 GC 日志文件大小 (-XX:GCLogFileSize)
    • 作用:设置每个 GC 日志文件的大小。

示例:

-XX:GCLogFileSize=10M
  • 13. 设置线程堆栈大小 (-Xss)
    • 作用:设置每个线程的堆栈大小。

示例:

-Xss1m
  • 14. 设置最大直接内存大小 (-XX:MaxDirectMemorySize)
    • 作用:设置 JVM 可以使用的最大直接内存大小。

示例:

-XX:MaxDirectMemorySize=256m
  • 15. 设置最大内联方法大小 (-XX:MaxInlineSize)
    • 作用:设置方法内联的最大字节码大小。

示例:

-XX:MaxInlineSize=35
  • 16. 设置最大内联层次深度 (-XX:MaxRecursiveInlineLevel)
    • 作用:设置递归内联的最大层次深度。

示例:

-XX:MaxRecursiveInlineLevel=1
  • 17. 设置最大内联方法大小 (-XX:LargeMethodThreshold)
    • 作用:设置认为是“大”方法的阈值字节数。

示例:

-XX:LargeMethodThreshold=32768
  • 18. 启用 TLAB (-XX:+UseTLAB)
    • 作用:启用 Thread Local Allocation Buffers (TLAB)。

示例:

-XX:+UseTLAB
  • 19. 设置 TLAB 大小 (-XX:TLABSize)
    • 作用:设置 TLAB 的初始大小。

示例:

-XX:TLABSize=1m
  • 20. 设置 TLAB 最大浪费比例 (-XX:TLABRefillWasteFraction)
    • 作用:设置 TLAB 再填充时允许的最大浪费比例。

示例:

-XX:TLABRefillWasteFraction=64

示例

以下是一个综合示例,展示了如何配置一些常用的 JVM 调优参数:

java -Xms512m -Xmx2g -Xmn768m -XX:NewRatio=3 -XX:SurvivorRatio=6 \
     -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:ConcGCThreads=4 \
     -XX:InitiatingHeapOccupancyPercent=45 -XX:+UseCompressedOops \
     -XX:+UseCompressedClassPointers -XX:MetaspaceSize=256m \
     -XX:MaxMetaspaceSize=512m -XX:+DoEscapeAnalysis -XX:+UseTLAB \
     -Xss1m -Xloggc:/path/to/gc.log -verbose:gc -XX:+PrintGCDetails \
     -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+UseGCLogFileRotation \
     -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10M MyApplication

5.说一下 JVM 有哪些垃圾回收器?

Java 虚拟机(JVM)提供了多种垃圾回收器(Garbage Collectors),每种垃圾回收器都有其独特的特性和适用场景。以下是 JVM 中常见的几种垃圾回收器及其详细说明:

1. Serial Garbage Collector

适用场景:

  • 单线程环境。
  • 简单的应用程序或小型系统。

特点:

  • 单线程:使用单个线程进行垃圾回收,会导致应用程序在垃圾回收期间暂停(Stop-The-World, STW)。
  • 简单高效:适用于内存较小且不需要高并发的应用程序。
  • 低开销:由于只有一个线程参与垃圾回收,开销较低。

启用参数:

-XX:+UseSerialGC

示例

java -XX:+UseSerialGC MyApplication

2. Parallel Garbage Collector (Throughput Collector)

适用场景:

  • 多核处理器。
  • 需要高吞吐量的应用程序。
  • 对延迟要求不高。

特点:

  • 多线程:使用多个线程并行执行垃圾回收,减少 STW 时间。
  • 高吞吐量:适合需要最大化 CPU 使用率的批处理作业。
  • 可配置性:可以通过参数调整线程数和堆大小。

启用参数:

-XX:+UseParallelGC

示例

java -XX:+UseParallelGC -XX:ParallelGCThreads=4 -Xmx2g MyApplication

3. Concurrent Mark-Sweep (CMS) Garbage Collector

适用场景:

  • 需要低延迟的应用程序。
  • 实时系统或交互式应用。
  • 内存较大且对停顿时间敏感。

特点:

  • 并发收集:大部分工作在应用程序运行时并发执行,减少 STW 时间。
  • 低停顿:适合需要快速响应的应用程序。
  • 碎片化问题:长时间运行可能导致严重的内存碎片化。
  • 已弃用:从 JDK 9 开始,CMS 收集器已被标记为过时,并将在未来的版本中移除。

启用参数:

-XX:+UseConcMarkSweepGC

示例

java -XX:+UseConcMarkSweepGC -Xmx2g MyApplication

4. G1 Garbage Collector

适用场景:

  • 大型堆内存。
  • 需要平衡吞吐量和延迟的应用程序。
  • 对停顿时间有一定要求。

特点:

  • 分区收集:将堆内存划分为多个区域(regions),按需进行垃圾回收。
  • 并发与并行:结合了 CMS 和 Parallel GC 的优点,支持并发标记和并行清除。
  • 预测性:能够预测未来的停顿时间,动态调整回收行为。
  • 低停顿:适合需要低延迟的应用程序。
  • 自动调优:内置了许多自适应机制,减少了手动调优的需求。

启用参数:

-XX:+UseG1GC

示例

java -XX:+UseG1GC -Xmx4g -XX:MaxGCPauseMillis=200 MyApplication

5. Z Garbage Collector (ZGC)

适用场景:

  • 极大堆内存(数百 GB 到 TB)。
  • 对停顿时间有严格要求的实时系统。
  • 高吞吐量需求。

特点:

  • 低停顿:承诺不超过 10 毫秒的停顿时间,即使在大规模堆内存下也是如此。
  • 并发收集:几乎所有的垃圾回收工作都是并发执行的。
  • 压缩堆:消除内存碎片,提高内存利用率。
  • 低延迟:非常适合需要极低延迟的应用程序。
  • 实验性:从 JDK 11 开始引入,仍在不断发展和完善。

启用参数:

-XX:+UseZGC

示例

java -XX:+UseZGC -Xmx1t MyApplication

6. Shenandoah Garbage Collector

适用场景:

  • 大型堆内存。
  • 对停顿时间有严格要求的实时系统。
  • 高吞吐量需求。

特点:

  • 低停顿:承诺不超过 10 毫秒的停顿时间。
  • 并发收集:大多数垃圾回收工作是并发执行的。
  • 压缩堆:通过压缩技术减少内存碎片。
  • 无分代:采用不分代的设计,简化了垃圾回收过程。
  • 成熟稳定:经过多年的开发和优化,已在生产环境中广泛应用。

启用参数:

-XX:+UseShenandoahGC

示例

java -XX:+UseShenandoahGC -Xmx4g MyApplication

7. Epsilon Garbage Collector (NoOp GC)

适用场景:

  • 测试和基准测试。
  • 不需要垃圾回收的应用程序(如嵌入式系统)。

特点:

  • 无操作:不进行任何垃圾回收,适用于不需要垃圾回收的特殊场景。
  • 内存泄漏:如果不小心使用,可能会导致内存泄漏。
  • 简单高效:适用于简单的测试环境或特定应用场景。

启用参数:

-XX:+UseEpsilonGC

示例

java -XX:+UseEpsilonGC -Xmx512m MyApplication

总结

选择合适的垃圾回收器取决于具体的应用需求和硬件资源。以下是一些常见的选择建议:

  • 单线程或小型应用:使用 Serial GC。
  • 多核处理器、高吞吐量需求:使用 Parallel GC。
  • 低延迟、实时系统:考虑 G1 GC 或 Shenandoah GC。
  • 极大堆内存、超低延迟:使用 ZGC 或 Shenandoah GC。
  • 测试和基准测试:使用 Epsilon GC。
;