在本地函数中会使用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. 【数据结构 C++】排序——冒泡、插入、选择、希尔、归并、快排、堆排序

    LeetCode 912. 排序数组 给你一个整数数组 nums,请你将该数组升序排列. 示例 1: 输入:nums = [5,2,3,1] 输出:[1,2,3,5] 示例 2: 输入:nums = ...

  2. 【UE4 C++】 获取Actor、Controller、Pawn、Character

    获取 Actor TActorIterator 遍历 可以用于遍历 Actor,也可以用于遍历 Component for (TActorIterator<AStaticMeshActor> ...

  3. 【c++ Prime 学习笔记】第10章 泛型算法

    标准库未给容器添加大量功能,而是提供一组独立于容器的泛型算法 算法:它们实现了一些经典算法的公共接口 泛型:它们可用于不同类型的容器和不同类型的元素 利用这些算法可实现容器基本操作很难做到的事,例如查 ...

  4. freemarker自定义指令

    最近项目中使用了spring boot搭建项目,使用spring security管理项目中的权限,使用freemarker作为视图层.为了将权限控制到按钮上,因此考虑直接使用spring secur ...

  5. Linux有什么可取之处竟如此受欢迎

    什么是Linux? Linux是一个操作系统软件.和Windows不同的是,Linux是一套开放源代码程序的.并可以自由传播的类Unix操作系统,它是一个支持多用户.多任务.多线程和多CPU的操作系统 ...

  6. 设计模式 - Bridge 桥模式

    Bridge桥模式也属于"的单一职责"模式中的典型模式.问题描述:我们绘制图形时,图形可以有不同形状以及不同颜色,比如圆形可以是红的,绿的,方形可以是红的绿的,如果用代码来描绘这些 ...

  7. hdu 1058 Humble Numbers(构造?枚举?)

    题意: 一个数的质因子如果只是2,3,5,7中的若干个.则这个数叫做humble number. 例如:1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 15, 16, 1 ...

  8. DeWeb --- Hello,World!

    1.新建一个DLL,命名为hello.dpr 2.新增一个Form.(File->New->VCL Form - Delphi),建议不要更改单元名称和Form名称,即分别为unit1.p ...

  9. cesium制作自己的骑行轨迹

    制作自己的骑行轨迹 马上国庆节了,计划骑车回家,突然想到把所有的骑行线路汇总一下,无奈码表和APP不支持这样的操作,出于职业病,在此操作一下. 我用的是黑鸟码表,可以导出fit运动轨迹,但是fit还需 ...

  10. redis的一般使用和常规配置

    https://www.cnblogs.com/HTLucky/p/12027889.html Redis(全称:Remote Dictionary Server 远程字典服务)是一个开源的使用ANS ...