2019独角兽企业重金招聘Python工程师标准>>>

调用JNI的时候,通常我们使用System.loadLibrary(String libname)来load JNI library, 同样也可以使用System.load(String fileName)来load JNI library,两者的区别是一个只需要设置库的名字,比如如果libA.so 只要输入A就可以了,而libA.so的位置可以同过设置 java.library.path 或者 sun.boot.library.path,后者输入的是完整路经的文件名。

而不论用什么方法,最后JNI 库是通过classloader 来加载的。

[java] view plain copy
  1. static void loadLibrary(Class fromClass, String name,
  2. boolean isAbsolute) {}

每个classloader 对象都有自己的nativeLibrary 数组,一个全局的systemNativeLibrary 数组,一个全局的已经加载过的loadLibraryNames数组,和一个正在加载过程中的记录栈nativeLibraryContext

对同一个classloader 对象可以重复加载相同的库,对不同的classloader只可以加载一次相同的库。

1. 这里定义的相同的库是指相同路经下的同一个文件

2.  这里同样指出的是同一个classloader对象,而不是同一种classloader类型,比如说如果一种classloader类型初始化成2个classloader对象,那么这两个对象就不能重复加载相同的库。

3. 重复加载,并不代表真的重复加载,而是代码中保护

[java] view plain copy
  1. for (int i = 0; i < size; i++) {
  2. NativeLibrary lib = (NativeLibrary)libs.elementAt(i);
  3. if (name.equals(lib.name)) {
  4. return true;
  5. }
  6. }

4. 如果加载其他classloader已经加载过的库,会抛出 UnsatisfiedLinkError ERROR

在tomcat上,在不同的war包里,想加载相同的库文件,因为在 tomcat上是使用不同的classloader的对象去加载不同的war包,建议库文件放置在不同的路径通过System.load去加载。

在博客java JNI (一)虚拟机中classloader的JNILibrary 中讨论了java中的Library 是由classloader 来load的,那我们来看看 classloader是如何去load 一个library的

ClassLoader.c

[cpp] view plain copy
  1. JNIEXPORT void JNICALL
  2. Java_java_lang_ClassLoader_00024NativeLibrary_load
  3. (JNIEnv *env, jobject this, jstring name)
  4. {
  5. const char *cname;
  6. jint jniVersion;
  7. jthrowable cause;
  8. void * handle;
  9. if (!initIDs(env))
  10. return;
  11. cname = JNU_GetStringPlatformChars(env, name, 0);
  12. if (cname == 0)
  13. return;
  14. handle = JVM_LoadLibrary(cname);
  15. if (handle) {
  16. const char *onLoadSymbols[] = JNI_ONLOAD_SYMBOLS;
  17. JNI_OnLoad_t JNI_OnLoad;
  18. int i;
  19. for (i = 0; i < sizeof(onLoadSymbols) / sizeof(char *); i++) {
  20. JNI_OnLoad = (JNI_OnLoad_t)
  21. JVM_FindLibraryEntry(handle, onLoadSymbols[i]);
  22. if (JNI_OnLoad) {
  23. break;
  24. }
  25. }
  26. if (JNI_OnLoad) {
  27. JavaVM *jvm;
  28. (*env)->GetJavaVM(env, &jvm);
  29. jniVersion = (*JNI_OnLoad)(jvm, NULL);
  30. } else {
  31. jniVersion = 0x00010001;
  32. }
  33. cause = (*env)->ExceptionOccurred(env);
  34. if (cause) {
  35. (*env)->ExceptionClear(env);
  36. (*env)->Throw(env, cause);
  37. JVM_UnloadLibrary(handle);
  38. goto done;
  39. }
  40. if (!JVM_IsSupportedJNIVersion(jniVersion)) {
  41. char msg[256];
  42. jio_snprintf(msg, sizeof(msg),
  43. "unsupported JNI version 0x%08X required by %s",
  44. jniVersion, cname);
  45. JNU_ThrowByName(env, "java/lang/UnsatisfiedLinkError", msg);
  46. JVM_UnloadLibrary(handle);
  47. goto done;
  48. }
  49. (*env)->SetIntField(env, this, jniVersionID, jniVersion);
  50. } else {
  51. cause = (*env)->ExceptionOccurred(env);
  52. if (cause) {
  53. (*env)->ExceptionClear(env);
  54. (*env)->SetLongField(env, this, handleID, (jlong)NULL);
  55. (*env)->Throw(env, cause);
  56. }
  57. goto done;
  58. }
  59. (*env)->SetLongField(env, this, handleID, ptr_to_jlong(handle));
  60. done:
  61. JNU_ReleaseStringPlatformChars(env, name, cname);
  62. }

1. JVM_LoadLibrary

jvm中load library 核心函数,实现也非常简单,在linux下调用了系统函数dlopen去打开库文件,详细可参考方法

[cpp] view plain copy
  1. void * os::dll_load(const char *filename, char *ebuf, int ebuflen)

2. JVM_FindLibraryEntry

JVM在加载库文件时候,会去尝试查找库中的JNI_ONLOAD方法的地址,而在Linux中调用了dlsym函数通过前面的dlopen加载库的指针去获取方法的地址,而dlsym在glibc2.0是非线程安全的,需要锁的保护,虽然在java中加载库已经有锁的保护,但只是针对同一个classloader对象的细粒度锁。

[cpp] view plain copy
  1. void* os::dll_lookup(void* handle, const char* name) {
  2. pthread_mutex_lock(&dl_mutex);
  3. void* res = dlsym(handle, name);
  4. pthread_mutex_unlock(&dl_mutex);
  5. return res;
  6. }

3. 方法JNI_OnLoad

JVM提供了一种方式允许你在加载库文件的时候做一些你想做的事情,也就是JNI_OnLoad方法

在2中提到过在加载动态链接库,JVM会去尝试查找JNI_OnLoad方法,同时也会调用该函数,这样你个人可以在函数里做一些初始化的事情,比如register native方法。

[cpp] view plain copy
  1. JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
  2. {}

JNI_OnLoad中返回的是JNI 的version,在1.6版本的情况下支持如下

[cpp] view plain copy
  1. jboolean Threads::is_supported_jni_version(jint version) {
  2. if (version == JNI_VERSION_1_2) return JNI_TRUE;
  3. if (version == JNI_VERSION_1_4) return JNI_TRUE;
  4. if (version == JNI_VERSION_1_6) return JNI_TRUE;
  5. return JNI_FALSE;
  6. }

完整的加载过程就是

首先先加载动态链接库,尝试查找JNI_OnLoad方法,并且运行方法,对我们来说从而实现可以自定义的初始化方法。

我们常用javah去生成JNI的头文件,然后去实现自己定义的JNI方法,使用这种方式比较传统,我们可以看到定义的格式甚至连名字都必须按照规范

[cpp] view plain copy
  1. JNIEXPORT jint JNICALL Java_test_symlink
  2. (JNIEnv *, jobject, jstring, jstring);

完整的结构是Java_classpath_classname_native method name,这样才能当jvm运行的时候根据这个命名规则去找到对应的native的方法。

实际上jvm也同时提供了直接RegisterNative方法手动的注册native方法

下面是一个代码的例子

[cpp] view plain copy
  1. static JNINativeMethod methods[] = {
  2. {"retrieveDirectives",  "()Ljava/lang/AssertionStatusDirectives;", (void *)&JVM_AssertionStatusDirectives}
  3. };
  4. (*env)->RegisterNatives(env, cls, methods,
  5. sizeof(methods)/sizeof(JNINativeMethod));

RegisterNative 函数中的参数

RegisterNative(JNIEnv, jclass cls, JNINativeMethod *methods,  jint number)

1. methods 是一个二维数组,代表着这个class里的每一个native方法所对应的实现的方法,在前面的例子中表示,一个native 方法retrieveDiretives, 返回值为AssertionStatusDirectives, 所对应的执行的本地方法是JVM_AssertionStatusDirectives

2. 后面的number 代表要指定的native的数量

RegisterNative的实现

RegisterNative 的实现非常简单,就是将class里面native的方法的地址+1指向执行的c代码的函数地址也就是上面的&JVM_AssertionStatusDirectives

[cpp] view plain copy
  1. address* native_function_addr() const          { assert(is_native(), "must be native"); return (address*) (this+1);

这是jvm当初始化类的时候,class的调用层级关系

instanceKlass::initialize() 
     -> instanceKlass::initialize_impl() 
           -> instanceKlass::link_class() 
                 -> instanceKlass::link_class_impl() 
                      -> instanceKlass::rewrite_class() 
                           -> Rewriter::rewrite() 
                                -> Rewriter::Rewriter() 
                                    -> methodOopDesc::link_method()

在方法methodOopDesc::link_method 设置到了对应的natvive方法的解释entry

[cpp] view plain copy
  1. void methodOopDesc::link_method(methodHandle h_method, TRAPS) {
  2. assert(_i2i_entry == NULL, "should only be called once");
  3. assert(_adapter == NULL, "init'd to NULL" );
  4. assert( _code == NULL, "nothing compiled yet" );
  5. // Setup interpreter entrypoint
  6. assert(this == h_method(), "wrong h_method()" );
  7. address entry = Interpreter::entry_for_method(h_method); //找到对应的方法 entry
  8. assert(entry != NULL, "interpreter entry must be non-null");
  9. // Sets both _i2i_entry and _from_interpreted_entry
  10. set_interpreter_entry(entry); //并且把entry设置到了methodoop中
  11. ...
  12. }

找到对应的方法类型的entry

函数entry_for_method 是从_entry_table数组中找到对应的entry

[cpp] view plain copy
  1. static address    entry_for_method(methodHandle m)            { return _entry_table[method_kind(m)]; }

在函数中TemplateInterpreterGenerator::generate_all,我们可以看到初始化了_entry_table entry数组,而这是在jvm初始化的时候(jint init_globals())初始化的

[cpp] view plain copy
  1. #define method_entry(kind)                                                                    \
  2. { CodeletMark cm(_masm, "method entry point (kind = " #kind ")");                    \
  3. Interpreter::_entry_table[Interpreter::kind] = generate_method_entry(Interpreter::kind);  \
  4. }
  5. // all non-native method kinds
  6. method_entry(zerolocals)
  7. method_entry(zerolocals_synchronized)
  8. method_entry(empty)
  9. method_entry(accessor)
  10. method_entry(abstract)
  11. method_entry(method_handle)
  12. method_entry(java_lang_math_sin  )
  13. method_entry(java_lang_math_cos  )
  14. method_entry(java_lang_math_tan  )
  15. method_entry(java_lang_math_abs  )
  16. method_entry(java_lang_math_sqrt )
  17. method_entry(java_lang_math_log  )
  18. method_entry(java_lang_math_log10)
  19. // all native method kinds (must be one contiguous block)
  20. Interpreter::_native_entry_begin = Interpreter::code()->code_end();
  21. method_entry(native)
  22. method_entry(native_synchronized)
  23. Interpreter::_native_entry_end = Interpreter::code()->code_end();
  24. #undef method_entry

而对应的不同的方法,使用不同的entry 是在函数generate_method_entry里定义的

[cpp] view plain copy
  1. address AbstractInterpreterGenerator::generate_method_entry(
  2. AbstractInterpreter::MethodKind kind) {
  3. // determine code generation flags
  4. bool synchronized = false;
  5. address entry_point = NULL;
  6. switch (kind) {
  7. case Interpreter::zerolocals             :                                                                             break;
  8. case Interpreter::zerolocals_synchronized: synchronized = true;                                                        break;
  9. case Interpreter::native                 : entry_point = ((InterpreterGenerator*) this)->generate_native_entry(false); break;
  10. case Interpreter::native_synchronized    : entry_point = ((InterpreterGenerator*) this)->generate_native_entry(true);  break;
  11. case Interpreter::empty                  : entry_point = ((InterpreterGenerator*) this)->generate_empty_entry();       break;
  12. case Interpreter::accessor               : entry_point = ((InterpreterGenerator*) this)->generate_accessor_entry();    break;
  13. case Interpreter::abstract               : entry_point = ((InterpreterGenerator*) this)->generate_abstract_entry();    break;
  14. case Interpreter::method_handle          : entry_point = ((InterpreterGenerator*) this)->generate_method_handle_entry();break;
  15. case Interpreter::java_lang_math_sin     : // fall thru
  16. case Interpreter::java_lang_math_cos     : // fall thru
  17. case Interpreter::java_lang_math_tan     : // fall thru
  18. case Interpreter::java_lang_math_abs     : // fall thru
  19. case Interpreter::java_lang_math_log     : // fall thru
  20. case Interpreter::java_lang_math_log10   : // fall thru
  21. case Interpreter::java_lang_math_sqrt    : entry_point = ((InterpreterGenerator*) this)->generate_math_entry(kind);    break;
  22. default                                  : ShouldNotReachHere();                                                       break;
  23. }
  24. if (entry_point) {
  25. return entry_point;
  26. }
  27. return ((InterpreterGenerator*) this)->
  28. generate_normal_entry(synchronized);
  29. }

我们可以看到在 native,和native_synchronized的情况下,使用了generate_native_entry

在methodoop设置了entry

在方法methodOopDesc::link_method 中,设置了_i2i_entry,和_from_interpreted_entry为entry 也是就在native的情况下设置了generate_native_entry
[cpp] view plain copy
  1. void set_interpreter_entry(address entry)      { _i2i_entry = entry;  _from_interpreted_entry = entry; }

Hotspot主要有两种解释器,而下面我们主要讨论的是 Template Intepreter也叫asm interprete解释器, 文章下面的介绍基本都是基于template解释器

我们举一个invokespecial的例子,下面是templateTable方法解释invokespecial的代码

[cpp] view plain copy
  1. void TemplateTable::invokespecial(int byte_no) {
  2. transition(vtos, vtos);
  3. assert(byte_no == f1_byte, "use this argument");
  4. prepare_invoke(rbx, noreg, byte_no);
  5. // do the call
  6. __ verify_oop(rbx);
  7. __ profile_call(rax);
  8. __ jump_from_interpreted(rbx, rax);
  9. }

函数prepare_invoke

函数prepare_invoke的层级调用关系

TemplateTable::prepare_invoke

-> TemplateTable::load_invoke_cp_cache_entry

-> TemplateTable::resolve_cache_and_index

在函数中resolve_cache_and_index可以看到

1. 首先先检查constantpoolcache,是否将方法指针保存到到线程的constantpoolcache里,如果有在方法里会使用jcc跳转到Label resolved去,而Lable resolved 在方法第一次运行结束后bind到函数的末尾。

2. 如果cache里没有那么会尝试用interpreterRuntime:resolve_invoke去找到正确的method, 并保存到constant pool cache 里

[cpp] view plain copy
  1. case Bytecodes::_invokevirtual:
  2. case Bytecodes::_invokespecial:
  3. case Bytecodes::_invokestatic:
  4. case Bytecodes::_invokeinterface:
  5. entry = CAST_FROM_FN_PTR(address, InterpreterRuntime::resolve_invoke);
  6. break;
而函数
interpreterRuntime:resolve_invoke
-->LinkResolver::resolve_invoke
-->LinkResolver::resolve_special_call
-->LinkResolver::linktime_resolve_special_method
-->LinkResolver::resolve_method
不论什么调用方式,最后都会调用LinkResolver::resolve_method 找到真实的调用方法,通过runtime_resolve_special_method把method指针作为methodhandle存放到CallInfo 中传回InterperterRuntime::resolve_invoke中,同时在CallInfo::set_common当设置-Xcomp情况下,决定是否需要编译方法。
我们可以看到方法prepare_invoke,已经找到了methodoop指针并且存放到寄存器rbx中

函数jump_from_interpreted

[cpp] view plain copy
  1. void InterpreterMacroAssembler::jump_from_interpreted(Register method, Register temp) {
  2. prepare_to_jump_from_interpreted();
  3. if (JvmtiExport::can_post_interpreter_events()) {
  4. Label run_compiled_code;
  5. // JVMTI events, such as single-stepping, are implemented partly by avoiding running
  6. // compiled code in threads for which the event is enabled.  Check here for
  7. // interp_only_mode if these events CAN be enabled.
  8. get_thread(temp);
  9. // interp_only is an int, on little endian it is sufficient to test the byte only
  10. // Is a cmpl faster (ce
  11. cmpb(Address(temp, JavaThread::interp_only_mode_offset()), 0);
  12. jcc(Assembler::zero, run_compiled_code);
  13. jmp(Address(method, methodOopDesc::interpreter_entry_offset()));
  14. bind(run_compiled_code);
  15. }
  16. jmp(Address(method, methodOopDesc::from_interpreted_offset()));
  17. }

我们看到跳转到了methodoop中的_from_interpreted_entry,也就是在前面的博客里(java JNI (四) 初始化JNI方法)说的generate_native_entry 中

在这篇博客并没有太多的涉及到native方法的调用,而是asm解释器在解释一个方法的时候如何link到所对应method,并且找到处理method的entry。

在前面的博客中已经提到过JNI的entry是generate_native_entry,也就是说方法generate_native_entry才是最终调用的我们自己写的库文件里的方法

针对不同的解释器的类型,会调用不同的generate_native_entry,下面主要讨论的还是以template interpreter为主

如果是X86的,可以参考templateInterpreter_x86_64.cpp

[cpp] view plain copy
  1. address InterpreterGenerator::generate_native_entry(bool synchronized) {
  2. ....
  3. {
  4. Label L;
  5. __ movptr(rax, Address(method, methodOopDesc::native_function_offset()));
  6. ExternalAddress unsatisfied(SharedRuntime::native_method_throw_unsatisfied_link_error_entry());
  7. __ movptr(rscratch2, unsatisfied.addr());
  8. __ cmpptr(rax, rscratch2);
  9. __ jcc(Assembler::notEqual, L);
  10. __ call_VM(noreg,
  11. CAST_FROM_FN_PTR(address,
  12. InterpreterRuntime::prepare_native_call),
  13. method);
  14. __ get_method(method);
  15. __ verify_oop(method);
  16. __ movptr(rax, Address(method, methodOopDesc::native_function_offset()));
  17. __ bind(L);
  18. }
  19. .....
  20. __ call(rax);
  21. .....
  22. }

具体我们来看prepare_native_call方法的实现

[cpp] view plain copy
  1. IRT_ENTRY(void, InterpreterRuntime::prepare_native_call(JavaThread* thread, methodOopDesc* method))
  2. methodHandle m(thread, method);
  3. assert(m->is_native(), "sanity check");
  4. // lookup native function entry point if it doesn't exist
  5. bool in_base_library;
  6. if (!m->has_native_function()) {
  7. NativeLookup::lookup(m, in_base_library, CHECK);
  8. }
  9. // make sure signature handler is installed
  10. SignatureHandlerLibrary::add(m);
  11. // The interpreter entry point checks the signature handler first,
  12. // before trying to fetch the native entry point and klass mirror.
  13. // We must set the signature handler last, so that multiple processors
  14. // preparing the same method will be sure to see non-null entry & mirror.
  15. IRT_END

在代码中

if (!m->has_native_function()) {
    NativeLookup::lookup(m, in_base_library, CHECK);
  }

首先先检查一下是不是已经在method里面定义了native的方法,也就是我们前面提到的JNI中的(RegisterNatives方法)中是不是已经单独RegisterNatives注册了native方法

如果没有的话,将按照javah生成的JNI头文件里的方法的名字来绑定,也就是lookup里做的事情,然后设置回method的native的方法中去,保证调用只初始化一次,不是每次调用都去查找一遍

这样在方法prepare_native_call 中,我们可以和前面的博客(java JNI 实现原理 (三) JNI中的RegisterNatives方法),完整的联系起来。

下面大概的介绍一下,完整的native call的过程

a.  初始化一些参数,把方法的一些信息放到对应的寄存器上

b.  检查是否需要锁,如果需要,锁到对应的object

c.  设置几个handler, signature hanlder, result handler, mirror handler(static 方法)

d.  找到 native 方法的指针,设置到rax寄存器中

e.  传入JNIEnv 对象在native方法第一个参数(因为JNIEnv 对象在Java 代码中的native 方法没有,而在自己定义的native方法里用来取得jni的运行环境的,所以需要在这里额外传入)

f.  设置java栈信息 last_java_frame

g.  设置线程信息为_thread_in_native

h.  运行native 方法

i.   在多核的情况下,使用Membar清楚内存cache

j.   检查是否在safepoint的点

k. 还原java 栈信息 reset_last_java_frame

l.  对结果进行一些处理

m. 处理过程中的异常

n.  释放锁

o.  调用result hanlder 的到结果

转载于:https://my.oschina.net/u/1398304/blog/309607

读书笔记之inside JVM(5)相关推荐

  1. 读书笔记之inside JVM(4)

    2019独角兽企业重金招聘Python工程师标准>>> 在我们常用的Jstack, Jmap 用于分析java虚拟机的状态的工具,通过起另一个虚拟机通过运行sun.tools包下的j ...

  2. 读书笔记——深入理解JVM(JVM自动内存管理)

    简介 本系列为<深入理解Java虚拟机-JVM高级特性与最佳实践>一书的阅读笔记. 本书开头介绍了JVM发展的历史,接着介绍了JVM是如何实现自动内存管理的. 本章节主要介绍: JVM的存 ...

  3. 【读书笔记】实战JAVA虚拟机JVM故障诊断与性能优化 读书笔记

    文章目录 1.概述 1.1 **第一章:初探java虚拟机** 1.2 认识java虚拟机的基本结构 1.3 常用Java虚拟机参数 1.4 垃圾回收器 1.5 垃圾收集器以及内存分配 1.6 性能监 ...

  4. 读书笔记之《深入理解Java虚拟机:JVM高级特性与最佳实践》

    本篇带来的是周志明老师编写的<深入理解Java虚拟机:JVM高级特性与最佳实践>,十分硬核! 全书共分为 5 部分,围绕内存管理.执行子系统.程序编译与优化.高效并发等核心主题对JVM进行 ...

  5. Inside The C++ Object Model 读书笔记

    深度探索 C++ 对象模型 读书笔记之一  C++对象布局 读完了这本书,要是不做一些笔记.总结一下,是怎么也说不过去的.这本书个我们从头至尾详细的解释了C++对象模型如何为我们服务,是的了解了就是服 ...

  6. 《Java编程思想》读书笔记(二)

    三年之前就买了<Java编程思想>这本书,但是到现在为止都还没有好好看过这本书,这次希望能够坚持通读完整本书并整理好自己的读书笔记,上一篇文章是记录的第一章到第十章的内容,这一次记录的是第 ...

  7. 《Java: The Complete Reference》等书读书笔记

    春节期间读了下<Java: The Complete Reference>发现这本书写的深入浅出,我想一个问题,书中很多内容我们也知道,但是为什么我们就写不出这样一本书,这么全面,这么系统 ...

  8. 《JavaScript面向对象精要》读书笔记

    JavaScript(ES5)的面向对象精要 标签: JavaScript 面向对象 读书笔记 2016年1月16日-17日两天看完了<JavaScript面向对象精要>(参加异步社区的活 ...

  9. Java 内存分配——Thinking in Java 4th 读书笔记

    做开发多年,一直忙于项目,从没好好的整理知识,从现在开始,尽量每周多抽时间整理知识,分享在博客,在接下来的博客中,我将为大家分享我读<Java编程思想4th>英文版读书笔记,一来便于知识的 ...

  10. 《深入理解 Java 内存模型》读书笔记(上)(干货,万字长文)

    目录 0. 前提 1. 基础 1.1 并发编程的模型分类 1.1.1 通信 1.1.2 同步 1.2 JAVA 内存模型的抽象 2. 重排序 2.1 处理器重排序 2.2 内存屏障指令 2.3 HAP ...

最新文章

  1. 是时候改变自学编程方法了,这篇国外网友的教程被fast.ai创始人点赞
  2. Design Pattern in Java[Challenge 2.1]
  3. 目前电子计算机已经发展到,目前电子计算机已经发展到什么计算机?
  4. spring注解注入IOC
  5. 02-普通轮播图-上下滚动
  6. mariadb 创建用户及授权
  7. 腾讯广告算法大赛 | 复赛第二周最佳进步奖得主心得分享
  8. linux无线网卡断断续续,关于ubuntu16无线网卡RTL8723BE频繁掉线及信号不足的解决方法...
  9. Python入门--顺序结构,选择结构,对象的布尔值
  10. Linux学习笔记:常用100条命令(一)
  11. c语言 16进制编辑器,十六进制编辑器(010 Editor)
  12. 【neo4j】知识图谱实战---构建红楼梦知识图谱
  13. python中reduce是什么意思_python中的reduce是什么
  14. 手把手教你绘制自定义地图
  15. VS2010下破解Visual Assist X
  16. Ubuntu/Linux用户管理与权限管理(超详细解析)
  17. 阿里腾讯头条美团等iOS面试总结
  18. 便捷式储能电源核心技术--单相逆变器设计
  19. python绘制混淆矩阵(2s-AGCN结果分析)
  20. 【独角日“爆”】携程或计划从纳斯达克退市;雷军入驻B站;台积电市值反超英特尔...

热门文章

  1. 《嵌入式Linux基础教程学习笔记一》
  2. fg、bg、jobs、、ctrl + z命令
  3. php_字符编码浅谈_积累中。。。
  4. POJ 2923 Relocation(状压DP)题解
  5. HBuilder的app自动更新
  6. Linux SendMail发送邮件失败诊断案例(四)
  7. linux# 解读wmctrl一览输出的项目
  8. 1990-2000年事务处理流程图和数据流图试题分析
  9. 我的5年Python7年R,述说她们的差异在哪里?
  10. 怎样看出一个人有数学天赋?