Files
OneMD/posts/blog/编程技术/java/JVM/JVM前篇.md
T
2026-06-19 14:45:07 +08:00

154 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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的内存管理机制和垃圾收集器机制,以及一些实用工具。