상수 풀
고친 사람 github-actions[bot]
상수 풀은 자바 클래스 파일이 코드에서 쓰는 이름과 상수를 한곳에 모아 두고 인덱스로 꺼내 쓰게 해 줍니다.
메서드를 부르는 명령어도 부를 메서드를 이 표의 항목 인덱스 하나로 가리킵니다.
그래서 컴파일된 코드를 펼쳐 보면 명령어 옆에 #4 같은 인덱스가 붙어 있습니다.
쉽고 빠른 이해
자바 컴파일러가 만든 클래스 파일 안의 이름·상수 목록입니다. 메서드를 부르는 명령어 invokevirtual #4 는 이 목록의 4번 항목을 가리킵니다. 그 항목이 Example 클래스의 addtwo 메서드를 알려 줍니다.
명령어는 클래스나 객체가 실행 중에 메모리 어디에 놓이는지에 기대지 않습니다. 그래서 부를 메서드를 주소가 아니라 이름으로 가리킵니다. 그 이름을 모아 둔 곳이 상수 풀입니다. 이 목록이 없으면 명령어가 가리킬 이름이 없습니다. 이름으로 가리켰으니 실행 중에 그 이름을 실제 메서드와 잇는 일이 따로 필요합니다. 이 일을 해석(resolve)이라고 부릅니다.
- 클래스 이름·메서드 이름·문자열·큰 숫자를 목록의 항목에 하나씩 담습니다
- 명령어는 값 대신 항목 인덱스를 적습니다
- 항목끼리도 인덱스로 가리킵니다. 메서드 항목(
Methodref)이 클래스 항목(Class)과 이름·타입 항목(NameAndType)을 가리킵니다. 그 끝에는 글자를 바이트로 담은 항목(Utf8)이 있습니다
대가도 있습니다. 항목은 한 클래스에 65535 개까지입니다. 이름이나 문자열 하나는 65535 바이트까지입니다. long·double 상수는 항목을 둘씩 먹습니다.
상세
JVM(Java Virtual Machine, 자바 가상 머신)은 자바 컴파일러 javac 가 만든 클래스 파일을 읽어 실행합니다.
클래스 파일의 바이트 구조는 JVMS(Java Virtual Machine Specification, 자바 가상 머신 명세)가 정합니다.
이 편은 Java SE(Standard Edition) 21 판을 따릅니다.
클래스 파일 하나는 ClassFile 구조 하나로 이루어집니다.
상수 풀은 이 ClassFile 구조 안에 들어 있는 표입니다. 구조 안에서 쓰는 이름은 constant_pool 입니다.
이 표는 여러 구조를 모은 표입니다. 이 구조들은 문자열 상수, 클래스와 인터페이스 이름, 필드 이름, 그리고 클래스 파일 곳곳에서 가리키는 그 밖의 상수를 나타냅니다.
표의 한 줄을 항목(entry)이라고 부릅니다. 항목을 가리키는 번호가 인덱스(index)입니다.
메서드를 부르는 명령어가 이 인덱스를 씁니다.
예를 들어 5 invokevirtual #4 // Method Example.addtwo(II)I 는 4번 항목이 나타내는 메서드를 부르라는 명령입니다.
#4 는 인덱스 4 입니다. 맨 앞 5 는 메서드 코드 시작에서 센 이 명령어의 바이트 위치입니다.
// 뒤의 주석은 4번 항목이 무엇을 담았는지 알려 줍니다.
주석 끝의 (II)I 는 그 메서드의 디스크립터입니다. 디스크립터는 필드나 메서드의 타입을 글자로 적은 문자열입니다.
주소 대신 기호 정보를 가리키는 까닭
메서드 몸체는 JVM 명령어로 적힙니다. 흔히 바이트코드라고 부르는 것입니다.
JVM 명령어는 클래스 · 인터페이스 · 클래스 인스턴스 · 배열이 실행 중에 메모리에 어떻게 놓이는지에 기대지 않습니다.
그래서 부를 메서드를 메모리 주소로 가리키지 않습니다.
대신 명령어는 constant_pool 표 안의 기호 정보를 가리킵니다.
기호 정보는 메모리 주소 대신 이름으로 적은 정보입니다.
Example.addtwo(II)I 처럼 클래스 이름, 메서드 이름, 디스크립터를 글자로 적은 것이 그런 정보입니다.
이 기호 정보를 모아 두는 표가 상수 풀입니다.
명령어는 이 표의 인덱스만 갖습니다. 이름 자체는 표의 항목이 담습니다. 그래서 상수 풀이 없으면 명령어가 가리킬 기호 정보가 없습니다. 이름을 명령어에 바로 적지 않고 표에 모아 인덱스로 가리키는 까닭은 명세의 상수 풀 절에 따로 나오지 않습니다(미확인).
이름으로 적었으니 실행 중에 그 이름을 실제 대상과 이어 줘야 합니다. 이 일을 해석(resolve)이라고 부릅니다. 메서드 참조와 필드 참조가 이렇게 실행 중에 해석해야 하는 것입니다.
항목의 인덱스와 태그
표 앞에는 constant_pool_count 가 붙습니다.
이 값은 표에 들어 있는 항목 수에 1을 더한 것입니다. 그래서 인덱스는 1부터 constant_pool_count - 1 까지입니다.
유효한 인덱스는 0보다 크고 constant_pool_count 보다 작은 인덱스입니다.
long·double 상수에만 예외가 있습니다. 아래 「표현 한계」에서 다룹니다.
모든 항목은 같은 구조로 시작합니다.
cp_info {
u1 tag;
u1 info[];
}
구조 선언의 u1·u2·u4 는 필드 크기입니다. 각각 1바이트 · 2바이트 · 4바이트를 차지합니다.
그래서 u2 필드는 16비트입니다. constant_pool_count 와 문자열 길이 length 가 둘 다 u2, 곧 16비트 필드입니다.
항목의 첫 바이트가 태그(tag)입니다. 태그 값이 그 항목의 종류를 정합니다. 종류는 다시 뒤따르는 바이트의 형식을 정합니다. 종류는 17가지입니다. 아래에서 자주 나오는 것은 다섯입니다.
| 항목 | 나타내는 것 |
|---|---|
Utf8 |
문자열 값의 바이트. 클래스 이름 · 메서드 이름 · 디스크립터 · 문자열의 글자가 전부 이 항목에 담깁니다 |
Class |
클래스나 인터페이스 |
Methodref |
메서드 |
NameAndType |
필드나 메서드의 이름과 디스크립터 묶음 |
String |
String 타입의 상수 객체. 글자는 Utf8 항목을 가리켜 얻습니다 |
17가지 전부와 항목의 바이트 배치는 「형태」에 있습니다.
클래스 파일 안에서 상수 풀은 버전 번호 바로 뒤, 클래스 자신에 대한 정보보다 앞에 놓입니다.
block-beta
columns 1
a["magic · 4바이트"]
b["minor_version · 2바이트"]
c["major_version · 2바이트"]
d["constant_pool_count · 2바이트"]
block:pool
columns 1
p1["1번 항목 · tag 1바이트 + info"]
p2["2번 항목 · tag 1바이트 + info"]
p3["… constant_pool_count - 1 번 항목까지"]
end
f["클래스 자신에 대한 정보 · access_flags 부터 attributes 까지"]
그림 맨 위 magic 은 파일 맨 앞의 4바이트 필드입니다. minor_version·major_version 이 버전 번호입니다.
맨 아래 묶음은 this_class(이 클래스) · fields(필드) · methods(메서드) · attributes(속성) 같은 필드들입니다. 이 편에서는 따로 풀지 않습니다.
항목마다 info 길이가 종류에 따라 다릅니다. 그래서 항목의 시작 위치가 고정되지 않습니다.
다음 항목이 어디서 시작하는지 알려면 앞 항목을 읽어 나가야 합니다.
Class·Methodref·NameAndType 은 태그만 보면 길이가 정해집니다. Utf8 항목은 태그 다음의 length 필드까지 읽어야 끝을 압니다.
런타임 상수 풀과의 관계
런타임 상수 풀(run-time constant pool)은 클래스 파일의 constant_pool 표를 실행 중에 쓰는 형태로 나타낸 것입니다. 클래스마다, 인터페이스마다 따로 있습니다.
클래스 파일 속 표와 같은 표는 아닙니다.
런타임 상수 풀은 여러 종류의 상수를 담습니다. 컴파일 때 이미 아는 수치 리터럴부터 실행 중에 해석해야 하는 메서드·필드 참조까지입니다. 하는 일은 전통적인 프로그래밍 언어의 심볼 테이블(symbol table, 이름과 그 정보를 짝지어 두는 표)과 비슷합니다. 다만 흔한 심볼 테이블보다 넓은 범위의 데이터를 담습니다.
담긴 내용도 다릅니다. 런타임 상수 풀의 문자열 상수는 String 인스턴스에 대한 참조입니다.
클래스 파일의 String 항목에는 인스턴스가 없습니다. Utf8 항목의 인덱스 하나만 있습니다(「경계」).
flowchart TD
subgraph F["클래스 파일 · 명세 4장"]
CP["constant_pool 표"] --> S1["String 항목 · Utf8 항목의 인덱스"]
end
subgraph R["실행 중 · 명세 2·3·6장"]
RCP["런타임 상수 풀"] --> S2["문자열 상수 · String 인스턴스 참조"]
end
CP -->|클래스별 실행 중 표현| RCP
명세 안에서도 이 인덱스는 두 이름으로 나옵니다.
클래스 파일 구조를 정하는 대목은 명령어가 constant_pool 표의 기호 정보를 가리킨다고 씁니다.
컴파일 예제와 명령어 설명은 같은 번호를 런타임 상수 풀의 인덱스라고 부릅니다.
앞은 파일 속 표를, 뒤는 그 표를 실행 중에 나타낸 것을 두고 하는 말입니다.
표현 한계
상수 풀의 상한은 16비트 필드 두 개에서 나옵니다.
항목 수를 정하는 constant_pool_count 와 문자열 길이를 담는 CONSTANT_Utf8_info 의 length 입니다.
둘 다 클래스 파일 포맷 자체에서 오는 한계입니다.
여기에 항목 구조와 명령어에서 나오는 제약 셋이 더해집니다. 문자열 바이트가 표준 UTF-8(Unicode Transformation Format 8-bit)이 아니라는 것, long·double 이 항목을 둘 먹는다는 것, 인덱스를 한 바이트로 적는 명령어가 있다는 것입니다.
글자 수가 아닌 바이트 수로 재는 문자열 길이
필드와 메서드의 이름, 필드와 메서드의 디스크립터, 그 밖의 상수 문자열 값은 길이가 65535 까지입니다.
속성(attribute)은 ClassFile 구조 끝의 attributes 처럼 attribute_info 로 선언되는 구조입니다. 그 한 종류인 ConstantValue 속성이 가리키는 문자열도 이 상한을 받습니다.
이 상한을 만드는 것은 CONSTANT_Utf8_info 구조의 length 필드입니다. 부호 없는 16비트 필드입니다.
명세는 이 상한을 먼저 65535 characters(글자)라고 씁니다. 그런데 바로 뒤 문장에서는 상한이 인코딩된 글자 수가 아니라 인코딩한 바이트 수에 걸립니다. UTF-8 은 어떤 글자를 2바이트나 3바이트로 적습니다. 그래서 여러 바이트로 적히는 글자가 들어간 문자열은 상한에 더 빨리 닿습니다. 한글 음절처럼 3바이트로 적히는 글자만으로 채우면 한 문자열에 21845 자까지 들어갑니다.
length 필드 값도 문자열 길이가 아니라 bytes 배열의 바이트 수입니다.
이 상한을 넘는 문자열을 컴파일러가 어떻게 막는지는 자료에 없습니다(미확인).
표준 UTF-8 이 아닌 modified UTF-8
Utf8 항목의 bytes 배열에는 값이 0 인 바이트가 들어갈 수 없습니다. 0xf0 부터 0xff 까지의 바이트도 들어갈 수 없습니다.
문자열 내용은 modified UTF-8 로 적습니다. 표준 UTF-8 과 다른 점은 둘입니다.
| 표준 UTF-8 | modified UTF-8 | |
|---|---|---|
널 문자 (char)0 |
1바이트 형식 | 2바이트 형식. 그래서 문자열 안에 널 바이트가 박히지 않습니다 |
| 1·2·3바이트 형식 | 씁니다 | 씁니다 |
| 4바이트 형식 | 씁니다 | JVM 이 알아보지 못합니다. 대신 자기만의 3바이트 형식 두 번을 씁니다 |
같은 문자열이라도 이 두 경우에는 클래스 파일에 적힌 바이트가 표준 UTF-8 로 적은 바이트와 다릅니다. 이모지처럼 표준 UTF-8 이 4바이트로 적는 글자는 3바이트 형식 두 번, 곧 6바이트를 먹습니다. 앞의 65535 바이트 상한도 그만큼 빨리 찹니다.
항목 둘을 먹는 long 과 double
8바이트 수치 상수인 long 과 double 은 상수 풀에서 항목을 둘 차지합니다.
CONSTANT_Long_info 나 CONSTANT_Double_info 가 인덱스 n 의 항목이면 표에서 다음으로 쓸 수 있는 항목은 인덱스 n+2 입니다.
인덱스 n+1 은 유효해야 합니다. 곧 0보다 크고 constant_pool_count 보다 작은 범위 안에 있어야 합니다. 그래도 그 인덱스는 쓸 수 없는 것으로 칩니다.
block-beta columns 1 a["인덱스 n · Long 또는 Double 항목"] b["인덱스 n+1 · 유효하지만 쓸 수 없음"] c["인덱스 n+2 · 다음 항목"]
그래서 long·double 이 들어간 표에서는 인덱스의 개수보다 담긴 값의 개수가 적습니다.
명세는 이 규칙 뒤에 이렇게 덧붙입니다. "In retrospect, making 8-byte constants take two constant pool entries was a poor choice." 돌이켜 보면 8바이트 상수에 항목 둘을 먹인 것은 잘못 고른 방식이었다는 뜻입니다.
한 클래스의 항목 수 65535
클래스 하나 또는 인터페이스 하나의 상수 풀은 항목이 65535 개까지입니다.
이 상한을 만드는 필드는 ClassFile 구조의 16비트 constant_pool_count 입니다.
이 상한은 클래스 하나나 인터페이스 하나가 얼마나 복잡해질 수 있는지를 묶는 내부 한계로도 작동합니다.
인덱스를 한 바이트로 적는 ldc
상수를 스택에 올리는 명령어 ldc 는 인덱스를 부호 없는 1바이트로 적습니다. 1바이트로 적을 수 있는 수는 255 까지입니다.
ldc_w 는 인덱스를 두 바이트 indexbyte1·indexbyte2 로 적습니다. 둘을 (indexbyte1 << 8) | indexbyte2 로 합쳐 16비트 인덱스를 만듭니다.
두 명령어 모두 첫 바이트는 명령어 자체를 나타내는 값입니다. ldc 는 0x12, ldc_w 는 0x13 입니다.
ldc 명령어의 바이트 배치입니다.
packet-beta 0-7: "ldc · 0x12" 8-15: "index"
ldc_w 명령어의 바이트 배치입니다.
packet-beta 0-7: "ldc_w · 0x13" 8-15: "indexbyte1" 16-23: "indexbyte2"
ldc_w 는 인덱스 폭 말고는 ldc 와 같습니다.
ldc_w 는 런타임 상수 풀 항목이 많아 더 큰 인덱스가 필요할 때만 ldc 대신 쓰입니다. 그 경우 같은 상수를 올리는 명령어가 1바이트 길어집니다.
두 명령어 모두 long·double 항목은 올리지 못합니다. 그 둘은 전부 ldc2_w 로 올립니다. 인덱스가 좁은 판은 따로 없습니다.
예시
아래 두 목록은 명세에 실린 예제입니다. 두 목록 모두 명령어가 값 대신 #1·#4 같은 인덱스를 가집니다.
그 인덱스가 가리키는 항목은 줄 끝 주석이 알려 줍니다.
예제 코드는 Oracle JDK(Java Development Kit) 1.0.2 의 javac 가 만든 것입니다. 이것을 javap 가 찍는 형식으로 옮겨 적었습니다.
javap 는 클래스 파일을 사람이 읽는 글로 풀어 찍는 JDK 도구입니다.
주석 가운데 일부는 javap 가 찍은 것입니다. 나머지는 예제를 쓴 저자가 붙였습니다.
한 줄의 형식은 <index> <opcode> [<operand1> ...] [<comment>] 입니다.
형식의 <index> 는 상수 풀 인덱스가 아닙니다. 맨 앞 수, 곧 메서드 코드 시작에서 센 바이트 위치입니다.
목록에서는 런타임 상수 풀 인덱스인 피연산자 앞에 # 이 붙습니다. 줄 끝 주석은 그 항목이 무엇인지 밝힙니다.
add12and13 — 메서드 호출이 가리키는 항목
소스입니다. addTwo 는 앞선 예제에서 인스턴스 메서드로 정의한 것입니다.
int add12and13() {
return addTwo(12, 13);
}
컴파일한 목록입니다.
Method int add12and13()
0 aload_0 // Push local variable 0 (this)
1 bipush 12 // Push int constant 12
3 bipush 13 // Push int constant 13
5 invokevirtual #4 // Method Example.addtwo(II)I
8 ireturn // Return int on top of operand stack;
// it is the int result of addTwo()
invokevirtual 은 인스턴스 메서드를 부르는 명령어입니다. 호출할 메서드는 객체의 실행 중 타입으로 고릅니다.
이 명령어의 인자는 런타임 상수 풀 항목 인덱스 하나입니다. 그 항목이 주는 것은 셋입니다.
객체의 클래스 타입 이름, 부를 메서드 이름, 그 메서드의 디스크립터입니다.
flowchart TD
subgraph CODE["메서드 add12and13 의 코드"]
I["5 invokevirtual #4"]
end
subgraph POOL["상수 풀"]
E["인덱스 4 의 항목"]
end
I -->|인덱스 4| E
E --> V1["클래스 이름 · Example"]
E --> V2["메서드 이름 · addtwo"]
E --> V3["디스크립터 · (II)I"]
주석 Method Example.addtwo(II)I 가 그 셋을 차례로 보입니다.
클래스는 Example, 메서드는 addtwo, 디스크립터는 (II)I 입니다.
(II)I 는 int 둘을 넘겨 int 결과를 돌려받는 이 호출과 짝이 맞습니다. 목록은 12 와 13 을 int 상수로 올립니다. 마지막 주석은 결과를 int 라고 밝힙니다.
주석의 소문자 addtwo 는 예제 원문의 표기입니다. 소스 쪽 이름은 addTwo 입니다.
「상세」의 표에서 메서드를 나타내는 항목은 Methodref 였습니다. 「형태」에서 보듯 Methodref 항목 하나도 인덱스를 따라가면 클래스 이름 · 메서드 이름 · 디스크립터 셋에 닿습니다.
다만 4번 항목이 어느 종류인지를 목록이 밝히지는 않습니다.
useManyNumeric — 숫자 상수가 항목을 쓰는 경우와 안 쓰는 경우
소스입니다.
void useManyNumeric() {
int i = 100;
int j = 1000000;
long l1 = 1;
long l2 = 0xffffffff;
double d = 2.2;
...do some calculations...
}
컴파일한 목록입니다.
Method void useManyNumeric()
0 bipush 100 // Push small int constant with bipush
2 istore_1
3 ldc #1 // Push large int constant (1000000) with ldc
5 istore_2
6 lconst_1 // A tiny long value uses small fast lconst_1
7 lstore_3
8 ldc2_w #6 // Push long 0xffffffff (that is, an int -1)
// Any long constant value can be pushed with ldc2_w
11 lstore 5
13 ldc2_w #8 // Push double constant 2.200000
// Uncommon double values are also pushed with ldc2_w
16 dstore 7
...do those calculations...
바이트 위치 8 의 주석 (that is, an int -1) 은 16진수 0xffffffff 를 int 로 읽으면 -1 이라는 뜻입니다.
32비트가 모두 1인 int 는 2의 보수 표기에서 -1 입니다.
다섯 값 가운데 셋만 상수 풀을 거칩니다.
| 소스 값 | 명령어 | 상수 풀 |
|---|---|---|
int 100 |
bipush 100 |
안 거칩니다. 값 100 이 명령어의 피연산자로 들어 있습니다 |
int 1000000 |
ldc #1 |
1번 항목 |
long 1 |
lconst_1 |
안 거칩니다 |
long 0xffffffff |
ldc2_w #6 |
6번 항목 |
double 2.2 |
ldc2_w #8 |
8번 항목 |
int·long·float·double 값과 String 인스턴스 참조는 ldc·ldc_w·ldc2_w 로 다룹니다.
byte·char·short 정수 상수와 작은 int 값은 bipush·sipush·iconst_<i> 로 컴파일할 수 있습니다.
목록의 bipush 100 과 ldc #1 이 그 갈림을 보입니다. bipush 100 은 값 자체를 가집니다. ldc #1 은 인덱스를 가집니다.
long 이 담긴 인덱스 6 다음 double 이 인덱스 8 인 것은 long 이 항목 둘을 먹어 인덱스 7 을 쓸 수 없다는 「표현 한계」의 규칙과 맞아떨어집니다. 다만 예제 설명이 이 둘을 잇지는 않습니다.
javap 가 상수 풀을 찍는 코드 — OpenJDK jdk-21-ga
OpenJDK 저장소 jdk-21-ga 태그의 javap 소스에서는 두 파일이 상수 풀 출력을 맡습니다.
ClassWriter.java 는 verbose 옵션이 켜졌을 때만 상수 풀을 찍습니다.
if (options.verbose) {
println();
indent(+1);
println("minor version: " + cf.minor_version);
println("major version: " + cf.major_version);
// …
print("this_class: #" + cf.this_class);
if (cf.this_class != 0) {
tab();
print("// " + constantWriter.stringValue(cf.this_class));
}
// …
constantWriter.writeConstantPool();
} else {
찍는 순서는 버전 번호, 클래스 정보, 그리고 상수 풀입니다.
this_class 값에도 # 을 붙여 찍습니다. 옆 주석은 constantWriter.stringValue 로 풀어 찍습니다. 이 필드 값도 상수 풀 인덱스로 다룬다는 뜻입니다.
ConstantWriter.java 의 writeConstantPool() 이 표를 찍습니다.
println("Constant pool:");
indent(+1);
int width = String.valueOf(constant_pool.size()).length() + 1;
int cpx = 1;
while (cpx < constant_pool.size()) {
print(String.format("%" + width + "s", ("#" + cpx)));
try {
CPInfo cpInfo = constant_pool.get(cpx);
print(String.format(" = %-18s ", cpTagName(cpInfo)));
cpx += cpInfo.accept(v, null);
} catch (ConstantPool.InvalidIndex ex) {
// should not happen
}
}
indent(-1);
Constant pool: 머리줄 아래로 항목마다 한 줄씩 찍습니다.
줄은 #인덱스 로 시작합니다. 인덱스는 폭을 맞춰 오른쪽 정렬로 찍힙니다. 그 뒤에 = 와 항목 종류 이름이 옵니다.
종류 이름은 18글자 폭에 왼쪽 정렬로 찍힙니다.
인덱스는 1부터 셉니다. 한 항목을 찍은 뒤에는 cpInfo.accept(v, null) 이 돌려준 값만큼 인덱스를 넘깁니다.
이 반복문에서 인덱스를 늘리는 곳은 이 한 줄뿐입니다. 그래서 long·double 항목 뒤의 쓸 수 없는 인덱스를 건너뛰는 일도 이 반환값이 맡는 것으로 읽힙니다.
반환값을 정하는 코드는 자료에 없습니다(미확인).
종류 이름은 cpTagName 이 만듭니다.
String cpTagName(CPInfo cpInfo) {
String n = cpInfo.getClass().getSimpleName();
return n.replace("CONSTANT_", "").replace("_info", "");
}
항목을 나타내는 클래스 이름에서 CONSTANT_ 와 _info 를 뗀 것이 줄에 찍힙니다.
구조 이름 CONSTANT_Methodref_info 에 같은 규칙을 대면 Methodref 가 됩니다.
항목 내용이 찍히는 방식과 찍힌 출력 표본은 자료에 없습니다(미확인).
형태
상수 풀의 항목은 전부 태그 1바이트로 시작합니다. 이름을 가진 항목은 인덱스를 따라가면 결국 Utf8 항목에 닿습니다.
이 절은 태그 표와, 메서드 참조 하나를 이루는 네 구조 Class·Methodref·NameAndType·Utf8 의 바이트 배치를 봅니다.
이 절을 읽고 나면 Methodref 항목 하나가 어느 항목들을 거쳐 클래스 이름 · 메서드 이름 · 디스크립터에 닿는지 따라갈 수 있습니다.
항목 종류를 정하는 태그 표
항목 종류마다 태그 값을 정한 표입니다. 뒤 두 열은 원래 표의 class file format 열과 Java SE 열을 옮긴 것입니다.
| 종류 | 태그 | 클래스 파일 판 | Java SE |
|---|---|---|---|
CONSTANT_Utf8 |
1 | 45.3 | 1.0.2 |
CONSTANT_Integer |
3 | 45.3 | 1.0.2 |
CONSTANT_Float |
4 | 45.3 | 1.0.2 |
CONSTANT_Long |
5 | 45.3 | 1.0.2 |
CONSTANT_Double |
6 | 45.3 | 1.0.2 |
CONSTANT_Class |
7 | 45.3 | 1.0.2 |
CONSTANT_String |
8 | 45.3 | 1.0.2 |
CONSTANT_Fieldref |
9 | 45.3 | 1.0.2 |
CONSTANT_Methodref |
10 | 45.3 | 1.0.2 |
CONSTANT_InterfaceMethodref |
11 | 45.3 | 1.0.2 |
CONSTANT_NameAndType |
12 | 45.3 | 1.0.2 |
CONSTANT_MethodHandle |
15 | 51.0 | 7 |
CONSTANT_MethodType |
16 | 51.0 | 7 |
CONSTANT_Dynamic |
17 | 55.0 | 11 |
CONSTANT_InvokeDynamic |
18 | 51.0 | 7 |
CONSTANT_Module |
19 | 53.0 | 9 |
CONSTANT_Package |
20 | 53.0 | 9 |
네 항목의 바이트 배치
오프셋은 항목 첫 바이트에서 센 바이트 위치입니다. 구조 선언의 u1·u2 크기를 차례로 더한 값입니다.
| 항목 | 오프셋 | 길이 | 필드 | 뜻 |
|---|---|---|---|---|
CONSTANT_Class_info |
0 | 1 | tag |
7 |
| 1 | 2 | name_index |
Utf8 항목 인덱스. 클래스나 인터페이스의 이름 |
|
CONSTANT_Methodref_info |
0 | 1 | tag |
10 |
| 1 | 2 | class_index |
Class 항목 인덱스. 이 메서드를 멤버로 가진 타입 |
|
| 3 | 2 | name_and_type_index |
NameAndType 항목 인덱스. 메서드의 이름과 디스크립터 |
|
CONSTANT_NameAndType_info |
0 | 1 | tag |
12 |
| 1 | 2 | name_index |
Utf8 항목 인덱스. 필드나 메서드의 이름 |
|
| 3 | 2 | descriptor_index |
Utf8 항목 인덱스. 필드 디스크립터나 메서드 디스크립터 |
|
CONSTANT_Utf8_info |
0 | 1 | tag |
1 |
| 1 | 2 | length |
bytes 배열의 바이트 수. 문자열의 글자 수가 아닙니다 |
|
| 3 | length |
bytes |
문자열 바이트. modified UTF-8 |
인덱스 필드는 전부 두 조건을 받습니다. 값이 이 표의 유효한 인덱스여야 합니다. 그리고 그 인덱스의 항목이 표에 적은 종류여야 합니다.
Class 항목. name_index 가 가리키는 Utf8 항목은 유효한 클래스 이름이나 인터페이스 이름을 담습니다. 이 이름은 이진 이름(binary name)을 내부 형식(internal form)으로 적은 것입니다.
앞 「예시」에서 invokevirtual #4 가 준 클래스 이름 Example 도 이렇게 적은 이름입니다. 두 용어를 정의하는 대목은 자료에 없습니다(미확인).
배열도 객체라서 anewarray·multianewarray 명령어는 Class 항목으로 배열 "클래스"를 가리킬 수 있습니다. new 명령어는 그러지 못합니다.
배열 클래스의 이름은 그 배열 타입의 디스크립터입니다. int[][] 는 [[I 입니다. Thread[] 는 [Ljava/lang/Thread; 입니다.
배열 타입 디스크립터는 차원이 255 이하일 때만 유효합니다.
Methodref 항목. 필드와 인터페이스 메서드도 같은 모양의 구조를 씁니다.
CONSTANT_Fieldref_info(태그 9)와 CONSTANT_InterfaceMethodref_info(태그 11)도 tag·class_index·name_and_type_index 세 필드입니다.
class_index 가 가리키는 타입은 Fieldref 에서는 클래스든 인터페이스든 됩니다. Methodref 에서는 인터페이스가 아닌 클래스 타입이어야 합니다(should). InterfaceMethodref 에서는 인터페이스 타입이어야 합니다(should).
name_and_type_index 가 가리키는 디스크립터는 Fieldref 면 필드 디스크립터, 나머지 둘이면 메서드 디스크립터여야 합니다.
Methodref 의 메서드 이름이 < 로 시작하면 그 이름은 특수 이름 <init> 이어야 합니다. <init> 은 인스턴스 초기화 메서드를 뜻합니다. 그 메서드의 반환 타입은 void 여야 합니다.
NameAndType 항목. 필드나 메서드를 나타내되 어느 클래스나 인터페이스에 속하는지는 적지 않습니다.
name_index 는 한정되지 않은 필드·메서드 이름이나 특수 이름 <init> 을 담은 Utf8 항목을 가리킵니다.
Utf8 항목. 클래스 이름 · 메서드 이름 · 디스크립터 · 문자열의 글자를 바이트로 담습니다.
String 타입 상수 객체를 나타내는 String 항목(태그 8)과는 다른 종류입니다. String 항목도 글자는 Utf8 항목을 가리켜 얻습니다(「경계」).
바이트 규칙과 길이 상한은 「표현 한계」에 있습니다.
인덱스로 잇는 참조 사슬
Methodref 항목 하나에서 출발해 인덱스를 따라가면 이름 문자열 셋에 닿습니다.
클래스 쪽은 Class 항목을 한 번 거칩니다. 이름과 디스크립터 쪽은 NameAndType 항목을 한 번 거칩니다.
flowchart TD
M["Methodref 항목 · 태그 10"] -->|class_index| C["Class 항목 · 태그 7"]
M -->|name_and_type_index| NT["NameAndType 항목 · 태그 12"]
C -->|name_index| U1["Utf8 항목 · 클래스 이름"]
NT -->|name_index| U2["Utf8 항목 · 메서드 이름"]
NT -->|descriptor_index| U3["Utf8 항목 · 메서드 디스크립터"]
NameAndType 항목은 클래스를 적지 않습니다. 그래서 어느 클래스의 메서드인지는 Methodref 항목의 class_index 가 정합니다.
경계
자바에서 문자열 리터럴이 모인다는 「String pool」(문자열 상수 풀)도 이 상수 풀인가. 아닙니다.
클래스 파일 상수 풀의 String 항목이 담는 것은 문자열 인스턴스가 아닙니다. 인스턴스를 만들 코드 포인트 시퀀스를 가리키는 인덱스입니다. 코드 포인트는 유니코드가 글자마다 매긴 번호입니다.
String pool 은 실행 중에 String 인스턴스를 모아 두는 모음입니다.
둘은 문자열 상수를 만드는 한 절차에서 이어질 뿐 서로 다른 것입니다.
담는 것이 다릅니다. 상수 풀의 CONSTANT_String_info 항목(태그 8)은 string_index 필드 하나를 가집니다.
이 인덱스는 Utf8 항목을 가리킵니다. 그 Utf8 항목은 String 객체를 초기화할 유니코드 코드 포인트 시퀀스를 나타냅니다.
그리고 상수 풀은 클래스 하나나 인터페이스 하나마다 따로 있습니다.
String pool 은 String.intern() 의 API(Application Programming Interface) 문서에 나오는 모음입니다.
이 문자열 모음(a pool of strings)은 처음에 비어 있습니다. String 클래스가 혼자(privately) 관리합니다.
intern 은 equals 로 같은 문자열이 모음에 이미 있으면 그 문자열을 돌려줍니다. 없으면 부른 String 객체를 모음에 넣고 그 참조를 돌려줍니다.
그래서 두 문자열 s·t 에 대해 s.intern() == t.intern() 은 s.equals(t) 가 참일 때, 그리고 그때에만 참입니다.
둘은 문자열 상수를 만들 때 이어집니다. 문자열 상수는 String 인스턴스에 대한 참조입니다. 이 참조는 CONSTANT_String_info 로부터 만들어집니다.
JVM 은 그 항목이 준 코드 포인트 시퀀스를 보고 둘 중 하나로 갑니다.
flowchart TD
S["클래스 파일 · String 항목"] -->|string_index| U["Utf8 항목 · 코드 포인트 시퀀스"]
U --> Q{"같은 코드 포인트의 String 에 intern 이 불렸나"}
Q -->|있다| R1["그 String 인스턴스를 문자열 상수로 쓴다"]
Q -->|없다| R2["새 String 인스턴스를 만든다"]
R2 --> R3["새 인스턴스에 String.intern 을 부른다"]
어느 갈래로 가든 문자열 상수는 String.intern 이 한 번은 불린 인스턴스를 가리킵니다.
자바 언어 쪽에서 봐도 결과는 같습니다.
문자열 리터럴은 언제나 같은 String 인스턴스를 가리킵니다. 문자열 리터럴과 상수 표현식 값인 문자열이 유일한 인스턴스를 나눠 쓰도록 String.intern 을 실행한 것처럼 "interned" 되기 때문입니다.
모든 리터럴 문자열과, 값이 문자열인 상수 표현식은 intern 됩니다.
정리하면 클래스 파일 상수 풀의 String 항목은 String pool 에 들어가는 입구 가운데 하나입니다.
intern 은 리터럴이 아닌 아무 String 객체에도 부를 수 있습니다. 그래서 String pool 에는 상수 풀을 거치지 않은 문자열도 들어갑니다.
flowchart TD
subgraph C1["클래스 하나의 상수 풀"]
A["String 항목"]
end
subgraph C2["다른 클래스의 상수 풀"]
B["String 항목"]
end
X["리터럴이 아닌 String 객체"]
subgraph P["String 클래스가 혼자 관리하는 String pool"]
POOL["String 인스턴스 모음"]
end
A -->|문자열 상수를 만들 때 intern| POOL
B -->|문자열 상수를 만들 때 intern| POOL
X -->|intern 호출| POOL
관련 항목
상수 풀을 품고 정의하는 파일과 명세
클래스 파일 · JVMS · JLS · Java SE · OpenJDK
상수 풀을 만들고 읽고 실행하는 프로그램
javac · javap · JVM · 컴파일러 · JDK
상수 풀 항목의 하위 종류
CONSTANT_Utf8_info · CONSTANT_Class_info · CONSTANT_String_info · CONSTANT_Methodref_info · CONSTANT_Fieldref_info · CONSTANT_InterfaceMethodref_info · CONSTANT_NameAndType_info · CONSTANT_Long_info · CONSTANT_Double_info · CONSTANT_MethodHandle_info · CONSTANT_InvokeDynamic_info · CONSTANT_Dynamic_info
항목 인덱스를 피연산자로 쓰는 JVM 명령어
바이트코드 · ldc · ldc_w · ldc2_w · invokevirtual · anewarray · multianewarray
항목 안의 이름과 문자열을 적는 표기 규칙
디스크립터 · 이진 이름 · modified UTF-8 · UTF-8 · 코드 포인트
실행 중에 상수 풀을 이어받는 구조
런타임 상수 풀 · 심볼 테이블 · 기호 참조 · 해석 (JVM)
이름이 겹쳐 헷갈리는 문자열 모음
String pool · 문자열 인턴 · String.intern
다른 이름: constant pool · constant_pool