GC垃圾收集时用户线程的运行情况详解

后端 潘老师 7小时前 5 ℃ (0)

Java开发的面试中,经常会被问到一个有趣的问题:“当GC垃圾收集时,是不是所有的用户线程都停止了呢?”今天咱们就来深入探讨一下这个问题。实际上,在GC垃圾收集的过程中,执行本地代码的线程依然可以运行。那么,这些线程要是改变了对象中的引用关系,或者创建了新的对象,会不会导致GC出现错误,进而引发一系列问题呢?下面通过实际例子来一探究竟。

一、证明GC期间执行native函数的线程仍在运行

为了证明在GC期间,执行native函数的线程仍然在运行,我们编写一个JVMTIAgent。这个Agent在Java虚拟机启动时,通过-agentpath来挂载。在Agent里,用C/C++实现一个native方法。具体代码如下:

#include "include/cn_hotspotvm_TestJNI.h"

#include <jvmti.h>
#include <stdio.h>
#include "pthread.h"

// 垃圾收集开始时回调
static void JNICALL GarbageCollectionStart(jvmtiEnv *jvmti) {}
// 垃圾收集结束时回调
static void JNICALL GarbageCollectionFinish(jvmtiEnv *jvmti) {}

JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *vm, char *options, void *reserved) {
    jvmtiEnv *jvmti = NULL;
    jvmtiCapabilities capabilities = {0};
    jvmtiEventCallbacks callbacks = {0};
    jint result;

    // 1.获取JVMTI环境
    if (vm->GetEnv((void **) &jvmti, JVMTI_VERSION_1_2) != JNI_OK) {
        fprintf(stderr, "Failed to get JVMTI environment\n");
        return JNI_ERR;
    }

    // 2.设置事件回调
    callbacks.GarbageCollectionStart = &GarbageCollectionStart;
    callbacks.GarbageCollectionFinish = &GarbageCollectionFinish;
    if ((result = jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks))) != JVMTI_ERROR_NONE) {
        fprintf(stderr, "SetEventCallbacks failed: %d\n", result);
        return JNI_ERR;
    }

    // 3.启用GC事件通知能力
    capabilities.can_generate_garbage_collection_events = 1;
    if ((result = jvmti->AddCapabilities(&capabilities)) != JVMTI_ERROR_NONE) {
        fprintf(stderr, "AddCapabilities failed: %d\n", result);
        return JNI_ERR;
    }

    // 4.注册事件监听
    if ((result = jvmti->SetEventNotificationMode(JVMTI_ENABLE,
          JVMTI_EVENT_GARBAGE_COLLECTION_START, NULL)) != JVMTI_ERROR_NONE) {
        fprintf(stderr, "Enable GC start failed: %d\n", result);
        return JNI_ERR;
    }
    if ((result = jvmti->SetEventNotificationMode(JVMTI_ENABLE,
           JVMTI_EVENT_GARBAGE_COLLECTION_FINISH, NULL)) != JVMTI_ERROR_NONE) {
        fprintf(stderr, "Enable GC finish failed: %d\n", result);
        return JNI_ERR;
    }

    return JNI_OK;
}

在这个代码里,首先获取JVMTI环境,接着设置事件回调,让程序在垃圾收集开始和结束时能执行相应的操作。然后启用GC事件通知能力,并注册事件监听,这样就能在垃圾收集的不同阶段触发相应的回调函数了。

在HotSpot中,是通过JvmtiGCMarker类来完成回调的。在JvmtiGCMarker类的构造函数里回调GC开始函数,在析构函数里调用GC结束函数。具体代码如下:

JvmtiGCMarker::JvmtiGCMarker() {
  if (JvmtiExport::should_post_garbage_collection_start()) {
    JvmtiExport::post_garbage_collection_start();
  }
}

JvmtiGCMarker::~JvmtiGCMarker() {
  if (JvmtiExport::should_post_garbage_collection_finish()) {
    JvmtiExport::post_garbage_collection_finish();
  }
}

void JvmtiExport::post_garbage_collection_start() {
  Thread* thread = Thread::current(); // this event is posted from vm-thread.
  JvmtiEnvIterator it;
  for (JvmtiEnv* env = it.first(); env != NULL; env = it.next(env)) {
    if (env->is_enabled(JVMTI_EVENT_GARBAGE_COLLECTION_START)) {
      JvmtiThreadEventTransition jet(thread);
      jvmtiEventGarbageCollectionStart callback = env->callbacks()->GarbageCollectionStart;
      if (callback != NULL) {
        (*callback)(env->jvmti_external());
      }
    }
  }
}

void JvmtiExport::post_garbage_collection_finish() {
  Thread *thread = Thread::current();
  JvmtiEnvIterator it;
  for (JvmtiEnv* env = it.first(); env != NULL; env = it.next(env)) {
    if (env->is_enabled(JVMTI_EVENT_GARBAGE_COLLECTION_FINISH)) {
      JvmtiThreadEventTransition jet(thread);
      jvmtiEventGarbageCollectionFinish callback = env->callbacks()->GarbageCollectionFinish;
      if (callback != NULL) {
        (*callback)(env->jvmti_external());
      }
    }
  }
}

JvmtiGCMarker类的使用过程是这样的:当VMThread获取到垃圾收集任务时,YGC会执行PSScavenge::invoke_no_policy(),FGC会执行PSParallelCompact::invoke_no_policy(),不管是YGC还是FGC,都会由VM_ParallelGCFailedAllocation::doit()函数调用。在VM_ParallelGCFailedAllocation::doit()函数里,会在VMThread线程进入函数时,调用SvgGCMarker的构造函数,在函数返回前,调用析构函数。这里要注意,VMThread是在安全点内完成GC开始函数和结束函数的回调的,正常情况下,此时业务线程应该不再运行了。

接下来看完整的实例代码:

package cn.hotspotvm;

public class TestJNI {
    public native int inc(int value);

    public static void main(String[] args) throws InterruptedException {

        new Thread(() -> {
            try {
                // 等待下面的inc()函数调用
                Thread.sleep(2000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 在inc()函数调用后触发FGC
            System.gc();
        }).start();

        // 传入0,在native函数中会加数值后返回
        int r = new TestJNI().inc(0);
        System.out.println(r);
    }
}
WaitableMutex mutex; // 互斥锁
static bool volatile isEnd = false;

JNIEXPORT jint JNICALL Java_cn_hotspotvm_TestJNI_inc
        (JNIEnv *env, jobject obj, jint value) {
    mutex.lock();
    mutex.wait();
    while(!isEnd){
        value++;
    }
    mutex.unlock();
    return value;
}

static void JNICALL GarbageCollectionStart(jvmtiEnv *jvmti) {
    mutex.notify();
}

static void JNICALL GarbageCollectionFinish(jvmtiEnv *jvmti) {
    isEnd = true;
}

在这个实例中,main线程先执行Java_cn_hotspotvm_TestJNI_inc()函数,导致main()函数在wait()处等待。另一个线程调用System.gc()后,VMThread线程会调用回调函数GarbageCollectionStart(),让main()线程开始执行加一的逻辑。在GC结束时,停止加1逻辑,并返回结果。在本地机器上运行,某次的结果为3699329 ,这就证明了在GC垃圾回收期间,执行native函数的线程确实在运行。

二、native线程操作Java对象的影响及处理方式

既然知道了native线程在GC期间可以运行,那么如果它操作了Java对象,会不会引起应用程序错误呢?其实,按照规则,native函数原则上不允许直接操作Java对象。要是真的需要操作,只能通过JNI来进行。JNI里定义了很多操作Java对象的方法,比如下面这个创建对象的例子:

JNIEXPORT jobject JNICALL Java_cn_hotspotvm_TestJNI_createObject(JNIEnv *env, jobject) {
    // 1. 获取jclass
    jclass clazz = env->FindClass("cn/hotspotvm/TestJNI");

    // 2. 获取构造函数ID
    jmethodID constructorId = env->GetMethodID(clazz, "<init>", "()V");

    // 3. 创建对象
    jobject obj = env->NewObject(clazz, constructorId);
    return obj;
}

在调用NewObject()函数时,因为涉及到Java对象,所以当这个线程进入HotSpot世界时,如果GC垃圾收集还在进行,当前线程会被阻塞,直到GC完成后才会被唤醒,然后继续执行。通过JNI接口,能保证线程不会干扰到GC的正常工作。

不过,有时候为了提高效率,native中还是可以直接操作Java对象的,但在操作前,需要先进入临界区。比如下面这个对int数组每个元素加1的例子:

public class TestJNI {
    // 对int数组每个元素+1
    public native void processIntArray(int[] array);
} 
#include <jni.h>

JNIEXPORT void JNICALL Java_cn_hotspotvm_NativeArrayProcessor_processIntArray(
    JNIEnv *env, jobject obj, jintArray arr) {

    jint *c_array = NULL;
    jboolean isCopy = JNI_FALSE;

    // 1. 进入临界区获取数组指针
    c_array = (jint*) env->GetPrimitiveArrayCritical(arr, &isCopy);
    if (c_array == NULL) {
        return; // 内存不足或JVM不支持时返回NULL
    }

    // 2. 操作数组(临界区内禁止调用其他JNI函数!)
    jsize length = env->GetArrayLength(arr);
    for (int i = 0; i < length; i++) {
        c_array[i] += 1; // 每个元素+1
    }

    // 3. 退出临界区(必须严格配对调用)
    env->ReleasePrimitiveArrayCritical(arr, c_array, 0);
}

在这个例子中,通过GetPrimitiveArrayCritical()进入临界区,返回一个直接指向堆中数组首地址的指针,在临界区内对数组进行操作,操作完成后,通过ReleasePrimitiveArrayCritical()退出临界区。这里要注意,在临界区内禁止调用其他JNI函数。

为什么要设置临界区呢?假如在返回数组首地址时,GC把数组移动到了其他地方,那么在native中操作的数组就成了无效数组,会导致错误。当线程进入临界区时,会阻塞GC垃圾收集,当最后一个线程离开时,会触发一个原因为_gc_locker的GC垃圾收集。临界区的存在,主要是为了让native线程能更高效地操作数组。要是没有临界区,在操作数组时,就需要把数组拷贝到C堆上再进行操作,如果数组很大,会严重影响应用程序的效率。

此外,句柄也是一个重要的设计。句柄是一种间接引用,和直接引用不同,它把所有引用集中在句柄区,这样GC就能更高效地扫描。native函数通过句柄可以安全地操作对象。比如,假设GC把对象Oop1从Eden区移动到了To区,只需要更新句柄中封装的引用地址就可以了,这样就能保证native函数对对象的操作不会出错。

通过以上的分析和实例,相信大家对GC垃圾收集时用户线程的运行情况,以及native线程操作Java对象的相关问题有了更深入的理解。这部分知识在Java开发中非常重要,尤其是在性能优化和底层原理探究方面,希望大家能好好掌握。


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/back/18027.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】