Java字符数组似乎需要每个字符超过2个字节的空间。

15

当我运行以下程序(使用"java -Xmx151M -cp . com.some.package.xmlfun.Main"命令运行):

package com.some.package.xmlfun;
public class Main {

    public static void main(String [] args) {
        char [] chars = new char[50 * 1024 * 1024];

    }
}

我需要将最大内存增加到至少151M(-Xmx151M)。因此,当我增加数组大小时,限制需要增加:

  • 50 * 1024 * 1024 -> -Xmx151M
  • 100 * 1024 * 1024 -> -Xmx301M
  • 150 * 1024 * 1024 -> -Xmx451M

为什么看起来 Java 需要每个字符3个字节,而不是文档所建议的2个字节?

同样地,当我创建一个long类型的数组时,它似乎需要12个字节,而不是8个字节;对于int类型,它需要6个字节而不是4个字节。通常情况下,它看起来需要数组大小*元素大小*1.5。

使用以下命令编译:- javac \com\som\package\xmlfun\\*java

使用以下命令运行:- java -Xmx151M -cp . com.some.package.xmlfun.Main


好问题...我不认为仅靠对齐约束就能解释那个。 - fge
1
com.some.package.xmlfun 是一个有效的包名吗? - johnchen902
4个回答

8
我想你所看到的现象可以很容易地解释为JVM中堆是如何组织的。
当您向JVM传递参数-Xmx时,您正在定义应该是什么最大堆大小。但是,它与您可以分配的最大数组大小没有直接关系。
在JVM中,垃圾回收器负责为对象分配内存并清理死亡对象。它是垃圾回收器决定如何组织堆。
通常有一个称为Eden空间,然后是两个survivor space和最后的tenured generation。所有这些都在堆内,GC将最大堆分成它们之间。有关这些内存池的更多详细信息,请查看此出色的答案:https://dev59.com/q3M_5IYBdhLWcg3wt1g0#1262474 我不知道默认值是什么,它们可能确实取决于您的系统。我刚刚使用sudo jmap PID检查了在运行Ubuntu 64位和Oracle的Java 7的系统上运行的应用程序中,内存池如何划分堆。机器有1.7GB内存。
在那种配置下,我只向JVM传递了-Xmx,GC将堆分成以下几部分:
  • Eden空间约占27%
  • 每个survivor space约占3%
  • tenured generation约占67%
如果您有类似的分布,这意味着您的151MB的最大连续块位于tenured generation中,约为100MB。由于数组是内存的连续块,而且您无法让一个对象跨越多个内存池,所以这解释了您所看到的行为。
您可以尝试玩弄垃圾回收器参数。请在此处检查垃圾回收器参数:http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html 对我来说,您的结果似乎非常合理。

好的观点,使用 Runtime#freeMemorytotalMemory 进行测试会更加可靠。 - Marko Topolnik
然而,如果 main() 函数就像 OP 所制作的那样简单,那将是一种浪费内存的行为... 几乎等于数组本身大小的一半。我不相信不同的堆区域可以解释这样的开销。 - fge
@fge,我使用的是64位Linux系统和Oracle Java 7的默认配置。它似乎将3%的堆分配给“from”空间,另外3%分配给“to”空间。然后,它将大约27%分配给“eden”空间,最后将剩余的67%分配给“tenured”空间。因此,如果OP使用150MB的堆启动程序,则最大的连续内存区域将有大约100MB。这可能被称为“浪费内存”,具体取决于您的看法。 - Bruno Reis
那些“细节”应该在你的回答中提到,这确实解释了很多问题,事实上它几乎可以解释结果... - fge

6
在Java HotSpot VM中,堆被分为“新生代”和“老年代”。数组必须在它们之一中。新/老年代大小比的默认值为2。 (实际上表示old/new=2) 因此,通过一些简单的数学运算可以证明,一个151MB的堆可以具有50.33MB的新生代和100.67MB的老年代。同样,一个150MB的堆恰好有100MB的老年代。您的数组+其他所有内容(例如args)将耗尽这100MB,因此会产生OutOfMemoryError
我试图运行:
java -Xms150m -Xmx150m -XX:+PrintGCDetails Main > c.txt

并从 c.txt 中获取以下内容:

(...)
堆
 PSYoungGen      总共 44800K,已使用 3072K (地址...)
  eden 区域 38400K,已使用 8% (...)
  from 区域 6400K,已使用 0% (...)
  to   区域 6400K,已使用 0% (...)
 ParOldGen       总共 102400K,已使用 217K (...)
  对象区域 102400K,已使用 0% (...)
 PSPermGen       总共 21248K,已使用 2411K (...)
  对象区域 21248K,已使用 11% (...)

这里的空间大小可能与我的计算结果略有不同,但它们非常接近。


你有这个的源链接吗? - jlordo
这里说比例是1/2。不确定这是否是正确的参考资料,但这是需要考虑的事情,并且它并不违背这个答案的核心。 - zw324
@ZiyaoWei 是的,你说得对。但是这份文件本身很容易让人产生误解。 - johnchen902

1
如果您查看数据的大小(例如使用Visual GC),则会发现数组的大小确实为每个字符2个字节。
问题在于JVM试图将整个数组放入堆的旧生代中,而该代的大小受新生代/旧生代大小比例的限制。
使用-XX:NewRatio = 5运行将纠正此问题(默认值为2)。

0

我会尝试在Bruno的回答上进行补充。我现在尝试了这段代码:

public static void main(String[] args) throws IOException {
    char [] chars = new char[50 * 1024 * 1024];
    System.out.println(Runtime.getRuntime().freeMemory());
    System.out.println(Runtime.getRuntime().totalMemory());
    System.out.println(Runtime.getRuntime().maxMemory());
}

输出结果为:

38156248
143654912
143654912

显然,有40 MB的空闲用于JVM的其他目的。我最好的猜测是为了新生代空间。

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