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

2022-09-03 15:55:51

我正在尝试确定每个方法在运行时消耗了多少堆栈内存。为了完成这项任务,我设计了这个简单的程序,它将强制一个,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();
    }
}

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

   stack    i       delta
    128     1102
    256     2723    1621
    384     4367    1644

delta是由我计算的,它是最后一行的i和当前行之间的值。正如预期的那样,它是固定的。问题就在这里。据我所知,堆栈大小的内存增量是128k,这会产生每次调用80字节的内存使用量(这似乎被夸大了)。

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

我认为这可能与内存对齐有关,但事实是,在这种情况下,我们的值大约是64或128字节,我会说。

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

我做了几个假设,其中一些可能完全关闭了。既然如此,我全都洗耳恭听。

在有人开始问我为什么要这样做之前,我必须坦率。


答案 1

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

Java并不像许多语言那样适合大量使用递归。例如,它不做尾部调用优化,在这种情况下,这会导致你的程序永远运行。;)


答案 2

这个问题可能超出了我的脑海,也许你正在更深层次上谈论这个问题,但无论如何,我都会把我的答案扔在那里。

首先,你指的是什么?方法完成后,将从堆栈帧中弹出返回方法。因此,在执行方法 Frame 中不存储任何返回地址。return address pointer

方法 Frame 存储局部变量。由于它是静态的,并且没有参数,因此正如您所说,这些应该是空的,并且op堆栈和局部变量的大小在编译时是固定的,每个单元中的每个单元都是32位宽。但除此之外,该方法还必须具有对其所属类的常量池的引用。

此外,JVM规范指定了方法帧,这可以解释剩余的字节,具体取决于编译器。may be extended with additional implementation-specific information, such as debugging information.

全部来自帧上的 JVM 规范。

更新

搜索OpenJDK源代码会发现这一点,这似乎是在方法调用时传递给Frames的结构。对其中的期望提供了很好的见解:

/* 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;


推荐