之前面试被问到过“当GC垃圾收集时,是所有的用户线程都停止了吗?”,这一篇我们来探究一下这个问题。
其实执行本地代码的线程仍然可以运行,那么这些线程一旦改变了对象中的引用关系或创建了新的对象,这会不会造成GC错误,引发问题呢?
首先举一个例子,证明在GC期间,执行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;
- }
复制代码 简单编写了一个JVMTIAgent,这个Agent在Java虚拟机启动时通过-agentpath来挂载,在这个Agent中可以写一个native方法的C/C++实现,当垃圾收集开始时执行用户线程的运算,当垃圾收集结束时停止运算并返回,这样就能很好的证明有线程在GC垃圾收集器期间发生GC了。
我们看一下,HotSpot是在什么时候进行回调呢?这主要是使用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::loop()
- VMThread::evaluate_operation()
- VM_Operation::evaluate()
- VM_ParallelGCFailedAllocation::doit()
- ParallelScavengeHeap::failed_mem_allocate()
- PSScavenge::invoke()
- PSScavenge::invoke_no_policy()
- PSParallelCompact::invoke_no_policy()
复制代码 在VMThread获取到垃圾收集任务时,YGC会执行PSScavenge::invoke_no_policy(),FGC会执行PSParallelCompact::invoke_no_policy(),无论YGC还是FGC都会由VM_ParallelGCFailedAllocation::doit() 函数调用,在这个函数中有如下代码:- // 当执行这个函数时,线程已经进入了安全点
- void VM_ParallelGCFailedAllocation::doit() {
- // 在VMThread线程进入函数时,调用SvgGCMarker的构造函数,当函数返回前,调用析构函数
- SvcGCMarker sgcm(SvcGCMarker::MINOR);
- ParallelScavengeHeap* heap = (ParallelScavengeHeap*)Universe::heap();
- GCCauseSetter gccs(heap, _gc_cause);
- _result = heap->failed_mem_allocate(_size);
- // ...
- }
复制代码 这里要注意,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);
- }
- }
复制代码 native函数的实现如下:- 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函数原则上并不允许直接操作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。
在《深入剖析Java虚拟机HotSpot:源码剖析与实例详解》一书中的 执行本地代码线程进入安全点 一小节详细剖析过代码实现,这里简单给一个交互的图示。
调用的NewObject()函数会在GC垃圾收集器期间调用到SafepointSynchronize::block()阻塞,在GC执行完成后继续执行。
不过有时候为了效率,native中还是能直接操作Java对象的,不过在直接操作Java对象前,需要进入临界区才可以。举个例子如下:- public class TestJNI {
- // 对int数组每个元素+1
- public native void processIntArray(int[] array);
- }
复制代码 native的C/C++函数实现如下:- #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);
- }
复制代码 在操作Java堆中的基本类型数组时,可通过GetPrimitiveArrayCritical()进入临界区,通过ReleasePrimitiveArrayCritical()退出临界区。在调用GetPrimitiveArrayCritical()函数时返回了一个指针,这个指针不再是句柄,而是直接指向堆中数组首地址的指针,函数的实现如下:- JNI_ENTRY(void*, jni_GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy))
- GC_locker::lock_critical(thread);
- if (isCopy != NULL) {
- *isCopy = JNI_FALSE;
- }
- oop a = JNIHandles::resolve_non_null(array);
- BasicType type;
- if (a->is_objArray()) {
- type = T_OBJECT;
- } else {
- type = TypeArrayKlass::cast(a->klass())->element_type();
- }
- void* ret = arrayOop(a)->base(type);
- return ret;
- JNI_END
复制代码 调用GC_locker::lock_critical()函数进入临界区,这里就不多介绍了,后续会详细介绍。
在如上函数中,最重要的就是调用了JNIHandles::resolve_non_null()函数获取句柄里封装的对象引用,直接返回了这个对象引用。
如果在返回数组首地址时,GC将数组从一个地方移动到另外一个地方,此时在native中操作的数组其实是一个无效数组,这样就会出现错误,为了防止这样的问题,才会有临界区。
当线程进入临界区时,会阻塞GC垃圾收集,当最后一个线程离开时,会触发一个原因为_gc_locker的GC垃圾收集。
临界区是为了让native线程高效操作数组,如果没有临界区,那么我们就需要在做数组操作时,将数组拷贝到C堆上,然后做才行,如果拷贝的数组很大,这会严重影响应用程序效率的。
这里还涉及到了句柄,句柄也是一种设计,也能让native函数可以很好的和GC配合起来,如下所示。
与直接引用比起来,句柄就是一种间接引用,不过将所有引用集中在句柄区就能让GC高效的扫描,native函数通过句柄也能安全操作对象,假设GC将对象Oop1从Eden区移动到了To区,只需要将句柄中封装的引用地址更新为最新地址即可。如下图所示。
更多文章可访问:JDK源码剖析网
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |