第十章 用Java实现JVM之本地方法调用

第十章 用Java实现JVM之本地方法调用 用Java实现JVM目录第零章 用Java实现JVM之随便说点什么第一章 用Java实现JVM之JVM的准备知识第二章 用Java实现JVM之命令行工具第三章 用Java实现JVM之查找Class文件第四章 用Java实现JVM之解析class文件第五章 用Java实现JVM之运行时数据区第六章 用Java实现JVM之指令集和解释器第七章 用Java实现JVM之类和对象第八章 用Java实现JVM之方法调用和返回第九章 用Java实现JVM之数组和字符串第十章 用Java实现JVM之本地方法调用第十一章 用Java实现JVM之异常处理第十二章 用Java实现JVM之结束文章目录用Java实现JVM目录前言本地方法代码实现注册和查找本地方法调用本地方法实现本地方法初始化基础类型测试总结前言上一篇我们已经实现了字符串和数组今天开启新的征程继续往下。聚焦于本地方法本地方法提起“本地方法”这个词可能很多人一时间会感到陌生“这是什么是指在本地调用的方法吗”实际上在Java的语境中“本地方法”并不是我们日常写的普通方法而是指由非 Java 语言通常是 C/C编写的方法然后通过JNIJava Native Interface的方式被Java所调用那为什么Java需要调用C写的方法既然Java是跨平台的高级语言为什么还要依赖底层语言来实现一些功能难道Java自己不够强大吗Java表示自己也很无奈啊。其实这并不是Java的能力问题而是它的边界问题。有些功能确实不是Java擅长或适合做的。比如操作系统级别的功能Java本身是一种运行在虚拟机上的语言和操作系统之间是隔着JVM的。但有些系统调用比如访问硬件、操控内存地址、与驱动交互等必须依赖原生语言比如C/C性能要求极高的场景C/C在执行效率上天然比Java更靠近机器层面比如要做一些高频率的图像处理、音视频编解码、数据库底层引擎等使用本地方法可以显著提高性能复用已有的成熟库很多早期开发的底层库、驱动程序甚至商业SDK都是用C/C写的直接复用比重新用Java实现一套更划算也更稳定因此本地方法实际上是Java与底层系统打交道的一座桥梁是Java向下“伸手”的一种方式。我们可以把它理解为一种“Java自己做不了就让专业选手C/C上”的机制。这也是为什么很多涉及底层优化、系统调用、硬件控制的Java应用中我们常常能看到native关键字的身影。它的存在就是为了告诉JVM“这部分逻辑不是我写的是由底层实现的去调用对应的本地库吧。”总之本地方法不是 Java 的软肋而是它在保持自身平台无关性的同时灵活“借力”的一种表现代码实现注册和查找本地方法扒拉了半天道理我都懂可是要从哪里开始啊上面说的跟实现一点关系都没有。老样子把自己当成JVM的设计师在已知两种不同语言之间存在着相互调用你会怎么做如果是两个不同语言之间互相调用现在是Java调用C/C那么需要在JVM执行方法的时候识别并且注册本地方法。识别好说本身就有native关键字。至于注册嘛还是用Map结构key就是方法签名value就是具体的实现类。我们先定义一个接口任何本地方法都必须实现这个接口。JNativeFunction代码如下/** * 执行本地方法接口 * * author hqd */FunctionalInterfacepublicinterfaceJNativeFunction{/** * 执行本地方法 * * param jThread */voidinvoke(JThreadjThread);}这个接口到时候肯定有很多实现可不能自己一个个去new。多实现-策略-工厂。现在看到策略或者多实现第一反应就是搭配工厂用起来方便省的一一去找别人还得知道你有哪些实现。不过单纯的工厂不足以创建出来对应的本地方法实现还不知道根据什么来创建这里就取个巧通过包名来。本地方法实现的包目录如下我们只需要获取本地方法所在类使用的包名获取最后一个路径就能匹配上了。JNativeRegistryFactory代码如下/** * author hqd */publicclassJNativeRegistryFactory{publicstaticJNativeFunctiongetInstance(JMethodjMethod){StringclassPathjMethod.getJClass().getClassName();/** * 获取简单类名 */StringsimpleNameclassPath.substring(classPath.lastIndexOf(/)1);classPathclassPath.substring(0,classPath.lastIndexOf(/));/** * 获取包名 */StringpackagePathclassPath.substring(classPath.lastIndexOf(/)1);JNativeFunctionfunctionnull;try{StringpackageNamecom.hqd.jjvm.jnative.packagePath.;ClassJNativeRegistryinstanceClazz(ClassJNativeRegistry)Class.forName(packageNameJsimpleNameNativeRegistry);instanceClazz.getConstructor().newInstance();functionJNativeRegistry.getJNativeFunction(jMethod);}catch(Exceptione){thrownewRuntimeException(get native function errore.getMessage());}returnfunction;}}而后在定义一个注册器方便管理本地方法。JNativeRegistry代码如下/** * 注册本地方法 * author hqd */publicclassJNativeRegistry{publicstaticfinalMapString,JNativeFunctionJNATIVE_METHOD_MAPnewConcurrentHashMap();protectedstaticvoidregisterNatives(JThreadjThread){/** * TODO 啥也不干 */}publicstaticvoidregistry(JMethodmethod,JNativeFunctionfunction){registry(method.getJClass().getClassName(),method.getName(),method.getDescriptor(),function);}publicstaticJNativeFunctiongetJNativeFunction(JMethodmethod){Stringnamemethod.getJClass().getClassName().method.getName().method.getDescriptor();returnJNATIVE_METHOD_MAP.get(name);}}调用本地方法那JVM在找到本地方法后要怎么进行调用呢既然是C/C意味着没有方法体不存在任何字节码。Java虚拟机规范也没有规定如何实现和调用本地方法没规定那就意味着可以随意发挥可以充分的空间来发挥自己的想象力。于是我们盯上了Java虚拟机规范预留了两条指令本地方法不是没有指令吗那我们就给他指令。方法调用不是就是压栈吗那就在压栈的时候插入我们自己的指令。我们先来实现指令。ReserveInstruction代码如下/** * author hqd */publicclassReserveInstructionextendsAbstractInstruction{publicReserveInstruction(JThreadjThread){super(jThread);}Overridepublicvoidexecute(InstructionTypeinstructionType){JMethodjMethodgetJMethod();JThreadjThreadgetJThread();switch(instructionType){caseBREAKPOINT:{break;}caseIMPDEP1:{JNativeFunctionjNativeFunctionJNativeRegistryFactory.getInstance(jMethod);jNativeFunction.invoke(jThread);break;}caseIMPDEP2:{break;}}}}再来先把之前的hack删掉我们不需要他了。RefInstruction新增createNativeFrame方法代码如下/** * 创建本地方法栈帧 * param jThread * param jMethod */privatevoidcreateNativeFrame(JThreadjThread,JMethodjMethod){jMethod.setMaxLocals(jMethod.getArgSlotCount());jMethod.setMaxStack(4);//创建指令StringBuildercodenewStringBuilder(FE);ClassUtil.getClassByDescriptor(jMethod.getDescriptor());switch(jMethod.getDescriptor().charAt(jMethod.getDescriptor().length()-1)){caseV:{// returncode.append(B1);break;}caseD:{// dreturncode.append(AF);break;}caseF:{// freturncode.append(AE);break;}caseJ:{// lreturncode.append(AD);break;}caseC:caseS:caseB:caseZ:caseI:{// ireturncode.append(AC);break;}default:{// areturncode.append(B0);break;}}jMethod.setCode(newInstructionCode(code.toString()));jThread.createStackFrame(jMethod);}而后在把之前创建本地方法栈帧的TODO替换成对应方法实现本地方法好了准备工作已经做好了接下来可以实现本地方法了。由于本地方法众多这里就不一一展开说明。这里就是Object本地方法为例。我们使用的是Java语言实现基本调用原本方法就行了。嘿嘿。JObjectNativeRegistry代码如下publicclassJObjectNativeRegistryextendsJNativeRegistry{privatestaticfinalJObjectNativeRegistryinstancenewJObjectNativeRegistry();protectedstaticfinalStringCLASS_PATHjava/lang/Object;static{registry(CLASS_PATH,registerNatives,()V,JNativeRegistry::registerNatives);registry(CLASS_PATH,hashCode,()I,JObjectNativeRegistry::hashCode0);registry(CLASS_PATH,getClass,()Ljava/lang/Class;,JObjectNativeRegistry::getClass0);registry(CLASS_PATH,clone,()Ljava/lang/Object;,JObjectNativeRegistry::clone0);registry(CLASS_PATH,notifyAll,()V,JObjectNativeRegistry::notifyAll0);}privatestaticvoidnotifyAll0(JThreadjThread){}privatestaticvoidclone0(JThreadjThread){JObjectjObjectjThread.getJvmStack().getLocalVarsRefVal(0);try{JClassjcjObject.getJClass().getLoader().loadJClass(Cloneable.class.getName());if(JClass.isImplements(jObject.getJClass(),jc)){thrownewCloneNotSupportedException(jObject.getJClass().getClassName() is not implements jc.getClassName());}jThread.getJvmStack().pushOperandStackRefVal(jObject.clone());}catch(Exceptione){thrownewRuntimeException(invoke clone0 errore.getMessage());}}privatestaticvoidgetClass0(JThreadjThread){JObjectthisObjjThread.getJvmStack().getTop().getLocalVars().getRefVal(0);jThread.getJvmStack().getTop().pushRef(thisObj.getJClass().getJObject());}protectedstaticvoidhashCode0(JThreadjThread){inthashCodejThread.getJvmStack().getLocalVarsRefVal(0).getData().hashCode();jThread.getJvmStack().pushOperandStackIntVal(hashCode);}publicstaticJObjectNativeRegistrygetInstance(){returninstance;}}初始化基础类型现在我们已经可以处理本地方法了其中难免遇到一些基础类型还需要对基础类型进行加载。修改JClassLoader#initLoad()方法使其加载基础类型代码如下protectedvoidinitLoad(){try{JClassjClassthis.loadJClass(Class.class.getName());jClass.setJObject(JMethodArea.getJClass(Class.class.getName()).newJObject());jClassthis.loadJClass(Object.class.getName());jClass.setJObject(JMethodArea.getJClass(Class.class.getName()).newJObject());this.loadJClass(String.class.getName());loadPrimitiveClass(float);loadPrimitiveClass(int);loadPrimitiveClass(double);loadPrimitiveClass(long);loadPrimitiveClass(byte);loadPrimitiveClass(boolean);loadPrimitiveClass(char);loadPrimitiveClass(short);}catch(ClassNotFoundExceptione){thrownewRuntimeException(e);}}privateJClassloadPrimitiveClass(StringclassName){JClassjClassnewJClass();jClass.setAccessFlags(CommonAccessFlag.ACC_PUBLIC.getHex());jClass.setLoader(this);jClass.setState(JClassState.FULLY_INITIALIZED);jClass.setClassName(className);bindingJClassObj(jClass);JMethodArea.putJClass(jClass);returnJMethodArea.getJClass(className);}测试ok今天的任务也完成了。接下来进入测试环节。定义StringTest类代码如下publicclassStringTest{publicstaticvoidmain(String[]args){Strings1abc1;Strings2abc1;System.out.println(s1s2);// trueintx1;Strings3abcx;System.out.println(s1s3);// falses3s3.intern();System.out.println(s1s3);// true}}再添加一个测试入口类NativeTest代码如下publicclassNativeTest{publicstaticvoidmain(String[]args){CmdCommandcmdCommandnewCmdCommand();cmdCommand.parseCmd(args);}}idea添加如下配置-XjreD:\Oracle\Java\jdk1.8.0_281\jre-cpZ:\code\jjvm\ch09\target\test-classescom.hqd.jjvm.jnative.StringTest测试结果如下两个运行结果一直说明我们字符串池没有问题总结今天主要讲述本地方法调用整个JVM的实现进度已经开始倒计时了。。。