JVM6에 대한 심층적인 이해 -- 피연산자 스택과 관련된 명령어 세트 분석

함께 쓰는 습관을 들이세요! "너겟 데일리 뉴플랜 · 4월 업데이트 챌린지" 참여 6일차입니다. 클릭하시면 이벤트 내용을 보실 수 있습니다.

머리말

이전 글에서 자바 바이트코드의 구조에 대해 이야기했는데, 그 중 가장 중요한 것은 상수 풀의 내용과 메소드 테이블의 내용이 다양한 JVM 명령어를 포함하고 있는 메소드 테이블의 작동 원리입니다.

텍스트

먼저 이전 글에서 언급했듯이 자바 메소드가 호출될 때마다 스택 프레임이 생성되고 메소드 호출과 리턴이 스택 프레임의 푸시와 팝이라는 기본적인 이해가 있어야 합니다.

그리고 이 스택 프레임도 피연산자 스택과 지역 변수 영역 두 부분으로 나뉩니다.이전에는 지역 변수 영역이 배열이라고 했으며, 이 피연산자 스택은 스택을 사용하여 계산을 유지합니다. 그리고 다양한 명령어가 스택과 로컬 변수 배열에서 작동하여 복잡한 논리를 완성합니다.

피연산자 스택

우선 이것은 스택이다.특성상 FILO만 가능하다.해석과 실행과정에서 자바 메소드에 스택 프레임을 할당할 때마다 JVM은 종종 추가 공간을 계산된 피연산자와 반환값을 저장하기 위한 피연산자 스택 result.

즉, 각 명령어를 실행하기 전에 JVM은 명령어의 피연산자가 피연산자 스택으로 푸시되어야 합니다. 명령어를 실행할 때 JVM은 명령어에 필요한 피연산자를 팝하고 결과를 반환합니다. 명령을 스택에 다시 푸시합니다 .

예를 들어 다음 그림:

이미지.png

스택의 맨 위 요소가 1과 2인 피연산자 스택만 봅니다. 이 때 더하기 iadd 명령어를 실행하기 위해 스택에서 1과 2를 팝한 다음 계산된 값 3을 푸시합니다. 스택에:

이미지.png

iadd 명령어는 피연산자 스택의 처음 2개 요소에만 관심을 갖기 때문에 ? 수정하지 않은 요소입니다.

중복 명령

피연산자 스택이 먼저 언급되기 때문에 바이트코드에는 피연산자 스택에 직접 작용하는 여러 명령이 있습니다. 가장 일반적인 명령은 dup: 스택의 맨 위 요소를 복사하고 pop: 스택의 맨 위 요소를 버리는 것입니다.

여기서는 스택의 최상위 요소를 직접 복사할 수 있는 dup 명령에 대해 설명합니다.예를 들어 다음 코드를 살펴보겠습니다.

public void foo(){
    Object obj = new Object();
}
复制代码

그런 다음 해당 JVM 명령은 다음과 같습니다.

 public void foo();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class java/lang/Object
         3: dup
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: astore_1
         8: return
      LineNumberTable:
        line 6: 0
        line 7: 8

复制代码

我们仅仅看Code区域,这里的new指令会产生未初始化的引用,比如上面第一行指令过后,会生成一个指向一块已分配、未初始化的内存引用压入到操作数栈中,然后需要以这个引用调用构造器,那么这个引用就会出栈,但是构造方法没有返回值再进行入栈,所以当new完之后,需要调用dup进行复制。

假如new完后引用R0入栈,dup后R1入栈,构造器拿着R1进行初始化,初始化完后,R0还在栈中,由于R1和R0指向同一块内存引用,所以R0指向的内存完成了初始化,而且栈顶元素是R0。

注意这里要dup的关键是构造器函数不会返回值。

pop指令

pop指令也是直接操作操作数栈的,它的作用是移除栈顶元素。

比如下面代码:

public static boolean bar(){
    return true;
}

public void foo(){
    bar();
}
复制代码

我们在foo中调用bar,但是不使用bar的返回值,上述代码的JVM指令如下:

public void foo();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: invokestatic  #2                  // Method bar:()Z
         3: pop
         4: return
      LineNumberTable:
        line 10: 0
        line 11: 4
复制代码

正常来说,当调用invokestatic指令后,会返回一个ture压入栈顶,但是我们不需要用这个返回值,所以可以调用pop指令把这个返回值再出栈。

注意pop和dup只能复制和出栈一个栈单元,对于long和double来说,需要使用pop2和dup2指令来复制和出栈2个栈单元。

加载常量到栈中的指令

加载常量到栈中的指令属于基础指令,也是非常常见的,这里分为2种:一种是直接加载常量到栈中,还有一种是加载常量池中的常量到栈中,指令如下:

이미지.png

这里其实理解很好理解,xconst指令就是加载常用x类型的常量值到栈中,而bipush和sipush则是加载1个字节和2个字节能代表的int值,而ldc则是加载常量池中的常量。

比如下面代码:

public static final int a = 100;

public static final String s = "hello";

public void foo(){
    int b = 2;
    int c = a + b;
    System.out.println(s);
}
复制代码

上面代码在foo中,我们定义了值为2的b,以及在外面定义了常量值,下面是JVM指令:

 public void foo();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_2
         1: istore_1
         2: bipush        100
         4: iload_1
         5: iadd
         6: istore_2
         7: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: ldc           #4                  // String hello
        12: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        15: return
      LineNumberTable:
        line 10: 0
        line 11: 2
        line 12: 7
        line 13: 15

复制代码

不难发现 iconst_2就是把2入栈,bipush 100就是把100入栈,ldc #4 就是把常量池中的#4入栈,即 hello 入栈。

总结

本篇文章内容较为基础,但是十分重要,我们要明白JVM的指令集是基于栈来操作的,以及一些简单的指令来操作栈;而对于栈帧中另一个部分局部变量区,我们在下一篇文章继续解析,同时理解一些更复杂的指令集。

추천

출처juejin.im/post/7083438839301144612