最近,我遇到了一个奇怪的情况:我的程序的内存使用量超出了用于堆的最大值。即使在运行GC之后,部分内存也不可用。我已经知道将JVM内存的一部分分配给本机内存,并将部分本机内存分配给C代码,但是我的程序中甚至没有一行本机代码。在多次检查和分析代码后,我发现了一个有趣的问题。在深入探讨问题之前,让我们看一下Java内存的概念。
Java中的内存管理
JVM将内存分为两个主要空间,堆和本机内存。堆空间用于分配Java对象,而本机内存是OS可用的内存。Java 7和8在内存管理模型上有一个关键区别。Java 7具有PermGen;PermGen是堆中的内存区域,JVM使用它来存储类元数据,静态内容,原始变量。Java 8取消了PermGen并添加了元空间。实际上,Metaspace和PermGen做相同的事情。主要区别在于PermGen是Java堆的一部分,而Metaspace不是该堆的一部分。而是元空间是本机内存的一部分, 仅受主机操作系统限制。
本机内存
本机内存是普通JVM堆之外的内存区域,但通常是操作系统为JVM进程保留的总内存的一部分。本机内存的一部分分配给了C堆;C堆是Java程序中本机C程序使用的空间。
本机内存跟踪(NMT)
NMT是一种内存跟踪工具,用于监视程序的本机内存使用情况。您可以使用jcmd实用程序访问NMT数据。NMT默认情况下未启用;您可以通过将以下参数添加到JVM选项来启用它:
-XX:NativeMemoryTracking=detail-XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics
为了在执行期间监视和跟踪内存更改,您应该使用以下命令:
jcmd
jcmd 92165 VM.native_memory detail.diff.
考虑到监视是基于时间的方法,这意味着您应该将时间T2中的内存状态与时间T1中的状态进行比较,因此您需要使用以下命令将T1的状态标记为基线:
jcmd 92165 VM.native_memory baseline
我还按时间顺序执行了以下命令:
2020年12月19日9:07:10 IRS:jcmd 92165 VM.native_memory baseline
2020年12月19日下午12:07:10国税局:jcmd 92165 VM.native_memory detail.diff.
的输出是这样的:
reserved 3892 KB for Thread Stack
from [Thread::record_stack_base_and_size()+0xca]
[0x9f586000 - 0x9f791000] reserved 1680KB for Thread Stack
from [Thread::record_stack_base_and_size()+0xca]
我尝试jcmd 92165 VM.native_memory detail.diff在不同的时间执行命令,并且每时每刻都得到不同的结果,这意味着我的本机内存使用量随着时间的推移而增长。
所以有什么问题?!
在上一部分中,我说过本机内存会随着时间增长,并且可以确定程序中没有本机代码,但是那是什么问题呢?我在监视的同时开始跟踪程序,我发现了一件有趣的事情:程序中使用了java.util.zip代码。一旦我到达此代码,本机内存的使用就会大大增加。问题很明显。该程序包在内部进行分类并使用本机代码。如果您在jdk8 source上查看JDK源代码,则可以找到类似这样的有趣代码,以及基于本机代码和C的某些类的实现:
src/share/native/java/util/zip/ZipFile.c
JNIEXPORT jlong JNICALL
Java_java_util_zip_ZipFile_open(JNIEnv *env, jclass cls, jstring name,
jint mode, jlong lastModified,
jboolean usemmap)
{
const char *path = JNU_GetStringPlatformChars(env, name, 0);
char *msg = 0;
jlong result = 0;
int flag = 0;
jzfile *zip = 0;
if (mode & OPEN_READ) flag |= O_RDONLY;
if (mode & OPEN_DELETE) flag |= JVM_O_DELETE;
if (path != 0) {
zip = ZIP_Get_From_Cache(path, &msg, lastModified);
if (zip == 0 && msg == 0) {
ZFILE zfd = 0;
#ifdef WIN32
zfd = winFileHandleOpen(env, name, flag);
if (zfd == -1) {
/* Exception already pending. */
goto finally;
}
#else
zfd = JVM_Open(path, flag, 0);
if (zfd < 0) {
throwFileNotFoundException(env, name);
goto finally;
}
#endif
zip = ZIP_Put_In_Cache0(path, zfd, &msg, lastModified, usemmap);
}
if (zip != 0) {
result = ptr_to_jlong(zip);
} else if (msg != 0) {
ThrowZipException(env, msg);
free(msg);
} else if (errno == ENOMEM) {
JNU_ThrowOutOfMemoryError(env, 0);
} else {
ThrowZipException(env, "error in opening zip file");
}
finally:
JNU_ReleaseStringPlatformChars(env, name, path);
}
return result;
}
JNIEXPORT jint JNICALL
Java_java_util_zip_ZipFile_getTotal(JNIEnv *env, jclass cls, jlong zfile)
{
jzfile *zip = jlong_to_ptr(zfile);
return zip->total;
}
JNIEXPORT jboolean JNICALL
Java_java_util_zip_ZipFile_startsWithLOC(JNIEnv *env, jclass cls, jlong zfile)
{
jzfile *zip = jlong_to_ptr(zfile);
return zip->locsig;
}
JNIEXPORT void JNICALL
Java_java_util_zip_ZipFile_close(JNIEnv *env, jclass cls, jlong zfile)
{
ZIP_Close(jlong_to_ptr(zfile));
}
请记住,本机代码不仅在这些类中。有许多组件(例如与设备交互的组件或通信组件)可能使用本机代码。
原文链接:http://codingdict.com