【问题标题】:What is the maximum size of a Java .class file?Java .class 文件的最大大小是多少?
【发布时间】:2017-02-17 10:05:25
【问题描述】:

.class 文件是一个相当 well documented format 的文件,它定义了部分和大小,因此也定义了最大大小。

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

其他限制是什么?而且,如果您要尽可能制作最大的.class 文件,它会是多大?

如果需要,请将答案限制为 Java。这意味着如果 Scala 或 Clojure(或...)更改了一些限制,请忽略这些值。

【问题讨论】:

  • @sᴜʀᴇsʜᴀᴛᴛᴀ 否:第一个仅显示方法大小(我在问题中引用了它)。第二个是关于 Java 文件大小(它可以是无限的,因为空格被丢弃了)。这个问题是关于.class 文件的最大大小。
  • 仅作记录:最大尺寸在实际中并不重要。如果你最终得到的类文件超过了这个限制,这意味着你的 Java 源代码必须是巨大的。如此巨大以至于它几个数量级太大而无法被认为是合理的。当然,这是一个有趣的理论问题,但是当您的日常工作受到答案的影响时,很可能是:您做的事情非常错误;-)
  • @GhostCat 是的,这是一个理论问题,而不是实际问题。实际上,我对文件系统可以处理多少个文件更感兴趣,因为我通常将我的代码剪切成短文件而不是大文件;)
  • 限制不仅仅是理论上的。在处理生成的源代码或生成的字节码时,限制确实很重要。至于文件的数量,现代文件系统完全有能力处理巨大和/或大量的文件,因此无需担心。

标签: java jvm specifications .class-file


【解决方案1】:

JVM 规范没有强制限制类文件,并且由于类文件是可扩展的容器,支持arbitrary custom attributes,您甚至可以根据需要将其最大化。

因此,u4 类型的每个 attribute has a size field 最多可以指定多个 2³²-1 (4GiB)。由于在实践中,JRE API(ClassLoader 方法、Instrumentation API 和Unsafe)都始终使用byte[]ByteBuffer 来描述类文件,因此不可能创建具有超过 2³¹-1 字节 (2GiB)。

换句话说,即使是单个自定义属性的大小也可能超过实际可加载类的大小。但是一个类可以有 65535 个属性,加上 65535 个字段,每个字段有 65535 个自己的属性和 65535 个方法,每个方法最多也有 65535 个属性。

如果您进行数学计算,您会得出结论,仍然格式良好的类文件的理论最大值可能超过任何实际存储空间(超过 2⁶⁵ 字节)。

【讨论】:

  • 我不知道该格式及其属性机制的可扩展性,我认为所有内容都在.class 格式中进行了说明。我的错。这几乎是任何人都可以给出的最终答案。我会在接下来的几天内将您的答案标记为已接受,以便其他有见地的答案出现。
  • 结论是绝对正确的,我已经编写了一个program,它创建了一个2,147,483,647字节的有效类文件,可以成功加载到HotSpot JVM中,但不能再加载一个字节。但是,原因不是 JRE API,因为引导类加载器不使用 byte[]ByteBufferUnsafe。但是,HotSpot classFileParser 也依赖于 int 变量来处理类文件流。
  • @apangin:你的主类通常是通过应用程序类加载器加载的,这是一个专门的URLClassLoader,因此绑定到众所周知的 API。如果引导加载程序实现受到同样的限制,我不会感到惊讶,但在现实生活场景中,引导加载程序读取共享类数据 (.jsa) 存档而不是类文件。
  • @apangin:好吧,依赖引导加载程序会违反格式良好的 Java 程序的定义。无论如何,重要的是实际考虑,例如如果我实现了一个类转换器,那么 JVM 是否知道加载巨大类文件的方法并不重要,因为 Instrumentation API 要求它创建一个适合字节数组的表示。同样,如果我生成一个要在运行时实例化的类文件,我必须面对这样一个事实,即没有 JRE 方法接受更大的文件(没有 Java 接口到引导加载程序)等等。
  • @apangin: JVMTI 不是 Java API,JVM 不强制提供这个 API。 Instrumentation API 是一个 Java API,但仍然不能用于任意 Java 应用程序,而只能用于 Java 代理,这又不是一个干净的 Java 程序可以依赖的强制性功能。
【解决方案2】:

使用嵌套的 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 文件在使用 Java 8 javac 编译时会生成 105,236,439 字节的 .class 文件。您还可以添加更多构造函数,尽管存在 javac 会因OutOfMemoryError 而失败的风险(使用javac -J-Xmx4G 来克服这个问题)。

【讨论】:

  • 不错的一个。现在,让我们将它与 try-with-resource 结合起来……
  • 这太棒了。 :)
  • 值得注意的是,此行为仅适用于 Java 1.6 之后的标准编译器,其中 finally 块被编译为内联字节码,而不是使用 JSR 指令。请参阅this question answer 了解更多信息。
【解决方案3】:

具有方法的类的理论、半现实限制很可能受到常量池的限制。所有方法只能有 64K。 java.awt.Component 有 2863 个常量和 83548 个字节。具有相同字节/常量比率的类将在 1.9 MB 处用完常量池。相比之下,像 com.sun.corba.se.impl.logging.ORBUtilSystemException 这样的类会用完大约 3.1 MB。

对于大型类,您可能会用完常量池中大约 2 - 3 MB 的常量。

根据合同,sun.awt.motif.X11GB18030_1$Encoder 充满了大型常量字符串,大小为 122KB,只有 68 个常量。这个类没有任何方法。

为了进行实验,我的编译在大约 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)

【讨论】:

  • 那些包含 64KiB 的 a=a+a; 的方法呢?几乎没有任何常数,但有很多陈述。您的答案比我实际预期的要低几个数量级(至少 4 GiB+)。你谈到(半)现实的限制,我不记得在我的问题中提出过任何这样的限制。
  • 我知道,虽然我认为您的回答是实用的,但我不认为它回答了理论部分。问题是关于 .class 文件,而不是被编译成 .class 文件的 .java 文件。
  • @OlivierGrégoire 经过实验,编译器似乎无法处理 1 GB 或更大的源文件。我怀疑这可以解决,尽管我怀疑 Oracle 是否会认为值得这样做。
  • 我刚刚检查了the source code。正如预期的那样,限制是由int 溢出引起的,因此,限制是2GiB,而不是1GiB
  • 堆栈跟踪清楚地指向ByteBuffer,试图分配一些字节。从这个地方,我无法推断出与chars 的关系。但问题是关于类文件而不是源文件......
猜你喜欢
  • 2021-04-27
  • 1970-01-01
  • 2015-05-04
  • 2011-01-26
  • 2013-04-22
  • 1970-01-01
  • 2013-02-26
  • 2012-06-30
  • 2011-03-13
相关资源
最近更新 更多