This commit is contained in:
Qiu
2026-06-19 14:45:07 +08:00
commit a94cd3e890
428 changed files with 35429 additions and 0 deletions
@@ -0,0 +1,154 @@
---
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 <jni.h>/* 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的内存管理机制和垃圾收集器机制,以及一些实用工具。