본문 바로가기

Programing/JVM(Java, Kotlin)

[Java] 지역변수 이름과 죄수번호...?

자바관련 카페에 올라온 질문.

질문

String str = null;

이 상태는 str이 객체를 참조하지 않고 스택영역에 str변수 이름으로 null값이 세팅되었다면

String name = "";

이 상태는 힙영역에 빈 공간으로 객체가 생성되고 주소 번지가 스택영역에 리턴이 되는건지요..

슬기로운 감빵생활과 자바

자바에서 지역변수의 이름은 런타임시에 어떻게 될까.

첫 코드에서 str 이라는 이름의 변수 이름은 계속 남아 있을까?

tvN에서 했던 슬기로운 감빵생활을 보면서 나는 자바의 지역변수와 유사한 점이 많다고 생각했다.

사회에서 '김제혁'이라고 불리던 이름이 형무소에 가면서 '7102'라는 죄수번호라는 신원을 구별하는 번호로 바뀐다.

밖에서 김제혁이라는 이름은 감빵내에서는 7102로 불린다.

자바코드에서는 존재하던 변수이름은 컴파일이 되면서 비슷하게 숫자로 바뀌게 된다.

이는 디컴파일 하면 변수이름이 var1, var2 등으로 바뀌어 있는 것을 통해 유추해볼 수 있다.

HttpServletResponse.class

원래는 이런 코드이다.

 

마치 첫 번째 있는 변수는 0번, 두 번째 있는 변수는 1번 처럼 말이다.

만약 아래의 코드의 경우 컴파일이 되면

public class AboutString {
    public static void main(String[] args) {
        String str = null;
    }
}
String str = null;

부분은 아래의 형태의 바이트코드로 바뀐다.

0x1
0x4c

위의 바이트들을 사람이 더 이해하기 쉬운 명령어로 바꾸면 다음과 같다.

ACONST_NULL
ASTORE_1

null 객체 참조(reference)를 operand stack에 push 하고,

operand stack에 있는 참조를 지역변수 1번에 store한다.

 

위의 한글로 된 문장이 이해가 안된다면 JVM의 동작에 대해 먼저 이해할 필요가 있다.

어떠한 method가 수행(invoke)하면 새로운 frame 이라는 것이 생성된다.

이 frame에는 local variable이라고 알려진 지역 변수들의 배열을 가지고 있는 저장 공간을 가지고 있다.

또한 각각의 frame에는 LIFO(last in first out) 구조의 스택(stack)이 존재하는데 이를 operand stack이라고 부른다.

지역변수를 설정하는 표현식도 operand stack을 통해 수행이 되는데 그것이 바로 위의 문장이라고 보면 된다.

ACONST_NULL (0x1) 가 연산이 되면 아래와 같이 operatnd stack에 null 객체의 레퍼런스가 들어간다.

이후 ASTORE_1 명령에 의해 operand stack의 레퍼런스는 지역변수에 들어가게 된다.

Run-Time Constant Pool과 문자열

자바에서는 상수를 위한 저장공간을 가지고 있다.

public class AboutString {
    public static void main(String[] args) {
        String str = "";
    }
}

위의 String str = ""; 는 아래의 형태의 바이트코드로 바뀌게 된다.

0x12 0x02
0x4C

마찬가지로 더 이해하기 쉬운 명령어로 바꾸면 다음과 같다.

LDC ""
ASTORE_1

LDC는 unsigned byte 타입의 index의 값을 인자로 가지는 명령어이다.

현재 클래스의 run-time constant pool에 저장된 문자열을 operand stack에 넣고

ASTORE_1 명령으로 지역변수 인덱스 1로 해당 레퍼런스를 넣는 것이 끝이다.

run-time constant pool의 위치?

JVM 스펙 4장 4절에 따르면 JVM 명령어는 특정 run-time 레이아웃에 의존하지 않는다고 적혀있다.

Java 6까지의 HotSpot의 구현에서 constant pool은 PermGen(Permanent Generation) 영역에 속했다.

이 영역은 GC 대상에서 제외되는데 객체의 생명주기가 영구적일 것으로 생각되는 정보를 저장한다.

예를들면 클래스 이름, 생성정보, 필드정보, 메서드 정보 등의 클래스의 메타 데이터 정보가 이에 속한다.

 

하지만 Java7부터는 상수 풀이 Heap 영역으로 변경되었다. JDK-6962931

또한 Java 8 HotSpot에서는 Metaspace 영역이 추가되면서 PermGen 영역이 없어졌다. JEP 122

클래스 파일 내부에는 constant_pool 테이블이 존재하고 아래와 같은 포맷으로 되어 있다.

cp_info {
 u1 tag;
 u1 info[];
}

상수 풀 태그는 여러가지가 있는데 문자열을 위한 태그값은 8이다.

CONSTANT_String_info {
 u1 tag;
 u2 string_index;
}

tag : CONSTANT_String (0x8)

string_index : CONSTANT_Utf8_info 구조체의 인덱스

 

CONSTANT_String_info 구조체가 가리키는 CONSTANT_Utf8_info 구조체는 아래와 같다.

여기에 실질적인 데이터가 들어간다.

CONSTANT_Utf8_info {
 u1 tag;
 u2 length;
 u1 bytes[length];
}

tag : CONSTANT_Utf8 (0x1)

length : 다음에 나오는 bytes의 길이 (결과 문자열의 길이가 아님)

bytes : 문자열의 바이트들을 담고 있는 byte들의 배열, 빈문자열의 경우와 0xF0 ~ 0xFF사이의 이 bytes는 없다.

 

위의 구조를 보면 문자열의 길이의 제한이 있음을 알 수 있다. (0xFF 0xFF : 65,535)

만약 이 길이를 초과하면 컴파일시 아래와 같은 에러가 나타난다.

java: constant string too long

해결방안 Fixing “constant string too long” Build Error

 

따라서 자바언어에서 "" 로 나타낼 수 있는 빈문자열은 CONSTANT_Utf8 구조체로는 아래와 같이 표현된다.

01 00 00

tag가 0x01 이고, 길이가 0인 문자열.

 

컴파일 옵션

javac 로 컴파일 할 때 -g:vars 옵션으로 디버깅 정보를 포함시킬 수 있을지 여부를 결정할 수 있다.

이런 정보로는 g:source,lines,vars 세 가지가 있는데 기본적으로 소스와 줄번호는 포함한다.

따라서 변수 정보는 포함하지 않게 된다.

InteliJIDEA의 경우 디컴파일을 해보면 변수 이름이 잘 보이는데 아마도 vars 옵션을 켜서 바이트코드에서 변수명을 추론할 수 있게 하는 것으로 보인다.