본문 바로가기

자바(Java)

배열의 메모리는 연속일까? 메모리 주소 조회 (Java)

메모리의 계층 구조와 배열


자바에서 메모리는 어떻게 관리될까요? code - data - heap - stack의 계층으로 메모리는 저장됩니다. 우리가 배열의 인스턴스를 생성한다고 생각해봅시다. 여기서 우변의 new int[10]은 length가 10인 int 배열의 인스턴스를 생성한다는 의미입니다. 자바에서 new를 사용한 인스턴스 생성은 동적할당을 의미하므로 heap 영역에 생성됩니다. 좌변의 numbers는 4 byte의 용량을 가진 참조변수입니다. 어느 객체를 참조하던지 상관없이 참조변수는 항상 같은 크기로 할당됩니다. 따라서 참조변수를 저장하는데 필요한 메모리 공간은 compile 시에 결정할 수 있습니다. 컴파일 타임에 결정되는 지역 변수, 매개 변수는 stack에 저장되죠.

int[] numbers = new int[10];

여기서 생겼던 궁금증은 C언어의 경우 배열의 주소는 연속적으로 존재한다는 것을 알고있습니다. 이 사실을 포인터로 참조해서 주소를 출력해서 확인해볼 수 있습니다. C언어는 포인터 연산이 가능하기 때문입니다. 아래 그림처럼 배열의 주소 공간은 일정한 크기를 가지고 메모리 상에서 연속으로 할당됩니다.

 

하지만 자바는 어떨까요? 자바는 JVM을 통해서 메모리를 관리합니다. 자바에서 객체의 배열을 생성했을 때 C언어처럼 메모리 주소 상에서 연속으로 생성되는지 궁금해서 한 번 직접 생성해서 주소까지 확인해보았습니다.


연속 메모리 상에 있는지 조회하기


먼저,  Repository 객체는 nodeList라는 node의 배열을 가지고 있습니다. 생성자를 통해서 nodeList를 메모리 상에서 연속적으로 존재하도록 초기화했습니다.

public class Repository {
    private Node[] nodeList;
	public Repository(int number) {
        nodeList = new Node[number];
        for (int i = 0; i < number; i++) {
            nodeList[i] = new Node();
        }
    }
}

 

초기 메모리 주소가 배열에서는 연속적으로 생성되는 것인지 확인하고 싶었다. 그러나, 자바는 C언어와 달리 JVM이 주기적으로 메모리를 관리하고, 따라서 절대적인 메모리 주소가 의미가 없습니다. 그래서인지 보안상의 이유와 합쳐서 주소를 반환하는 기능을 제공하지 않고 오히려 막아두었습니다. 대신, 주소의 개념이 필요할 때 hashcode를 사용합니다. 그렇지만 Unsafe 라이브러리를 사용해서 인스턴스를 생성했을 때의 주소를 반환받을 수 있습니다. 그래서 Stackoverflow에서 printAddress를 구현해놓은 함수를 찾아 실행했습니다. 이하 printAddress 함수의 소스코드입니다. (출처 - https://stackoverflow.com/questions/8820164/is-there-a-way-to-get-a-reference-address)

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collections;

public class OrderOfObjectsAfterGCMain {
    static final Unsafe unsafe = getUnsafe();
    static final boolean is64bit = true; // auto detect if possible.

    public static void main(String... args) {
        Double[] ascending = new Double[16];
        for(int i=0;i<ascending.length;i++)
            ascending[i] = (double) i;

        Double[] descending = new Double[16];
        for(int i=descending.length-1; i>=0; i--)
            descending[i] = (double) i;

        Double[] shuffled = new Double[16];
        for(int i=0;i<shuffled.length;i++)
            shuffled[i] = (double) i;
        Collections.shuffle(Arrays.asList(shuffled));

        System.out.println("Before GC");
        printAddresses("ascending", ascending);
        printAddresses("descending", descending);
        printAddresses("shuffled", shuffled);

        System.gc();
        System.out.println("\nAfter GC");
        printAddresses("ascending", ascending);
        printAddresses("descending", descending);
        printAddresses("shuffled", shuffled);

        System.gc();
        System.out.println("\nAfter GC 2");
        printAddresses("ascending", ascending);
        printAddresses("descending", descending);
        printAddresses("shuffled", shuffled);
    }

    public static void printAddresses(String label, Object... objects) {
        System.out.print(label + ": 0x");
        long last = 0;
        int offset = unsafe.arrayBaseOffset(objects.getClass());
        int scale = unsafe.arrayIndexScale(objects.getClass());
        switch (scale) {
            case 4:
                long factor = is64bit ? 8 : 1;
                final long i1 = (unsafe.getInt(objects, offset) & 0xFFFFFFFFL) * factor;
                System.out.print(Long.toHexString(i1));
                last = i1;
                for (int i = 1; i < objects.length; i++) {
                    final long i2 = (unsafe.getInt(objects, offset + i * 4) & 0xFFFFFFFFL) * factor;
                    if (i2 > last)
                        System.out.print(", +" + Long.toHexString(i2 - last));
                    else
                        System.out.print(", -" + Long.toHexString( last - i2));
                    last = i2;
                }
                break;
                case 8:
                    throw new AssertionError("Not supported");
        }
        System.out.println();
    }

    private static Unsafe getUnsafe() {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            return (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }
}

결과는 아래처럼 원하는 값을 얻지못했습니다. 이유는 JVM이 어떻게 메모리를 사용하는지(32-bit, 64-bit)에 따라 오브젝트의 인덱스가 영향을 받고 주소를 찾는 방식이 영향을 받기 때문이었습니다. 다만, Unsafe 예제 코드 실행을 통해서 메모리 heap 상에 생성된 배열이 Garbage Collecting 이후로 메모리 주소가 변경됨을 확인할 수 있었습니다. 여기까지의 삽질을 마무리로 자바에서 메모리 주소 추적을 실패로 마쳤습니다.

---영상클립 생성---
cdfa: 0x8
제목1: 0x8
facd: 0x8
제목2: 0x8
baee: 0x8
제목3: 0x8
aafe: 0x8