本机内存可能导致未知的内存泄漏


最近,我遇到了一个奇怪的情况:我的程序的内存使用量超出了用于堆的最大值。即使在运行GC之后,部分内存也不可用。我已经知道将JVM内存的一部分分配给本机内存,并将部分本机内存分配给C代码,但是我的程序中甚至没有一行本机代码。在多次检查和分析代码后,我发现了一个有趣的问题。在深入探讨问题之前,让我们看一下Java内存的概念。

Java中的内存管理

JVM将内存分为两个主要空间,堆和本机内存。堆空间用于分配Java对象,而本机内存是OS可用的内存。Java 7和8在内存管理模型上有一个关键区别。Java 7具有PermGen;PermGen是堆中的内存区域,JVM使用它来存储类元数据,静态内容,原始变量。Java 8取消了PermGen并添加了元空间。实际上,Metaspace和PermGen做相同的事情。主要区别在于PermGen是Java堆的一部分,而Metaspace不是该堆的一部分。而是元空间是本机内存的一部分, 仅受主机操作系统限制。

java-7-8-memory.jpg

本机内存

本机内存是普通JVM堆之外的内存区域,但通常是操作系统为JVM进程保留的总内存的一部分。本机内存的一部分分配给了C堆;C堆是Java程序中本机C程序使用的空间。

本机内存跟踪(NMT)

NMT是一种内存跟踪工具,用于监视程序的本机内存使用情况。您可以使用jcmd实用程序访问NMT数据。NMT默认情况下未启用;您可以通过将以下参数添加到JVM选项来启用它:

-XX:NativeMemoryTracking=detail-XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics

为了在执行期间监视和跟踪内存更改,您应该使用以下命令:

jcmd VM.native_memory detail.diff

是Linux中Java程序的确切进程ID。在我的情况下,我的是'92165',因此我执行了以下命令:

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