【问题标题】:Inferring a method's stack memory use in Java在 Java 中推断方法的堆栈内存使用情况
【发布时间】:2012-03-14 23:48:49
【问题描述】:

我正在尝试确定每个方法在运行时消耗多少堆栈内存。为了完成这个任务,我设计了这个简单的程序,它只会强制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 和当前的 i 之间的值。正如预期的那样,它是固定的。这就是问题所在。据我所知,堆栈大小的内存增量为 128k,这会产生类似于每次调用 80 字节的内存使用量(这似乎被夸大了)。

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

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

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

我做了几个假设,其中一些可能完全不成立。既然如此,我全都听好了。

在有人开始问我为什么要这样做之前I must be frank..

【问题讨论】:

    标签: java jvm profiling stack


    【解决方案1】:

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

    首先,return address pointer 指的是什么?当一个方法完成时,返回方法从堆栈帧中弹出。因此,在执行方法 Frame 中没有存储返回地址。

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

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

    全部来自JVM Specification on Frames.

    更新

    搜索 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;
    

    Source

    【讨论】:

    • 这是一些有见地的信息,先生。不过,您能否推测一下,为什么每个方法调用似乎需要 80 个字节?
    • 我可以告诉你我自己的 JVM 实现在 Frame 结构中保存了哪些信息?
    • @devouredelysium 用 OpenJDK 源代码更新了我的答案。
    【解决方案2】:

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

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

    【讨论】:

    • 是的,我忘了明确说明 24 字节包括 2 个变量和返回地址。
    • "可能还有其他上下文信息被保存,即使您认为它不需要保存。" 这就是我想知道的!我正在向任何可以解决这个问题的人提供饼干和酒精!
    • 在 JNI 调用中,包含 jenv(环境)和 jclass(类)。解决这个问题的最佳方法是阅读 OpenJDK 代码。
    猜你喜欢
    • 2010-09-21
    • 2012-01-26
    • 1970-01-01
    • 2012-01-03
    • 2014-05-23
    • 2018-06-06
    • 2021-09-03
    • 2012-04-23
    • 2011-11-30
    相关资源
    最近更新 更多