Java 進程內存占用除了JVM 運行時數據區,還有直接內存(Direct Memory)區域及 JVM 程序自身也會占用內存
- 直接內存(Direct Memory)區域:直接內存通過使用Native堆外內存來存儲數據,這意味著數據不會被JVM的垃圾回收機制自動回收。與JVM堆內存相比,直接內存的分配和釋放成本較高,因為它涉及與操作系統交互和內存管理的開銷,也可能導致OOM異常出現
- JVM 程序自身:JVM本身是個本地程序,還需要其他的內存去完成各種基本任務,比如,JIT Compiler 在運行時對熱點方法進行編譯,就會將編譯后的方法儲存在 Code Cache 里面;GC 等功能需要運行在本地線程之中,類似部分都需要占用內存空間
JVM內存區域劃分詳見 Java 內存區域與內存溢出異常
堆外內存
JVM 的堆外內存是指分配在JVM堆之外的內存空間,它不受JVM的垃圾回收機制管理。 以下是幾種常見的JVM堆外內存:
- 直接字節緩沖區(Direct ByteBuffers):Direct ByteBuffer是JVM堆外內存的一種形式,它通過使用Native堆外內存來存儲數據。
- NIO(New I/O)內存映射文件(Memory-mapped Files):NIO提供了一種將文件映射到內存的方式,這種內存映射文件將文件的內容直接映射到堆外內存中,可以通過內存訪問的方式來讀寫文件。
- JNI(Java Native Interface):JNI允許Java程序與本地代碼進行交互,可以在本地代碼中分配和管理堆外內存。
堆外內存可以使用Native Memory Tracking 或 Arthas memory 進行監控及診斷
直接字節緩沖區
在實際使用中,Java 會盡量對 Direct Buffer 僅做本地 IO 操作,對于很多大數據量的 IO 密集操作,可能會帶來非常大的性能優勢,因為:
- Direct Buffer 可以通過
ByteBuffer.allocateDirect()
方法來創建,它的數據存儲在堆外內存中,生命周期內內存地址都不會再發生更改,進而內核可以安全地對其進行訪問,很多 IO 操作會很高效 - 減少了堆內對象存儲的可能額外維護工作,所以訪問效率可能有所提高
Direct Buffer 創建和銷毀過程中,都會比一般的堆內 Buffer 增加部分開銷,所以通常都建議用于長期使用、數據較大的場景。
可以使用JVM參數設定直接內存限制
-XX:MaxDirectMemorySize=512M
大多數垃圾收集過程中,都不會主動收集 Direct Buffer,它的垃圾收集過程,就是基于 Cleaner(一個內部實現)和幻象引用(PhantomReference)機制,其本身不是 public 類型,內部實現了一個 Deallocator 負責銷毀的邏輯。對它的銷毀往往要拖到full GC
的時候,所以使用不當很容易導致OutOfMemoryError
Direct Buffer 回收方式:
- 在應用程序中,顯式地調用
System.gc()
來強制觸發。 - 另外一種思路是,在大量使用 Direct Buffer 的部分框架中,框架會自己在程序中調用釋放方法(Netty 就是這么做的,有興趣可以參考其實現PlatformDependent0)
- 重復使用 Direct Buffer
NIO
Java NIO(New I/O)是Java提供的一套用于高效處理I/O操作的API,引入自JDK 1.4版本。相對于傳統的Java I/O(IO流)API,Java NIO提供了更靈活、更高效的非阻塞I/O操作方式,適用于構建高性能的網絡應用程序。
Java NIO的核心概念包括以下幾個部分:
- 通道(Channel):通道是數據源和數據目標之間的連接,可以通過通道讀取和寫入數據。通道可以是雙向的,可以從通道中讀取數據,也可以向通道中寫入數據
- 緩沖區(Buffer):緩沖區是一個固定大小的數據容器,用于存儲讀取和寫入的數據。通過緩沖區可以更高效地讀寫數據,避免頻繁的數據拷貝操作。緩沖區可以讀取和寫入不同類型的數據,如字節、字符、整數等
- 選擇器(Selector):選擇器是用于多路復用非阻塞I/O操作的組件。可以通過選擇器同時管理多個通道,使得單線程可以處理多個通道的I/O操作,提高系統的性能和吞吐量
NIO提供了一種將文件映射到內存的方式,這種內存映射文件將文件的內容直接映射到堆外內存中。這種方式在處理大型文件時可以提供更高的性能和效率
JNI
使用JNI(Java Native Interface)可以在Java程序中通過調用本地代碼來使用JVM堆外內存。JNI提供了一種機制,使得Java程序可以與本地代碼進行交互,調用本地代碼中的函數和訪問本地內存
通過JNI,Java程序可以直接訪問和操作本地內存,例如在C或C++中使用
malloc()
和free()
函數進行內存分配和釋放
JNI操作JVM堆外內存具體步驟
- 定義本地方法:在Java類中聲明本地方法,使用native關鍵字標記。
public class NativeMemoryExample {public native void allocateMemory(int size);public native void freeMemory();
}
- 生成本地方法的頭文件:使用Java的javac命令編譯Java源文件,然后使用javah命令生成本地方法的頭文件。
javac NativeMemoryExample.java
javah NativeMemoryExample
這將生成名為NativeMemoryExample.h
的頭文件
- 實現本地方法:在本地代碼中實現Java類中聲明的本地方法。在本地方法中可以使用C/C++等編程語言來操作堆外內存
#include "NativeMemoryExample.h"
#include <stdlib.h>JNIEXPORT void JNICALL Java_NativeMemoryExample_allocateMemory(JNIEnv *env, jobject obj, jint size) {void *buffer = malloc(size);// 使用buffer進行堆外內存操作
}JNIEXPORT void JNICALL Java_NativeMemoryExample_freeMemory(JNIEnv *env, jobject obj) {// 釋放之前分配的堆外內存free(buffer);
}
- 編譯本地代碼:使用C/C++編譯器將本地代碼編譯為共享庫(或動態鏈接庫)
gcc -shared -fpic -o libNativeMemoryExample.so NativeMemoryExample.c
- 加載本地庫:在Java程序中使用System.loadLibrary()方法加載本地庫
public class Main {static {System.loadLibrary("NativeMemoryExample");}public static void main(String[] args) {NativeMemoryExample example = new NativeMemoryExample();example.allocateMemory(1024); // 調用本地方法分配堆外內存// ...example.freeMemory(); // 調用本地方法釋放堆外內存}
}
通過以上步驟,Java程序可以使用JNI調用本地方法,在本地代碼中進行對JVM堆外內存的分配和釋放操作。需要注意的是,在使用JNI時應謹慎管理內存,避免內存泄漏和溢出,確保正確地釋放分配的堆外內存
參考資料:
- Java Native Interface
- Direct Buffer
- Native Memory Tracking