native到CPU

Native

  • 所谓的native准确的说是借由虚拟机实现的JNI接口调用的操作系统提供的API
  • JNI使得class中的ACC_NATIVE标至的方法能借由JNI类的实例转换为JNI规范(如全限定名)的c实现方法实例(已经由.lib在虚拟机初始化时加载或者借由已经加载的类库的load方法,用java等语言加入内存),该实例会调用本地方法栈中的方法(操作系统提供的API)

.h、.cpp、.lib和.dll

.h头文件和.cpp是编译时必须的,lib是链接时需要的,dll是运行时需要的。

.h:声明函数接口

.cpp:c++语言实现的功能源码

.lib :

LIB有两种,一种是静态库,比如C-Runtime库,这种LIB中有函数的实现代码,一般用在静态连编上,它是将LIB中的代码加入目标模块(EXE或者DLL)文件中,所以链接好了之后,LIB文件就没有用了。

一种LIB是和DLL配合使用的,里面没有代码,代码在DLL中,这种LIB是用在静态调用DLL上的,所以起的作用也是链接作用,链接完成了,LIB也没用了。至于动态调用DLL的话,根本用不上LIB文件。 目标模块(EXE或者DLL)文件生成之后,就用不着LIB文件了。

.dll:

动态链接库英文为DLL,是Dynamic Link Library的缩写。DLL是一个包含可由多个程序,同时使用的代码和数据的库。

当程序使用 DLL 时,具有以下的优点: 使用较少的资源,当多个程序使用同一个函数库时,DLL 可以减少在磁盘和物理内存中加载的代码的重复量(运行时需要的库是需要加入内存的)。

.h和.cpp编译后会生成.lib和.dll 或者 .dll 文件

我们的程序引用别的文件的函数,需要调用其头文件,但是头文件找到相应的实现有两种方式,一种是同个项目目录下的其他cpp文件(公用性差),一种是链接时的lib文件(静态,lib中自己有实现代码),一种是运行时的dll文件,一种是lib和dll 的结合(动态,lib放索引,dll为具体实现)

还要指定编译器链接相应的库文件。在IDE环境下,一般是一次指定所有用到的库文件,编译器自己寻找每个模块需要的库;在命令行编译环境下,需要指定每个模块调用的库。

一般不开源的系统是后面三种方式,因为可以做到接口开放,源码闭合

静态链接库

静态链接库(Static Libary,以下简称“静态库”),静态库是一个或者多个obj文件的打包,所以有人干脆把从obj文件生成lib的过程称为Archive,即合并到一起。比如你链接一个静态库,如果其中有错,它会准确的找到是哪个obj有错,即静态lib只是壳子,但是静态库本身就包含了实际执行代码、符号表等等。

如果采用静态链接库,在链接的时候会将lib链接到目标代码中,结果便是lib 中的指令都全部被直接包含在最终生成的 EXE 文件中了。

这个lib文件是静态编译出来的,索引和实现都在其中。

静态编译的lib文件有好处:给用户安装时就不需要再挂动态库了。但也有缺点,就是导致应用程序比较大,而且失去了动态库的灵活性,在版本升级时,同时要发布新的应用程序才行。

动态链接库(DLL)

.dll + .lib : 导入库形式,在动态库的情况下,有两个文件,而一个是引入库(.LIB)文件,一个是DLL文件,引入库文件包含被DLL导出的函数的名称和位置,DLL包含实际的函数和数据,应用程序使用LIB文件链接到所需要使用的DLL文件,库中的函数和数据并不复制到可执行文件中,因此在应用程序的可执行文件中,存放的不是被调用的函数代码,而是DLL中所要调用的函数的内存地址,这样当一个或多个应用程序运行是再把程序代码和被调用的函数代码链接起来,从而节省了内存资源。

从上面的说明可以看出,DLL和.LIB文件必须随应用程序一起发行,否则应用程序将会产生错误。

.dll形式: 单独的可执行文件形式,因为没有lib 的静态载入,需要自己手动载入,LoadLibary调入DLL文件,然后再手工GetProcAddress获得对应函数了,若是java 会调用System的LoadLibary,但是也是调用JVM中对于操作系统的接口,使用操作系统的LoadLibary等方法真正的将.dll读入内存,再调用生成的相应函数。

.dll+ .lib和.dll本质上是一样的,只是前者一般用于通用库的预设置,是的我们通过lib直接能查询到.dll文件,不用我们自己去查询,虽会消耗一部分性能,但是实用性很大。.dll 每一个需要到的文件都需自己调用加载命令,容易出错与浪费较多时间(但是我们测试时却可以很快的看出功能实现情况,而且更灵活地调用)

JNI

JNI是Java Native Interface的缩写,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植,它允许Java代码和其他语言写的代码进行交互。

java生成符合JNI规范的C接口文件(头文件):

  1. 编写带有native声明的方法的java类

  2. 使用javac命令编译所编写的java类

  3. 然后使用javah + java类名生成扩展名为h的头文件

  4. 使用C/C++实现本地方法

  5. 将C/C++编写的文件生成动态连接库 (linux gcc windows 可以用VS)

编写范例:https://blog.csdn.net/wzgbgz/article/details/82979728

生成的.h的样例:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include "jni.h"
/* Header for class NativeDemo */
 
#ifndef _Included_NativeDemo
#define _Included_NativeDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     NativeDemo
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_NativeDemo_sayHello
  (JNIEnv *, jobject);
 
#ifdef __cplusplus
}
#endif
#endif

“jni.h” 是必须要导入的,因为JNIEXPORT等都需要他的支持才行,而且有些方法中需要借助里面的函数。

Java_NativeDemo_sayHello这样的规范命名是生成的.dll在被操作系统dlopen读取入内存时返回的handle能经由dlsym截取出正确的函数名,他可能将xxx.dll全都加载入内存,放入一个handle或者一个handle集合中,这时就需要包的全限定类名来确定到底获取的是handle中的哪个方法了

JNIEnv ,jobject ,jclass

1. JNIEnv类实际代表了Java环境,通过这个JNIEnv 指针,就可以对Java端的代码进行操作。例如,创建Java类的对象,调用Java对象的方法,获取Java对象的属性等等,JNIEnv的指针会被JNI传入到本地方法的实现两数中來对Java端的代码进行操作。

JNIEnv类中有很多函数用可以用如下所示其中:TYPE代表属性或者方法的类型(比如:int float double byte ......)

1.NewObject/NewString/New<TYPE>Array
2.Get/Set<TYPE>Field
3.Get/SetStatic<TYPE>Field
4.Call<TYPE>Method/CallStatic<TYPE>Method等许许多多的函数

2. jobject代表了在java端调用本地c/c++代码的那个类的一个实例(对象)。在修改和调用java端的属性和方法的时候,用jobject 作为参数,代表了修改了jobject所对应的java端的对象的属性和方法

3. jclass : 为了能够在c/c++中使用java类,JNI.h头文件中专门定义了jclass类型来表示java中的Class类

JNIEvn中规定可以用以下几个函数来取得jclass

1.jclass FindClass(const char* clsName) ;
2.jclass GetObjectClass(jobject obj);
3.jclass GetSuperClass(jclass obj);

JNI原理

我们编译xxx.h和xxx.cpp生成了dll文件,运行java文件JNI会帮我们调用dll中的方法, 但是java对象是如何具体调用他的我们不清楚

我们自己实现的dll需要大概如下的模板:

Test.java

package hackooo;
public class Test{
        static{
            	// java层调用.dll文件进入内存,但是底层仍是由虚拟机调用JNI用C实现对操作系统的提供的接口加载入内存
                System.loadLibrary("bridge");
        }
        public native int nativeAdd(int x,int y);
        public int add(int x,int y){
                return x+y;
        }
        public static void main(String[] args){
                Test obj = new Test();
                System.out.printf("%d\n",obj.nativeAdd(2012,3));
                System.out.printf("%d\n",obj.add(2012,3));
        }
}

我们需要先看到System.loadLibrary("bridge")的作用

@CallerSensitive
public static void loadLibrary(String libname) {
    // Runtime类是Application进程的建立后,用来查看JVM当前状态和控制JVM行为的类
    // Runtime是单例模式,且只能用静态getRuntime获取,不能实例化
    // 其中load是加载动态链接库的绝对路径方法
    // loadLibrary是读取相对路径的,动态链接库需要在java.library.path中,一般为系统path,也可以设置启动项的 -VMoption
    // 通过ClassLoader.loadLibrary0(fromClass, filename, true);中的第三个参数判断
    Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
}

java.lang.Runtime

 @CallerSensitive
    public void loadLibrary(String libname) {
        loadLibrary0(Reflection.getCallerClass(), libname);
    }

    synchronized void loadLibrary0(Class<?> fromClass, String libname) {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkLink(libname);
        }
        if (libname.indexOf((int)File.separatorChar) != -1) {
            throw new UnsatisfiedLinkError(
    "Directory separator should not appear in library name: " + libname);
        }
        // false,调用相对路径
        ClassLoader.loadLibrary(fromClass, libname, false);
    }

java.lang.ClassLoader

static void loadLibrary(Class<?> fromClass, String name,
                        boolean isAbsolute) {
    // 通过方法区中的class类找到相应的类加载器
    ClassLoader loader =
        (fromClass == null) ? null : fromClass.getClassLoader();
    if (sys_paths == null) {
        // 加载的绝对路径
        // 系统环境变量
        usr_paths = initializePath("java.library.path");
        // 我们启动时加入的依赖项
        sys_paths = initializePath("sun.boot.library.path");
    }
    if (isAbsolute) {
        // 若是决定路径,调用真正的执行方法
        if (loadLibrary0(fromClass, new File(name))) {
            return;
        }
        throw new UnsatisfiedLinkError("Can't load library: " + name);
    }
    if (loader != null) {
        // 判断当前类加载器及其双亲是否有该lib的类信息
        String libfilename = loader.findLibrary(name);
        if (libfilename != null) {
            File libfile = new File(libfilename);
            if (!libfile.isAbsolute()) {
                throw new UnsatisfiedLinkError(
"ClassLoader.findLibrary failed to return an absolute path: " + libfilename);
            }
            if (loadLibrary0(fromClass, libfile)) {
                return;
            }
            throw new UnsatisfiedLinkError("Can't load " + libfilename);
        }
    }
    // 查询sys_paths路径下是否有.dll文件
    for (int i = 0 ; i < sys_paths.length ; i++) {
        File libfile = new File(sys_paths[i], System.mapLibraryName(name));
        if (loadLibrary0(fromClass, libfile)) {
            return;
        }
        libfile = ClassLoaderHelper.mapAlternativeName(libfile);
        if (libfile != null && loadLibrary0(fromClass, libfile)) {
            return;
        }
    }
    // 查询usr_paths路径下是否有.dll文件
    if (loader != null) {
        for (int i = 0 ; i < usr_paths.length ; i++) {
            File libfile = new File(usr_paths[i],
                                    System.mapLibraryName(name));
            if (loadLibrary0(fromClass, libfile)) {
                return;
            }
            libfile = ClassLoaderHelper.mapAlternativeName(libfile);
            if (libfile != null && loadLibrary0(fromClass, libfile)) {
                return;
            }
        }
    }
    // Oops, it failed
    throw new UnsatisfiedLinkError("no " + name + " in java.library.path");
}
private static boolean loadLibrary0(Class<?> fromClass, final File file) {
    // Check to see if we're attempting to access a static library
    // 查看是否调用的lib为静态链接库
    String name = findBuiltinLib(file.getName());
    boolean isBuiltin = (name != null);
    // 若是静态链接库则跳过,否则获取file的路径
    if (!isBuiltin) {
        boolean exists = AccessController.doPrivileged(
            new PrivilegedAction<Object>() {
                public Object run() {
                    return file.exists() ? Boolean.TRUE : null;
                }})
            != null;
        if (!exists) {
            return false;
        }
        try {
            name = file.getCanonicalPath();
        } catch (IOException e) {
            return false;
        }
    }
    ClassLoader loader =
        (fromClass == null) ? null : fromClass.getClassLoader();
    // 
    Vector<NativeLibrary> libs =
        loader != null ? loader.nativeLibraries : systemNativeLibraries;
    synchronized (libs) {
        int size = libs.size();
        for (int i = 0; i < size; i++) {
            NativeLibrary lib = libs.elementAt(i);
            if (name.equals(lib.name)) {
                return true;
            }
        }

        synchronized (loadedLibraryNames) {
            if (loadedLibraryNames.contains(name)) {
                throw new UnsatisfiedLinkError
                    ("Native Library " +
                     name +
                     " already loaded in another classloader");
            }
            /* If the library is being loaded (must be by the same thread,
             * because Runtime.load and Runtime.loadLibrary are
             * synchronous). The reason is can occur is that the JNI_OnLoad
             * function can cause another loadLibrary invocation.
             *
             * Thus we can use a static stack to hold the list of libraries
             * we are loading.
             *
             * If there is a pending load operation for the library, we
             * immediately return success; otherwise, we raise
             * UnsatisfiedLinkError.
             */
            //如果我们突然发现library已经被加载,可能是我们执行一半被挂起了或者其他线程在synchronized前也调用了该classLoader,执行JNI_OnLoad又一次调用了启用了同个线程中过的另一个loadLibrary方法,加载了我们的文件
            //之所以是同个线程中的,因为run一个application对应一个java.exe/javaw.extin进程,一个JVM实例,一个Runtime实例,且其是实现了synchronized的。
            // 查看此时nativeLibraryContext中存储了什么
            int n = nativeLibraryContext.size();
            for (int i = 0; i < n; i++) {
                NativeLibrary lib = nativeLibraryContext.elementAt(i);
                if (name.equals(lib.name)) {
                    if (loader == lib.fromClass.getClassLoader()) {
                        return true;
                    } else {
                        throw new UnsatisfiedLinkError
                            ("Native Library " +
                             name +
                             " is being loaded in another classloader");
                    }
                }
            }
            NativeLibrary lib = new NativeLibrary(fromClass, name, isBuiltin);
            nativeLibraryContext.push(lib);
            try {
                // 尝试加载
                lib.load(name, isBuiltin);
            } finally {
                nativeLibraryContext.pop();
            }
            if (lib.loaded) {
                // 加入已加载Vetor中
                loadedLibraryNames.addElement(name);
                libs.addElement(lib);
                return true;
            }
            return false;
        }
    }
}
native void load(String name, boolean isBuiltin);

最后的load是虚拟机中实现的方法(用来加载我们自己要加入的.dll的),我们通过调用他来调用操作系统的API来真正将其放入内存

而那些已经编译好的库函数,虚拟机初始化时就调用LoadLibrary(Linux是dlopen)等操作系统API(本地方法栈)加入了内存中

(windows的)LoadLibrary与dlopen原理相似,若是还未加载过的dll,会调用相关方法,windows会用DLL_PROCESS_ATTACH调用DllMain 方法,若是成功则返回一个handle对象可以调用GetProcAddress(linux 为dlsym)获得函数进行使用。

load是在jVM初始化就加载了lib文件,通过jvm.h就能通过该lib找到调用的函数的入口,调用相应的.dll二进制文件

LoadLibrary是操作系统初始化时加载的windows.lib加载入内存的,我们需要调用windows.h文件,调用该函数的.dll入内存(延迟加载的话)

我们java中的native方法的实现和到此时load便接轨了,我们来看看native如何被解析的

编译:

javac hackooo/Test.java
javap -verbose hackooo.Test

Test.class:

  public native int nativeAdd(int, int);
    flags: ACC_PUBLIC, ACC_NATIVE

  public int add(int, int);
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1       
         1: iload_2       
         2: iadd          
         3: ireturn       
      LineNumberTable:
        line 8: 0

普通的“add”方法是直接把字节码放到code属性表中,而native方法,与普通的方法通过一个标志“ACC_NATIVE”区分开来。java在执行普通的方法调用的时候,可以通过找方法表,再找到相应的code属性表,最终解释执行代码,那么,对于native方法,在class文件中,并没有体现native代码在哪里,只有一个“ACC_NATIVE”的标识,那么在执行的时候改怎么找到动态链接库的代码呢?

到了这一步,我们就需要开始钻研JVM到底运行逻辑是什么了

刚开始时,我们通过javac 编译一个xxx.java 成一个字节码文件,javac进行前端编译时包括了词法分析,语法分析生成抽象语法树,在生成字节码指令流(编译期)后交由解释器/即时编译器进行解释/编译优化(运行期)

然后用java xxx 命令在操作系统中初始化一个进程,这个进程为我们分配了一块内存空间,我们开始新建一个JVM(或者说是JRE)在该内存中并进行初始化(该步骤是操作系统通过java这个命令(其为windows的一个脚本),调用其他系统命令将我们预先编译好的二进制指令集放入CPU运行生成)

虚拟机的实例创建好后,java脚本的最后一条命令便是执行JVM中的main方法,jvm会帮我们创建BoostrapClassLoader,其是用C实现的,并不符合加入class区后的实例化流程,因此我们的java代码并不能引用他,创建完他后,BoostrapClassLoader会帮我们将一些jdk的核心class文件通过它加载入方法区中,紧接着JVM会通过launcher的c实现通过JNI(还需看源码确定是不是这样,JNI是JVM初始化时创建的?不在JVM运行时区域中,在执行引擎中),依据导入java实现的Launcher的class信息通过帮我们创建sun.misc.Launcher对象并初始化(单例),他的创建还会伴随着ExtClassLoader的初始化和appClassLoader的创建(三层和双亲),这里涉及类的加载过程.

更好的了解java实现的ClassLoaderhttps://blog.csdn.net/briblue/article/details/54973413

接着,线程会默认调用APPClassLoader帮我们将命令中的 xxx参数的class装入方法区(之所以要通过classLoader来加载是为了只在需要时我们加载类,而不是全部加载,节约内存空间,而这里加载的class不止硬盘,只要是二进制字节流就可以),并为main函数在java栈中预留一个栈帧,经生成的后端编译器的实例进行字节码的解释执行优化和编译优化代替执行(后端编译器大部分既有解释器又有编译器参数设置,决定如何编译优化).

从APPClassLader将class装入方法区开始,就是类的加载过程了

具体流程是

  1. 加载(既可以由JVM本身加载入方法区,也可自定义的classLoder选取需要加载的class,通过JNI调用)

    通过一个类的全限定类名来获取定义此类的二进制字节流

    将这个字节流所代表的静态结构转化为方法区的运行时数据结构

    在内存(堆)中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口(单例模式)

    至于什么时候加载,除了遇到new、getstatic、putstatic、invokestatic四条指令时,必须立刻加载··到初始化完成

  2. 验证(java源码本身的编译是相对安全的,但是字节码的内容可能会加入恶意代码,因此需要验证)

    文件格式验证(字节流的各个部分划分是否符合规范)

    元数据验证(对元数据信息中的数据类型检验)

    字节码校验(对方法体中的内容进行校验,较为复杂耗时,jdk6后可以将权重部分移向了javac)

    符号引用校验(在解析阶段同时进行)

  3. 准备

    正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。从概念上讲,这些变量所使用的内存都应当在方法区中进行分配,但需要注意的是方法区本身是一个逻辑层面的概念,其实现在不同的版本,不同的虚拟机上可能分布在不同的内存空间,如同JMM之于JVM一般

    jdk 8之前,HotSpot团队选择把收集器的分代扩展至方法区,由垃圾收集器统一收集,省去专门写一个独立的管理方法区的方法,而方法区的存储内容与前面的分代的更新换代条件大不相同,所以专门划分了个永久代,但这容易导致更多的内存溢出问题

    jdk6hotspot就将舍弃永久代放进了发展策略,逐步改用成了用直接内存(Direct Memory)中的元空间等来存储方法区的内容,实现单独的回收管理,

    jdk7已经将字符串常量池、静态变量等移出,jdk8以全部移出

    jdk8 时类变量会随着Class对象一起存放到Java堆中,类型信息则放到了直接内存中了。

    图网上找的(其中类信息也称为静态常量池)

    java内存结构

  4. 解析

    解析阶段是java虚拟机将常量池内的符号引用(存放在方法区的常量池中)替换为直接引用(我们当初在堆中创建的Class对象的具体内存地址)的过程,即将我们最初的ACC_NATIVE等字面量进替换。

    加载阶段只是将字节码按表静态翻译成字节码对应的表示按约定大小划分入内存中,常量池中只存放字面量并被翻译的方法表中的方法引用作为所存储内存的部分信息保存,只有在解析阶段才专门将常量池中的字符引用依据Class对象中分出的各个内存中预先存储的部分信息匹配返回地址换成直接引用。放入运行时常量池直接调用

    • 至jdk13常量池中存有 17类常量表,每一个tag用u1长度(两个字节)代表一类常量表,对应的常量表中规定了后面需要读取多少字节分别,分为几个部分代表哪些东西。

    我们需要了解一份class文件大概有哪些信息(xx信息便是xx表集合)

    解析可以发生在任何时间,包括运行时再被确定也是可能的,只要求了在执行anewarray,checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, 等17个用于操作符号引用的字节码指令之前,需要对他们所使用的符号引用进行解析

    符号引用可以将第一次的解析结果进行缓存,如在运行时直接引用常量池中的记录。不过对于invokedynamic指令,上面的规则就不使用了,它要求程序在解释器基于栈或者编译器基于寄存器解读方法时实际运行到这条指令时,解析动作才能进行。

    解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这七类

    分别对应CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info,这前四种基本都是在解析时便可以替换为直接引用

    CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_Dynamic_info和CONSTANT_InvokeDynamic_info

    这四种于动态语言联系紧密,为此我们需要明白解析与分派的区别

    先从前四种开始说起

    我们前面的8个符号引用,分别有自己单独的常量表,其中记录了去往哪查询自己的代码的索引值,去调用字段表和方法表中对于字段和方法的定义

    编译器通过方法区预存的常量表解读了class文件中的字节码中的各个常量,创建了常量池,但是常量池中的存储仍是依据字面量的索引,由字面量项保存了一些字面量实现的信息,并没有真正的内存保留他,而我们的字段表,方法表等依据name_index引用常量池中的常量项,但他们只保存声明的部分,至于初始化和方法体的实现,常常是放置在code中,code一般会在字段表或方法表的后面,在加载阶段放入方法区时分配内存

    而我们的解析的作用就是将如CONSTANT_Class_info的字符引用找到字面量记录的全限定类名交由classLoader加载(加载阶段传输时为字段表、方法表分配内存空间,将这个字节流所代表的静态结构转化为方法区的运行时数据结构,放入运行时常量池中了)

    将字段表中的对于CONSTANT_Fieldref_info存储的索引的字面量的读取出的简单名称和字段描述符去匹配class_index中由类加载器加载出来的类是否有相应字段,有则返回直接引用

    方法解析与接口方法解析也是与字段大致一样的查询逻辑,但是都只是找到了方法的入口,并非实现了其中的代码 ,这时候我们可以思考一下native的直接引用的地址是哪里呢,个人认为此时已经是相应的javah实现的.h文件的实现cpp了(还不知道如何调试查看)

    而到了方法调用阶段,则需要依据方法类型来判断方法是在编译期可知,运行期不可变还是依据分派配合动态语言进行解析

    其中方法调用是指别的类或方法对本法的调用时,其调用语句对应成字节码指令会是什么样子

    方法的调用并不如同字段、方法等的入口等将字符引用换成直接引用保存一个入口就可,而是依据code中的字节码转换成相应的指令命令,使得引用时可以直接调用指令进行方法的执行,其中jvm若是解释执行,则是依据操作栈来进行字节码指令的运作的通过调用操作系统对CPU操作的API来实现功能,若是基于编译后实现的寄存器的,则是直接交由寄存器硬件实现的指令集执行(如x86).

    而如何执行code中的指令,就需要方法类型的区分,其也是依据字节码指令来的:

    1. invokestatic 用于调用静态方法
    2. invokespecial 用于调用实例构造器()方法、私有方法、父类中的方法
    3. invokevirtual 用于调用所有的虚方法
    4. invokeinterface 用于调用接口方法,会在运行时再确定一个实现该接口的对象
    5. invokedynamic 现在运行时动态解析出调用点限定符所引用的方法,然后在执行该方法

    其中invokestatic和invokespecial是在符合运行时指令集是固定的(包括1和2的四种和final,final是用invokevirtual实现,但是因为不可修改),一个方法的多个语句,在常量池中为多个常量,其中每个方法或字段开始都是字面量,解析都会换到class的对应方法的内存地址,其中,方法的实现都在其结构最后的code集合中,code集合每一条都为相应的指令集,其中对方法的调用会标志为相应的静态类型字节码指令,在字节码指令的参数中便直接固定了跳转的常量池项。

    而其他方法称为虚方法(如普通方法,又重写了,不像前面的静态类型便是自己class中的方法地址直接返回,而是需要依据上面的指针类型进行运行时解析过程,如invokevirtual的解析便依据操作数栈的元素指向的对象的实际类型中查找与常量中的描述符与简单名称都相符的方法,再权限校验,若是不通过或没有则继续往父类搜索和验证的过程,这是在执行到运行时常量池的该方法,方法的code集合中有invokevirtual指令才开始依据栈顶的元素的实际类型,在相应的class中查询匹配,在匹配权限校验通过后再返回直接引用到字节码指令参数中)

    而虚方法需要依靠分派调用(重载与重写)

    1. 静态分派(重载):编译后的class文件的code中的方法调用已在指令后参数记录了调用的常量项的编号,而该常数项因为是静态会更早解析,所以此时指向的该常数项是直接引用
    2. 动态分派(重写):也有指令与方法的常数项编号作为参数,但其会将此时操作数栈的栈顶的对象中的方法与常量的描述符与简单名称匹配,再在此时将符号引用改为对象方法的直接引用
    3. 单分派与多分派

    为了提高动态分派效率,我们还专门在方法区中建立了虚方法表

    其中无论静态还是非静态方法都存在方法区的运行时常量池中,是为了出于不同对象不需要重复为方法的相同指令集开辟重复的地址空间,而静态变量与局部变量等,便是在各个对象实例的堆的内存空间中(其中静态的放在Class对象中,至于Class对象的具体作用,便是对接自己的class的运行时常量池实现与不同的对象间的交接处),通过this区别调用的非静态方法的对象。

  5. 最后便是初始化,用,收敛初始化类变量等,<其中client 会经常调用,准备阶段的初始化是系统变量的默认值,这里是我们自定义的>将运行权重转移到程序本身的code实现上

此时Thread帮助我们将方法入栈并进行管理,每个栈帧都有对运行常量池中的方法的入口的引用,但是《java虚拟机规范》中并没有规定解析阶段发生的具体时间,因此运行时常量池并非刚开始便是全部解析的了(若是运行时解析的需要借助new等指令依据指令参数解析需要的符号引用,如引用其他类的方法时,需要new一个其他的类来通过classLoader进行class的导入,若是非静态内部类,也需要通过new将该类的在运行常量池中的符号引用替换为直接引用),我们开始依据运行时常量池中的方法顺序依据直接引用的地址调用code中的字节码指令(此时,解释器帮我们将字节码指令借助字节码指令表翻译成相应CPU指令集格式,无论是哪种指令集,指令集只是我们将二进制按位数划分助记而已,已经都是0101这种cpu能解读的模式了,但都需要按照(操作码字段|地址码字段)来传送给CPU,不同的指令集只是将二进制串划分成不同的段交给CPU,CPU需要依据自身对寄存器/栈的存取方式设计自己的指令集(如不同数量的寄存器),CPU所能读取指令长度会依据指令的操作码判断是几地址指令,比如add它可以有00,01,10,11 分别表示1234地址指令,解释器帮我们将字节码指令转为二进制指令)

若是基于栈的解释执行,我们会依据各个方法创建栈帧,并用栈帧中的操作数栈作为存取空间,实现字节码指令对操作系统对于CPUapi的调用运行code中的字节码指令,而字节码指令基本上都是零地址指令(他会对指令的读取和数值的取出读入等由几个固定栈结构进行操作)。若是经过编译的,则是依据编译器,则依据寄存器的硬件实现的指令集进行解读。两者的不同主要在运行时前者需要将操作数出栈计算再入栈保存,而后者则可以在cpu计算后直接保存回寄存器操作数地址的位置上。

若是基于栈的解释执行,我们会依据各个方法创建栈帧,并用栈帧中的操作数栈实现字节码指令对操作系统对于CPUapi的调用运行code中的字节码指令,而字节码指令基本上都是零地址指令(他会对指令的读取和数值的取出读入等由几个固定栈结构进行操作)。若是经过编译的,则是依据编译器,则依据寄存器的硬件实现的指令集进行解读。两者的不同主要在运行时前者需要将操作数出栈计算再入栈保存,而后者则可以在cpu计算后直接保存回寄存器操作数地址的位置上。

无论是c还是java,都是最后都是经过CPU对内存中某个内存地址那一部分的存储值依据指令集进行修改,jni也不过是起到使得c方法编译后的指令集的地址查询能符合java地址直接引用的规则,而其会将入口地址放入lib中使得能通过c中的表查询到入口(c入口地址都通过链接写到了lib中,而java的虚方法还接收者需要运行时根据实际类型选择版本等),因此无论是JNI中java对于C对象的调用还是c对于java对象的调用,只要有相应的地址,源码编译成的相应的指令集都可以实现对不同语言对象的操作,操作系统也无外乎用自己实现的指令集组合用cpu修改其他各个硬件的电平状态来达到控制所有硬件各种语言的目的。

而解释器和编译器通过操作数栈或者寄存器都调用系统API的实现,都是基于执行引擎调用该些后端编译器进行的,等javac自己加上去的方法会调用执行引擎依据自己的实现选择使用上两者。

执行引擎是我们与操作系统交互的最直接的部分,我们最后将class类加入方法区后并不是就可以直接加入对JVM的其他结构,而是需要执行引擎使用后端编译器进行解释编译时,javac输出的字节码指令流,基本上是一种基于栈的指令集结构,是解释器和即时编译器运行优化的方式,是基本将中间码在JVM栈上运行的,由栈保存值的,

而提前编译编译后的或者即时编译后的直接的二进制文件,则是多基于寄存器直接实现(如x86的二地址指令集),但若是源码启动,需要你的程序刚开始需要较长的时间去编译,若是二进制版本的,则需要为每一个指令集专门编译一个版本而且也不一定完全适配,效率也没有源码编译的更快(但其实相差无几)

我们这时候也不难想象ACC_NATIVE是如何通过本地方法栈找到对c方法地址的直接引用放入运行时常量池中,调用方法时java栈通过操作数栈找到虚拟机c的方法指令的位置(而其中多是对操作系统API的调用),将方法中的指令经由CPU(用户线程)计算结果传给操作系统API(也是地址,再调用操作系统实现的指令,至于是直接汇编语言编译结果还是高级语言的编译结果就不得而知了),操作系统将自身编译的指令送入CPU计算,返回我们想要的结果的了,到了这一步我终于明白为什么知道面试官为什么喜欢懂得操作系统内核的了,因为操作系统中实现了很多如网络,shell显示,IO的,其中的API就是相应实现后编译的指令集的入口(而无论是Linux还是windows都是通过查询API再依据全限定类名等命名规则查询匹配返回入口地址的,因此可以无论开不开源只要有api就可以调用系统的功能,至于哪块内存在存储这部分匹配功能的,是系统还是实例实现的,是调用后直接调用触发还是如何处理暂时不清楚),而且要考虑很多的优化和并发,其中特别是要自己实现用户线程去调用CPU还是要自己的用户线程调用操作系统的API经过操作系统的内核线程使用CPU,线程调用CPU后得到的运算结果,要自己去调用IO等还是回操作系统的API实现都是很复杂的需要考虑编译器能否实现准确的编译后能否适配的,还需要借助汇编语言来查看调试优化,太难了

我们JVM等各种结构也是源码的抽象,便于我们理解和使用

本地方法栈和操作系统的关系可以参考:https://blog.csdn.net/yfqnihao/article/details/8289363

posted @ 2020-09-18 23:55  eternal_heathens  阅读(349)  评论(1编辑  收藏  举报