章
目
录
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开发中非常重要,尤其是在性能优化和底层原理探究方面,希望大家能好好掌握。