掘金 后端 ( ) • 2024-04-01 20:04
gc

本文基于 OpenJDK17 进行讨论

在 JDK NIO 针对堆外内存的分配场景中,我们经常会看到 System.gc 的身影,比如当我们通过 FileChannel#map 对文件进行内存映射的时候,如果 JVM 进程虚拟内存空间中的虚拟内存不足,JVM 在 native 层就会抛出 OutOfMemoryError

当 JDK 捕获到 OutOfMemoryError 异常的时候,就会意识到此时进程虚拟内存空间中的虚拟内存已经不足了,无法支持本次内存映射,于是就会调用 System.gc 强制触发一次 GC ,试图释放一些虚拟内存出来,然后再次尝试来 mmap 一把,如果进程地址空间中的虚拟内存还是不足,则抛出 IOException

private Unmapper mapInternal(MapMode mode, long position, long size, int prot, boolean isSync)
        throws IOException
{
            try {
                    // If map0 did not throw an exception, the address is valid
                    addr = map0(prot, mapPosition, mapSize, isSync);
                } catch (OutOfMemoryError x) {
                    // An OutOfMemoryError may indicate that we've exhausted
                    // memory so force gc and re-attempt map
                    System.gc();
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException y) {
                        Thread.currentThread().interrupt();
                    }
                    try {
                        addr = map0(prot, mapPosition, mapSize, isSync);
                    } catch (OutOfMemoryError y) {
                        // After a second OOME, fail
                        throw new IOException("Map failed", y);
                    }
              }

}

再比如,我们通过 ByteBuffer#allocateDirect 申请堆外内存的时候

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>
{
   public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }
}

会首先通过 Bits.reserveMemory 检查当前 JVM 进程的堆外内存用量是否超过了 -XX:MaxDirectMemorySize 指定的最大堆外内存限制,通过检查之后才会调用 UNSAFE.allocateMemory 申请堆外内存。

class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer
{
    DirectByteBuffer(int cap) {                   // package-private

        ...... 省略 .....   
        // 检查堆外内存整体用量是否超过了 -XX:MaxDirectMemorySize
        // 如果超过则尝试对堆外内存进行回收,回收之后还不够的话则抛出 OutOfMemoryError
        Bits.reserveMemory(size, cap);   
        // 底层调用 malloc 申请虚拟内存
        base = UNSAFE.allocateMemory(size);

        ...... 省略 .....   
    }
}

如果没有通过 Bits.reserveMemory 的检查,则 JVM 先会尝试通过释放当前已经被回收的 direct buffer 背后引用的 native memory 挽救一下,如果释放之后堆外内存容量还是不够,那么就触发 System.gc()。

class Bits {  
    static void reserveMemory(long size, long cap) {
        ...... 省略 .......
        // 如果本次申请的堆外内容容量 cap 已经超过了 -XX:MaxDirectMemorySize
        // 则返回 false,表示无法满足本次堆外内存的申请
        if (tryReserveMemory(size, cap)) {
            return;
        }

        ...... 尝试释放已经被回收的 directBuffer 背后的 native memory  .......
        // 在已经被回收的 direct buffer 背后引用的 native memory 被释放之后
        // 如果还是不够,则走到这里
        System.gc();
        Thread.sleep(sleepTime);

        ...... 省略 .......
    }
}

通常情况下我们应当避免在应用程序中主动调用 System.gc,因为这会导致 JVM 立即触发一次 Full GC,使得整个 JVM 进程陷入到 Stop The World 阶段,对性能会有很大的影响。

但是在 NIO 的场景中,调用 System.gc 却是有必要的,因为 NIO 中的 DirectByteBuffer 非常特殊,当然了 MappedByteBuffer 其实也属于 DirectByteBuffer 的一种。它们背后依赖的内存均属于 JVM 之外(Native Memory),因此不会受垃圾回收的控制。

前面我们多次提过,DirectByteBuffer 只是 OS 中的这些 Native Memory 在 JVM 中的封装形式,DirectByteBuffer 这个 Java 类的实例是分配在 JVM 堆中的,但是这个实例的背后可能会引用着一大片的 Native Memory ,这些 Native Memory 是不会被 JVM 察觉的。

当这些 DirectByteBuffer 实例(位于 JVM 堆中)没有任何引用的时候,如果又恰巧碰到 GC 的话,那么 GC 在回收这些 DirectByteBuffer 实例的同时,也会将与其关联的 Cleaner 放到一个 pending 队列中。

    protected DirectByteBuffer(int cap, long addr,
                                     FileDescriptor fd,
                                     Runnable unmapper,
                                     boolean isSync, MemorySegmentProxy segment)
    {
        super(-1, 0, cap, cap, fd, isSync, segment);
        address = addr;
        // 对于 MappedByteBuffer 来说,在它被 GC 的时候,JVM 会调用这里的 cleaner
        // cleaner 近而会调用 Unmapper#unmap 释放背后的 native memory
        cleaner = Cleaner.create(this, unmapper);
        att = null;
    }

当 GC 结束之后,JVM 会唤醒 ReferenceHandler 线程去执行 pending 队列中的这些 Cleaner,在 Cleaner 中会释放其背后引用的 Native Memory。

但在现实的 NIO 使用场景中,DirectByteBuffer 却很难触发 GC,因为 DirectByteBuffer 的实例实在太小了(在 JVM 堆中的内存占用),而且通常情况下这些实例是被应用程序长期持有的,很容易就会晋升到老年代。

即使 DirectByteBuffer 实例已经没有任何引用关系了,由于它的实例足够的小,一时很难把老年代撑爆,所以需要等很久才能触发一次 Full GC,在这之前,这些没有任何引用关系的 DirectByteBuffer 实例将会持续在老年代中堆积,其背后所引用的大片 Native Memory 将一直不会得到释放。

DirectByteBuffer 的实例可以形象的比喻为冰山对象,JVM 可以看到的只是 DirectByteBuffer 在 JVM 堆中的内存占用,但这部分内存占用很小,就相当于是冰山的一角。

image

而位于冰山下面的大一片 Native Memory ,JVM 是察觉不到的, 这也是 Full GC 迟迟不会触发的原因,因此导致了大量的 DirectByteBuffer 实例的堆积,背后引用的一大片 Native Memory 一直得不到释放,严重的情况下可能会导致内核的 OOM,当前进程会被 kill 。

所以在 NIO 的场景下,这里调用 System.gc 去主动触发一次 Full GC 是有必要的。关于 System.gc ,网上的说法众多,其中大部分认为 —— “System.gc 只是给 JVM 的一个暗示或者是提示,但是具体 GC 会不会发生,以及什么时候发生都是不可预期的”。

这个说法以及 Java 标准库中关于 System.gc 的注释都是非常模糊的,那么在 System.gc 被调用之后具体会发生什么行为,我想还是应该到具体的 JVM 实现中去一探究竟,毕竟源码面前了无秘密,下面我们以 hotspot 实现进行说明。

public final class System {
   public static void gc() {
        Runtime.getRuntime().gc();
    }
}

public class Runtime {
   public native void gc();
}

System.gc 最终依赖的是 Runtime 类中定义的 gc 方法,该方法是一个 native 实现,定义在 Runtime.c 文件中。

// Runtime.c 文件
JNIEXPORT void JNICALL
Java_java_lang_Runtime_gc(JNIEnv *env, jobject this)
{
    JVM_GC();
}
// jvm.cpp 文件
JVM_ENTRY_NO_ENV(void, JVM_GC(void))
  // DisableExplicitGC 默认为 false,如果设置了 -XX:+DisableExplicitGC 则为 true
  if (!DisableExplicitGC) {
    EventSystemGC event;
    event.set_invokedConcurrent(ExplicitGCInvokesConcurrent);
    // 立即触发一次  full gc
    Universe::heap()->collect(GCCause::_java_lang_system_gc);
    event.commit();
  }
JVM_END

从 hotspot 的实现中我们可以看出,如果我们设置了 -XX:+DisableExplicitGC,那么调用 System.gc 则不会起任何作用,在默认情况下,System.gc 会立即触发一次 Full GC,这一点我们可以从 Universe::heap()->collect 方法的调用看得出来。而且会特殊注明引起本次 GC 的原因 GCCause 为 _java_lang_system_gc

JVM 堆的实例封装在 Universe 类中,我们可以通过 heap() 方法来获取 JVM 堆的实例,随后调用堆的 collect 方法在 JVM 堆中执行垃圾回收的动作。

// universe.hpp 文件
// jvm 堆实例
static CollectedHeap* _collectedHeap;
static CollectedHeap* heap() { return _collectedHeap; }

Java 堆在 JVM 源码中使用 CollectedHeap 类型来描述,该类型为整个 JVM 堆结构类型的基类,具体的实现类型取决于我们选择的垃圾回收器。比如,当我们选择 ZGC 作为垃圾回收器时,JVM 堆的类型是 ZCollectedHeap,选择 G1 作为垃圾回收器时,JVM 堆的类型则是 G1CollectedHeap。

JVM 在初始化堆的时候,会通过 GCConfig::arguments()->create_heap() 根据我们选择的具体垃圾回收器来创建相应的堆类型,具体的 JVM 堆实例会保存在 _collectedHeap 中,后续通过 Universe::heap() 即可获取。

// universe.cpp 文件
// jvm 堆实例
CollectedHeap*  Universe::_collectedHeap = NULL;

jint Universe::initialize_heap() {
  assert(_collectedHeap == NULL, "Heap already created");
  // 根据 JVM 参数  -XX: 指定的相关 gc 配置创建对应的 heap
  // 比如,设置了 -XX:+UseZGC,这里创建的就是 ZCollectedHeap
  _collectedHeap = GCConfig::arguments()->create_heap();

  log_info(gc)("Using %s", _collectedHeap->name());
  // 初始化 jvm 堆
  return _collectedHeap->initialize();
}

GCConfig 是 JVM 专门用于封装 GC 相关信息的类,具体创建堆的行为 —— create_heap(),则封装在 GCConfig 类中的 _arguments 属性中(GCArguments 类型)。这里也是一样,不同的垃圾回收器对应不同的 GCArguments,比如,ZGC 对应的是 ZArguments,G1 对应的是 G1Arguments。典型工厂,策略模式的应用,不同的 GCArguments 负责创建不用类型的 JVM 堆。

// gcConfig.cpp 文件
GCArguments* GCConfig::arguments() {
  assert(_arguments != NULL, "Not initialized");
  // 真正负责创建 jvm 堆的类
  return _arguments;
}

JVM 在启动的时候会对 GCConfig 进行初始化,通过 select_gc() 根据我们指定的 -XX: 相关 GC 配置选项来选择具体的 _arguments,比如,我们设置了 -XX:+UseZGC, 这里的 select_gc 就会返回 ZArguments 实例,并保存在 _arguments 属性中,随后我们就可以通过 GCConfig::arguments() 获取。

void GCConfig::initialize() {
  assert(_arguments == NULL, "Already initialized");
  _arguments = select_gc();
}

select_gc() 的逻辑其实非常简单,核心就是遍历一个叫做 IncludedGCs 的数组,该数组里包含的是当前 JVM 版本中所支持的所有垃圾回收器集合。比如,当我们通过 command line 指定了 -XX:+UseZGC 的时候,相关的 GC 参数 UseZGC 就会为 true,其他的 GC 参数都为 false,如果 JVM 在遍历 IncludedGCs 数组的时候发现,当前遍历元素的 GC 参数为 true,那么就会将对应的 _arguments (zArguments)返回。

// gcConfig.cpp 文件
// Table of included GCs, for translating between command
// line flag, CollectedHeap::Name and GCArguments instance.
static const IncludedGC IncludedGCs[] = {
   EPSILONGC_ONLY_ARG(IncludedGC(UseEpsilonGC,       CollectedHeap::Epsilon,    epsilonArguments,    "epsilon gc"))
        G1GC_ONLY_ARG(IncludedGC(UseG1GC,            CollectedHeap::G1,         g1Arguments,         "g1 gc"))
  PARALLELGC_ONLY_ARG(IncludedGC(UseParallelGC,      CollectedHeap::Parallel,   parallelArguments,   "parallel gc"))
    SERIALGC_ONLY_ARG(IncludedGC(UseSerialGC,        CollectedHeap::Serial,     serialArguments,     "serial gc"))
SHENANDOAHGC_ONLY_ARG(IncludedGC(UseShenandoahGC,    CollectedHeap::Shenandoah, shenandoahArguments, "shenandoah gc"))
         ZGC_ONLY_ARG(IncludedGC(UseZGC,             CollectedHeap::Z,          zArguments,          "z gc"))
};

IncludedGCs 数组的元素类型为 IncludedGC,用于封装具体垃圾回收器的相关配置信息:

// gcConfig.cpp 文件
struct IncludedGC {
  // GCArgument,如果我们通过 command line 配置了具体的垃圾回收器
  // 那么对应的 IncludedGC 类型中的 _flag 就为 true。
  // -XX:+UseG1GC 对应 UseG1GC,-XX:+UseZGC 对应 UseZGC
  bool&               _flag;
  // 具体垃圾回收器的名称
  CollectedHeap::Name _name;
  // 对应的 GCArguments,后续用于 create_heap
  GCArguments&        _arguments;
  const char*         _hs_err_name;
};

select_gc() 就是遍历这个 IncludedGCs 数组,查找 _flag 为 true 的数组项,然后返回其 _arguments。

GCArguments* GCConfig::select_gc() {
  // 遍历 IncludedGCs 数组
  FOR_EACH_INCLUDED_GC(gc) {
    // GCArgument 为 true 则返回对应的 _arguments
    if (gc->_flag) {
      return &gc->_arguments;
    }
  }
  return NULL;
}

#define FOR_EACH_INCLUDED_GC(var)                                            \
  for (const IncludedGC* var = &IncludedGCs[0]; var < &IncludedGCs[ARRAY_SIZE(IncludedGCs)]; var++)

当我们通过设置 -XX:+UseG1GC 选择 G1 垃圾回收器的时候,对应在 GCConfig 中的 _arguments 为 G1Arguments ,通过 GCConfig::arguments()->create_heap() 创建出来的 JVM 堆的类型为 G1CollectedHeap。

CollectedHeap* G1Arguments::create_heap() {
  return new G1CollectedHeap();
}

同理,当我们通过设置 -XX:+UseZGC 选择 ZGC 垃圾回收器的时候,JVM 堆的类型为 ZCollectedHeap。

CollectedHeap* ZArguments::create_heap() {
  return new ZCollectedHeap();
}

当我们通过设置 -XX:+UseSerialGC 选择 SerialGC 垃圾回收器的时候,JVM 堆的类型为 SerialHeap。

CollectedHeap* SerialArguments::create_heap() {
  return new SerialHeap();
}

当我们通过设置 -XX:+UseParallelGC 选择 ParallelGC 垃圾回收器的时候,JVM 堆的类型为 ParallelScavengeHeap。

CollectedHeap* ParallelArguments::create_heap() {
  return new ParallelScavengeHeap();
}

当我们通过设置 -XX:+UseShenandoahGC 选择 Shenandoah 垃圾回收器的时候,JVM 堆的类型为 ShenandoahHeap。

CollectedHeap* ShenandoahArguments::create_heap() {
  return new ShenandoahHeap(new ShenandoahCollectorPolicy());
}

现在我们已经明确了各个垃圾回收器对应的 JVM 堆类型,而 System.gc 本质上调用的其实就是具体 JVM 堆中的 collect 方法来立即触发一次 Full GC。

// jvm.cpp 文件
JVM_ENTRY_NO_ENV(void, JVM_GC(void))
  if (!DisableExplicitGC) {
    Universe::heap()->collect(GCCause::_java_lang_system_gc);
  }
JVM_END

下面我们就来结合具体的垃圾回收器看一下 System.gc 的行为,长话短说,先把结论抛出来:

  • 如果我们在 command line 中设置了 -XX:+DisableExplicitGC,那么调用 System.gc 则不会起任何作用。

  • 如果我们选择的垃圾回收器是 SerialGC,ParallelGC,ZGC 的话,那么调用 System.gc 就会立即触发一次 Full GC,整个 JVM 进程会陷入 Stop The World 阶段,调用 System.gc 的线程会一直阻塞,直到整个 Full GC 结束才会返回。

  • 如果我们选择的垃圾回收器是 CMS(已在 Java 9 中废弃),G1,Shenandoah,并且在 command line 中设置了 -XX:+ExplicitGCInvokesConcurrent 的话,那么在调用 System.gc 则会立即触发一次 Concurrent Full GC,JVM 进程不会陷入 Stop The World 阶段,业务线程和 GC 线程可以并发运行,而且调用 System.gc 的线程在触发 Concurrent Full GC 之后就立即返回了,不需要等到 GC 结束。

1. SerialGC

对于 SerialGC 来说,在调用 System.gc 之后,JVM 背后其实直接调用的是 SerialHeap 的 collect 方法。

// serialHeap.hpp 文件
class SerialHeap : public GenCollectedHeap {

}

由于 SerialHeap 继承的是 GenCollectedHeap,collect 方法是在 GenCollectedHeap 中实现的。

// genCollectedHeap.cpp 文件
void GenCollectedHeap::collect(GCCause::Cause cause) {
    // GCCause 为 _java_lang_system_gc 的时候会调用到这里
    // Stop-the-world full collection.
    collect(cause, OldGen);
}
void GenCollectedHeap::collect(GCCause::Cause cause, GenerationType max_generation) {
  collect_locked(cause, max_generation);
}

void GenCollectedHeap::collect_locked(GCCause::Cause cause, GenerationType max_generation) {
    // 在这里会触发 Full Gc 的运行
    VM_GenCollectFull op(gc_count_before, full_gc_count_before,
                         cause, max_generation);
    // 提交给 VMThread 来执行 Full Gc
    VMThread::execute(&op);
}

这里需要注意的是执行这段代码的线程依然是调用 System.gc 的 Java 业务线程,而 JVM 内部的相关操作,比如这里的 GC 操作,均是由 JVM 中的 VMThread 来执行的。

所以这里 Java 业务线程需要将 Full Gc 的任务 —— VM_GenCollectFull 通过 VMThread::execute(&op) 提交给 VMThread 来执行。而 Java 业务线程一直会在这里阻塞等待,直到 VMThread 执行完 Full Gc 之后,Java 业务线程才会从 System.gc 调用中返回。

这样设计也是合理的,因为毕竟 Full Gc 会让整个 JVM 进程陷入 Stop The World 阶段,所有 Java 线程必须到达 SafePoint 之后 Full Gc 才会执行,而我们通过 JNI 进入到 Native 方法的实现之后,由于 Native 代码不会访问 Java 对象、不会调用 Java 方法,不再执行任何字节码指令,所以 Java 虚拟机的堆栈不会发生改变,因此 Native 方法本身就是一个 SafePoint。在 Full Gc 没有结束之前,Java 线程会一直停留在这个 SafePoint 中。

void VMThread::execute(VM_Operation* op) {
  // 获取当前执行线程
  Thread* t = Thread::current();

  if (t->is_VM_thread()) {
    // 如果当前线程是 VMThread 的话,直接执行 VM_Operation(Full Gc)
    ((VMThread*)t)->inner_execute(op);
    return;
  }

  // doit_prologue 为执行 VM_Operation 的前置回调函数,Full Gc 之前执行一些准备校验工作。
  // 返回 true 表示可以执行本次 GC 操作, 返回 false 表示忽略本次 GC
  // JVM 可能会触发多次 GC 请求,比如多个 java 线程遇到分配失败的时候
  // 但我们只需要执行一次 GC 就可以了,其他 GC 请求在这里就会被忽略
  // 另外执行 GC 之前需要给 JVM 堆加锁,heap lock 也是在这里完成的。
  if (!op->doit_prologue()) {
    return;   // op was cancelled
  }
  // java 线程将 Full Gc 的任务提交给 VMThread 执行
  // 并且会在这里一直阻塞等待,直到 Full Gc 执行完毕。
  wait_until_executed(op);
  // 释放 heap lock,唤醒 ReferenceHandler 线程去执行 pending 队列中的 Cleaner
  op->doit_epilogue();
}

注意这里的 op->doit_epilogue() 方法,在 GC 结束之后就会调用到这里,而与 DirectByteBuffer 相关联的 Cleaner 正是在这里被触发执行的。

void VM_GC_Operation::doit_epilogue() {

  if (Universe::has_reference_pending_list()) {
    // 通知 cleaner thread 执行 cleaner,release native memory
    Heap_lock->notify_all();
  }
  // Heap_lock->unlock()
  VM_GC_Sync_Operation::doit_epilogue();
}

2. ParallelGC

对于 ParallelGC 来说,在调用 System.gc 之后,JVM 背后其实直接调用的是 ParallelScavengeHeap 的 collect 方法。

// This method is used by System.gc() and JVMTI.
void ParallelScavengeHeap::collect(GCCause::Cause cause) {
 
  VM_ParallelGCSystemGC op(gc_count, full_gc_count, cause);
  VMThread::execute(&op);
}

我们通过下面的 is_cause_full 方法可以知道 VM_ParallelGCSystemGC 执行的也是 Full Gc,同样也是需要将 Full Gc 任务提交给 VMThread 执行,Java 业务线程在这里阻塞等待直到 Full Gc 完成。

// Only used for System.gc() calls
VM_ParallelGCSystemGC::VM_ParallelGCSystemGC(uint gc_count,
                                             uint full_gc_count,
                                             GCCause::Cause gc_cause) :
  VM_GC_Operation(gc_count, gc_cause, full_gc_count, is_cause_full(gc_cause))
{
}
// 对于 System.gc  来说这里执行的是 full_gc
static bool is_cause_full(GCCause::Cause cause) {
  return (cause != GCCause::_gc_locker) && (cause != GCCause::_wb_young_gc)
         DEBUG_ONLY(&& (cause != GCCause::_scavenge_alot));
}

3. ZGC

对于 ZGC 来说,在调用 System.gc 之后,JVM 背后其实直接调用的是 ZCollectedHeap 的 collect 方法。JVM 会执行一个同步的 GC 操作,Java 业务线程仍然会在这里阻塞,直到 GC 完成才会返回。

// zCollectedHeap.cpp 文件
void ZCollectedHeap::collect(GCCause::Cause cause) {
  _driver->collect(cause);
}

// zDriver.cpp 文件
void ZDriver::collect(const ZDriverRequest& request) {
  switch (request.cause()) {
  // System.gc
  case GCCause::_java_lang_system_gc:
    // Start synchronous GC
    _gc_cycle_port.send_sync(request);
    break;

  ..... 省略 ,,,,,,
  }
}
template <typename T>
inline void ZMessagePort<T>::send_sync(const T& message) {
  Request request;

  {
    // Enqueue message
    // 随后 ZDriver 线程会异步从队列中取出 message,执行 gc
    MonitorLocker ml(&_monitor, Monitor::_no_safepoint_check_flag);
    request.initialize(message, _seqnum);
    _queue.insert_last(&request);
    // 唤醒 ZDriver 线程执行 gc
    ml.notify();
  }

  // java 业务线程在这里阻塞等待,直到 gc 完成
  request.wait();
}

4. G1

对于 G1 来说,在调用 System.gc 之后,JVM 背后其实直接调用的是 G1CollectedHeap 的 collect 方法。

// g1CollectedHeap.cpp 文件
void G1CollectedHeap::collect(GCCause::Cause cause) {
  try_collect(cause);
}

G1 这里首先会通过 should_do_concurrent_full_gc 方法判断是否发起一次 Concurrent Full GC,从下面的源码中可以看出,对于 System.gc 来说,该方法其实是对 ExplicitGCInvokesConcurrent 这个 GC 参数的判断。

当我们在 command line 中设置了 -XX:+ExplicitGCInvokesConcurrent 的话,ExplicitGCInvokesConcurrent 为 true,默认为 false。

bool G1CollectedHeap::should_do_concurrent_full_gc(GCCause::Cause cause) {
  switch (cause) {
    case GCCause::_g1_humongous_allocation: return true;
    case GCCause::_g1_periodic_collection:  return G1PeriodicGCInvokesConcurrent;
    case GCCause::_wb_breakpoint:           return true;
    // System.gc 会走这里的 default 分支
    default:                                return is_user_requested_concurrent_full_gc(cause);
  }
}

bool  G1CollectedHeap::is_user_requested_concurrent_full_gc(GCCause::Cause cause) {
  switch (cause) {
    // System.gc
    case GCCause::_java_lang_system_gc:                 return ExplicitGCInvokesConcurrent;

    ...... 省略 .....
  }
}

当我们设置了 -XX:+ExplicitGCInvokesConcurrent 的时候,System.gc 就会触发一次 Concurrent Full GC,GC 过程不需要经历 Stop The World 阶段,由 G1 相关的 Concurrent GC 线程来执行 Concurrent Full GC 而不是之前的 VMThread。

而且调用 System.gc 的 Java 业务线程在触发 Concurrent Full GC 之后就返回了,不需要等到 GC 执行完毕。

但在默认情况下,也就是没有设置 -XX:+ExplicitGCInvokesConcurrent 的时候,仍然会执行一次完整的 Full GC。

bool G1CollectedHeap::try_collect(GCCause::Cause cause) {
  assert_heap_not_locked();
  // -XX:+ExplicitGCInvokesConcurrent
  if (should_do_concurrent_full_gc(cause)) {
    // 由 Concurrent GC 线程来执行
    return try_collect_concurrently(cause,
                                    gc_count_before,
                                    old_marking_started_before);
  }  else {
    // Schedule a Full GC.
    VM_G1CollectFull op(gc_count_before, full_gc_count_before, cause);
    VMThread::execute(&op);
    return op.gc_succeeded();
  }
}

对于 CMS 来说,虽然它已经在 Java 9 中被废弃了,但从 Java 8 的源码中可以看出,CMS 这里的逻辑(System.gc )和 G1 是一样的,首先都会通过 should_do_concurrent_full_gc 方法来判断是否执行一次 Concurrent Full GC,都是取决于是否设置了 -XX:+ExplicitGCInvokesConcurrent ,否则执行完整的 Full GC。

5. Shenandoah

对于 Shenandoah 来说,在调用 System.gc 之后,JVM 背后其实直接调用的是 ShenandoahHeap 的 collect 方法。

void ShenandoahHeap::collect(GCCause::Cause cause) {
  control_thread()->request_gc(cause);
}

首先会通过 is_user_requested_gc 方法判断本次 GC 是否是由 System.gc 所触发的,如果是,则进入 handle_requested_gc 中处理,GCCause 为 java_lang_system_gc 。

// gcCause.hpp 文件
 inline static bool is_user_requested_gc(GCCause::Cause cause) {
    return (cause == GCCause::_java_lang_system_gc ||
            cause == GCCause::_dcmd_gc_run);
  }

如果我们在 command line 中设置了 -XX:+DisableExplicitGC,那么这里的 System.gc 将不会起任何作用。

// shenandoahControlThread.cpp
void ShenandoahControlThread::request_gc(GCCause::Cause cause) {
  assert(GCCause::is_user_requested_gc(cause) || ....... ,"only requested GCs here");
  // System.gc 
  if (is_explicit_gc(cause)) {
    if (!DisableExplicitGC) {
      // 没有设置 -XX:+DisableExplicitGC 的情况下会走这里
      handle_requested_gc(cause);
    }
  } else {
    handle_requested_gc(cause);
  }
}

bool ShenandoahControlThread::is_explicit_gc(GCCause::Cause cause) const {
  return GCCause::is_user_requested_gc(cause) ||
         GCCause::is_serviceability_requested_gc(cause);
}

调用 System.gc 的 Java 业务线程首先在 handle_requested_gc 方法中会设置 gc 请求标志 _gc_requested.set,ShenandoahControlThread 会定时检测这个 _gc_requested 标志,如果被设置了,则进行后续的 GC 处理。

Java 业务线程最后会一直阻塞在 handle_requested_gc 方法中,如果进行的是 Concurrent Full GC 的话,那么 GC 任务在被提交给对应的 Concurrent GC 线程之后就会唤醒 Java 业务线程。如果执行的是 Full GC 的话,那么当 VMthread 执行完 Full GC 的时候才会唤醒阻塞在这里的 Java 业务线程,随后 Java 线程从 System.gc 调用中返回。

void ShenandoahControlThread::handle_requested_gc(GCCause::Cause cause) {

  MonitorLocker ml(&_gc_waiters_lock);

  while (current_gc_id < required_gc_id) {
    // 设置 gc 请求标志,后续会由 ShenandoahControlThread 来执行
    _gc_requested.set();
    // java_lang_system_gc 
    _requested_gc_cause = cause;

    if (cause != GCCause::_wb_breakpoint) {
      // java 业务线程会在这里阻塞等待
      // 对于 Concurrent Full GC 来说,GC 在被触发的时候,java 线程就会被唤醒直接返回
      // 对于 Full GC 来说,java 线程需要等到 gc 被执行完才会被唤醒
      ml.wait();
    }
  }
}

ShenandoahControlThread 会根据一定的间隔时间来检测 _gc_requested 标志是否被设置,如果被设置则继续后续的 GC 处理:

  • 如果我们设置了 -XX:+ExplicitGCInvokesConcurrent,Shenandoah 会触发一次 Concurrent Full GC ,否则进行的是 Full GC ,这一点和 G1 的处理方式是一样的。

  • 最后通过 notify_gc_waiters() 唤醒在 handle_requested_gc 中阻塞等待的 java 线程。

void ShenandoahControlThread::run_service() {
  ShenandoahHeap* heap = ShenandoahHeap::heap();
  // 默认的一些设置,后面会根据配置修改
  GCMode default_mode = concurrent_normal;// 并发模式
  GCCause::Cause default_cause = GCCause::_shenandoah_concurrent_gc;

  while (!in_graceful_shutdown() && !should_terminate()) {
        // _gc_requested 如果被设置,后续则会处理  System.gc  的逻辑
        bool explicit_gc_requested = _gc_requested.is_set() &&  is_explicit_gc(_requested_gc_cause);
        // Choose which GC mode to run in. The block below should select a single mode.
        GCMode mode = none;

        if (explicit_gc_requested) {
             //  java_lang_system_gc
             cause = _requested_gc_cause;
             log_info(gc)("Trigger: Explicit GC request (%s)", GCCause::to_string(cause));
              // -XX:+ExplicitGCInvokesConcurrent
              if (ExplicitGCInvokesConcurrent) {
                    policy->record_explicit_to_concurrent();
                    // concurrent_normal 并发模式
                    mode = default_mode;
              } else {
                    policy->record_explicit_to_full();
                    mode = stw_full; // Full GC 模式
              }
        }

      switch (mode) {
        case concurrent_normal:
          // 由 concurrent gc 线程异步执行
          service_concurrent_normal_cycle(cause);
          break;
        case stw_full:
          // 触发 VM_ShenandoahFullGC ,由 VMthread 同步执行
          service_stw_full_cycle(cause);
          break;
        default:
          ShouldNotReachHere();
      }

      // If this was the requested GC cycle, notify waiters about it
      if (explicit_gc_requested || implicit_gc_requested) {
        // 唤醒在 handle_requested_gc 中阻塞等待的 java 线程
        notify_gc_waiters();
      }
  }
}