垃圾回收技术已经出现很久了,可以追溯到 20 世纪 60 年代,在 LISP 语言中就开始进行应用,而后的 Smalltalk,java,c# 等 语言更是一步一步地将其推向新的高潮。它广受技术专家的推崇,并被高度的评价,被认为是提高软件质量和生产力的一个有效的银弹,是一个具有革命性技术。由 于计算机的内存资源总是有限的,为了不同的程序运行,必须把不需要使用的内存回收,以便重新使用。假如那一天计算机的内存足够大,可以一年内创建的对象, 所占用的内存都卓卓有余时,就没有必要使用这种回收技术了。在C++/C的世界里,没有回收技术,其实就是需要开发人员自己负责把它使用完成的内存,主动 去删除它。在D alvik 虚拟机实现里,虽然它跟 java 虚拟机有本质上的区别,但在内存回收这一块,是没有区别的,可能使用垃圾回收的技术,就是一样的。因此,需要先学习计算领域里典型的垃圾回收算法。

 

在垃圾回收技术里,经典的算法主要有以下三种:引用计数、 MarkSweep 算法、 SemiSpaceCopy 算法。其它算法或者混合以上三种法来使用,根据不同的场合来选择不同的算法。

一、引用计数

这 种技术非常简单,就是使用一个变量记录这块内存或者对象的使用次数。比如在COM技术里,就是使用引用计数来确认这个COM对象什么时候删除的。当一个 COM对象给不同线程来使用时,由于不同的线程生命周期不一样,因此,没有办法知道这个COM对象到底在那个线程删除,只能使用引用计数来删除,否则还需 要不同线程之间添加同步机制,这样是非常麻烦和复杂的,如果COM对象有很多,就变成基本上不能实现了。引用计数的优点是:在对象变成垃圾时,可以马上进 行回收,回收效率和成本都是最低。因此,内存使用率最高,基本上没有时间花费,不需要把所有访问COM对象线程都停下来。缺点是:引用计数会影响执行效 率,每引用一次都需要更新引用计数,对于COM对象那是人工控制的,因此次数很少,没有什么影响。但在 Java 里是由编译程序来控制的,因此引用次数非常多。另外一个问题就是引用计数不能解决交叉引用,或者环形引用的问题。比如在一个环形链表里,每一个元素都引用前面的元素,这样首尾相连的链表,当所有元素都变成不需要时,就没有办法识别出来,并进行内存回收。



二、 Mark Sweep 算法

标 记-清除算法依赖于对所有存活对象进行一次全局遍历来确定哪此对象可以回收,遍历的过程从根出发,找到所有可到达对象,其它不可到达的对象就是垃圾对象, 可被回收。正如其名称所暗示的那样,这个算法分为两大阶段:标记和清除。这种分步执行的思路构成了现代垃圾收集算法的思想基础。与引用计数算法不同的是, 标记-清除算法不需要监测每一次内存分配和指针操作,只需要在标记阶段进行一次统计就行了。标记-清除算法可以非常自然的处理环形问题,另外在创建对象和 销毁对象时少了操作引用计数值的开销。不过,标记-清除算法也有一个缺点,就是需要标记和清除阶段中把所有对象停止执行。在垃圾回收器运行过程中,应用程 序必须暂时停止,并等到垃圾回收器全部运行完成后,才能重新启动应用程序运行。

 

下面就来先看看 Dalvik 虚拟机整个标记和清除中使用到那些函数,在文件 alloc/MarkSweep.h 里有函数 如下:

1 )调用函数 dvmHeapBeginMarkStep 来创建位图,并从对象位图里拷贝一份位图出来,以便后面对这个位图进行标记。

2 )调用函数 dvmHeapMarkRootSet 对所有根对象进行标记。

3 )调用函数 dvmHeapScanMarkedObjects 根据上一个函数给出的根对象位图,对每一个根相关的位图进行计算,如果这个根对象有被引用,就标记为使用。这个过程是递归调用的过程,从根开始不断重复地对子树进行标记的过程。

4 )调用函数 dvmHeapHandleReferences 对 JAVA 类对象的引用类型进行处理。主要处理三个直接的了类 :SoftReference,WeakReference,PhantomReference 。 SoftReference 对象封装了对引用目标的“软引用”; WeakReference 封装了对引用目标的“弱引用”;而 PhantomReference 封装了对引用目标的“影子引用。强引用禁止引用目标被垃圾收集,而软引用、弱引用和影子引用不禁止。

5 )调用函数 dvmHeapScheduleFinalizations 对未曾标记的对象进行完成调用,让每一个对象最后删除动作可以运行,以便后面从内存里把对象删除,相当于对象的析构作用。

6 )调用函数 dvmHeapSweepUnmarkedObjects 对未曾标记的对象进行清除操作,也就是删除没有再使用的对象。

7 )调用函数 dvmHeapFinishMarkStep 对已经删除的对象进行内存回收,可以调用堆管理函数改变目前堆使用的内存,并整理内存,就可以得到更多空闲的内存了。

 

这个过程,就是 Dalvik 虚拟机的整个标记和删除的算法过程,实际的代码会相当复杂,算法上是很清楚的,就是细节、时间方面要求相当严格,否则乱删除还在使用的对象,就导致整个虚拟机运行出错。

 



通过上面的学习,了解了垃圾回收的原理和过程。那么 Dalvik 虚 拟机是什么时候进行垃圾回收呢?要回答这个问题,那么得继续分析代码,继续进入下面的学习。其实,垃圾回收主要有两种方式,一种是虚拟机线程自动进行的, 一种是手动进行的。现在先来学习自动进行的方式,所谓自动方式,就是虚拟机创建一个线程,这个线程定时进行。虚拟机在初始化时,就进行创建这个线程,如下 的代码:

if  (gDvm. zygote ){
  if  (!dvmInitZygote())
  goto  fail;
}  else  {
  if  (!dvmInitAfterZygote())
  goto  fail;
}

 

在上面这段代码里调用函数 dvmInitAfterZygote ,在这个函数里就会调用函数 dvmSignalCatcherStartup 来创建垃圾回收线程,这个函数的代码如下:

bool dvmSignalCatcherStartup  ( void  )
{
gDvm. haltSignalCatcher =  false  ;
 
  if  (!dvmCreateInternalThread(&gDvm. signalCatcherHandle ,
  "SignalCatcher" , signalCatcherThreadStart,NULL))
  return  false  ;
 
  return  true  ;
}

 

通过上面的这段代码,就可以看到线程运行函数是 signalCatcherThreadStart ,在这个函数里就会调用函数 dvmCollectGarbage 来进行垃圾回收。代码如下:

void  dvmCollectGarbage (bool collectSoftReferences)
{
dvmLockHeap();
 
LOGVV( "ExplicitGC\n" );
dvmCollectGarbageInternal(collectSoftReferences);
 
dvmUnlockHeap();
}

 

在这个函数主要通过锁来锁住多线程访问的堆空间相关对象,然后直接就调用函数 dvmCollectGarbageInternal 来进行垃圾回收过程了,也就调用上面标记删除算法的函数。

 

另一种方式通过调用运行库的 GC 来回收,如下:

/*
* public void gc
*
* Initiate a gc
*/
static  void  Dalvik_java_lang_Runtime_gc  ( const  u4 * args, JValue *pResult)
{
UNUSED_PARAMETER(args);
 
dvmCollectGarbage( false  );
RETURN_VOID();
}

在这里也是调用函数 dvmCollectGarbage 来进行垃圾回收。手动的方式适合当需要内存,但线程又没有调用时进行。

 



现在开始学习虚拟机的初始化过程,先从 dvmStartup 函数开始,这个函数实现所有开始虚拟机的准备工作。

dvmAllocTrackerStartup 函数初始化跟踪显示系统,跟踪系统主要用生成调试系统的数据包。

dvmGcStartup 函数是用来初始化垃圾回收器。

dvmThreadStartup 函数是初始化线程列表和主线程环境参数。

dvmInlineNativeStartup 函数是分配内部操作方法的表格内存。

dvmVerificationStartup 函数是初始化虚拟机的指令码相关的内容,以便检查指令是否正确。

dvmRegisterMapStartup 函数是分配指令寄存器状态的内存。

dvmInstanceofStartup 函数是分配虚拟机使用的缓存。

dvmClassStartup 函数是初始化虚拟机最基本用的 JAVA 库。

dvmThreadObjStartup 函数是初始化虚拟机进一步使用的 JAVA 类库线程类。

dvmExceptionStartup 函数是初始化虚拟机使用的异常 JAVA 类库。

dvmStringInternStartup 函数是初始化虚拟机解释器使用的字符串哈希表。

dvmNativeStartup 函数是初始化本地方法库的表。

dvmInternalNativeStartup 函数是初始化内部本地方法,建立哈希表,方便快速查找到。

dvmJniStartup 函数是初始化 JNI 调用表,以便快速找到本地方法调用的入口。

dvmReflectStartup 函数是缓存 JAVA 类库里的反射类。

 

接着把下面这些类先进行初始化,如下:

static  const  char 
  " Ljava / lang /InternalError;" ,
  " Ljava / lang /StackOverflowError;" ,
  " Ljava / lang /UnsatisfiedLinkError;" ,
  " Ljava / lang /NoClassDefFoundError;" ,
NULL
};

初始化这些类,就是调用函数 dvmFindSystemClassNoInit 来初始化。

 

接着调用 dvmValidateBoxClasses 函数来初始化 JAVA 基本类型库,如下:

static  const  char 
  " Ljava / lang /Boolean;" ,
  " Ljava / lang /Character;" ,
  " Ljava / lang /Float;" ,
  " Ljava / lang /Double;" ,
  " Ljava / lang /Byte;" ,
  " Ljava / lang /Short;" ,
  " Ljava / lang /Integer;" ,
  " Ljava / lang /Long;" ,
NULL
};

这些类调用函数,不是上面使用系统函数来初始化,而是调用 dvmFindClassNoInit 来初始化。

 

调用 dvmPrepMainForJni 函数准备主线程里的解释栈可以调用 JNI 的方法;调用 registerSystemNatives 来注册 JAVA 库里的 JNI 方法;调用 dvmCreateStockExceptions 函数分配异常出错的内存;调用 dvmPrepMainThread 函数完成解释器主线程的初始化;调用 dvmDebuggerStartup 函数进行调试器的初始化;

最后调用 dvmInitZygote 或者 dvmInitAfterZygote 来初始化线程的模式,调用 dvmCheckException 函数检查是否有异常情况出现。

 

到这里就把整个虚拟机初始化流程完成。

 



在 Dalvik 虚拟机里,提供了一些 JNI 的调用测试函数,以便确认 JNI 的机制是否可以运 行, JNI 调用效率是否达到设计的目标,它是通过在 registerSystemNatives 函数初始化,然后调用 jniRegisterSystemMethods 函数来设置 JNI 函数。

 

JNI 的测试函数代码如下:

/*
* JNI registration
*/
static  JNINativeMethod gMethods[] = {
  /*name, signature, funcPtr */
{  "emptyJniStaticMethod0" ,  "()V" , emptyJniStaticMethod0 },
{  "emptyJniStaticMethod6" ,  "(IIIIII)V" ,emptyJniStaticMethod6 },
{  "emptyJniStaticMethod6L" ,
  "( Ljava / lang /String;[ Ljava / lang /String;[[I"
  " Ljava / lang /Object;[ Ljava / lang /Object;[[[[ Ljava / lang /Object;)V" ,
emptyJniStaticMethod6L },
};

这段代码是提供 JNI 测试函数的接口和相关实现的 C 函数入口。

 

int  register_org_apache_harmony_dalvik_NativeTestTarget  ( JNIEnv *env)
{
  int  result = jniRegisterNativeMethods(env,
  " org / apache /harmony/ dalvik /NativeTestTarget" ,
gMethods,NELEM(gMethods));

这段代码把 JNI 调用接口设置到包 org.apache.harmony.dalvik.NativeTestTarget 下面,这样在 Java 的应用程序里就可以调用了。

 

if  (result != 0) {
  /*print warning, but allow to continue */
LOGW( "WARNING:NativeTestTarget not registered\n" );
(*env)-> ExceptionClear (env);
}
  return  0;
}
 
/* 
* public static voidemptyJniStaticMethod0()
*
* For benchmarks, a do-nothingJNI method with no arguments.
*/
static  void  emptyJniStaticMethod0  ( JNIEnv *env,  jclass clazz)
{
  //This space intentionally left blank.
}

可见这些系统函数的代码,都空的结构,没有真实的代码运行,就可以用来测试 JNI 是否可以工作,测试 JNI 调用的时间需要多少,可以提供准确的时间。


垃圾回收技术已经出现很久了,可以追溯到 20 世纪 60 年代,在 LISP 语言中就开始进行应用,而后的 Smalltalk,java,c# 等 语言更是一步一步地将其推向新的高潮。它广受技术专家的推崇,并被高度的评价,被认为是提高软件质量和生产力的一个有效的银弹,是一个具有革命性技术。由 于计算机的内存资源总是有限的,为了不同的程序运行,必须把不需要使用的内存回收,以便重新使用。假如那一天计算机的内存足够大,可以一年内创建的对象, 所占用的内存都卓卓有余时,就没有必要使用这种回收技术了。在C++/C的世界里,没有回收技术,其实就是需要开发人员自己负责把它使用完成的内存,主动 去删除它。在D alvik 虚拟机实现里,虽然它跟 java 虚拟机有本质上的区别,但在内存回收这一块,是没有区别的,可能使用垃圾回收的技术,就是一样的。因此,需要先学习计算领域里典型的垃圾回收算法。

 

在垃圾回收技术里,经典的算法主要有以下三种:引用计数、 MarkSweep 算法、 SemiSpaceCopy 算法。其它算法或者混合以上三种法来使用,根据不同的场合来选择不同的算法。

一、引用计数

这 种技术非常简单,就是使用一个变量记录这块内存或者对象的使用次数。比如在COM技术里,就是使用引用计数来确认这个COM对象什么时候删除的。当一个 COM对象给不同线程来使用时,由于不同的线程生命周期不一样,因此,没有办法知道这个COM对象到底在那个线程删除,只能使用引用计数来删除,否则还需 要不同线程之间添加同步机制,这样是非常麻烦和复杂的,如果COM对象有很多,就变成基本上不能实现了。引用计数的优点是:在对象变成垃圾时,可以马上进 行回收,回收效率和成本都是最低。因此,内存使用率最高,基本上没有时间花费,不需要把所有访问COM对象线程都停下来。缺点是:引用计数会影响执行效 率,每引用一次都需要更新引用计数,对于COM对象那是人工控制的,因此次数很少,没有什么影响。但在 Java 里是由编译程序来控制的,因此引用次数非常多。另外一个问题就是引用计数不能解决交叉引用,或者环形引用的问题。比如在一个环形链表里,每一个元素都引用前面的元素,这样首尾相连的链表,当所有元素都变成不需要时,就没有办法识别出来,并进行内存回收。



二、 Mark Sweep 算法

标 记-清除算法依赖于对所有存活对象进行一次全局遍历来确定哪此对象可以回收,遍历的过程从根出发,找到所有可到达对象,其它不可到达的对象就是垃圾对象, 可被回收。正如其名称所暗示的那样,这个算法分为两大阶段:标记和清除。这种分步执行的思路构成了现代垃圾收集算法的思想基础。与引用计数算法不同的是, 标记-清除算法不需要监测每一次内存分配和指针操作,只需要在标记阶段进行一次统计就行了。标记-清除算法可以非常自然的处理环形问题,另外在创建对象和 销毁对象时少了操作引用计数值的开销。不过,标记-清除算法也有一个缺点,就是需要标记和清除阶段中把所有对象停止执行。在垃圾回收器运行过程中,应用程 序必须暂时停止,并等到垃圾回收器全部运行完成后,才能重新启动应用程序运行。

 

下面就来先看看 Dalvik 虚拟机整个标记和清除中使用到那些函数,在文件 alloc/MarkSweep.h 里有函数 如下:

1 )调用函数 dvmHeapBeginMarkStep 来创建位图,并从对象位图里拷贝一份位图出来,以便后面对这个位图进行标记。

2 )调用函数 dvmHeapMarkRootSet 对所有根对象进行标记。

3 )调用函数 dvmHeapScanMarkedObjects 根据上一个函数给出的根对象位图,对每一个根相关的位图进行计算,如果这个根对象有被引用,就标记为使用。这个过程是递归调用的过程,从根开始不断重复地对子树进行标记的过程。

4 )调用函数 dvmHeapHandleReferences 对 JAVA 类对象的引用类型进行处理。主要处理三个直接的了类 :SoftReference,WeakReference,PhantomReference 。 SoftReference 对象封装了对引用目标的“软引用”; WeakReference 封装了对引用目标的“弱引用”;而 PhantomReference 封装了对引用目标的“影子引用。强引用禁止引用目标被垃圾收集,而软引用、弱引用和影子引用不禁止。

5 )调用函数 dvmHeapScheduleFinalizations 对未曾标记的对象进行完成调用,让每一个对象最后删除动作可以运行,以便后面从内存里把对象删除,相当于对象的析构作用。

6 )调用函数 dvmHeapSweepUnmarkedObjects 对未曾标记的对象进行清除操作,也就是删除没有再使用的对象。

7 )调用函数 dvmHeapFinishMarkStep 对已经删除的对象进行内存回收,可以调用堆管理函数改变目前堆使用的内存,并整理内存,就可以得到更多空闲的内存了。

 

这个过程,就是 Dalvik 虚拟机的整个标记和删除的算法过程,实际的代码会相当复杂,算法上是很清楚的,就是细节、时间方面要求相当严格,否则乱删除还在使用的对象,就导致整个虚拟机运行出错。

 



通过上面的学习,了解了垃圾回收的原理和过程。那么 Dalvik 虚 拟机是什么时候进行垃圾回收呢?要回答这个问题,那么得继续分析代码,继续进入下面的学习。其实,垃圾回收主要有两种方式,一种是虚拟机线程自动进行的, 一种是手动进行的。现在先来学习自动进行的方式,所谓自动方式,就是虚拟机创建一个线程,这个线程定时进行。虚拟机在初始化时,就进行创建这个线程,如下 的代码:

if  (gDvm. zygote ){
  if  (!dvmInitZygote())
  goto  fail;
}  else  {
  if  (!dvmInitAfterZygote())
  goto  fail;
}

在上面这段代码里调用函数 dvmInitAfterZygote ,在这个函数里就会调用函数 dvmSignalCatcherStartup 来创建垃圾回收线程,这个函数的代码如下:

bool dvmSignalCatcherStartup  ( void  )
{
gDvm. haltSignalCatcher =  false  ;
 
  if  (!dvmCreateInternalThread(&gDvm. signalCatcherHandle ,
  "SignalCatcher" , signalCatcherThreadStart,NULL))
  return  false  ;
 
  return  true  ;
}

 

通过上面的这段代码,就可以看到线程运行函数是 signalCatcherThreadStart ,在这个函数里就会调用函数 dvmCollectGarbage 来进行垃圾回收。代码如下:

void  dvmCollectGarbage (bool collectSoftReferences)
{
dvmLockHeap();
 
LOGVV( "ExplicitGC\n" );
dvmCollectGarbageInternal(collectSoftReferences);
 
dvmUnlockHeap();
}

在这个函数主要通过锁来锁住多线程访问的堆空间相关对象,然后直接就调用函数 dvmCollectGarbageInternal 来进行垃圾回收过程了,也就调用上面标记删除算法的函数。

 

另一种方式通过调用运行库的 GC 来回收,如下:

/*
* public void gc
*
* Initiate a gc
*/
static  void  Dalvik_java_lang_Runtime_gc  ( const  u4 * args, JValue *pResult)
{
UNUSED_PARAMETER(args);
 
dvmCollectGarbage( false  );
RETURN_VOID();
}

在这里也是调用函数 dvmCollectGarbage 来进行垃圾回收。手动的方式适合当需要内存,但线程又没有调用时进行。

 



现在开始学习虚拟机的初始化过程,先从 dvmStartup 函数开始,这个函数实现所有开始虚拟机的准备工作。

dvmAllocTrackerStartup 函数初始化跟踪显示系统,跟踪系统主要用生成调试系统的数据包。

dvmGcStartup 函数是用来初始化垃圾回收器。

dvmThreadStartup 函数是初始化线程列表和主线程环境参数。

dvmInlineNativeStartup 函数是分配内部操作方法的表格内存。

dvmVerificationStartup 函数是初始化虚拟机的指令码相关的内容,以便检查指令是否正确。

dvmRegisterMapStartup 函数是分配指令寄存器状态的内存。

dvmInstanceofStartup 函数是分配虚拟机使用的缓存。

dvmClassStartup 函数是初始化虚拟机最基本用的 JAVA 库。

dvmThreadObjStartup 函数是初始化虚拟机进一步使用的 JAVA 类库线程类。

dvmExceptionStartup 函数是初始化虚拟机使用的异常 JAVA 类库。

dvmStringInternStartup 函数是初始化虚拟机解释器使用的字符串哈希表。

dvmNativeStartup 函数是初始化本地方法库的表。

dvmInternalNativeStartup 函数是初始化内部本地方法,建立哈希表,方便快速查找到。

dvmJniStartup 函数是初始化 JNI 调用表,以便快速找到本地方法调用的入口。

dvmReflectStartup 函数是缓存 JAVA 类库里的反射类。

 

接着把下面这些类先进行初始化,如下:

static  const  char 
  " Ljava / lang /InternalError;" ,
  " Ljava / lang /StackOverflowError;" ,
  " Ljava / lang /UnsatisfiedLinkError;" ,
  " Ljava / lang /NoClassDefFoundError;" ,
NULL
};

初始化这些类,就是调用函数 dvmFindSystemClassNoInit 来初始化。

 

接着调用 dvmValidateBoxClasses 函数来初始化 JAVA 基本类型库,如下:

static  const  char 
  " Ljava / lang /Boolean;" ,
  " Ljava / lang /Character;" ,
  " Ljava / lang /Float;" ,
  " Ljava / lang /Double;" ,
  " Ljava / lang /Byte;" ,
  " Ljava / lang /Short;" ,
  " Ljava / lang /Integer;" ,
  " Ljava / lang /Long;" ,
NULL
};

这些类调用函数,不是上面使用系统函数来初始化,而是调用 dvmFindClassNoInit 来初始化。

 

调用 dvmPrepMainForJni 函数准备主线程里的解释栈可以调用 JNI 的方法;调用 registerSystemNatives 来注册 JAVA 库里的 JNI 方法;调用 dvmCreateStockExceptions 函数分配异常出错的内存;调用 dvmPrepMainThread 函数完成解释器主线程的初始化;调用 dvmDebuggerStartup 函数进行调试器的初始化;

最后调用 dvmInitZygote 或者 dvmInitAfterZygote 来初始化线程的模式,调用 dvmCheckException 函数检查是否有异常情况出现。

 

到这里就把整个虚拟机初始化流程完成。

 



在 Dalvik 虚拟机里,提供了一些 JNI 的调用测试函数,以便确认 JNI 的机制是否可以运 行, JNI 调用效率是否达到设计的目标,它是通过在 registerSystemNatives 函数初始化,然后调用 jniRegisterSystemMethods 函数来设置 JNI 函数。

 

JNI 的测试函数代码如下:

/*
* JNI registration
*/
static  JNINativeMethod gMethods[] = {
  /*name, signature, funcPtr */
{  "emptyJniStaticMethod0" ,  "()V" , emptyJniStaticMethod0 },
{  "emptyJniStaticMethod6" ,  "(IIIIII)V" ,emptyJniStaticMethod6 },
{  "emptyJniStaticMethod6L" ,
  "( Ljava / lang /String;[ Ljava / lang /String;[[I"
  " Ljava / lang /Object;[ Ljava / lang /Object;[[[[ Ljava / lang /Object;)V" ,
emptyJniStaticMethod6L },
};
这段代码是提供 JNI 测试函数的接口和相关实现的 C 函数入口。
 
int  register_org_apache_harmony_dalvik_NativeTestTarget  ( JNIEnv *env)
{
  int  result = jniRegisterNativeMethods(env,
  " org / apache /harmony/ dalvik /NativeTestTarget" ,
gMethods,NELEM(gMethods));

这段代码把 JNI 调用接口设置到包 org.apache.harmony.dalvik.NativeTestTarget 下面,这样在 Java 的应用程序里就可以调用了。

if  (result != 0) {
  /*print warning, but allow to continue */
LOGW( "WARNING:NativeTestTarget not registered\n" );
(*env)-> ExceptionClear (env);
}
  return  0;
}
 
/* 
* public static voidemptyJniStaticMethod0()
*
* For benchmarks, a do-nothingJNI method with no arguments.
*/
static  void  emptyJniStaticMethod0  ( JNIEnv *env,  jclass clazz)
{
  //This space intentionally left blank.
}

 

可见这些系统函数的代码,都空的结构,没有真实的代码运行,就可以用来测试 JNI 是否可以工作,测试 JNI 调用的时间需要多少,可以提供准确的时间。