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:声明函数接口java

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

.lib :c++

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

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

.dll:windows

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

当程序使用 DLL 时,具备如下的优势: 使用较少的资源,当多个程序使用同一个函数库时,DLL 能够减小在磁盘和物理内存中加载的代码的重复量(运行时须要的库是须要加入内存的)。api

.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加载(加载阶段已扫描过一遍字段表、方法表等并已建立出了Class对象在堆中存放了静态的数据接口)

    将字段表中的对于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实例的内存中(class对象时读取方法表等),并返回地址将字符引用改成直接引用,这种方法称为非虚方法(当咱们将常量池的字符引用解析到属性表集合时

    而其余方法称为虚方法(如:Code),不像前面的静态类型直接就去查看Class实例是否有匹配返回地址,而是须要依据上面的五个指针类型进行是否直接查找直接引用仍是其余的实现再返回地址做为直接引用)

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

    1. 静态分派(重载)
    2. 动态分派(重写)
    3. 单分派与多分派

    为了提升动态分派效率,咱们还专门在方法区中创建了虚方法表

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

咱们已经知道咱们在加载阶段就在堆中实现了Class,使得咱们能后续能为常量池中的常量项进行解析,最后会将解析后的常量池放到运行时常量池中进行调用

经过 咱们开始依据运行时常量池中的方法顺序依据直接引用的地址调用code中的字节码指令(此时,不管是哪一种指令集,指令集只是咱们将二进制按位数划分助记而已,已经都是0101这种cpu能解读的模式了,但都须要按照(操做码字段|地址码字段)来传送给CPU,不一样的指令集只是将二进制串划分红不一样的段交给CPU,CPU所能读取指令长度会依据指令的操做码判断是几地址指令,好比add它能够有00,01,10,11 分别表示1234地址指令)

如果基于栈的解释执行,咱们会依据各个方法建立栈帧,并用栈帧中的操做数栈实现字节码指令对操做系统对于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就是相应实现后编译的指令集的入口,并且要考虑不少的优化和并发,其中特别是要本身实现用户线程去调用CPU仍是要本身的用户线程调用操做系统的API通过操做系统的内核线程使用CPU,线程调用CPU后获得的运算结果,要本身去调用IO等仍是回操做系统的API实现都是很复杂的须要考虑编译器可否实现准确的编译后可否适配的,还须要借助汇编语言来查看调试优化,太难了

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

相关文章
相关标签/搜索