• 第43篇JNI引用的管理(2)


    之前我们已经介绍了JNIHandleBlock,但是没有具体介绍JNIHandleBlock中存储的句柄,这一篇我们将详细介绍对这些句柄的操作。

    JNI句柄分为两种,全局和局部对象引用:

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

    (2)对于全局对象的引用,比如在编译任务compilerTask中会访问Method实例,这时候就需要把这些实例设置为全局的(否则在GC时可能会被回收)。

    下面我们就来详细介绍一下局部对象的引用和全局对象的引用。  

    1、局部对象引用

    当我们编写本地函数时,可能会调用JNI函数获取父类,获取父类的JNI函数jni_GetSuperclass()的实现如下:

    JNI_ENTRY(jclass, jni_GetSuperclass(JNIEnv *env, jclass sub))
      JNIWrapper("GetSuperclass");
      jclass obj = NULL;
      // ...
      obj = (super == NULL) ? NULL : (jclass) JNIHandles::make_local(super->java_mirror());
      return obj;
    JNI_END

    也就是获取sub类的父类时,如果查询到的父类super不为null,则在返回时需要返回句柄,此时就会调用JNIHandles::make_local()函数。调用的JNIHandles::make_local()函数的实现如下:

    jobject JNIHandles::make_local(Thread* thread, oop obj) {
      if (obj == NULL) {
        return NULL;  
      } else {
        assert(Universe::heap()->is_in_reserved(obj), "sanity check");
        return thread->active_handles()->allocate_handle(obj);
      }
    }
     
     
    jobject JNIHandles::make_local(JNIEnv* env, oop obj) {
      if (obj == NULL) {
        return NULL;       
      } else {
        JavaThread* thread = JavaThread::thread_from_jni_environment(env);
        // 确保obj是堆上的对象,因为只有堆上的对象才会移动,才会被GC回收
        assert(Universe::heap()->is_in_reserved(obj), "sanity check");
        return thread->active_handles()->allocate_handle(obj);
      }
    }  
    

    obj是堆中的对象,现在本地函数要通过句柄访问这个对象,此时会调用JNIHandles::make_local()函数创建出对应的句柄,这个句柄的内存空间是从JNIHandleBlock中分配出来的。 

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

    static JavaThread* thread_from_jni_environment(JNIEnv* env) {
        // 直接通过地址偏移得到JavaThread*,这是因为 
        // JavaThread类中定义了JNIEnv类型的字段    
        JavaThread *thread_from_jni_env = (JavaThread*)(
                                     (intptr_t)env - in_bytes(jni_environment_offset())
                      );
    
        // 最后检查线程是否已经终止状态,没有终止才返回该线程对象
        if (thread_from_jni_env->is_terminated()) {
           thread_from_jni_env->block_if_vm_exited();
           return NULL;
        } else {
           return thread_from_jni_env;
        }
    }
     
    

    在JNIHandles::make_local()函数中调用的JNIHandleBlock::allocate_handle()函数进行句柄分配。

    Java线程使用一个对象句柄存储块JNIHandleBlock来为其在本地函数中申请的临时对象创建对应的句柄。具体就是调用JNIHandleBlock::allocate_handle()函数分配句柄,此函数的实现如下:

    jobject JNIHandleBlock::allocate_handle(oop obj) {
      assert(Universe::heap()->is_in_reserved(obj), "sanity check");
      if (_top == 0) {
        for (JNIHandleBlock* current = _next;
                current != NULL;
                current = current->_next
        ){
          assert(current->_last == NULL, "only first block should have _last set");
          assert(current->_free_list == NULL,"only first block should have _free_list set");
          current->_top = 0;
          if (ZapJNIHandleArea)
              current->zap();
        }
        // 重转相关变量的值
        _free_list = NULL;
        _allocate_before_rebuild = 0;
        _last = this;
        if (ZapJNIHandleArea)
            zap();
      }
     
      // 当前的JNIHandleBlock::_handles中能够有空闲的slot分配句柄则分配后直接返回
      if (_last->_top < block_size_in_oops) {
        oop* handle = &(_last->_handles)[_last->_top++];
        *handle = obj;
        return (jobject) handle;
      }
     
      // 如果有空闲的句柄slot,则从列表中第1个空闲的句柄slot中分配句柄并返回
      if (_free_list != NULL) {
        oop* handle = _free_list;
        _free_list = (oop*) *_free_list;
        *handle = obj;
        return (jobject) handle;
      }
    
      // 如果_last后有空闲的JNIHandleBlock,则从此JNIHandleBlock中分配
      if (_last->_next != NULL) {
        _last = _last->_next;
        return allocate_handle(obj); // 递归调用
      }
     
      // 没有空闲的JNIHandleBlock,并且当前的JNIHandleBlock也无法分配句柄需要的内容,则调用rebuild_free_list
      if (_allocate_before_rebuild == 0) {
          rebuild_free_list();   
      } else {
        Thread* thread = Thread::current();
        Handle  obj_handle(thread, obj);
        // 关于allocate_block()函数在之前已经介绍过
        _last->_next = JNIHandleBlock::allocate_block(thread);
        _last = _last->_next;
        _allocate_before_rebuild--;
        obj = obj_handle();
      }
    
      // 再次尝试从当前的JNIHandleBlock中分配句柄需要的内存
      return allocate_handle(obj); // 递归调用
    }
     

    当_allocate_before_rebuild的值为0时,会调用rebuild_free_list()函数重建_free_list列表,_free_list列表中的各个slot都是分布在各个已经使用了的JNIHandleBlock中的空闲slot,所以重建就意味着我们要扫描所有已经使用了的slot,然后找到空闲的slot连接到这个单链表中,最大限度的重用slot。

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

    void JNIHandleBlock::rebuild_free_list() {
      assert(_allocate_before_rebuild == 0 && _free_list == NULL, "just checking");
      int free = 0;
      int blocks = 0;
      for (JNIHandleBlock* current = this; current != NULL; current = current->_next) {
        for (int index = 0; index < current->_top; index++) {
          oop* handle = &(current->_handles)[index];
          if (*handle ==  JNIHandles::deleted_handle()) {
            // 找到了空闲的slot,连接到单链表上
            *handle = (oop) _free_list;
            _free_list = handle;
            free++;
          }
        }
        blocks++;
      }
    
    
      // 当已经使用过的slot中有超过一半的slot都是空闲的,那么我们其实偏向于不再重新分配JNIHandleBlock,
      // 而是重用这些空闲slot;当空闲的slot少时,会计算出_allocate_before_rebuild值,这样就会偏向于
      // 从新的JNIHandleBlock中分配句柄
      int total = blocks * block_size_in_oops;
      int extra = total - 2*free;
      if (extra > 0) {
        // Not as many free handles as we would like - compute number of new blocks to append
        _allocate_before_rebuild = (extra + block_size_in_oops - 1) / block_size_in_oops;
      }
    }

    我们除了查找所有的空闲slot并连接到_free_list上之外,还要计算_allocate_before_rebuild值。如果计算出的_allocate_before_rebuild值大于0,那么我们查看JNIHandleBlock::allocate_handle()这个函数的逻辑,可以看到当无法分配句柄时,偏向于分配新的JNIHandleBlock,然后从JNIHandleBlock::_handles数组中分配句柄,否则从_free_list中重用空闲句柄,这就很好的避免了过度分配太多的JNIHandleBlock,如果过度分配太多的JNIHandleBlock,不但会加重GC扫描过程中的时间,也会占用更多的内存空间。

    释放句柄的函数如下:

    inline void JNIHandles::destroy_local(jobject handle) {
      if (handle != NULL) {
        // 使用JNIHandles::_deleted_handle来初始化,这个值在
        // JNIHandles::initialize()函数中会初始化为Object对象,用来
        // 表示这个slot为空,没有存储对象
        *((oop*)handle) = deleted_handle(); 
      }
    }

    代码实现非常简单,这里不再详细介绍。   

    2、全局对象引用

    在分配全局变量时,调用如下函数:

    jobject JNIHandles::make_global(Handle obj) {
      jobject res = NULL;
      if (!obj.is_null()) {
         MutexLocker ml(JNIGlobalHandle_lock);
         assert(Universe::heap()->is_in_reserved(obj()), "sanity check");
         res = _global_handles->allocate_handle(obj()); // 同样调用allocate_handle()函数处理
      } 
     
      return res;
    }

    在分配全局对象引用时,同样会调用allocate_handle()句柄,实现相对局部对象引用来说比较简单,通过一个全局的_global_handles来保存一个JNIHandleBlock的单链表,这个单链表中的JNIHandleBlock节点只增不减,所以如果某个时间点,全局对象引用足够多时会为GC带来不小负担,尤其是忘记释放全局对象引用会引起内存泄漏。

    调用JNIHandles::destroy_global()函数释放全局对象引用:

    void JNIHandles::destroy_global(jobject handle) {
      if (handle != NULL) {
        *((oop*)handle) = deleted_handle(); 
      }
    }

    函数的实现非常简单,这里不再介绍。

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

     

     

  • 相关阅读:
    第123讲:Hadoop集群管理之Namenode目录元数据结构详解学习笔记
    看待类和对象/C++的访问修饰符的作用
    c++之 reference vs point转
    关于 《C++网络编程+卷1+运用ACE和模式消除复杂性》的源码及例子
    C++之 new转
    第二次作业案例分析
    第一次作业四则运算
    【博客观后感】
    hello
    hlt指令
  • 原文地址:https://www.cnblogs.com/mazhimazhi/p/15686232.html
Copyright © 2020-2023  润新知