之前面试被问到过“当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区,只需要将句柄中封装的引用地址更新为最新地址即可。如下图所示。

 

GC垃圾收集时,居然还有用户线程在奔跑的更多相关文章

  1. GC 为什么要挂起用户线程? 什么愁什么怨?

    GC 为什么要挂起用户线程? 什么愁什么怨? 前言 JVM 系列文章的第一篇.敬请期待后续. 故障描述 某年某月某日 上午,线上发生故障,经过排查,发现某核心服务 Dubbo 接口超时. 故障根源 查 ...

  2. JVM04——七个GC垃圾收集器,一个都不能少

    了解了JVM内存区域与垃圾回收算法,今天将为各位带来关于垃圾收集器的知识.关注我的公众号「Java面典」了解更多 Java 相关知识点. Java 堆内存被划分为新生代和老年代两部分,因此 JVM 通 ...

  3. JVM学习笔记——GC垃圾收集器

    GC 垃圾收集器 Java 堆内存采用分代回收算法,因此 JVM 针对新生代和老年代提供了多种垃圾收集器. 1. Serial 收集器 Serial 收集器是单线程收集器,采用复制算法. 是最基本的垃 ...

  4. JAVA GC垃圾收集器的分析

    本篇文章主要介绍了"JAVA GC垃圾收集器的分析",主要涉及到JAVA GC垃圾收集器的分析方面的内容,对于JAVA GC垃圾收集器的分析感兴趣的同学可以参考一下.       ...

  5. 【JVM】-NO.110.JVM.1 -【GC垃圾收集器】

    Style:Mac Series:Java Since:2018-09-10 End:2018-09-10 Total Hours:1 Degree Of Diffculty:5 Degree Of ...

  6. Spark学习之路 (十四)SparkCore的调优之资源调优JVM的GC垃圾收集器

    一.概述 垃圾收集 Garbage Collection 通常被称为“GC”,它诞生于1960年 MIT 的 Lisp 语言,经过半个多世纪,目前已经十分成熟了. jvm 中,程序计数器.虚拟机栈.本 ...

  7. GC垃圾收集器分类

    参考https://blog.csdn.net/tjiyu/article/details/53983650 Java垃圾收集器组合: 新生代收集器:Serial.ParNew.Parallel Sc ...

  8. Spark(八)JVM调优以及GC垃圾收集器

    一JVM结构 1 Java内存结构 JVM内存结构主要有三大块:堆内存.方法区和栈. 堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间.From Survivo ...

  9. 【转】JDK5.0中JVM堆模型、GC垃圾收集详细解析

    基本概念 堆/Heap JVM管理的内存叫堆:在32Bit操作系统上有4G的限制,一般来说Windows下为2G,而Linux下为3G:64Bit的就没有这个限制.JVM初始分配的内存由-Xms指定, ...

  10. java - GC垃圾收集器详解(二)

    CMS收集器 CMS收集器(ConcurrentMarkSweep:并发标记清除)是一种以获取最短回收停顿时间为目标的收集器. 适合应用在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应 ...

随机推荐

  1. 天翼云加速落地紫金DPU实践应用,让算力供给更高效!

    近日,以"智驱创新·芯动未来"为主题的第三届DPU峰会在北京成功举办.会上,天翼云凭借紫金DPU在架构革新.算力释放.场景落地等多方面的成果,荣膺"2023芯星品牌奖&q ...

  2. Nmap 脚本使用

    Nmap 脚本使用 使用 Nmap 脚本是扩展 Nmap 功能的一种高效方式,允许用户执行从简单的服务检测到复杂的漏洞利用的各种任务.通过指定 --script 选项,并结合相应的脚本名称或类型,用户 ...

  3. Hive表误删恢复

    一.简介 因hive表删除后,hdfs文件会先放入回收站,定期清理回收站.在回收之前可以进行清理数据 二.恢复步骤 2.1 看表存储是否损坏select type from dw.ods_test1 ...

  4. Luogu P1777 帮助 题解 [ 紫 ] [ 线性 dp ] [ 状压 dp ]

    帮助:大毒瘤!!!调了我2h,拍了我2h,最后没调出来,重写才AC.wdnmd. 思路 这题主要是线性 dp ,而状压 dp 只是最后在统计答案时的一个辅助. 首先定义 \(dp[i][j][k]\) ...

  5. selenium 进入页面提示 503 Service Temporarily Unavailable

    进入三级页面提示503 Service Temporarily Unavailable,如果手动刷新页面重新加载成功 网上看都是如何配置及原因的,没告诉如何解决 于是我想,如果是这样的话,执行刷新操作 ...

  6. 多机器的键鼠互通——Synergy/Deskflow配置记录

    Synergy (1.14.6) 情况一样,那么感觉就是机器之间TCP连接有问题,测试不同 一些测试命令 ss -tlnp | grep 24800 # 查看端口情况 sudo lsof -i :24 ...

  7. next.js 添加 PWA 渐进式WEB应用(service-worker) 支持

    本文仅作为 next 系列文章中的一部分,其他 next 文章参考: https://blog.jijian.link/categories/nextjs/ 去 github 搜索了一把,估计是我关键 ...

  8. Qt QCheckBox设置复选框的大小

    文章目录 Qt设计QCheckBox样式表 QCheckBox的各部分代表的样式表 Qt QCheckBox设置复选框的大小 Qt设计QCheckBox样式表 QCheckBox的各部分代表的样式表 ...

  9. 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器

    需求背景 阿里云服务器到期了,正好家里有闲置的电脑,还有公网IP,打算装个linux服务器使用.本文章主要重点是实现远程连接虚拟机内服务器,打通网络连接,更多玩法大家可以自行探索. ps: 公网IP自 ...

  10. Golang 入门 : 常量

    常量 相对于变量而言,常量是在程序使用过程中,不会改变的数据.有些地方你需要将定义好的常量重复使用,代码中你不允许它的值改变.例如 圆周率 在程序执行过程中不会改变. 常量的声明 const Pi f ...