JVM内存布局与 JNA 调用本地方法原理详解

JVM 内存布局与 JNA 调用本地方法原理详解

JVM 内存布局详解#

JVM 内存布局随着 JDK 版本不同而不同, 但是大致布局以及运行原理相同, 我们选择 JDK1.8 的内存布局解释.
下图是JDK 1.8 的内存布局的示意图:

img

程序计数器(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.allocateMemoryDirectByteBuffer). 通过本地代码(C/C++)直接调用操作系统 API(如 malloc).
管理接口 Java 层提供操作接口, UnsafeDirectByteBuffer 包装了分配的内存. 完全依赖 C/C++ 的内存管理工具(如指针操作).
语言依赖 完全在 Java 中操作, 使用 Java 提供的工具类或方法来访问和管理内存. 必须通过 JNI 或其他桥接机制从 Java 调用 C/C++ 函数.
释放机制 DirectByteBuffer 可依赖 JVM 的 Cleaner 机制自动释放(但不及时). 由 C/C++ 开发者显式调用 freedelete 来释放内存.
线程安全性 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 与其他进程交互, 有下列的主要用途:

  1. 调用操作系统功能:
    Java 无法直接访问的系统级资源(如文件描述符、网络接口、设备驱动)需要通过本地方法来操作. 示例: 通过本地方法实现文件锁定、访问系统进程信息等.
  2. 性能优化:
    对于高性能需求的场景, 例如图像处理, 数据压缩等, 可以通过本地方法调用更高效的 C/C++ 实现.
  3. 调用已有的动态库:
    如果已有功能以 C/C++ 的形式提供, 而不希望重新用 Java 实现, 可以通过本地方法直接调用现有的动态链接库.

Java 本地方法调用的方式#

Java 使用 JNI (Java Native Interface) 作为调用本地方法的接口, 调用过程如下图所示:
img

JNI 是由 JVM 提供的一个原生接口, 用于让 Java 调用本地代码(如 C/C++ 编写的动态链接库).它通过手动编写桥接层, 完成 Java 与底层代码之间的绑定与交互.
这种机制虽然功能强大, 但需要开发者编写额外的头文件及绑定代码, 增加了复杂性.

JNA (Java Native Access) 基于 JNI 实现, 提供了更高级别且简单的 API, 用于调用本地代码.与 JNI 不同, JNA 不需要手动编写桥接层, 而是通过动态代理和反射直接调用本地函数.

JNA 的主要特点是无需编写桥接代码, 其基本流程如下:


C 代码实现#

Copy
#include <stdio.h> int add(int a, int b) { return a + b; }

编译生成动态链接库:

Copy
gcc -shared -o libnative.so -fPIC native.c

Java 中调用动态链接库#

通过 JNA 调用动态链接库, 不需要写头文件或桥接代码, 直接调用即可:

Copy
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 方法栈.
  • 调用本地方法时, 线程使用的是每个线程私有的 本地方法栈.

本地方法的内存操作#

在执行本地方法时, 涉及以下两部分内存:

  1. 本地方法栈:
    本地方法中的局部变量和调用信息存储在本地方法栈中.这是线程私有的, 作用类似于 JVM 的线程栈.
  2. 堆外内存:
    动态链接库中可能会通过 malloc 动态分配内存, 或者通过指针操作访问额外的内存空间.这些操作会申请 JVM 堆外的内存区域(Off-Heap Memory).

堆外内存的分配和释放由动态链接库自行管理, 与 JVM 的垃圾回收器无关.这是因为动态链接库与 JVM 是独立的运行单元, 其内存操作直接基于操作系统提供的用户态内存管理(如 brkmmap).

本质上, 动态链接库申请的堆外内存位于 JVM 进程所属的虚拟地址空间, 由操作系统分配和管理.


总结#

  • JNI 是 Java 调用本地代码的原生接口, JNA 基于 JNI 提供了更简化的调用方式.
  • 动态链接库加载到 JVM 进程的动态链接库代码区中, 并通过本地方法栈执行.
  • 本地方法可能涉及 JVM 堆外内存的操作, 需手动管理内存, 避免内存泄漏.
  • JNA 的简化特性使其更适合快速调用外部动态链接库, 但在性能上可能略逊于 JNI.

posted @   虾野百鹤  阅读(72)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 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
点击右上角即可分享
微信分享提示
CONTENTS