게임 스크립트 언어로 java 활용 (2/3) |
| programming/general 2003/02/20 06:56 |
이번에는 구현해 본 것을 토대도 정리해봤습니다. (심각하게 테스트는 많이 못했습니다만, 어차피 셈플이고 제안 수준이라 구현에 의미를 두고 있습니다.) 게임쪽과 관련된 아이디어나 코드들은 다음 글에...
1. JVM 구현하기
명령어들을 처리하기 위해서는 기본적으로 스택 메모리와 로컬 메모리가 구성되어야 합니다. (로컬 메모리는 스택 메모리에 매핑이 용이하도록 배려하고 있습니다. - 함수별 local 변수 개수를 이용할 경우)
메소드(함수)를 실행 했을 때 로컬 메모리에는 오브젝트의 주소와 인자가 세팅되어 있습니다. (static 함수가 아닌 경우 첫번째 값은 오브젝트의 포인터 입니다.)
위와 같은 자바 코드는 아래와 같은 자바 바이트 코드로 변환됩니다.
함수가 호출 될 때 스택은 제일 위에 8이, 그 아래에 32 와 this 에 해당하는 오브젝트 어드레스가 위치함을 예상할 수 있습니다. 이때 this 부터 각 순서대로 세팅되어 add 함수에서는
이 세팅된 상태로 실행되는 것입니다. 그래서 add 코드가 아래와 같으므로 40 이 리턴될 것입니다.
즉 명령어에서 얘기하는 local 값들은 오브젝트 포인터과 인자 값이 세팅된 메모리 값이라고 이해하시면 됩니다. 단순히 인자 값을 받아들이는 것이 전부는 아니고, 그 곳에 값을 저장하기도 합니다. 또한 초기화는 사용하는 로컬변수는 인자의 수와 일치하는 것은 아닙니다. 만약 위의 add 를
로 구현하게 되면 새로운 변수 d 가 추가로 사용되기 때문에
이와 같은 바이트 코드가 생성 됩니다. 초기화된 로컬 변수들은 3개(this + a + b)이지만 4개의 로컬 변수가 사용되는 경우가 되겠습니다.
위에서처럼 static 을 사용할 경우 static 이 아닌 것과 비교해보시면 오브젝트 포인터는 사용하지 않고 바로 local#0 부터 인자 값이 세팅됨을 알 수 있습니다.
로컬 변수를 세팅할 때는 인자의 개수와 static 함수인지가 중요합니다.
스택에 관한 것은 별도의 설명은 생략하겠습니다. 로컬 변수와 스택의 상태를 같이 생각하시면 바이트 코드를 쉽게 이해하실 수 있을 것입니다.
2. 자바의 데이타 형
자바에서 사용하는 데이타형은 일반적으로 c 에서 다루는 것과 비슷하지만 약간 다르기 때문에 약간 주의를 해야 합니다.
먼저 실수형의 경우 모두 sign 형인 것이 특징이고 일반적으로 c 에서 32비트로 처리하는 long 이 자바에서는 64비트형으로 사용되고 있습니다. 그리고 char 은 문자형인데, 내부적으로 unicode 를 사용하기 때문에 16비트를 가지고 있다는 점이 다릅니다.
3. 명령어 처리
명령어는 약 200여개 정도 되지만 의미가 직관적이고 분명해서 처리하는 데 큰 어려움은 없습니다. 그룹이 특별히 정해져 있지는 않지만 처리되는 유형별로 분류해 보겠습니다. 세부적인 설명은 The Java Virtual Machine Specification 에 나와 있습니다.
- 상수
단순히 스택에 상수들을 올려 놓는 명령어들입니다.
- 지역변수
로컬 변수에 스택의 내용을 저장하거나 가져오는 명령어들입니다. 빈도수가 높은 로컬 변수들은 아예 별도의 명령어가 할당되어 있는 것이 특징입니다.
특별한 검증 과정을 생략한다면 4바이트를 단순히 카피하는 수준인 I****, F****, A**** 는 동일한 명령이라고 봐도 무관합니다.
- 스택
스택의 내용을 제거, 복사하는 명령입니다.
- 연산
더하기 곱하기 같은 연산 명령들입니다. 데이타 별로 명령을 나눌 수 있고, integer 형은 비트 연산 명령들이 더 있습니다. short, byte, char 형들은 별도의 연산 명령이 없고 integer 형으로 통합됩니다.
- 타입변환
- 분기
기본적인 조건 분기 명령어들입니다. integer 의 경우 비교과 분기 명령이 합쳐져 있습니다만 long, float, double 등은 비교과 분기 명령이 분리되어 있습니다.
기타 분기 명령입니다.
- 객체
클래스 오브젝트의 맴버들을 스택에 올리거나 스택의 내용을 저장하는 명령입니다
클래스 함수를 호출하는 명령입니다
클래스 타입 캐스트를 위한 검사 명령입니다
- 메모리 할당
메모리를 할당하는 명령들입니다
- 메모리 사용
메모리의 내용을 스택에 올리거나 저장하는 명령입니다. 자바에서는
전체적으로 살펴보면 스택 기반이다 보니 일반적인 CPU 상의 명령어들보다 (RISC 계열 보다도) 명령이 간결한 편입니다. 메모리 할당 같은 추상적인 명령들은 일반적인 컴퓨터의 opcode에서는 볼 수 없는 것들이기도 합니다. 클래스 파일의 상수부를 많이 참조하게 되는 데, 정보가 아주 친절하게 제공되고 있습니다.
4. 가비지 컬렉터
자바의 가장 큰 특징이라면 별도의 메모리 해제 과정이 없다는 것입니다. 아주 지능적으로 메모리 중에 사용되지 않는 메모리를 검사하는 데 이를 가비지 컬렉트라고 합니다.
이해를 쉽게 하기 위해 그냥 일반 C 코드를 이용해서 설명해보겠습니다.
기본적으로는 사용되고 있는 메모리는 해제하지 않는 다는 것에서 시작합니다. 그럼 지금 사용되고 있는 메모리는 어떻게 검사하는 가를 판단할 근거를 마련해야 할 것입니다.
<-1-> 지점에서 가비지 컬렉트를 한다고 생각해보겠습니다. 사용되지 않는 메모리는 A 란 것을 눈으로 봤을 때 직관적으로 알 수 있습니다만, 프로그래밍할 때는 그 근거가 무엇이냐가 중요할 것입니다.
A 가 사용되지 않는 다고 단정할 수 있는 근거는, 바로 할당된 메모리가 저장된 변수가 없다는 것입니다.
가비지 컬렉트하는 지점에서 유효한 메모리는 g_test 와 관련된 메모리 밖에 없기 때문입니다.
이처럼 할당한 메모리 중에서 1차적으로 사용중인 메모리는 유효한 변수들을 검색해서 찾아낼 수 있습니다.
main 이 실행되어 <-2-> 지점에서 가비지 컬랙트를 한다면 체크해야 할 변수는 어떤 것입니까 ? test 함수에서 어떤 일이 있던 가에 상관없이 g_a 에 저장된 메모리만 가비지 컬렉트 이후에 사용할 수 있을 것입니다.
test 에서 수천번의 메모리를 할당이 이루어졌다고 해도 test 를 벗어나서 사용할 수 있는 메모리는 g_a 를 통해서 외부에 전달되는 메모리 밖에 없을 것입니다.
이처럼 유효한 변수는 global 맴버가 되는 데, 자바에서의 클래스 static 필드맴버가 여기에 해당됩니다.
이와 같이 글로벌 변수가 없다면 <-3-> 이전에 testcode 에서 어떤 일을 했건 간에 이 후에 사용할 수 있는 메모리는 b 에 저장된 것 외에는 없을 것입니다. (testcode 에서 할당된 a는 <-3-> 지점에서 사용할 방법이 없습니다. )
이처럼 실행되는 지점에서 유효한 지역 변수에 저장된 메모리도 체크 해야 합니다. 자바 가상 머신에서 스택에 있거나 현재 local 변수에 할당된 것에 해당됩니다.
만약 <-4->와 같은 위치에서 가비지 컬렉트를 한다고 가정하고 살펴보면 할당된 모든 메모리는 살아 있어야 함을 알 수 있을 것입니다. A는 b->next 로 B는 b->next->a 로 C는 b로 D는 b->a 로 접근할 수 있기 때문입니다.
단순히 유효한 변수만 검사한다면 b 외에는 모두 제거될 수도 있습니다. 하지만 b를 살펴보면 b는 하위에 메모리 주소들을 물고 있는 것을 알 수 있습니다. "b 는 유효하므로 b 에 저장된 메모리들도 유효" 하다는 것을 적용해보면 원하는 결과를 얻을 수 있습니다.
이처럼 할당된 메모리가 내부에 포인터들을 가지고 있다면 그 포인터들도 유효하다는 룰을 적용하면 현재 할당된 모든 메모리를 검색해낼 수 있습니다.
위의 조건들을 종합해서 위의 코드의 <-5-> 위치에서 사용하지 않는 메모리를 체크해 보겠습니다.
먼저 유효한 변수를 위처럼 구할 수 있습니다. 그리고 할당된 메모리는 우선은 모두 유효하지 않다고 설정해둡니다. 이제 유효한 변수들로부터 유효한 메모리인지 체크합니다.
결과적으로 B 가 유효하지 않은 메모리하는 것을 구할 수 있습니다.
이러한 과정을 통해서 사용 불가능한 메모리를 구하는 것이 가비지 컬렉트입니다. 알고리즘 자체는 완벽하기 때문에 안정적으로 메모리를 사용할 수 있습니다. 다만 할당된 메모리를 모두 검색해야 하기 때문에 자주 실행한다면 시스템적으로 부담이 될 수도 있습니다. (주로 메모리가 부족해지는 시점에 자동적으로 실행되는 것으로 알려져 있지만, 자바 시스템에선 특별히 그 조건을 한정짓지 않고 있습니다.)
5. 샘플코드
공부하려고 구입했던 따끈한 "Java Virtual machine Specification (2nd ed.)" 과 Programming for the java virtual machine 의 번역서 "자바 가상머신 프로그래밍" 기반으로 구현한 인터프리터 형식의 jvm 입니다. (자바 바이트 코드 인터프리터에 가깝습니다.)
클래스 로더, 스택 인터프리터, 힙 매니저(가비지 컬렉터) 등의 jvm 기본 구성은 모두 이루어져 있습니다. 명령어도 거의 구현해 놓았습니다. 다만, long 형 연산은 우선 생략된 상태이고, throw 같은 예외처리는 구현하지 않았습니다. (그냥 개인적인 취향 ^^)
힙 메모리 관리 부분은 테스트 하던 것과 별개로 그냥 더블 링크드 리스트로 무식하게 관리하는 루틴으로 대체해서 올립니다. (원래 쓰던 코드가 워낙 복잡해서 알아보기 쉬운 쪽으로 수정했습니다.)
가비지 컬렉트 할 때 포인터를 가지고 있는 가와 포인터의 위치등에 대한 정보가 필요한 데, 처리를 쉽게 하기 위해서 레퍼런스 변수들을 앞에 몰아놓고 '처음부터 n 개가 레퍼런스 값이다' 는 식으로 처리해놓았습니다.
local 변수는 jvm 의 스택 메모리의 일부분을 사용하지 않고 그냥 따로 잡아서 사용했습니다. (이해하기는 좀 더 쉬울 거 같습니다.) 내부적으로 java/lang/String 은 char * 로 이해하셔도 됩니다. (그 형태로 넘기기 때문에...)
나름대로는 설계를 쓰기 편하게 한다고 했는 데, 좀 조잡해 보이네요. (아직도 실험 중인 과정이니 의미는 두지 마세요. T_T )
다음 글에 다루겠지만 SubSystem 을 통해서 자바에서 c 의 JDumbSystem 을 이용하고 있습니다. 그리고 자바에서 호출하는 클래스가 없으면 스택이 심각하게 꼬이기 때문에, java/lang/Object 는 더미 클래스로 작성해서 등록했습니다.
자바 셈플은 그냥 간단하게 상속 정도 테스트 해 보는 수준으로 작성했습니다.
1. JVM 구현하기
명령어들을 처리하기 위해서는 기본적으로 스택 메모리와 로컬 메모리가 구성되어야 합니다. (로컬 메모리는 스택 메모리에 매핑이 용이하도록 배려하고 있습니다. - 함수별 local 변수 개수를 이용할 경우)
메소드(함수)를 실행 했을 때 로컬 메모리에는 오브젝트의 주소와 인자가 세팅되어 있습니다. (static 함수가 아닌 경우 첫번째 값은 오브젝트의 포인터 입니다.)
| int add(int a, int b) { return a + b; } void testcode() { int c; c = add(32, 8); .... } |
위와 같은 자바 코드는 아래와 같은 자바 바이트 코드로 변환됩니다.
| 000 aload_0 001 bipush 32 003 bipush 8 005 invokevirtual ; reference [class:"test", name:"add", desc:"(II)I"] ... |
함수가 호출 될 때 스택은 제일 위에 8이, 그 아래에 32 와 this 에 해당하는 오브젝트 어드레스가 위치함을 예상할 수 있습니다. 이때 this 부터 각 순서대로 세팅되어 add 함수에서는
| local#0 = this local#1 = 32 local#2 = 8 |
이 세팅된 상태로 실행되는 것입니다. 그래서 add 코드가 아래와 같으므로 40 이 리턴될 것입니다.
| 000 iload_1 ; local#1 001 iload_2 ; local#2 002 iadd 003 ireturn |
즉 명령어에서 얘기하는 local 값들은 오브젝트 포인터과 인자 값이 세팅된 메모리 값이라고 이해하시면 됩니다. 단순히 인자 값을 받아들이는 것이 전부는 아니고, 그 곳에 값을 저장하기도 합니다. 또한 초기화는 사용하는 로컬변수는 인자의 수와 일치하는 것은 아닙니다. 만약 위의 add 를
| int add(int a, int b) { int d = a + b; return d; } |
로 구현하게 되면 새로운 변수 d 가 추가로 사용되기 때문에
| # methods(26) name:"add", desc:"(II)I", access: max stack:2, max locals:4 000 iload_1 ; local#1 001 iload_2 ; local#2 002 iadd 003 istore_3 ; local#3 004 iload_3 ; local#3 005 ireturn |
이와 같은 바이트 코드가 생성 됩니다. 초기화된 로컬 변수들은 3개(this + a + b)이지만 4개의 로컬 변수가 사용되는 경우가 되겠습니다.
| static int add(int a, int b) { return a + b; } void testcode() { int c; c = add(32, 8); ... } |
위에서처럼 static 을 사용할 경우 static 이 아닌 것과 비교해보시면 오브젝트 포인터는 사용하지 않고 바로 local#0 부터 인자 값이 세팅됨을 알 수 있습니다.
| # methods(26) name:"add", desc:"(II)I", access:static 000 iload_0 ; local#0 001 iload_1 ; local#1 002 iadd 003 ireturn # methods(27) name:"testcode", desc:"()V", access: 000 bipush 32 002 bipush 8 004 invokestatic ; reference [class:"test", name:"add", desc:"(II)I"] ... |
로컬 변수를 세팅할 때는 인자의 개수와 static 함수인지가 중요합니다.
스택에 관한 것은 별도의 설명은 생략하겠습니다. 로컬 변수와 스택의 상태를 같이 생각하시면 바이트 코드를 쉽게 이해하실 수 있을 것입니다.
2. 자바의 데이타 형
자바에서 사용하는 데이타형은 일반적으로 c 에서 다루는 것과 비슷하지만 약간 다르기 때문에 약간 주의를 해야 합니다.
| ] boolean : true / false 값 ] byte : -128 에서 127 의 값을 가지는 8비트형 ] short : -32768 에서 32767 까지의 값을 가지는 16비트형 ] int : -2147483648 에서 2147483647 까지의 값을 가지는 32비트형 ] long : -9223372036854775808 에서 9223372036854775807 까지의 값을 가지는 64비트형 ] char : 0 에서 65535 값을 가지는 16비트형 ] float : 32비트 부동 소숫점 (IEEE754) ] double : 64비트 부동 소숫점 |
먼저 실수형의 경우 모두 sign 형인 것이 특징이고 일반적으로 c 에서 32비트로 처리하는 long 이 자바에서는 64비트형으로 사용되고 있습니다. 그리고 char 은 문자형인데, 내부적으로 unicode 를 사용하기 때문에 16비트를 가지고 있다는 점이 다릅니다.
3. 명령어 처리
명령어는 약 200여개 정도 되지만 의미가 직관적이고 분명해서 처리하는 데 큰 어려움은 없습니다. 그룹이 특별히 정해져 있지는 않지만 처리되는 유형별로 분류해 보겠습니다. 세부적인 설명은 The Java Virtual Machine Specification 에 나와 있습니다.
- 상수
| ACONST_NULL, ICONST_M1, ICONST_0, ICONST_1, ICONST_2, ICONST_3, ICONST_4, ICONST_5, LCONST_0, LCONST_1, FCONST_0, FCONST_1, FCONST_2, DCONST_0, DCONST_1, BIPUSH, SIPUSH, LDC, LDC_W, LDC2_W |
단순히 스택에 상수들을 올려 놓는 명령어들입니다.
- 지역변수
| ILOAD, LLOAD, FLOAD, DLOAD, ALOAD, ILOAD_0, ILOAD_1, ILOAD_2, ILOAD_3, LLOAD_0, LLOAD_1, LLOAD_2, LLOAD_3, FLOAD_0, FLOAD_1, FLOAD_2, FLOAD_3, DLOAD_0, DLOAD_1, DLOAD_2, DLOAD_3, ALOAD_0, ALOAD_1, ALOAD_2, ALOAD_3 ISTORE, LSTORE, FSTORE, DSTORE, ASTORE, ISTORE_0, ISTORE_1, ISTORE_2, ISTORE_3, LSTORE_0, LSTORE_1, LSTORE_2, LSTORE_3, FSTORE_0, FSTORE_1, FSTORE_2, FSTORE_3, DSTORE_0, DSTORE_1, DSTORE_2, DSTORE_3, ASTORE_0, ASTORE_1, ASTORE_2, ASTORE_3 |
로컬 변수에 스택의 내용을 저장하거나 가져오는 명령어들입니다. 빈도수가 높은 로컬 변수들은 아예 별도의 명령어가 할당되어 있는 것이 특징입니다.
특별한 검증 과정을 생략한다면 4바이트를 단순히 카피하는 수준인 I****, F****, A**** 는 동일한 명령이라고 봐도 무관합니다.
- 스택
| POP, POP2, DUP, DUP_X1, DUP_X2, DUP2, DUP2_X1, DUP2_X2, SWAP |
스택의 내용을 제거, 복사하는 명령입니다.
- 연산
| IADD, LADD, FADD, DADD, ISUB, LSUB, FSUB, DSUB, IMUL, LMUL, FMUL, DMUL, IDIV, LDIV, FDIV, DDIV, IREM, LREM, FREM, DREM, INEG, LNEG, FNEG, DNEG, ISHL, LSHL, ISHR, LSHR, IUSHR, LUSHR, IAND, LAND, IOR, LOR, IXOR, LXOR, IINC |
더하기 곱하기 같은 연산 명령들입니다. 데이타 별로 명령을 나눌 수 있고, integer 형은 비트 연산 명령들이 더 있습니다. short, byte, char 형들은 별도의 연산 명령이 없고 integer 형으로 통합됩니다.
- 타입변환
| I2L, I2F, I2D, L2I, L2F, L2D, F2I, F2L, F2D, D2I, D2L, D2F, I2B, I2C, I2S |
- 분기
| LCMP, FCMPL, FCMPG, DCMPL, DCMPG, IFEQ, IFNE, IFLT, IFGE, IFGT, IFLE, IF_ICMPEQ, IF_ICMPNE, IF_ICMPLT, IF_ICMPGE, IF_ICMPGT, IF_ICMPLE, IF_ACMPEQ, IF_ACMPNE, IFNULL, IFNONNULL |
기본적인 조건 분기 명령어들입니다. integer 의 경우 비교과 분기 명령이 합쳐져 있습니다만 long, float, double 등은 비교과 분기 명령이 분리되어 있습니다.
| GOTO, JSR, RET, TABLESWITCH, LOOKUPSWITCH, GOTO_W, JSR_W |
기타 분기 명령입니다.
- 객체
| GETSTATIC, PUTSTATIC, GETFIELD, PUTFIELD |
클래스 오브젝트의 맴버들을 스택에 올리거나 스택의 내용을 저장하는 명령입니다
| INVOKEVIRTUAL, INVOKESPECIAL, INVOKESTATIC, INVOKEINTERFACE |
클래스 함수를 호출하는 명령입니다
| INSTANCEOF, CHECKCAST |
클래스 타입 캐스트를 위한 검사 명령입니다
- 메모리 할당
| NEW, NEWARRAY, ANEWARRAY, ARRAYLENGTH, MULTIANEWARRAY |
메모리를 할당하는 명령들입니다
- 메모리 사용
| IALOAD, LALOAD, FALOAD, DALOAD, AALOAD, BALOAD, CALOAD, SALOAD, IASTORE, LASTORE, FASTORE, DASTORE, AASTORE, BASTORE, CASTORE, SASTORE, |
메모리의 내용을 스택에 올리거나 저장하는 명령입니다. 자바에서는
전체적으로 살펴보면 스택 기반이다 보니 일반적인 CPU 상의 명령어들보다 (RISC 계열 보다도) 명령이 간결한 편입니다. 메모리 할당 같은 추상적인 명령들은 일반적인 컴퓨터의 opcode에서는 볼 수 없는 것들이기도 합니다. 클래스 파일의 상수부를 많이 참조하게 되는 데, 정보가 아주 친절하게 제공되고 있습니다.
4. 가비지 컬렉터
자바의 가장 큰 특징이라면 별도의 메모리 해제 과정이 없다는 것입니다. 아주 지능적으로 메모리 중에 사용되지 않는 메모리를 검사하는 데 이를 가비지 컬렉트라고 합니다.
이해를 쉽게 하기 위해 그냥 일반 C 코드를 이용해서 설명해보겠습니다.
기본적으로는 사용되고 있는 메모리는 해제하지 않는 다는 것에서 시작합니다. 그럼 지금 사용되고 있는 메모리는 어떻게 검사하는 가를 판단할 근거를 마련해야 할 것입니다.
| int * g_test=0; g_test = new int [32]; // <- A g_test = new int [24]; // <- B <-1-> |
<-1-> 지점에서 가비지 컬렉트를 한다고 생각해보겠습니다. 사용되지 않는 메모리는 A 란 것을 눈으로 봤을 때 직관적으로 알 수 있습니다만, 프로그래밍할 때는 그 근거가 무엇이냐가 중요할 것입니다.
A 가 사용되지 않는 다고 단정할 수 있는 근거는, 바로 할당된 메모리가 저장된 변수가 없다는 것입니다.
가비지 컬렉트하는 지점에서 유효한 메모리는 g_test 와 관련된 메모리 밖에 없기 때문입니다.
이처럼 할당한 메모리 중에서 1차적으로 사용중인 메모리는 유효한 변수들을 검색해서 찾아낼 수 있습니다.
| int * g_a; void test(int a) { int * fd, *fdfd; ... } void main() { test(232); test(22); <-2-> .... } |
main 이 실행되어 <-2-> 지점에서 가비지 컬랙트를 한다면 체크해야 할 변수는 어떤 것입니까 ? test 함수에서 어떤 일이 있던 가에 상관없이 g_a 에 저장된 메모리만 가비지 컬렉트 이후에 사용할 수 있을 것입니다.
test 에서 수천번의 메모리를 할당이 이루어졌다고 해도 test 를 벗어나서 사용할 수 있는 메모리는 g_a 를 통해서 외부에 전달되는 메모리 밖에 없을 것입니다.
이처럼 유효한 변수는 global 맴버가 되는 데, 자바에서의 클래스 static 필드맴버가 여기에 해당됩니다.
| void testcode() { int *a = new int [123]; ... } void main() { int * b; ... testcode(); ... <-3-> ... } |
이와 같이 글로벌 변수가 없다면 <-3-> 이전에 testcode 에서 어떤 일을 했건 간에 이 후에 사용할 수 있는 메모리는 b 에 저장된 것 외에는 없을 것입니다. (testcode 에서 할당된 a는 <-3-> 지점에서 사용할 방법이 없습니다. )
이처럼 실행되는 지점에서 유효한 지역 변수에 저장된 메모리도 체크 해야 합니다. 자바 가상 머신에서 스택에 있거나 현재 local 변수에 할당된 것에 해당됩니다.
| struct Obj { int * a; Obj * next; }; Obj * a, * b; a = new Obj; // A a->a = new int [23]; // B a->next = NULL; b = new Obj; // C b->a = new int [21]; // D b->next = a; a = 0; <-4-> |
만약 <-4->와 같은 위치에서 가비지 컬렉트를 한다고 가정하고 살펴보면 할당된 모든 메모리는 살아 있어야 함을 알 수 있을 것입니다. A는 b->next 로 B는 b->next->a 로 C는 b로 D는 b->a 로 접근할 수 있기 때문입니다.
단순히 유효한 변수만 검사한다면 b 외에는 모두 제거될 수도 있습니다. 하지만 b를 살펴보면 b는 하위에 메모리 주소들을 물고 있는 것을 알 수 있습니다. "b 는 유효하므로 b 에 저장된 메모리들도 유효" 하다는 것을 적용해보면 원하는 결과를 얻을 수 있습니다.
| b 유효 => C b 는 유효한 Obj 이므로 b 에 저장된 b->a, b->next 유효 => D, A b->next 는 유효한 Obj 이므로 b->next 에 저장된 b->next->a, b->next->next 유효 => B |
이처럼 할당된 메모리가 내부에 포인터들을 가지고 있다면 그 포인터들도 유효하다는 룰을 적용하면 현재 할당된 모든 메모리를 검색해낼 수 있습니다.
| int ** a, * b, * c; a = new int * [3]; // A b = new int [4]; // B c = new int [9]; // C a[0] = new int [3]; // D a[1] = c; a[2] = new int [7]; // E b = c; <-5-> |
위의 조건들을 종합해서 위의 코드의 <-5-> 위치에서 사용하지 않는 메모리를 체크해 보겠습니다.
| 유효한 변수는 a, b, c 할당된 메모리는 A, B, C, D, E |
먼저 유효한 변수를 위처럼 구할 수 있습니다. 그리고 할당된 메모리는 우선은 모두 유효하지 않다고 설정해둡니다. 이제 유효한 변수들로부터 유효한 메모리인지 체크합니다.
| a 에 저장된 A 는 유효 (a는 3개의 포인터를 가짐) a[0] 에 저장된 D 는 유효 a[1] 에 저장된 C 는 유효 a[2] 에 저장된 E 는 유효 b 에 저장된 C 는 유효 c 에 저장된 C 는 유효 |
결과적으로 B 가 유효하지 않은 메모리하는 것을 구할 수 있습니다.
이러한 과정을 통해서 사용 불가능한 메모리를 구하는 것이 가비지 컬렉트입니다. 알고리즘 자체는 완벽하기 때문에 안정적으로 메모리를 사용할 수 있습니다. 다만 할당된 메모리를 모두 검색해야 하기 때문에 자주 실행한다면 시스템적으로 부담이 될 수도 있습니다. (주로 메모리가 부족해지는 시점에 자동적으로 실행되는 것으로 알려져 있지만, 자바 시스템에선 특별히 그 조건을 한정짓지 않고 있습니다.)
5. 샘플코드
공부하려고 구입했던 따끈한 "Java Virtual machine Specification (2nd ed.)" 과 Programming for the java virtual machine 의 번역서 "자바 가상머신 프로그래밍" 기반으로 구현한 인터프리터 형식의 jvm 입니다. (자바 바이트 코드 인터프리터에 가깝습니다.)
클래스 로더, 스택 인터프리터, 힙 매니저(가비지 컬렉터) 등의 jvm 기본 구성은 모두 이루어져 있습니다. 명령어도 거의 구현해 놓았습니다. 다만, long 형 연산은 우선 생략된 상태이고, throw 같은 예외처리는 구현하지 않았습니다. (그냥 개인적인 취향 ^^)
힙 메모리 관리 부분은 테스트 하던 것과 별개로 그냥 더블 링크드 리스트로 무식하게 관리하는 루틴으로 대체해서 올립니다. (원래 쓰던 코드가 워낙 복잡해서 알아보기 쉬운 쪽으로 수정했습니다.)
가비지 컬렉트 할 때 포인터를 가지고 있는 가와 포인터의 위치등에 대한 정보가 필요한 데, 처리를 쉽게 하기 위해서 레퍼런스 변수들을 앞에 몰아놓고 '처음부터 n 개가 레퍼런스 값이다' 는 식으로 처리해놓았습니다.
local 변수는 jvm 의 스택 메모리의 일부분을 사용하지 않고 그냥 따로 잡아서 사용했습니다. (이해하기는 좀 더 쉬울 거 같습니다.) 내부적으로 java/lang/String 은 char * 로 이해하셔도 됩니다. (그 형태로 넘기기 때문에...)
나름대로는 설계를 쓰기 편하게 한다고 했는 데, 좀 조잡해 보이네요. (아직도 실험 중인 과정이니 의미는 두지 마세요. T_T )
다음 글에 다루겠지만 SubSystem 을 통해서 자바에서 c 의 JDumbSystem 을 이용하고 있습니다. 그리고 자바에서 호출하는 클래스가 없으면 스택이 심각하게 꼬이기 때문에, java/lang/Object 는 더미 클래스로 작성해서 등록했습니다.
자바 셈플은 그냥 간단하게 상속 정도 테스트 해 보는 수준으로 작성했습니다.
댓글을 달아 주세요
나는 너에 합의한다 이다. 그것은 이렇게 이다.
너는 아주 좋은 보는 위치가 있는다!
좋은 위치는 그것 찾아본 즐겼다!