JVM内存布局与 JNA 调用本地方法原理详解
JVM 内存布局与 JNA 调用本地方法原理详解
JVM 内存布局详解#
JVM 内存布局随着 JDK 版本不同而不同, 但是大致布局以及运行原理相同, 我们选择 JDK1.8 的内存布局解释.
下图是JDK 1.8 的内存布局的示意图:
程序计数器(PC)#
这个是当前线程正在执行的字节码行号指示器, 类似于实际的PC, 根据这里面的内存数据来确定程序接下来执行的指令. 在JAVA中, 每个线程都有一个, 相互隔离, 线程之间的切换就是基于程序计数器. 如果执行的是方法, 这里记录的是虚拟机字节码指令的地址. 注意:当执行的是Native方法的时候为空(Undefined).
因为只存储一个指令, 所以它不会出现任何 OutOfMemoryError.
Java虚拟机栈#
每个线程私有, 里面装的多个栈帧, 每个栈帧对于的一个方法. 里面存储的是Java方法的内存模型. 相当于描述的是一个方法需要的内容. 逻辑上类似于操作系统中的进程栈, 每个线程都有一个虚拟机栈, 每个栈中都有多个栈帧. 每个方法的执行过程都是栈帧的进栈于出栈, 类似于进程中的函数调用.
每个栈帧存就是对方法的描述, 栈帧中存储局部变量, 局部变量是一个方法内使用的变量, 包括各种数据类型的临时变量. (boolean、byte、char、short、int、float、long、double 类型), 以及对象的引用. 对象本身并不会存储在线程栈中, 即使这个对象是在方法中新建的, 线程栈中也仅会保存对象的引用, 作为局部变量, 而对象本身则存储在JVM的堆空间中.
异常情况:线程请求的栈深度大于虚拟机允许的深度, 将抛出StackOverflowError异常. 如果虚拟机栈可以动态扩展, 当扩展的时候没有申请到内存的时候抛出OutOfMemoryError.
本地方法栈#
每个线程都有自己的本地方法栈, 这时线程私有的, 用于线程支持对本地方法的调用, 本地方法栈(Native Method Stack)是为调用本地方法(Native Method)服务的, 一个典型的例子是使用 JNI(Java Native Interface)调用用 C/C++ 编写的本地代码. 本地方法通常用于调用操作系统的 API, 使用已有的 C++ 库, 与硬件交互等, 本地方法栈也就是本地方法代码执行的地方.
Java堆#
Java虚拟机管理最大的一块, 线程共享, 存放对象实例和数组.分新生代(1/3)和老年代(2/3), 新生代还可以分Eden(8/10)、From Survivor(1/10) 、To Survivor(1/10), 是主要根据垃圾清理来分的.
异常情况: 无法再对对象实例分配, 并且堆也无法扩展时, 将抛出OutOfMemoryError.
方法区#
线程共享, 主要存储被虚拟机加载的类信息, 常量, 静态变量, 即时编译器编译后的代码. 运行时常量池也是方法区的一部分, 比如String有一个常量池, 他就是放到这个里面的. 类似于进程的代码区以及全局变量区.
异常情况: 当方法区无法满足内存分配时, 将抛出OutOfMemoryError异常.
直接内存#
NIO通过使用Native函数库直接分配对外内存, 因为JVM本质上是一个进程, 这部分内存实际上是 JVM 进程的堆内存, java 程序可以在内部申请这块内存使用, 这种使用方式相当于绕过了 JVM 的内存管理与自动回收机制, 内存管理需要开发者手动管理. Java 支持两种方法使用这块内存, 分别是 JVM 自身提供的接口, 这块内存直接被 Java 代码使用, 开发者可以自由的使用申请到的堆外内存. 另一种是 Native 代码申请内存, 这部分内存用于 JNA 调用本地代码的时候使用. 这两种使用方式的相同点是, 两者均使用的是 JVM 这个进程的用户的虚拟空间的堆的内存空间, 不同点是:
特性 | 堆外内存(Off-Heap Memory) | Native 代码申请的内存 |
---|---|---|
分配方式 | 使用 JVM 提供的接口(如 Unsafe.allocateMemory 或 DirectByteBuffer ). |
通过本地代码(C/C++)直接调用操作系统 API(如 malloc ). |
管理接口 | Java 层提供操作接口, Unsafe 或 DirectByteBuffer 包装了分配的内存. |
完全依赖 C/C++ 的内存管理工具(如指针操作). |
语言依赖 | 完全在 Java 中操作, 使用 Java 提供的工具类或方法来访问和管理内存. | 必须通过 JNI 或其他桥接机制从 Java 调用 C/C++ 函数. |
释放机制 | DirectByteBuffer 可依赖 JVM 的 Cleaner 机制自动释放(但不及时). |
由 C/C++ 开发者显式调用 free 或 delete 来释放内存. |
线程安全性 | Java 层可能提供一些线程安全特性(如 ByteBuffer 的读写同步). |
完全依赖 C/C++ 代码的实现. |
调试和诊断工具支持 | JVM 提供了一些工具(如 jmap )可以查看堆外内存的使用情况. |
必须依赖 C/C++ 的调试工具(如 valgrind ). |
异常情况: 不受Java堆大小限制, 但是受机器的物理内存限制, 当各个内存区域大于机器物理内存的时候, 会出现OutOfMemoryError.
JNA 调用本地方法详解#
首先我们需要明白什么是本地方法:
本地方法#
本地方法(Native Method)是指使用非 Java 语言(通常是 C 或 C++)编写的函数, 通过 Java 提供的接口在 JVM 中调用这些方法. "本地"指的是些方法与当前运行环境(操作系统和硬件)紧密关联, 直接使用底层的系统资源或外部库, 而不依赖 JVM 本身的实现. 本地方法用 native 关键字在 Java 中声明, 但实现部分由其他语言完成, 通常通过动态链接库(如 .so 或 .dll 文件)提供.
本地方法的用途#
由于本地方法可以跳出 JVM 与其他进程交互, 有下列的主要用途:
- 调用操作系统功能:
Java 无法直接访问的系统级资源(如文件描述符、网络接口、设备驱动)需要通过本地方法来操作. 示例: 通过本地方法实现文件锁定、访问系统进程信息等. - 性能优化:
对于高性能需求的场景, 例如图像处理, 数据压缩等, 可以通过本地方法调用更高效的 C/C++ 实现. - 调用已有的动态库:
如果已有功能以 C/C++ 的形式提供, 而不希望重新用 Java 实现, 可以通过本地方法直接调用现有的动态链接库.
Java 本地方法调用的方式#
Java 使用 JNI (Java Native Interface) 作为调用本地方法的接口, 调用过程如下图所示:
JNI 是由 JVM 提供的一个原生接口, 用于让 Java 调用本地代码(如 C/C++ 编写的动态链接库).它通过手动编写桥接层, 完成 Java 与底层代码之间的绑定与交互.
这种机制虽然功能强大, 但需要开发者编写额外的头文件及绑定代码, 增加了复杂性.
JNA (Java Native Access) 基于 JNI 实现, 提供了更高级别且简单的 API, 用于调用本地代码.与 JNI 不同, JNA 不需要手动编写桥接层, 而是通过动态代理和反射直接调用本地函数.
JNA 的主要特点是无需编写桥接代码, 其基本流程如下:
C 代码实现#
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
编译生成动态链接库:
gcc -shared -o libnative.so -fPIC native.c
Java 中调用动态链接库#
通过 JNA 调用动态链接库, 不需要写头文件或桥接代码, 直接调用即可:
import com.sun.jna.Library;
import com.sun.jna.Native;
public class JNADemo {
// 定义接口, 继承 Library
public interface NativeLibrary extends Library {
NativeLibrary INSTANCE = Native.load("native", NativeLibrary.class); // 加载动态库
int add(int a, int b); // 定义本地方法
}
public static void main(String[] args) {
int result = NativeLibrary.INSTANCE.add(5, 3); // 调用本地方法
System.out.println("Result: " + result);
}
}
JNA 本地方法调用的原理#
JNA 的工作流程看似简洁, 但背后的运行机制涉及 JVM 如何加载和调用外部程序.我们可以从 代码加载 和 执行过程 两个角度分析其原理.
动态链接库的加载位置#
JVM 本质上是一个进程.Java 程序调用动态链接库的过程实质上是 JVM 进程加载和调用动态链接库.这与 C 程序调用动态链接库类似:
动态链接库的代码会被加载到 JVM 进程的虚拟地址空间中的动态链接库代码区(Code Segment).在操作系统中, 每个程序的虚拟地址空间都有自己的代码区, 用于存储加载的动态链接库.
本地方法的执行方式#
当线程调用动态链接库中的函数时, 其执行方式与调用 Java 方法类似, 都是通过 程序计数器 和 栈调用 完成.
区别在于:
- 调用 Java 方法时, 线程使用的是 JVM 的 Java 方法栈.
- 调用本地方法时, 线程使用的是每个线程私有的 本地方法栈.
本地方法的内存操作#
在执行本地方法时, 涉及以下两部分内存:
- 本地方法栈:
本地方法中的局部变量和调用信息存储在本地方法栈中.这是线程私有的, 作用类似于 JVM 的线程栈. - 堆外内存:
动态链接库中可能会通过malloc
动态分配内存, 或者通过指针操作访问额外的内存空间.这些操作会申请 JVM 堆外的内存区域(Off-Heap Memory).
堆外内存的分配和释放由动态链接库自行管理, 与 JVM 的垃圾回收器无关.这是因为动态链接库与 JVM 是独立的运行单元, 其内存操作直接基于操作系统提供的用户态内存管理(如 brk
或 mmap
).
本质上, 动态链接库申请的堆外内存位于 JVM 进程所属的虚拟地址空间, 由操作系统分配和管理.
总结#
- JNI 是 Java 调用本地代码的原生接口, JNA 基于 JNI 提供了更简化的调用方式.
- 动态链接库加载到 JVM 进程的动态链接库代码区中, 并通过本地方法栈执行.
- 本地方法可能涉及 JVM 堆外内存的操作, 需手动管理内存, 避免内存泄漏.
- JNA 的简化特性使其更适合快速调用外部动态链接库, 但在性能上可能略逊于 JNI.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
2019-12-27 Note of Compression of Neural Machine Translation Models via Pruning