--- title: JVM前篇 date: 2024-08-25 tags: [JVM] --- ## 前言 jvm规范 ![image 52.png](JVM前篇/image52.png) ## 概述 首先我们要了解虚拟机的具体定义,我们所接触过的虚拟机有安装操作系统的虚拟机,也有我们的Java虚拟机,而它们所面向的对象不同,Java虚拟机只是面向单一应用程序的虚拟机,但是它和我们接触的系统级虚拟机一样,我们也可以为其分配实际的硬件资源,比如最大内存大小等。 并且Java虚拟机并没有采用传统的PC架构,比如现在的**HotSpot虚拟机**,实际上采用的是`**基于栈的指令集架构**`,而我们的传统程序设计一般都是`基于``**寄存器**``的指令集架构`,这里我们需要回顾一下`计算机组成原理`中的CPU结构: ![image 1 30.png](JVM前篇/image130.png) > 省略了C语言在不同架构下编译出的汇编程序 C语言在不同的CPU架构下,实际上得到的汇编代码也不一样,并且在arm架构下并没有和x86架构一样的寄存器结构,因此只能使用不同的汇编指令操作来实现。所以这也是为什么C语言不支持跨平台的原因,依赖于硬件的支持。 Java利用了JVM,它提供了很好的平台无关性(当然,JVM本身是不跨平台的),我们的Java程序编译之后,并不是可以由平台直接运行的程序,而是由JVM运行,同时,我们前面说了,JVM(如HotSpot虚拟机),实际上采用的是`基于栈的指令集架构`,它并没有依赖于寄存器,而是更多的利用操作栈来完成,这样不仅设计和实现起来更简单,并且也能够更加方便地实现跨平台,不太依赖于硬件的支持。 > 省略分析字节码(class)文件分析 实际上我们发现,JVM执行的命令基本都是**入栈出栈**等,而且大部分指令都是没有==操作数(字面量)==的,传统的汇编指令有一操作数、二操作数甚至三操作数的指令,Java相比C编译出来的汇编指令,执行起来会更加复杂,实现某个功能的指令条数也会更多,所以Java的执行效率实际上是不如C/C++的,虽然能够很方便地实现跨平台,但是性能上大打折扣,所以在性能要求比较苛刻的Android上,采用的是**定制版的JVM**,并且是==基于寄存器的指令集架构==。此外,在某些情况下,我们还可以使用JNI机制来通过Java调用C/C++编写的程序以提升性能(也就是本地方法,使用到native关键字) ## jvm历史 JVM会根据当前代码的进行判断,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler) ![image 2 20.png](JVM前篇/image220.png) ## JVM启动流程 ![image 3 17.png](JVM前篇/image317.png) ## JNI调用本地方法 Java还有一个**JNI**机制,它的全称:==Java Native Interface==,即Java本地接口。它允许在Java虚拟机内运行的Java代码与其他编程语言(如C/C++和汇编语言)编写的程序和库进行交互(在Android开发中用得比较多)比如我们现在想要让C语言程序帮助我们的Java程序实现a+b的运算,首先我们需要创建一个本地方法: ```Java public class Main { public static void main(String[] args) { System.out.println(sum(1, 2)); } //本地方法使用native关键字标记,无需任何实现,交给C语言实现 public static native int sum(int a, int b); } ``` 创建好后,接着点击构建(编译)按钮,会出现一个out文件夹,也就是生成的class文件在其中,接着我们直接生成对应的C头文件: ```Shell javah -classpath out/production/SimpleHelloWorld -d ./jni com.test.Main ``` 生成的头文件位于jni文件夹下: ```C /* DO NOT EDIT THIS FILE - it is machine generated */ \#include /* Header for class com_test_Main */ \#ifndef _Included_com_test_Main \#define _Included_com_test_Main \#ifdef __cplusplus extern "C" { \#endif/* * Class: com_test_Main * Method: sum * Signature: (II)V */ JNIEXPORT void JNICALL Java_com_test_Main_sum (JNIEnv *, jclass, jint, jint); \#ifdef __cplusplus } \#endif#endif ``` 接着我们在CLion中新建一个C++项目,并引入刚刚生成的头文件,并导入jni相关头文件(在JDK文件夹中)首先修改CMake文件: ```Plain cmake_minimum_required(VERSION 3.21) project(JNITest) include_directories(/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/include) include_directories(/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/include/darwin) set(CMAKE_CXX_STANDARD 14) add_executable(JNITest com_test_Main.cpp com_test_Main.h) ``` 接着就可以编写实现了,首先认识一下引用类型对照表: ![image 4 15.png](JVM前篇/image415.png) 所以我们这里直接返回a+b即可: ```C++ \#include "com_test_Main.h"JNIEXPORT jint JNICALL Java_com_test_Main_sum (JNIEnv * env, jclass clazz, jint a, jint b){ return a + b; } ``` 接着我们就可以将cpp编译为动态链接库,在MacOS下会生成`.dylib`文件,Windows下会生成`.dll`文件,我们这里就只以MacOS为例,命令有点长,因为还需要包含JDK目录下的头文件: ```Shell gcc com_test_Main.cpp -I /Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/include -I /Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/include/darwin -fPIC -shared -o test.dylib -lstdc++ ``` 编译完成后,得到`test.dylib`文件,这就是动态链接库了。 最后我们再将其放到桌面,然后在Java程序中加载: ```Java public class Main { static { System.load("/Users/nagocoler/Desktop/test.dylib"); } public static void main(String[] args) { System.out.println(sum(1, 2)); } public static native int sum(int a, int b); } ``` 运行,成功得到结果: ```shell /home/nagocoler/jdk-jdk8-b120/build/linux-x86_64-normal-server-slowdebug/jdk/bin/java Main Hello World! Process finished with exit code 0 ``` ### 总结 - 在Java中编写方法接口(即native修饰的方法) - 编译生成class文件,根据clas文件生成JNI头文件(C头文件),其中包含你在C代码中需要实现的函数声明(生成一个c头文件,头文件说明了需要的函数) - 创建C项目,引入生成的头文件,编写需要实现的函数 - 使用==CMake==将C编译成动态链接共享库 - 运行Java程序(需要调用系统类对共享库进行加载) 通过了解JVM的一些基础知识,我们心目中大致有了一个JVM的模型,在下一章,我们将继续深入学习JVM的内存管理机制和垃圾收集器机制,以及一些实用工具。