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

6.8 KiB
Raw Blame History

title, date, tags
title date tags
JVM前篇 2024-08-25
JVM

前言

jvm规范

image 52.png

概述

首先我们要了解虚拟机的具体定义,我们所接触过的虚拟机有安装操作系统的虚拟机,也有我们的Java虚拟机,而它们所面向的对象不同,Java虚拟机只是面向单一应用程序的虚拟机,但是它和我们接触的系统级虚拟机一样,我们也可以为其分配实际的硬件资源,比如最大内存大小等。

并且Java虚拟机并没有采用传统的PC架构,比如现在的HotSpot虚拟机,实际上采用的是**基于栈的指令集架构**,而我们的传统程序设计一般都是基于``**寄存器**``的指令集架构,这里我们需要回顾一下计算机组成原理中的CPU结构:

image 1 30.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启动流程

image 3 17.png

JNI调用本地方法

Java还有一个JNI机制,它的全称:==Java Native Interface==,即Java本地接口。它允许在Java虚拟机内运行的Java代码与其他编程语言(如C/C++和汇编语言)编写的程序和库进行交互(在Android开发中用得比较多)比如我们现在想要让C语言程序帮助我们的Java程序实现a+b的运算,首先我们需要创建一个本地方法:

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头文件:

javah -classpath out/production/SimpleHelloWorld -d ./jni com.test.Main

生成的头文件位于jni文件夹下:

/* 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文件:

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

所以我们这里直接返回a+b即可:

\#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目录下的头文件:

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程序中加载:

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);
}

运行,成功得到结果:

/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的内存管理机制和垃圾收集器机制,以及一些实用工具。