Java虚拟机(九)--方法调用(二)

本文深入探讨了Java字节码的结构及其与源代码的对应关系,详细解析了JVM如何处理方法调用,包括栈帧的创建、局部变量表的使用以及操作数栈的深度。同时,阐述了方法调用过程中的动态链接机制,以及JVM为每个线程分配堆栈空间的策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

源码:

public class Test {
    public static void main(String[] args) {
        add(5,8,null);
    }

    public static long add(int a, long b,String[] args) {
        long c=a+b;
        long d=c+9;
        return d;
    }
}

字节码:

Classfile /D:/study/test/Test.class
  Last modified 2018-9-25; size 584 bytes
  MD5 checksum a5fc1b1ad627c6f12a1e0fc39589bd4e
  Compiled from "Test.java"
public class test.Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #8.#30         // java/lang/Object."<init>":()V
   #2 = Long               8l
   #4 = Methodref          #7.#31         // test/Test.add:(IJ[Ljava/lang/String;)J
   #5 = Long               9l
   #7 = Class              #32            // test/Test
   #8 = Class              #33            // java/lang/Object
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               Ltest/Test;
  #16 = Utf8               main
  #17 = Utf8               ([Ljava/lang/String;)V
  #18 = Utf8               args
  #19 = Utf8               [Ljava/lang/String;
  #20 = Utf8               add
  #21 = Utf8               (IJ[Ljava/lang/String;)J
  #22 = Utf8               a
  #23 = Utf8               I
  #24 = Utf8               b
  #25 = Utf8               J
  #26 = Utf8               c
  #27 = Utf8               d
  #28 = Utf8               SourceFile
  #29 = Utf8               Test.java
  #30 = NameAndType        #9:#10         // "<init>":()V
  #31 = NameAndType        #20:#21        // add:(IJ[Ljava/lang/String;)J
  #32 = Utf8               test/Test
  #33 = Utf8               java/lang/Object
{
  public test.Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Ltest/Test;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=1, args_size=1
         0: iconst_5
         1: ldc2_w        #2                  // long 8l
         4: aconst_null
         5: invokestatic  #4                  // Method add:(IJ[Ljava/lang/String;)J
         8: pop2
         9: return
      LineNumberTable:
        line 5: 0
        line 6: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  args   [Ljava/lang/String;

  public static long add(int, long, java.lang.String[]);
    descriptor: (IJ[Ljava/lang/String;)J
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=8, args_size=3
         0: iload_0
         1: i2l
         2: lload_1
         3: ladd
         4: lstore        4
         6: lload         4
         8: ldc2_w        #5                  // long 9l
        11: ladd
        12: lstore        6
        14: lload         6
        16: lreturn
      LineNumberTable:
        line 9: 0
        line 10: 6
        line 11: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      17     0     a   I
            0      17     1     b   J
            0      17     3  args   [Ljava/lang/String;
            6      11     4     c   J
           14       3     6     d   J
}
SourceFile: "Test.java"

add方法最大栈深度为4个槽(执行a+b时,a和b都占2个槽位,所以是4),局部变量表共8(b,c,d各占2个,a和args占一个)个槽,参数数量3个.

jvm在计算完Java方法除入参以外参数所需分配的堆栈空间,然后执行空间分配。
从call_stub例程进入entry_point例程时:
1.暂存返回地址(保证Java方法的入参(在call_stub栈顶)和Java方法的局部变量表连成一片。)
2.获取Java函数第一个入参在堆栈中的位置(当前栈顶上移n-1个指针宽度。计算入参时,默认全部当指针类型处理)
3.为局部变量分配空间(如果除了参数外,没有其它局部变量,则跳过这一步)
4.构建栈帧的一部分fixed frame,固定帧。
栈帧是个容器,存放的是函数内部的局部变量。多个栈帧连起来,就是堆栈(堆不是指堆区,只是描述多个站的关系)。
JVM为每个线程分配了一块独立的内存区域,作为线程的堆栈。64位默认每个线程堆栈空间大小为1M。1M(可以支持至少一万多个函数顺序调用,或者递归调用)一般算大的,降低堆栈大小,可以提高最大线程数。


JVM的栈帧

栈帧由:局部变量表、固定帧和操作数栈组成。
在编译成字节码时,栈帧中需要多大的局部变量表、多深的操作数栈都已经完全确定,并保存到字节码文件中的方法表的Code属性中。

每一个栈帧都包含一个指向运行时常量池中改栈帧所属方法的引用,持有这个引用
是为了支持方法调用过程中的动态链接。class文件的常量池中有大量的符号引用,
字节码的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用
一部分会在类加载阶段或第一次使用时化为直接引用,这种转化称为静态解析。
另一部分在每一次运行期间转化为直接引用,这部分叫做动态链接。
所以动态链接的关键是Java方法栈中持有指向常量池的指针。

创建栈帧

1、恢复return address
return address是调用方的下一条指令的内存地址,恢复return address就是把寄存器eax(保存的就是return address)中的地址存放到栈中。
return address是紧跟在局部变量表之后的,也是固定帧的第一个元素。
2、开辟新的栈帧
此时,会把调用方的栈底保存。
3、保存最后一个入参地址
用于堆栈回收
4、计算Java方法第一个字节码的地址
5、将methodOop压栈(用于方法调用过程中读取到方法的全部信息)
6、将常量池缓存ConstanPoolCache压栈
常量池缓存中的内容都是直接引用
7、Java方法第一个入参的地址压栈(在edi中,也是指向局部变量表的指针)
8、将第四步计算的字节码地址压栈
9、将当前栈顶地址压栈(也就是操作数栈的栈底)


局部变量表

局部变量表每个变量所占的槽位是以slot为单位,而slot的长度为一个引用类型的长度(4字节或者8字节)。

除了long和double类型占用2个slot以外,其它类型都占用1个slot。
也就是说,在64位机器上,long和double在局部变量表中占用16个字节。
由下面的表可以看出,b,c,d都是占用2个槽位的。
1+2+1+2+2=8,locals=8;
args_size=3,表示有3个入参,不是指入参占用3个槽位。

  public static long add(int a, long b,String[] args) {
        long c=a+b;
        long d=c+9;
        return d;
    }
    stack=4, locals=8, args_size=3   
  LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      17     0     a   I
            0      17     1     b   J
            0      17     3  args   [Ljava/lang/String;
            6      11     4     c   J
           14       3     6     d   J

待续,。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值