Contents
JVM_Stack Frame
   Oct 17, 2022     6 min read

Stack Frame

Stack Frame

  • Stack Frame이라는 것은 Thread가 수행하고 있는 Application을 Method단위로 기록하는 곳이다.
  • Stack Frame은 Method를 실행하면 Class의 메타 정보를 이용하여 적절한 크기로 생성된다.
  • Stack Frame의 크기는 Complier 시점에서 이미 결정된다. 그렇기 때문에 가변의 크기가 아니다.

Untitled

Local Variable Section

  • Local Variable Section은 Method의 Parameter Variable(매개변수)과 Local Variable(지역변수)들을 저장한다.
  • Local Variable Section은 배열로 구성되어있으며 인덱스를 통해서 데이터에 접근한다.
  • Parameter Variable는 선언된 순서로 인덱스 할당한다.
  • Local Variable는 Compiler가 인덱스를 할당한다. 할당 한다는 것은 Local Variable Section에 저장공간을 마련한다는 의미.
public int testMethod(int a,
                     char b,
                     long c,
                    float d,
                   Object e,
                   double f,
                   String g,
                     byte h,
                    short i,
                  boolean j) {
        return 0;
}

위의 예제는 기본타입(Primitive Type)참조타입(Reference Type)의 각각 의경우가 Local Variable Section에 저장되는 값이 다르다.

어떻게 다를까? 그림을 통해서 알아보자

Untitled 1

위 그림을 보면 int a는 할당 받아 변수를 저장할 준비가 되어있다. 하지만 e, greference로 할당 받았다.

Local Variable Sectionreference형으로 저장되는 것을 표현한 것이라고 할 수 있다.

Local Variable Section에는 객체의 정보가 직접적으로 들어가있는 것이아니라 Heap의 위치를 말해주는 Reference를 저장한다는 것이다.

실제 객체는 Heap에 저장되는 것이다.

따라서 Integer 형과 int형 중 어떤 것이 성능에서 유리할까 ? 라는 질문이 생기면 당연히 int형이다. Local Variable Section에 Integer형으로 선언한 변수는 Reference형으로 저장되어서 Stack에서 Heap으로 넘어가기때문이다. 그 과정에서 CPU가 연산을 더 한다는 비용을 초래한다.

따라서 Local Variable Section를 통해서 Heap을 찾아가는 경우 CPU 사용률을 높이게 된다.

처음에 인덱스 0번 ? reference 형의 hidden this 는 무엇일까 ?

그림은 reference형의 hidden this라고 되어있다. 이것은 예제의 testMethod()의 어디에서도 선언한 적이 없는 Parameter Variable이다. 이것은 Local Method 혹은 Instanc Method에 무조건 포함되는 것으로써 여기에 저장된 Reference를 통해 Heap에 있는 Class의 Instance 데이터를 찾아가게 된다.

Local Method가 아닌 Class Method, 즉 static으로 선언한 Method의 경우는 Reference 정보가 존재하지 않는다.

Class Variable과 마찬가지로 instance 에 속한 것이 아니라 Class 자체에 속해있는 것이다.

Local Variable Section에서는 int 형? 왜 ?

Local Variable Section에서는 char, byte, short, boolean형으로 선언한 것들이 int 형으로 할당 되어있다.

기본형타입(primitive Type) 중 boolean형은 다른 기본타입과 다르다. 다른 형태의 타입들은 JVM에서 직접 지원하는 형태이다. 하지만 boolean같은 경우 다르다.

byte, short, char는 Local Variable Section이나 Operand Stack에서는 int 형으로 변환하여 저장되고 Heap 등의 다른 곳에서는 원래의 형으로 원복하여 저장한다.

JVM에서 직접 지원하지않는 boolean은 Stack Frame에서 int형으로 바뀌어서 저장된다. byte, short, char와는 달리 JVM내부에서 원복되는 일은 없다. 즉 Heap이나 다른 메모리영역에서도 int형을 유지한다는 것이다. JVM에서 boolean 형은 그저 하나의 숫자에 불과하다.

인덱스의 숫자가 띄엄띄엄 있는 이유는?

long형과 double형에서 인덱스의 수치가 넘어갔다. 다른 기본형 타입과 다르게 long과 double형은 두 개의 엔트리를 차지한다. 이유는 대형 타입이기 때문이다.

  • Local Variable Section에서는 long형과 double형을 찾아 갈 때 첫 엔트리의 인덱스를 통해 찾아며 두 개의 엔트리를 항상 연속으로 저장한다.

Operand Stack

  • Operand Stack : JVM이 프로그램을 수행하면서 연산을 위해 사용되는 데이터 및 그 결과를 Operand Stack에 넣고 처리한다. Operand Stack을 간단히 말하면 JVM의 작업 공간이라고 할 수 있다.
public class JvmInternal{
	public void operandStack(){
		int a, b, c;
		a = 5;
		b = 6;
		c = a + b;
	}
}
  • 위 코드 JvmInternal 클래스를 컴파일한 후 javap -c JvmInternal 하면 bytecode를 볼 수 있다.

Untitled 2

➡️참고

  • invokeinterface: 인터페이스 메서드 호출
  • invokespecial: 생성자, private 메서드, 슈퍼 클래스의 메서드 호출
  • invokestatic: static 메서드 호출
  • invokevirtual: 인스턴스 메서드 호출

➡️아래는 Operand Stack Local Variable Section에서의 연산과정 그림이다.

Untitled 3

  • Operand Stack는 Array 구성되어 있다. 하지만 Local Variable Section 처럼 인덱스를 사용하여 데이터를 처리하지 않는다.

➡️위코드를 해석하면

  • iconst_5는 상수 5를 push하라는 것을 의미하는 bytecode이다.
  • istore_1는 Local Variable Section의 1번 인덱스에 값을 저장하는 것을 의미한다. (위에 그림참고하면 이해가 더 잘 된다. )
  • bipush 6은 상수 6을 Stack에 Push하라는 의미이다. Stack Frame에서 byte, short 형 등은 내부에서 int형이 의미가 없어지는 것은 아니다. 이것은 값의 범위에 따라 내부적으로 구분되어 -128에서 -1까지와 6에서 127까지의정수는 byte로 인식한다. -32768에서 -127까지, 128에서의 32767까지는 short로 인식한다. 6은 byte의 범위 안에 있으므로 bipush라는 Bytecode로 처리되었다.
  • istore_2는 Local Variable Section의 2번 인덱스에 값을 저장하는 것을 의미한다. 그림을 보면 2번 인덱스에 상수 6이 들어가 있다.
  • iload_1은 Local Variable Section의 1번 인덱스에서 int 형의 값을 로드하라는 의미이다. 1번 인덱스의 값이 Operand Stack으로 로드 되어 올라가있다.
  • iload_2는 Local Variable Section의 2번 인덱스에서 int형의 값을 로드하라는 의미이다.
  • iadd는 int형의 값들을 더하라는 의미이다. 연산의 대상이 되는 Operand Stack의 값들이 모두 Pop되어 연산에 사용되었다. 그리고 이 결과값이 다시 Operand Stack에 Push되었음을 그림에서 나타내고 있다.
  • istrore_3은 Local Variable Section의 3번 인덱스에 값을 저장하는 것을 의미한다. Operand Stackdㅔ 잠시 저장된 연산의 결과값이 Pop되어 3번 인덱스에 저장되었다.
  • return은 Method의 수행을 마치고 Stack Frame을 나가는 것을 의미한다.

Frame Data

  • Constant Pool Resolution 정보
  • Normal Method Return
  • Exception Dispatch

Constant Pool Resolution

Constant Pool보다 먼저 Resolution의미부터 알아보자.

Class File Format에서 Java는 모든 참조 정보를 Symbolic Reference로 가지고 있다.

Symbolic Reference는 JVM에서 실제로 접근 할 수 있는 실질적인 DIrect Reference로 변경이 되는데 이러한 것을 Resolution이라 한다.

즉, Resolution이란 Symbolic Reference로 표현된 Entry를 찾아 Direct Reference로 변경하는 과정을 의미한다.

Class의 모든 Symbolic Reference는 Method Area의 Constant Pool 이라는 곳에 저장되어 있기 때문에 Resolution을 Constant Pool Resolution이라고 부르는 것이다.

Frame Data에 저장된 Constant Pool Resolution 정보는 관련 Constant Pool의 Pointer정보이다.

JVM은 이 Pointer정보를 이용하여 필요할 때마다 Constant Pool을 찾아 간다.

보통 상수를 가져올 때 Constant Pool의 Entry 정보를 참조하기도 하지만 다른 Class를 참조하거나, Method를 수행하거나 아니면 특정 변수들을 접근할 때에도 Constant Pool을 참조해야 한다. 다시 말해 Java의 모든 Reference는 Sybolic Reference이기 때문에 Class나 Method 그리고 변수나 상수에 접근할 때에도 이러한 Resolution이 수행된다. 특정 Object가 특정 Class나 Interface에 의존관계가 있는지 확인하기 위해서도 Constant Pool의 Entry를 참조한다.

JVM Stack애 존재하는 여러 Stack Frame 중에 어떤 Stack Frame으로 찾아 돌아가야 하는 걸까 ?

Frame Data에서 알 수 있다.

Frame Data에는 자신을 호출한 Stack Frame의 Instruction Pointer가 들어있다.

Method가 종료되면 JVM은 이 정보를 PC Register에 설정하고 Stack Frame을 빠져 나간다.

만약 이 Method가 반환 값이 있다면 이 반환 값을 다음 번 Current Frame, 즉 자신을 호출한 Method의 Stack Frame의 Operand Stack에 Push하는 작업도 병행한다.

Method가 Exception을 발생시키며 강제 종료되었을 경우

  • Exception을 핸들링 해주어야함.
  • Exception 정보를 Frame Data에 저장하고 있음
  • Frame Data에서 관리하는 Exception 정보는 Exception의 Reference이다.
  • 각 Class file은 Exception Table을 가지고 있다.
  • Exception이 발생하면 JVM은 이를 참조하여 Catch절 에 해당하는 Bytecode로 점프하게 됨.
public class JvmInternal2{
	public void operandStack(){
		int a, b, c;
		a = 5;
		b = 6;
		try{
      c = a + b;
    }catch(NullPointerException e){
			c = 0;
		}
	}

}

Untitled

  • Method Operandstack()의 Bytecode 아래에 바로 Exception Table이 나타나는 것을 확인할 수 있음

Exception Table은 4가지 요소로 구성됨

  • from
    • try 블록이 시작되는 ByteCode의 엔트리 넘버를 의미하고
  • to
    • try블록이 끝나는 엔트리 넘버를 의미한다.
  • targer
    • try가 발생했을 때 점프해야 할 엔트리 넘버를 의미한다.
  • type
    • 정의한 Exception을 의미한다. try 블록 내에서 Exception이 발생하거나 Throw되면 이 Exception Object가 type의 Class정보와 비교하게 된다.
    • Class 정보와 일치하게 될 경우 → target으로 점프하여 수수행한다.
    • Class 정보와 일치하지 않을 경우 → JVM은 Current Frame을 종료하고 이 Method를 호출한 Method의 Stack Frame에 이 Exception을 다시 전져 처리를 반복한다.

Stack Frame을 마치고..

JVM을 조금 씩 알고 깊게 지식을 습득하니깐 코드가 어떻게 돌아가는지 느낌이 온다. 잘 모르고 코드를 칠 수는 있지만 나만의 강점인 상상력에 보태는 배경지식이 없어서 코드가 돌아가는 것을 상상할 수가 없었다. 조금씩 생기니깐 에러를 발견할 때 어디서 부터 잘못된지 ? 바로 알 수는 없지만 추론할 때 더 넓은 추론이 가능해졌다. 너무 깊게 들어간게 아닌가 ?? 라는 생각도 조금 든다.. ㅋㅋ;;

자러가야지… 졸립다.

giphy