在Java中推断方法的堆栈内存使用情况

15

我正在尝试确定每个方法在运行时使用了多少堆栈内存。为了完成这个任务,我设计了这个简单的程序,它会强制引发一个StackOverflowError

public class Main {
    private static int i = 0;

    public static void main(String[] args) {
        try {
            m();
        } catch (StackOverflowError e) {
            System.err.println(i);
        }
    }

    private static void m() {
        ++i;
        m();
    }
}

打印一个整数,告诉我 m() 函数被调用了多少次。我手动设置了 JVM 的堆栈大小(-Xss VM 参数)为不同的值(128k、256k、384k),得到了下面的结果:

   stack    i       delta
    128     1102
    256     2723    1621
    384     4367    1644

delta是由我计算的,它表示当前行与上一行i值之间的差。正如预期的那样,它是固定的。问题就在这里。我知道栈大小内存增量是128k,这意味着每次调用大约使用80个字节的内存(这似乎夸张了)。

在BytecodeViewer中查找m(),我们可以得到一个最大深度为2的堆栈。我们知道这是一个静态方法,没有this参数传递,并且m()没有参数。我们还必须考虑返回地址指针。因此,每个方法调用应该使用大约3 * 8 = 24个字节(我假设每个变量占用8个字节,当然可能完全错误。是吗?)。即使比这多一点,比如说48字节,我们仍然远远低于80字节的值。

我以为可能与内存对齐有关,但事实上,在那种情况下,我们会得到大约64或128个字节的值,我想。

我正在64位Windows7操作系统下运行64位JVM。

我做出了几个假设,其中一些可能完全错误。如果是这种情况,我很乐意听取建议。

在任何人开始问我为什么要这样做之前,我必须坦白一点..

2个回答

4

您需要在堆栈中包含指令指针(8字节),即使您认为不需要保存其他上下文信息,也可能会保存。对齐方式可以是16字节,也可以是8字节,就像堆一样。例如,它即使没有返回值也可以保留8字节的空间。

Java并不像许多其他编程语言那样适合大量使用递归。例如,它不执行尾递归优化,这种情况下会导致程序无限运行。 ;)


是的,我忘了明确说明24字节包括2个变量和返回地址。 - devoured elysium
3
即使您认为不需要保存其他上下文信息,实际上可能仍然会保存。这就是我想知道的内容!我会给任何能够解决问题的人提供饼干和酒精! - devoured elysium
1
在JNI调用中,包括jenv(环境)和jclass(类)。最好的方法是阅读OpenJDK代码来解决它。 - Peter Lawrey

2
这个问题可能超出了我的能力范围,也许你在更深层次上谈论这个问题,但我还是会尽力回答。
首先,你所指的“返回地址指针”是什么?当一个方法完成时,返回方法将从堆栈帧中弹出。因此,在执行方法帧内部不存储返回地址。
方法帧存储局部变量。由于它是静态的且没有参数,所以应该是空的,就像你所说的那样,而操作数栈和本地变量的大小在编译时固定,每个单元都是32位宽。但除此之外,该方法还必须具有对其所属类的常量池的引用。
此外,JVM规范指定方法帧“可以使用其他特定于实现的信息进行扩展,例如调试信息。” 这可能取决于编译器,可以解释剩余的字节。
以上内容均来自JVM规范
更新:
研究OpenJDK源代码发现,这似乎是传递给方法调用的帧的结构。这为我们提供了对内部情况的很好洞察力。
/* Invoke types */

#define INVOKE_CONSTRUCTOR 1
#define INVOKE_STATIC      2
#define INVOKE_INSTANCE    3

typedef struct InvokeRequest {
    jboolean pending;      /* Is an invoke requested? */
    jboolean started;      /* Is an invoke happening? */
    jboolean available;    /* Is the thread in an invokable state? */
    jboolean detached;     /* Has the requesting debugger detached? */
    jint id;
    /* Input */
    jbyte invokeType;
    jbyte options;
    jclass clazz;
    jmethodID method;
    jobject instance;    /* for INVOKE_INSTANCE only */
    jvalue *arguments;
    jint argumentCount;
    char *methodSignature;
    /* Output */
    jvalue returnValue;  /* if no exception, for all but INVOKE_CONSTRUCTOR */
    jobject exception;   /* NULL if no exception was thrown */
} InvokeRequest;

源代码


先生,那是一些有见地的信息。不过,您能推测一下为什么每个方法调用似乎需要80字节吗? - devoured elysium
我可以告诉您我的JVM实现在Frame结构中持有哪些信息? - Jivings
@devouredelysium 我更新了我的答案,加入了OpenJDK源代码。 - Jivings

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