Java JAR文件的内存使用与类文件的内存使用比较

9
最近我将我的大型Java应用程序改为以JAR文件形式发布,而不是单独的类文件。我有405个JAR包,其中包含5000个类文件。我的问题是,当我以JAR文件方式运行程序(classpath是通配符,以获取所有JAR包),Java会不断地使用更多的内存。我曾经看到内存超过2GB,并且似乎Java没有进行全局垃圾回收来保持内存较低。如果我使用解压后的JAR文件(只有类文件)运行完全相同的程序,则Java的内存使用率要低得多(<256MB),并保持不变。这发生在Windows 7(x64)和Windows Server(x64)上的Oracle Java 8中。为什么将应用程序打包成JAR文件会改变内存使用情况?此外,我已经以JAR文件的形式运行了很长时间,并将内存最大限制设置为128MB,没有出现任何问题,因此我没有内存泄漏。
使用Classpath中的JAR文件: With JAR files in classpath 使用Classpath中的类文件: With class files in classpath 编辑:我接受了@K Erlandsson提供的答案,因为我认为这是最好的解释,这只是Java的一个丑陋的怪癖。谢谢大家(特别是@K Erlandsson)的帮助。

2
这是堆使用还是总体占用空间?它在 Web 容器中运行吗?它有自定义的类加载机制吗? - biziclop
1
从远程诊断很难,但如果有足够的内存(并且-Xmx足够高),则完整的GC会发生得更少。很可能是多次打开所有这些JAR文件留下了很多垃圾,而不需要清理应用程序就会不断增长。最终将发生完整的GC并收集所有内容。如果是这种情况,则保持使用-Xmx低的最大堆可以是一件好事的好例子。 - biziclop
当访问JAR文件以获取类时,可能会创建更多的临时对象。 - ZhongYu
我理解如果程序在处理JAR文件时使用了更大的基础内存量,但是它不应该持续增长。无论是类文件还是JAR文件都没有受到限制的最大内存(Xmx),Java表示在这两种情况下的最大内存量为3GB。我正在进行另一个jvisualvm.exe跟踪,以便我可以附上一张图片。 - Adam
1
这是一个问题吗?JVM会在需要时进行垃圾回收,这就是为什么当您将堆限制为128MB时,它可以正常工作的原因。但是,如果您有一个大堆,并且没有完全使用它,那么JVM没有停止正在进行的操作来执行大型垃圾回收的事实肯定是一件好事,而不是坏事,对吧? - Dawood ibn Kareem
显示剩余11条评论
2个回答

3
首先需要注意的是,堆上完全使用了多少内存并不总是很有趣,因为许多使用的内存可能是垃圾,并且将在下一次 GC 时清除。
你需要关注的是由活动对象使用的堆内存。你在评论中写道:

我不知道这是否重要,但如果我使用 jvisualvm.exe 强制进行 GC(标记扫描),堆内存使用量将下降,几乎清除了所有堆内存。

这很重要。非常重要。这意味着当你使用你的 jars 时看到更高的堆使用率时,你看到的是更多的垃圾,而不是更多的被活动对象占用的内存。当你执行 GC 并清除所有内容时,垃圾就会被清除,一切都好了。
从 jar 文件中加载类将比从类文件中加载类消耗更多的内存。jar 文件需要被打开、查找和读取。这需要更多的操作和更多的临时数据,而不仅仅是打开一个特定的 .class 文件并读取它。
由于大部分堆使用情况会在 GC 时被清除,所以这种额外的内存消耗并不是你需要非常担心的事情。
你还写道:

Java 将不断使用更多的内存。我看到内存超过了 2GB,似乎 Java 没有进行停止-世界垃圾收集以保持内存较低。

这是典型的行为。GC 只在 JVM 认为必要时运行。JVM 将根据内存行为进行调整。 编辑: 现在我们看到你的 jConsole 图像中有一个已提交堆内存的差异(250MB vs 680MB)。已提交的堆是堆的实际大小。这将根据 JVM 认为对应用程序性能最好的设置而变化(最高可达 -Xmx),但它几乎只会增加,几乎不会减少。
对于 jar 的情况,JVM 为你的应用程序分配了更大的堆。可能是由于在初始类加载期间需要更多的内存。然后 JVM 认为更大的堆会更快。
当你有一个更大的堆,更多的已提交内存,有更多的内存可以在运行 GC 之前使用。这就是为什么你会看到两种情况下内存使用情况的差异。 最重要的是:你看到的所有额外使用都是垃圾,而不是活动对象,所以除非你有实际的问题,否则无需担心这种行为,因为内存将在下一个 GC 上被回收。

1
奇怪的事情是(我很快会有 jconsole.exe 的图片),JAR 类路径会导致内存继续增长,而不像类文件那样。我认为 JAR 首先会造成更多的垃圾是有道理的,但是当类路径只使用其中的一小部分(<128m)时,它不应该导致 JAR 文件最终使用 1GB 的 RAM。 - Adam
@Adam 但是你看到的不仅仅是两种情况下不同的垃圾回收行为吗?初始爆发会以不同的方式调整垃圾回收。 - K Erlandsson
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Adam
是的,我同意基础内存量肯定可以有所不同,但我不明白(正如您在我的最新编辑中可以看到的图片)为什么JAR文件的内存使用量会比类文件运行时更大。 - Adam
@Adam 为什么不把 -Xmx 设置为 128 Mb 呢?从你所写的内容来看,这似乎已经足够了,然后所有这些“问题”都会消失。 - K Erlandsson
显示剩余3条评论

0

从类路径加载资源是很常见的。当资源来自于一个JAR文件时,URL对象将保持对JAR文件条目的引用。这可能会增加一些内存消耗。可以通过禁用默认URL缓存来禁用此缓存。

禁用默认URL缓存的API非常笨拙:

public static void disableUrlConnectionCaching() {
    // sun.net.www.protocol.jar.JarURLConnection leaves the JarFile instance open if URLConnection caching is enabled.
    try {
        URL url = new URL("jar:file://valid_jar_url_syntax.jar!/");
        URLConnection urlConnection = url.openConnection();
        urlConnection.setDefaultUseCaches(false);
    } catch (MalformedURLException e) {
        // ignore
    } catch (IOException e) {
        // ignore
    }
}

在应用程序启动时禁用默认的URL缓存。

Tomcat已经默认禁用了URL缓存,因为它还会导致文件锁定问题,并防止在运行中的应用程序中更新jar文件。

https://github.com/apache/tomcat/blob/5bbbcb1f8ca224efeb8e8308089817e30e4011aa/java/org/apache/catalina/core/JreMemoryLeakPreventionListener.java#L408-L423


这段代码看起来像是会禁用未来加载的JAR文件的URL缓存,对吗?如果这些JAR文件已经在类路径上并且(我猜)已经被缓存了,这个方法还能起作用吗? - Adam
是的,情况就是这样。只有在您的应用程序使用ClassLoader/Class.getResource从类路径加载资源时才有帮助。 - Lari Hotari

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接