GC垃圾收集时用户线程的运行情况详解
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 environmentn"); return JNI_ERR; } // 2.设置事件回调 callbacks.GarbageCollectionStart = &GarbageCollectionStart; callbacks.GarbageCollectionFinish = &GarbageCollectionFinish; if ((result = jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks))) != JVMTI_ERROR_NONE) { fprintf(stderr, "SetEventCallbacks failed: %dn", 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: %dn", 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: %dn", 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: %dn", 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开发中非常重要,尤其是在性能优化和底层原理探究方面,希望大家能好好掌握。