【原创】JVM系列03 | Java栈—方法是如何调用的? - 云+社区

腾讯云 · · 156 次点击 · · 开始浏览    

本文是何适 JVM 修仙系列第 3 篇,文末有本系列文章汇总。

上一篇介绍 Java 虚拟机结构时讲到 Java 栈同 Java 方法的调用密切相关,那么这篇就来探究下 Java 栈到底和方法的调用有什么关系。Java 栈分如下几部分介绍:

  1. 栈帧
  2. 局部变量表
  3. 操作数栈
  4. 帧数据

1. 栈帧

线程被创建的时候 Java 栈被创建,Java 栈中保存着栈帧。线程中方法被调用时,对应着一个栈帧被压入 Java 栈;方法返回时,对应的栈帧从 Java 栈中弹出;方法中抛出异常时,对应的栈帧也会将栈帧弹出。

举例说明:方法 1 中调用方法 2,方法 2 中调用方法 3,方法 3 中调用方法 4,当执行到方法 4 时,栈内存结构如下图:

举例模拟栈溢出

因为每次方法调用都会生成一个栈帧,当函数调用数量很大,生成的栈帧内存超过 Java 栈总内存时,就会栈溢出 StackOverflowError。

-Xss参数指定最大栈内存,也就是函数调用的最大深度。

以下代码用递归调用来模拟这种情况:

public class StackTest {
    public static int count = 0;
    public static void test() {
        count++;
        test();
    }

    public static void main(String[] args) {
        try {
            test();
        } catch (Throwable e) {// 使用Throwable捕获,StackOverflowError是Error,不是Exception
            System.out.println("调用深度count=" + count);
            e.printStackTrace();
        }
    }
}

设置参数-Xss128K 执行以上代码,结果如下:

调用深度count=1088
java.lang.StackOverflowError
	at test.test1.StackTest.test(StackTest.java:6)
	at test.test1.StackTest.test(StackTest.java:7)
	at test.test1.StackTest.test(StackTest.java:7)
	at test.test1.StackTest.test(StackTest.java:7)
	···

在 test()方法执行了 1088 次之后,栈溢出。你可以尝试调整-Xss 参数查看不同的结果,我将-Xss 参数改为 256K 之后,可以执行 2736 次。

栈帧中包含局部变量表、操作数栈、帧数据。

2. 局部变量表

局部变量表用于保存函数参数和局部变量。

局部变量表中的变量只有在当前函数调用中有效,当函数调用结束后,栈帧销毁,局部变量表也就销毁了。

局部变量数量多,会使得栈帧更占内存,导致函数嵌套调用次数减少。局部变量表中的槽位是可以重用的,从而节省内存。如果一个局部变量过了作用域,那么它的槽位就可以被其他局部变量复用。

举例:局部变量占用栈内存

上文中的 test()方法没有局部变量,可以执行 2736 次。现在 test()方法中加入 10 个局部变量,同样 256K 的栈内存,可以递归 905 次 test()方法。

public class StackTest {
    public static int count = 0;
    public static void test() {
        long a=1,b=2,c=3,d=4,e=5,f=6,g=7,h=8,i=9,j=10;
        count++;
        test();
    }

    public static void main(String[] args) {
        try {
            test();
        } catch (Throwable e) {// 使用Throwable捕获,StackOverflowError是Error,不是Exception
            System.out.println("调用深度count=" + count);
            e.printStackTrace();
        }
    }
}

执行结果如下:

调用深度count=905
java.lang.StackOverflowError
	at test.test1.StackTest.test(StackTest.java:6)
	at test.test1.StackTest.test(StackTest.java:8)
	at test.test1.StackTest.test(StackTest.java:8)
	...

2.2 局部变量对垃圾回收的影响

你可以先不看注释,自己分析下局部变量会不会被回收,然后核对下注释。

栈帧中的局部变量槽位是可以复用的。如果局部变量 a 过了作用域,那么之后声明的局部变量 b 就可以复用 a 的槽位。

public class LocalVarGCTest {
    public static void main(String[] args) {
        localVarGC5();
    }

    /**
     * 数组被a引用,不能被回收
     * [GC (System.gc())  12206K->10968K(123904K), 0.0046455 secs]
     */
    public static void localVarGC1() {
        byte[] a = new byte[10 * 1024 * 1024];
        System.gc();
    }

    /**
     * 数组没有引用,可以被回收
     * [GC (System.gc())  12206K->704K(123904K), 0.0007060 secs]
     */
    public static void localVarGC2() {
        byte[] a = new byte[10 * 1024 * 1024];
        a = null;
        System.gc();
    }

    /**
     * 虽然a变量已经失效,但是因为方法还没结束,
     * a变量依然存在于局部变量表中,并指向数组,所以byte数组不能被回收
     * [GC (System.gc())  12206K->11000K(123904K), 0.0044869 secs]
     */
    public static void localVarGC3() {
        {
            byte[] a = new byte[10 * 1024 * 1024];
        }
        System.gc();
    }

    /**
     * a变量失效,c变量复用了a的槽位,使得byte数组的引用失效,所以byte数组会被回收
     * [GC (System.gc())  12206K->744K(123904K), 0.0008775 secs]
     */
    public static void localVarGC4() {
        {
            byte[] a = new byte[10 * 1024 * 1024];
        }
        int c = 10;
        System.gc();
    }

    /**
     * localVarGC1()方法中byte数组没有被回收
     * localVarGC1()执行完后,localVarGC1()的栈帧被销毁,byte数组也就没有了引用
     * localVarGC5()GC时可以将byte数组回收
     *
     * [GC (System.gc())  12206K->10896K(123904K), 0.0041066 secs]
     * [Full GC (System.gc())  10780K->540K(123904K), 0.0029822 secs]
     */
    public static void localVarGC5() {
        localVarGC1();
        System.gc();
    }
}

3 操作数栈

操作数栈用于保存计算过程的中间结果,作为计算过程中变量临时的存储空间。

4 帧数据

帧数据区用于支持常量池解析、方法返回和异常处理。

常量池解析:帧数据区保存着访问常量池的指针,当 JVM 执行到需要常量池数据的指令时,它都会通过帧数据区中指向常量池的指针来访问它。

方法返回:如果是通过 return 正常结束,则当前栈帧从 Java 栈中弹出,恢复发起调用的方法的栈。如果方法有返回值,JVM 会把返回值压入到发起调用方法的操作数栈。

异常处理:帧数据区保存着一个异常处理表,遇到异常就会查找异常处理表来处理,如果异常处理表中没有找到处理方法,则结束当前函数调用,抛出异常。

举例:异常处理表(Exception table)

from

to

target

type

4

16

19

any

19

21

19

any

表示在字节码偏移量 4-16 字节可能抛出任意异常,如果遇到异常,则跳转到字节码偏移量 19 处执行。

5. 总结

参考资料

  1. 《深入理解 Java 虚拟机(第 2 版) : JVM 高级特性与最佳实践》
  2. 《实战 Java 虚拟机 : JVM 故障诊断与性能优化》

JVM 系列文章汇总

【原创】JVM 系列 01 | 开篇 【原创】JVM 系列 02 | Java 虚拟机结构

本文来自:腾讯云

感谢作者:腾讯云

查看原文:【原创】JVM系列03 | Java栈—方法是如何调用的? - 云+社区

156 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传