Java 中的突发内存使用情况

我试图在Java中处理正确的内存使用和垃圾回收。无论如何,我都不是新手程序员,但在我看来,一旦Java触及一些内存,它就永远不会被释放给其他应用程序使用。在这种情况下,您必须确保峰值内存永远不会太高,否则您的应用程序将持续使用峰值内存使用量。

我写了一个小示例程序来演示这一点。它基本上有4个按钮...

  1. 用大约 25,000,000 个长字符串项填充类作用域变量。BigList = new ArrayList<string>()
  2. BigList.clear()
  3. 再次重新分配列表(以缩小列表大小)BigList = new ArrayList<string>()
  4. 电话 - 是的,我知道这并不意味着GC真的会运行,但这就是我们所拥有的。System.gc()

因此,接下来我在Windows,Linux和Mac OS上进行了一些测试,同时使用默认任务监视器来检查进程报告的内存使用情况。这是我的发现...

  • Windows - 抽取列表,调用清除,然后多次调用GC不会减少内存使用量。但是,使用然后多次调用 GC 重新分配列表会将内存使用率降低到起始级别。国际海事组织,这是可以接受的。new
  • Linux(我使用Mint 11发行版和Sun JVM) - 结果与Windows相同。
  • Mac OS - 我遵循了与上面相同的步骤,但即使重新初始化列表调用GC似乎也没有效果。该程序将使用数百MB的RAM,即使我的内存中没有任何东西。

任何人都可以向我解释一下吗?有些人告诉我一些关于“堆”记忆的事情,但我仍然不完全理解它,我不确定它是否适用于这里。根据我所听说的,无论如何,我都不应该看到我在Windows和Linux上的行为。

这只是Mac OS的“活动监视器”测量内存使用情况的方式的差异,还是发生了其他事情?我宁愿我的程序不会因大量RAM使用而闲置。感谢您的见解。


答案 1

Sun/Oracle JVM 不会向系统返回不需要的内存。如果您为它提供一个大的,最大的堆大小,并且您实际上在某个时候使用了该堆空间,则JVM不会将其返回给操作系统用于其他用途。其他JVM会这样做(JRockit曾经这样做过,但我认为它不再这样做了)。

因此,对于Oracles JVM,您需要调整应用程序和系统以达到峰值使用率,这就是它的工作原理。如果可以使用字节数组(例如处理图像或其他内容)管理您正在使用的内存,则可以使用映射的字节缓冲区而不是 Java 字节数组。映射的字节缓冲区直接从系统中获取,而不是堆的一部分。当你释放这些对象时(我相信它们是GC的,但不确定),内存将返回到系统。你可能不得不玩那个,假设它甚至完全适用。


答案 2

...但是在我看来,一旦Java触及了一些内存,它就永远消失了。你永远不会得到它回来。

这取决于你所说的“永远消失”是什么意思。

我还听说,一些JVM在准备就绪并能够时确实会将内存返回给操作系统。不幸的是,考虑到低级内存API通常的工作方式,JVM必须返回整个段,并且“疏散”一个段以便可以将其返回往往很复杂。

但我不会依赖这一点...因为有各种各样的事情可以阻止内存被归还。JVM 可能不会内存返还给操作系统。但它并不是“永远消失”,因为JVM将继续使用它。即使 JVM 再也不会接近使用高峰期,所有这些内存也将有助于使垃圾回收器更有效地运行。

在这种情况下,您必须确保峰值内存永远不会太高,否则您的应用程序将不断消耗数百MB的RAM。

事实并非如此。假设您采用的策略是从一个小堆开始并让它增长,那么 JVM 不会要求比峰值内存多得多的内存。JVM不会不断消耗更多的内存...除非您的应用程序存在内存泄漏,并且(因此)其峰值内存要求没有限制。

(OP下面的评论表明,这不是他想说的。即便如此,这是他所说的。


关于垃圾回收效率的主题,我们可以将高效垃圾回收器运行的成本建模为:

cost ~= (amount_of_live_data * W1) + (amount_of_garbage * W2)

其中 W1 和 W2 是(我们假设的)依赖于收集器的常量。(实际上,这是一种过度简化。第一部分不是活动对象数量的线性函数。但是,我声称这对以下内容无关紧要。

然后,收集器的效率可以表示为:

efficiency = cost / amount_of_garbage_collected

它(如果我们假设GC收集所有数据)扩展到

efficiency ~= (amount_of_live_data * W1) / amount_of_garbage + W2.

当 GC 运行时,

heap_size ~= amount_of_live_data + amount_of_garbage

所以

efficiency ~= W1 * (amount_of_live_data / (heap_size - amount_of_live_data) )
              + W2.

换句话说:

  • 随着堆大小的增加,效率趋向于一个常数 (W2),但是
  • 您需要很大比例的heap_size amount_of_live_data才能实现这一点。

另一点是,对于高效的复制收集器,W2 仅涵盖将垃圾对象占用的空间归零在“从空间”的成本。其余的(追踪,将活动对象复制到“到空间”,并将它们占据的“从空间”归零)是初始方程第一项的一部分;即 W1 涵盖。这意味着W2可能比W1小得多...并且最终方程的第一项在更长的时间内是有意义的。

显然,这是一个理论分析,成本模型是对真实垃圾收集器真正工作原理的简化。(它没有考虑应用程序正在执行的“实际”工作,也没有考虑占用太多内存的系统级影响。但是,数学告诉我,从GC效率的角度来看,一个大堆确实很有帮助。


推荐