Java的.class文件的最大大小是多少?

29

一个.class文件是一种相当良好记录的格式,它定义了各个部分和大小,因此也定义了最大大小。

例如,.class文件包含一个魔数(4个字节),一个版本(4个字节),常量池(可变大小)等。但是可以在多个级别上定义大小:您可以拥有65535个方法,每个方法限制为65535个字节。

其他限制是什么?如果要创建最大的.class文件,它会有多大?

如有必要,请将答案限制在Java中。这意味着如果Scala或Clojure(或...)更改了某些限制,请忽略这些值。


请问这些链接是否有帮助?它们分别是:https://dev59.com/k1XTa4cB1Zd3GeqP4sBB 和 https://dev59.com/c3VD5IYBdhLWcg3wDHDm。 - Suresh Atta
@sᴜʀᴇsʜᴀᴛᴛᴀ 不是,第一个只显示方法大小(我在问题中引用了它)。第二个是关于Java文件大小的(由于空格只是被丢弃,因此大小可以无限大)。这个问题是关于.class文件的最大大小。 - Olivier Grégoire
2
仅供记录:在实际情况下,最大大小不应该成为问题。如果你的类文件超过了这个限制,这意味着你的Java源代码必须是巨大的。如此之大,以至于被认为是不合理的几个数量级。当然,这是一个有趣的理论问题,但当你的日常工作受到答案的影响时,那么很有可能:你正在做一些非常错误的事情;-) - GhostCat
@GhostCat 是的,这是一个理论问题,而不是实际问题。实际上,我更感兴趣的是文件系统可以处理多少个文件,因为我通常会将我的代码分割成短文件而不是大文件 ;) - Olivier Grégoire
2
限制不仅仅是理论上的。当处理生成的源代码或生成的字节码时,限制确实很重要。至于文件数量,现代文件系统完全可以处理巨大和/或众多的文件,所以这方面不用担心。 - Kayaman
可能是关于这个问题的重复提问。 - zeeshan
3个回答

29

JVM规范没有规定类文件的限制,因为类文件是可扩展的容器,支持任意自定义属性,所以您甚至可以将其最大化。

每个属性都有一个大小字段,类型为u4,因此可以指定高达2³²-14GiB)的数字。由于在实践中,JRE API(ClassLoader方法、Instrumentation API和Unsafe)都一致使用byte[]ByteBuffer来描述类文件,因此不可能创建具有超过2³¹-1字节(2GiB)的类文件的运行时类。

换句话说,即使一个自定义属性的大小超过了实际可加载类的大小,但一个类最多可以有65535个属性、65535个字段,每个字段都有自己的65535个属性,再加上65535个方法,每个方法也可以有多达65535个属性。
如果你做一下计算,你会得出这样的结论:一个仍然格式良好的类文件的理论最大值可能超过任何实际存储空间(超过2⁶⁵字节)。

我之前并不知道这种格式的可扩展性,以及它的属性机制。我一直认为在.class格式中已经考虑到了所有的东西。我的错。这基本上是任何人可以给出的最终答案了。接下来几天我会将你的回答标记为被接受的答案,以便其他有见地的答案也能够出现。 - Olivier Grégoire
2
结论是完全正确的,我已经成功编写了一个程序(链接:https://gist.github.com/apangin/7c8cfb671c8683a751fa3fdae0bbb38f),它可以创建一个大小为2,147,483,647字节的有效类文件,并且可以成功加载到HotSpot JVM中,但再多一个字节就不行了。然而,原因并不是JRE API,因为引导类加载器既不使用byte[]也不使用ByteBufferUnsafe。但是,HotSpot classFileParser 也依赖于int变量来处理类文件流。 - apangin
1
@apangin: 你的主类通常是通过应用程序类加载器加载的,这是一种专门的URLClassLoader,因此与众所周知的API绑定。如果引导加载器实现受到同样类型的限制也不会让我感到惊讶,但在实际情况下,引导加载器读取共享类数据(.jsa)存档而不是类文件。 - Holger
1
@apangin:嗯,依赖引导加载程序会违反良好形式的Java程序的定义。无论如何,重要的是实际考虑因素,例如,如果我实现一个类转换器,JVM是否知道一种加载巨大类文件的方法并不重要,因为Instrumentation API需要创建适合字节数组的表示形式。同样,如果我生成一个类文件以在运行时实例化,我必须面对这样一个事实,即没有JRE方法接受更大的文件(没有Java接口到引导加载程序),等等。 - Holger
1
@apangin:JVMTI不是Java API,JVM不必提供此API。Instrumentation API是Java API,但仍然不可用于任意Java应用程序,而只能用于Java代理,这又不是干净的Java程序可以依赖的强制性功能。 - Holger
显示剩余2条评论

25

使用嵌套的finally块可以轻松地制作巨大的StackMapTable,因为javac不明智地为每个嵌套级别生成单独的变量。这可以使非常简单的方法产生几兆字节的数据:

class A {{
  int a;
  try {a=0;} finally {
  try {a=0;} finally {
  try {a=0;} finally {
  try {a=0;} finally {
  try {a=0;} finally {
  try {a=0;} finally {
  try {a=0;} finally {
  try {a=0;} finally {
  try {a=0;} finally {
  try {a=0;} finally {
  try {a=0;} finally {
  try {a=0;} finally {
  a=0;
  }}}}}}}}}}}}
}}

由于单个方法的代码大小限制,无法添加更多嵌套层级。您也可以使用实例初始化程序复制到每个构造函数来进行复制:

class A {{
  int a;
  try {a=0;} finally {
  try {a=0;} finally {
  try {a=0;} finally {
  try {a=0;} finally {
  try {a=0;} finally {
  try {a=0;} finally {
  try {a=0;} finally {
  try {a=0;} finally {
  try {a=0;} finally {
  try {a=0;} finally {
  try {a=0;} finally {
  try {a=0;} finally {
  a=0;
  }}}}}}}}}}}}
}
A() { }
A(int a) { }
A(char a) { }
A(double a) { }
A(float a) { }
A(long a) { }
A(short a) { }
A(boolean a) { }
A(String a) { }
A(Integer a) { }
A(Float a) { }
A(Short a) { }
A(Long a) { }
A(Double a) { }
A(Boolean a) { }
A(Character a) { }

}

编译使用Java 8 javac的这个简单Java文件将产生105,236,439字节的.class文件。您也可以添加更多构造函数,但有风险导致javac出现OutOfMemoryError(使用javac -J-Xmx4G来解决此问题)。


1
不错。现在,让我们将其与try-with-resource结合使用... - Holger
3
太棒了。 :) - Christopher Schultz
1
值得注意的是,此行为仅适用于遵循Java 1.6标准编译器,其中finally块被编译为内联字节码,而不是使用JSR指令。有关更多信息,请参见此问题答案 - RlonRyan

3

一个带有方法的类的理论上的、半现实的极限可能是由常量池所限制的。所有方法中的常量总数不能超过64K。java.awt.Component类有2863个常量和83548字节,具有相同字节/常量比率的类在1.9MB时就会耗尽常量池。相比之下,像com.sun.corba.se.impl.logging.ORBUtilSystemException这样的类则会在大约3.1MB处用尽常量池。

对于一个大型类,你很可能在常量池中用尽常量达到2-3MB左右。

相比之下,sun.awt.motif.X11GB18030_1$Encoder中充满了大量的常量字符串,这个类只有68个常量,但大小为122KB。该类没有任何方法。

经过实验,我的编译在21800个常量左右就会爆炸。

public static void main(String[] args) throws FileNotFoundException {
    try (PrintWriter out = new PrintWriter("src/main/java/Constants.java")) {
        out.println("class Constants {");
        for (int i = 0; i < 21800; i++) {
            StringBuilder sb = new StringBuilder();
            while (sb.length() < 100)
                sb.append(i).append(" ");
            out.println("private static final String c" + i + " = \"" + sb + "\";");
        }
        out.println("}");
    }
}

还有一个问题,似乎编译器会将文本加载到ByteBuffer中。这意味着源代码不能大于1 GB,否则编译器将出现错误。我猜测字节中的字符已经溢出成了负数。

java.lang.IllegalArgumentException
    at java.nio.ByteBuffer.allocate(ByteBuffer.java:334)
    at com.sun.tools.javac.util.BaseFileManager$ByteBufferCache.get(BaseFileManager.java:325)

1
我知道,虽然我认为你的回答很实用,但我不认为它回答了理论部分。问题是关于.class文件,而不是编译成.class文件的.java文件。 - Olivier Grégoire
1
通过实验,似乎编译器无法处理1 GB或更大的源文件。我怀疑这可以修复,但我怀疑Oracle是否认为值得这样做。 - Peter Lawrey
1
我刚刚查看了源代码。正如预期的那样,限制是由int溢出引起的,因此限制是2GiB,而不是1GiB - Holger
@Holger 将源代码的字节转换为字符后,大小为2 GiB。将文本转换回UTF8字符串后,这大约是1 GB的.java文件。如果使用三个字节的字符,则可能意味着约3 GB。 - Peter Lawrey
1
堆栈跟踪清楚地指向了ByteBuffer,试图分配一些字节。从这个地方,我无法推断出与char的关系。但问题本来就是关于类文件而不是源文件的... - Holger
显示剩余3条评论

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