
궁금증
GC 알고리즘 중, 마크-스윕 알고리즘은 회수 대상에 대해서 마킹을 진행하고, 마킹된 대상들을 쓸어담아서 회수한다. 따라서 메모리에 살아남은 객체들 사이사이에 파편화된 공간이 남게 된다.
이런 방식의 문제점은 메모리의 파편화가 생긴다는 것이다. 살아남은 객체 사이사이 공간들은 일정한 크기가 아니라 들쭉날쭉한 크기로 남게 될 것이고, 큰 객체가 생겨났을 때 해당 공간을 활용하지 못할 것이다.
GC 알고리즘에서는 마크-컴팩트 알고리즘 혹은 마크-카피 알고리즘을 통해 이 단편화 문제를 해결한다. 컴팩트 알고리즘은 살아남은 객체들을 메모리공간의 첫 번째 구역부터 차례로 차곡차곡 쌓고 남은 영역을 회수한다. 마크-카피 알고리즘은 살아남은 객체들을 사용하지 않는 공간에 차곡차곡 쌓고 남은 영역을 회수한다. 이 알고리즘들의 문제는 데이터를 복사하거나, 압축한다는 것이다. 이 시간대에서는 STW가 생기기 때문에 자바가 실행되지 못한다.
OS에서는 이 문제를 paging, segmentation과 같은 가상 메모리 기술을 활용해서 해결했다. 페이징의 경우 메모리를 일정한 크기인 페이지 단위로 공간을 만들고, 메모리에 올릴 데이터들도 페이지 단위의 크기로 잘라서 저장한다. 덕분에 내부 단편화 문제가 생기지만 이는 마지막 페이지에 대해서만 발생하고 외부 단편화 문제는 깔끔하게 해결된다. OS에서도 마찬가지로 압축과 같은 해결 방안이 있었지만, OS가 멈춰서는 안되기 때문에 이런 문제를 가상 메모리 기법을 활용해 해결하였다.
그럼 저장공간인 HDD와 SSD는 어떻게 해결했을까? 우분투의 기본 파일 시스템인 ext4를 기준으로 보면 extent로 나눠서 여기저기 저장한다. 큰 파일의 저장이 필요하면 파일 하나를 extent라는 덩어리로 여러 위치에 저장한다. 이 저장된 정보는 파일의 메타데이터에 저장해둔다. 예를 들어 파일의 0바이트부터 100MB까지는 디스크 블록 1000~ 32500에 있다. 100MB ~ 300MB는 디스크 블록 90000~ 120000에 있다. 블록은 디스크의 기본 단위라고 생각하면 된다. 따라서 디스크에서도 큰 파일을 한공간에 다 넣지 않는다.
그럼 JVM에서는 힙 영역은 왜 반드시 객체를 하나의 공간에 밀어넣어서 저장해야 하는걸까?
일단 내 생각
일단, 디스크나 메모리보다 객체는 매우 작은 데이터이다. 이 작은 데이터를 쪼개서 저장하고, 메타데이터를 저장/관리하는 비용이 운영체제의 메모리나, 디스크 파일시스템보다 훨씬 큰 비용일 것이라는 생각이 든다.
또, 파일 시스템은 정보를 저장하는 시스템이므로, 바로 실행될 대상들이 아니므로 속도보다는 저장에 초점을 맞춘게 아닐까 싶다.
JVM GC의 목적 자체가 실시간성이나 중단 시간이라는 목적도 있지만 처리량의 목적도 있기 때문에 어느정도의 트레이드 오프를 통해 잠깐의 중단 시간(STW)를 가져가는 방식을 사용한게 아닐까? 라는 생각도 든다.
GPT의 답변
- CPU 캐시와 성능 모델이 “연속 메모리”를 대전제로 삼고 있다. 객체가 연속된 메모리 상에 있어야 CPU가 빠르게 읽을 수 있다.
- 필드 접근 즉, 자바 객체 필드 접근이 offset 기반인데, 조각나 있으면 offset 계산 자체가 불가능하다.
- obj.fieldA에 대해 자바는 object_address + field_offset으로 계산한다.
- GC 자체가 “힙을 스캔하며 연속된 구조”를 가정한다. GC는 힙을 선형으로 훑으면서 객체를 따라간다.
- JVM의 핫스팟 객체 모델이 조각내기를 전재로 만들어지지 않았다. 조각을 내려면 오브젝트 레이아웃 완전히 변경해야하고 JIT, GC, JNI 등등이 다시 설계되어야 한다.
- OS에서의 paging lookup 비용, 디스크의 extent 조합 비용은 허용될 시간이지만 JVM의 객체 접근시간에서는 허용되지 않는다.
결론
gpt의 답변의 신뢰성을 위해 검색을 좀 해봤지만, 자바 객체가 반드시 연속된 구조를 가정하는지 찾지 못했다.
오히려 스택 오버플로우에서는 https://stackoverflow.com/questions/7812677/is-the-java-heap-memory-contiguous JVM 명세에는 연속된 저장을 가정하지 않는다고 되어있다는 말을 인용하며 그런 가정을 해서는 안된다고 되어있다.
"JVM 밑바닥부터" 라는 책에서는 "《자바 가상 머신 명세》 따르면 자바 힙은 물리적으로 떨어진 메모리에 위치해 상관없으나 논리적으로는 연속되어야 한다. 파일을 저장할 디스크 공간을 용하는 방식과 같다 파일 각각은 논리적으로 연속된 공간에 저장된다 ."라고 되어있다. 그럼 디슼크나 OS처럼 단편화 문제를 물리적으로 다른 위치에 저장시키고 논리적으로 연속된 공간에 저장하도록 만들면 되는게 아닌가 싶다.
자바 객체가 힙에 저장되는 구조도 살펴봤지만 정확히는 모르겠다.
혹시 이에 대한 의견이나 견해가 있으신분들은 댓글에 의견 부탁드립니다.!!!!!!!!!!!!!!!!!! 제발ㅠㅠㅠ !!!!!!!!
개앞맵시 번역가님의 답변
내가 보낸 메일은 다음과 같다.

책을 번역하신 개앞맵시님께 연락을 드렸는데 답장이 오면 추가로 작성하겠습니다. :)
답장이 하루도 지나지 않고 바로 왔다.
간단하게 정리하면 운영체제가 메모리를 논리 주소와 물리 주소로 나눠서 메모리에 올리는 프로그램을 잘게 쪼개 사용하는 방식은 MMU라는 CPU 내부 memory management unit가 물리, 논리 주소를 매핑하기 때문에 비용이 0에 가깝다. 디스크의 경우에는 논리, 물리 주소 매핑보다 데이터를 읽는데 걸리는 시간이 훨씬 느리기 때문에 오버헤드가 티도 나지 않는다.
하지만, JVM의 경우에는 바이트 코드를 어디서나 실행할 수 있도록 하기위한 VM으로, 소프트웨어로 동작한다. 따라서 MMU에 접근이 불가능하다. 따라서 소프트웨어적으로 매핑작업을 구현해야하는데 이 비용이 성능적으로 매우 떨어지기 때문에 이런 방식을 사용하지 않는다.
따라서 OS와 달리 JVM의 경우에는 memory compression(메모리 압축)하는 비용이 주소 매핑 비용보다 작기 때문에 이런 방식을 사용하는 방향으로 발전했다.
너무 친절하게 쉽게 설명해주셔서 너무 감사합니다.!! :)
출처
'JAVA' 카테고리의 다른 글
| G1, Garbage First 가비지 컬렉터 (1) | 2025.12.17 |
|---|---|
| [JAVA] 객체를 넘겨주지 않고, 외부에서 객체를 사용하기! (0) | 2024.12.11 |
| [JAVA] 생산자 소비자 문제와 자바의 Object.wait() notify() (2) | 2024.10.02 |
| [JAVA] ReentrantLock이 뭔가요? (1) | 2024.09.24 |
| [JAVA] java.util.concurrent.LockSupport 알아보자 (0) | 2024.09.24 |