在本地函数中会使用Java服务,这些服务都可以通过调用JNIEnv中封装的函数获取。我们在本地函数中可以访问所传入的引用类型参数,也可以通过JNI函数创建新的 Java 对象。这些 Java 对象显然也会受到GC的影响。所以我们需要通过JNI 的局部引用(Local Reference)和全局引用(Global Reference)来保证不让GC回收这些本地函数中可能引用到的 Java 对象。

无论是局部引用还是全局引用,其实都是通过句柄进行引用。其中,局部引用所对应的句柄有两种存储方式,一是在本地方法栈帧中,主要用于存放 C 函数所接收的来自 Java 层面的引用类型参数;另一种则是线程私有的句柄块,主要用于存放本地函数运行过程中创建的局部引用(实际是通过JNI函数来完成来这些操作)。无论是传入的引用类型参数,还是通过JNI函数(除NewGlobalRefNewWeakGlobalRef之外)返回的引用类型对象,都属于局部引用。

关于句柄我们不应该陌生,在《深入剖析Java虚拟机:源码剖析与实例详解(基础卷)》一书中详细介绍过,Java栈在引用Java堆中的对象时会通过句柄的方式来引用,句柄指的是内存中 Java 对象的指针的指针。同时也介绍了HandleMark、HandleArea与Chunk这几个类的用法,它是为解决JVM内部的本地代码引用情况。当发生垃圾回收时,如果 Java 对象被移动了,那么句柄指向的指针值也将发生变动,但句柄本身保持不变。

HotSpot VM的JNI句柄是放在若干不同的区域里的,但不会放在GC堆中。传递参数用的句柄直接在栈上;局部句柄放在每个Java线程中的JNIHandleBlock里;全局句柄放在HotSpot VM全局的JNIHandleBlock里。

JNIHandles类的定义如下:

源代码位置:openjdk/hotspot/src/share/vm/runtime/jniHandles.hpp

class JNIHandles : AllStatic {
private:
// 保存全局引用的JNIHandleBlock链表的头元素
static JNIHandleBlock* _global_handles;
// 保存全局弱引用的JNIHandleBlock链表的头元素
static JNIHandleBlock* _weak_global_handles;
static oop _deleted_handle;
...
}
 

调用JNIHandles类的initialize()函数初始化如上的属性,如下:

void JNIHandles::initialize() {
_global_handles = JNIHandleBlock::allocate_block();
_weak_global_handles = JNIHandleBlock::allocate_block();
// 宏扩展为如下的形式:
// Thread* __the_thread__ = 0;
// ExceptionMark __em(__the_thread__);
EXCEPTION_MARK; Klass* k = SystemDictionary::Object_klass();
_deleted_handle = InstanceKlass::cast(k)->allocate_instance(CATCH);
}

HotSpot VM会在启动时调用init_globals()函数初始化全局模块,init_globals()函数会间接调用到JNIHandles::initialize()函数,在这个函数中对全局的变量分配对应的JNIHandleBlock块。所以说,全局对象的句柄存储在JNIHandleBlock中。

JNIHandle分为两种,全局和局部对象引用,大部分的对象引用属于局部对象引用,最终还是调用了JNIHandleBlock来管理,因为JNIHandle没有设计一个JNIHandleMark的机制,所以在创建局部对象引用时需要明确调用JNIHandles::mark_local()函数,在回收时也需要明确调用JNIHandles::destroy_local()函数。

在线程中定义的、与局部引用对象相关的变量如下:

// 保存活跃的JNIHandleBlock块,块中存储的句柄对象也是活跃的
JNIHandleBlock* _active_handles; // 保存空闲JNIHandleBlock块,在必要时进行重用
JNIHandleBlock* _free_handle_block; HandleMark* _last_handle_mark;

无论是全局还是局部对象引用,其句柄都存储在JNIHandleBlock块中。当需要分配一个新的块时,调用JNIHandleBlock::allocate_block()函数;当不需要块时,调用JNIHandleBlock::release_block()来释放JNIHandleBlock块。其中分配新块和释放块的操作的最典型应用就是在JavaCallWrapper类的构造函数和析构函数中,这个JavaCallWrapper我们在之前接触过,就是在介绍HotSpot VM调用Java主类的main()方法时,会调用到JavaCalls::call_helper()函数,这个函数中有如下调用:

{
// 使用JavaCallWrapper保存相关信息
JavaCallWrapper link(method, receiver, result, CHECK);
{
HandleMark hm(thread);
StubRoutines::call_stub()(
(address)&link,
result_val_address,
result_type,
method(),
entry_point,
args->parameters(),
args->size_of_parameters(),
CHECK
); result = link.result(); if (oop_result_flag) {
thread->set_vm_result((oop) result->get_jobject());
}
}
} // Exit JavaCallWrapper (can block - potential return oop must be preserved)

这个link会从C/C++函数调用到Java方法时,存储到栈上,如下图所示。

其中的call wrapper就是保存的link值。

其实任何从C/C++调用到Java方法时都会在C/C++的栈帧中保存call wrapper,其中保存的信息非常重要,因为寄生在C/C++栈中的C/C++函数和Java方法对应的栈帧混合在一起,我们有时候要遍历C/C++栈帧,有时候需要遍历Java栈帧,当C/C++函数或Java函数执行完成后,还要能正确恢复调用者的栈帧信息并执行,这里我们不对这些内容做过多介绍,我们只关心C/C++函数使用的局部变量句柄即可。 

如上图所示,在第1个C/C++栈帧(非当前执行的函数对应的C/C++栈帧)中可通过call wrapper找到JavaCallWrapper,然后通过JavaCallWrapper::_handles找到之前使用的JNIHandleBlock单链表,这样就能遍历到之前的C/C++栈帧中引用的堆中对象了。在第2个C/C++栈帧(当前正在执行的函数)中,通过Thread::_active_handles就能找到当前使用的JNIHandleBlock单链表,这样就能遍历引用的堆中对象了。对于Java栈引用的堆中对象来说,在《深入剖析Java虚拟机:源码剖析与实例详解(基础卷)》一书中介绍过,可通过HandleMark、HandleArea与Chunk等进行管理。

如果发生GC,那么需要遍历线程中的所有C/C++栈找到所有使用的JNIHandleBlock块,这样才能不产生漏标现象。

在JavaCallWrapper类中有如下属性定义:

JNIHandleBlock*  _handles; // 实际保存JNI引用的内存块的指针

在JavaCallWrapper构造函数中有如下实现:

JavaCallWrapper::JavaCallWrapper(
methodHandle callee_method,
Handle receiver,
JavaValue* result,
TRAPS
) {
JavaThread* thread = (JavaThread *)THREAD;
// ... // 分配一个新的JNIHandleBlock
JNIHandleBlock* new_handles = JNIHandleBlock::allocate_block(thread); // ... _thread = (JavaThread *)thread;
// 保存当前线程的active_handles
_handles = _thread->active_handles(); // 将新分配的JNIHandleBlock作为线程的active_handles
_thread->set_active_handles(new_handles);
}

无论是全局变量还是局部变量,都需要分配调用JNIHandleBlock::allocate_block()函数分配JNIHandleBlock。JNIHandleBlock类的定义如下:

class JNIHandleBlock : public CHeapObj<mtInternal> {
private:
enum SomeConstants {
// 每个JNIHandleBlock中只能分配出32个句柄,所以只能存储32个oop
block_size_in_oops = 32
}; // 句柄中保存的是oop,本地函数只能通过句柄来操作oop
oop _handles[block_size_in_oops];
// 下一个没有使用的_handles中的slot,可以在这个slot上存储oop,
// 然后返回此slot的地址给本地函数进行操作
int _top;
// 通过_next字段将所有的JNIHandleBlock连接成单链表
JNIHandleBlock* _next; // 指向JNIHandleBlock链表中的最后一个块,这个块中的_handles正在负责为当前线程分配句柄区域
JNIHandleBlock* _last;
JNIHandleBlock* _pop_frame_link; // 将空闲的句柄区域通过列表连接起来
oop* _free_list; // 将空闲的JNIHandleBlock通过如下字段连接成单链表,注意这是
// 一个静态变量,所以这个列表保存的JNIHandleBlock块可被任何线程重用
static JNIHandleBlock* _block_free_list;
// ...
}

其中各个属性的说明如下图所示。

注意,在线程中分配局部变量的句柄时,会从_last指向的JNIHandleBlock块的_handles数组中分配,如果top已经指向了_handles数组的下一个位置,则表示此数组已经无法分配出额外的句柄空间,需要调用JNIHandleBlock::allocate_block()函数分配一个新的JNIHandleBlock并连接到单链表中。

在JavaCallWrapper::JavaCallWrapper()构造函数中调用的JNIHandleBlock类的allocate_block()函数的实现如下:

JNIHandleBlock* JNIHandleBlock::allocate_block(Thread* thread)  {
JNIHandleBlock* block; // 如果当前线程的Thread::_free_handle_block中有空闲
// 的JNIHandleBlock,则从空闲的列表中获取即可
if (thread != NULL && thread->free_handle_block() != NULL) {
block = thread->free_handle_block();
thread->set_free_handle_block(block->_next);
}
else {
MutexLockerEx ml(JNIHandleBlockFreeList_lock,Mutex::_no_safepoint_check_flag);
if (_block_free_list == NULL) {
// 如果空闲列表中没有空闲的JNIHandleBlock,则分配一个新的JNIHandleBlock
// JNIHandleBlock的内存是通过调用os::malloc()函数进行分配的
block = new JNIHandleBlock();
_blocks_allocated++; if (ZapJNIHandleArea)
block->zap();
} else {
// 从JNIHandleBlock::_block_free_list中获取空闲块
block = _block_free_list;
_block_free_list = _block_free_list->_next;
}
} block->_top = 0;
block->_next = NULL;
block->_pop_frame_link = NULL; return block;
}

如上函数会在线程启动时调用,如在VMThread::run()、WatcherThread::run()和JavaThread::run()函数中调用,因为这几个函数都可能会执行native方法。当从线程的_free_handle_block和JNIHandleBlock::__block_free_list列表中都无法分配出空闲的JNIHandleBlock块时,就需要通过new关键字创建新的JNIHandleBlock了,JNIHandleBlock继承自CHeapObj<mtInternal>,所以会通过调用os::malloc()函数从本地内存中分配块的内存。

JavaCallWrapper::~JavaCallWrapper()析构函数的实现如下:

JavaCallWrapper::~JavaCallWrapper() {
// 校验执行析构的是同一个Java线程
assert(_thread == JavaThread::current(), "must still be the same thread"); // 获取当前线程的active_handles
JNIHandleBlock *_old_handles = _thread->active_handles();
// 恢复方法调用前的active_handles
_thread->set_active_handles(_handles); // ... // 释放方法调用中新分配的JNIHandleBlock
JNIHandleBlock::release_block(_old_handles, _thread);
}

析构函数在Java方法返回到C/C++函数时调用,调用JNIHandleBlock::release_block()函数就相当于在释放本地函数栈帧中的句柄。所以我们也能看到,一旦从本地函数中返回到Java 方法中,那么局部引用将失效。也就是说,垃圾回收器在标记垃圾时不再考虑这些局部引用。这就意味着,我们不能缓存局部引用,以供另一个线程或下一次 native 方法调用时使用。对于这种应用场景,我们需要借助 JNI 函数NewGlobalRef,将该局部引用转换为全局引用,以确保其指向的 Java 对象不会被垃圾回收。相应的,我们还可以通过 JNI 函数DeleteGlobalRef来消除全局引用,以便回收被全局引用指向的 Java 对象。

调用的release_block()函数的实现如下:

void JNIHandleBlock::release_block(JNIHandleBlock* block, Thread* thread) {
JNIHandleBlock* pop_frame_link = block->pop_frame_link(); if (thread != NULL ) {
if (ZapJNIHandleArea)
block->zap();
JNIHandleBlock* freelist = thread->free_handle_block();
block->_pop_frame_link = NULL;
thread->set_free_handle_block(block); // 将新的空闲块添加到列表头部,其它的空闲块添加到列表尾部
if ( freelist != NULL ) {
while ( block->_next != NULL )
block = block->_next;
block->_next = freelist;
}
block = NULL;
} if (block != NULL) {
MutexLockerEx ml(JNIHandleBlockFreeList_lock,Mutex::_no_safepoint_check_flag);
while (block != NULL) {
if (ZapJNIHandleArea)
block->zap();
// 如果函数传入的参数thread为NULL,那么会将block连接到静态变量
// _block_free_list列表中
JNIHandleBlock* next = block->_next;
block->_next = _block_free_list;
_block_free_list = block;
block = next;
}
}
// ...
} 

当线程不为NULL时,将空闲的JNIHandleBlock连接到Thread::_free_handle_block上,否则连接到JNIHandleBlock::_block_free_list上。一般来说,线程使用的JNIHandleBlock如果空闲了,都会连接到Thread::_free_handle_block上,但是当线程退出或者ClassLoaderData::_handles(用来对已经连接的对象的引用,之前介绍过)卸载时会归还给JNIHandleBlock::_block_free_list,这样其它的线程也能使用这些空闲的JNIHandleBlock,不像Thread::_free_handle_block一样,只能在本线程内重用。 

公众号 深入剖析Java虚拟机HotSpot 已经更新虚拟机源代码剖析相关文章到60+,欢迎关注,如果有任何问题,可加作者微信mazhimazh,拉你入虚拟机群交流

  

  

第42篇-JNI引用的管理(1)的更多相关文章

  1. 第43篇-JNI引用的管理(2)

    之前我们已经介绍了JNIHandleBlock,但是没有具体介绍JNIHandleBlock中存储的句柄,这一篇我们将详细介绍对这些句柄的操作. JNI句柄分为两种,全局和局部对象引用: (1)大部分 ...

  2. Jerry的WebClient UI 42篇原创文章合集

    我要感谢CRM On Premise, 因为在这个产品上做开发让我得以使用WebClient UI框架.有些朋友觉得这个SAP自己发明的基于HTML+ABAP的MVC框架,和现在流行的三驾马车(Ang ...

  3. Python 学习 第十篇 CMDB用户权限管理

    Python 学习 第十篇 CMDB用户权限管理 2016-10-10 16:29:17 标签: python 版权声明:原创作品,谢绝转载!否则将追究法律责任. 不管是什么系统,用户权限都是至关重要 ...

  4. Spring Boot 揭秘与实战(二) 数据存储篇 - 声明式事务管理

    文章目录 1. 声明式事务 2. Spring Boot默认集成事务 3. 实战演练4. 源代码 3.1. 实体对象 3.2. DAO 相关 3.3. Service 相关 3.4. 测试,测试 本文 ...

  5. JNI 引用问题梳理(转)

    局部引用: JNI 函数内部创建的 jobject 对象及其子类( jclass . jstring . jarray 等) 对象都是局部引用,它们在 JNI 函数返回后无效: 一般情况下,我们应该依 ...

  6. Java基础篇 - 强引用、弱引用、软引用和虚引用

    Java基础篇 - 强引用.弱引用.软引用和虚引用 原创零壹技术栈 最后发布于2018-09-09 08:58:21 阅读数 4936 收藏展开前言Java执行GC判断对象是否存活有两种方式其中一种是 ...

  7. 单例模式应用 | Shared_ptr引用计数管理器

    在我们模拟设计 shared_ptr 智能指针时发现,不同类型的 Shared_ptr 不能使用同一个引用计数管理器,这显然会造成内存上的浪费.因此我们考虑将其设计为单例模式使其所有的 Shared_ ...

  8. asp.net微信开发第四篇----已关注用户管理

    公众号可通过本接口来获取帐号的关注者列表,关注者列表由一串OpenID(加密后的微信号,每个用户对每个公众号的OpenID是唯一的)组成.一次拉取调用最多拉取10000个关注者的OpenID,可以通过 ...

  9. Unity UI和引用的管理中心

    我们来谈谈Unity的UI, 通常会写一些UI页面,当A页面需要去操作B页面的时候. 至少要获取B页面的引用吧! 一般新人都会在组件的写一个public GameObject UIB页面的属性, 然后 ...

随机推荐

  1. 【数据结构与算法Python版学习笔记】递归(Recursion)——优化问题与策略

    分治策略:解决问题的典型策略,分而治之 将问题分为若干更小规模的部分 通过解决每一个小规模部分问题,并将结果汇总得到原问题的解 递归算法与分治策略 递归三定律 体现了分支策略 应用相当广泛 排序 查找 ...

  2. 【数据结构与算法Python版学习笔记】树——利用二叉堆实现优先级队列

    概念 队列有一个重要的变体,叫作优先级队列. 和队列一样,优先级队列从头部移除元素,不过元素的逻辑顺序是由优先级决定的. 优先级最高的元素在最前,优先级最低的元素在最后. 实现优先级队列的经典方法是使 ...

  3. Prometheus之告警规则的编写

    Prometheus之告警规则的编写 一.前置知识 二.需求 三.实现步骤 1.编写告警规则 2.修改prometheus.yml执行告警规则的位置 3.配置文件截图 4.页面上看告警数据信息 5.查 ...

  4. 使用jQuery-UI来实现一个Ajax的自动完成功能(自动填充搜索框的下拉值)

    首先你要在.net拓展包中去搜索  jquery ui (Combined Libray)安装这么个文件 第二部   在控制器中添加我们根据输入搜索框的值获取符合的记录集的action 第三步  有了 ...

  5. Redis 客户端重试指南

    本作品采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可. 在互联网服务中,特别是在云环境下,网络及硬件环境复杂,所有应用程序都可能遇到暂时性故障.暂时性故障包括瞬时的网络抖动,服务暂时不可 ...

  6. spark搭建

    1.上传解压,配置环境变量 配置bin目录 2.修改配置文件 conf cp spark-env.sh.template spark-env.sh 增加配置 export SPARK_MASTER_I ...

  7. robot_framewok自动化测试--(6)Collections 库

    Collections 库 Collections 库同样为 Robot Framework 标准类库,它所提供的关键字主要用于列表.索引.字典的处理. 在使用之前需要在测试套件(项目)中添加: 1. ...

  8. 基于霸道秉火的STM32F103ZET6嵌入式开发之------基于定时TIM3的PWM实验

    1:PWM脉冲宽度调制 STM32 的定时器除了 TIM6 和 7.其他的定时器都可以用来产生 PWM 输出.其中高级定时器 TIM1 和 TIM8 可以同时产生多达 7 路的 PWM 输出.而通用定 ...

  9. windows下端口占用

    1,netstat -ano | findstr 1235 2,taskkill /pid 9772 /f

  10. sqlalchemy insert on duplicate update

    sqlalchemy insert on duplicate update from sqlalchemy.dialects.mysql import insert insert_stmt = ins ...