Daily-It

개발, AI, 인프라, 자동화와 일상 IT 제품 후기를 직접 써보며 정리하는 기술 블로그입니다.

Java 객체는 왜 메모리를 많이 쓸까: heap, reference, GC 관점

요약

Java를 오래 쓰다 보면 객체지향적으로 모델을 잘 만들고 싶지만, 성능 문제가 생기는 순간 primitive나 배열 중심의 코드로 내려가고 싶은 유혹이 생깁니다. 이유는 단순합니다. Java 객체는 표현력이 좋은 대신 런타임 비용이 있습니다.

이 글은 Java 객체가 왜 primitive보다 무겁게 느껴지는지, heap allocation, reference, object header, GC, CPU cache 관점에서 정리합니다. Project Valhalla가 왜 value class와 flattening을 이야기하는지도 이 배경을 알면 더 잘 이해됩니다.

목차

Java 객체는 값만 들고 있지 않다

Java에서 객체는 필드 값만 가지고 있는 것이 아닙니다. JVM이 객체를 관리하기 위한 메타데이터도 필요합니다. 객체 identity, 동기화, GC 추적, 클래스 정보 접근 같은 것들이 모두 객체 모델 안에 들어 있습니다.

예를 들어 다음 타입을 생각해볼 수 있습니다.

record Point(int x, int y) {}

겉으로 보면 int x, int y 두 개만 있으니 8바이트면 충분해 보입니다. 하지만 실제 객체로 다뤄질 때는 객체 header, 정렬, reference 접근 비용 등이 붙습니다. 배열에 넣으면 Point 값 자체가 배열에 들어가는 것이 아니라 Point 객체를 가리키는 reference가 들어갑니다.

이 구조는 유연합니다. 하지만 작은 객체가 대량으로 생기는 경우에는 부담이 됩니다.

heap allocation과 reference 비용

Java 객체는 일반적으로 heap에 생성됩니다. 물론 JIT와 escape analysis가 일부 객체를 최적화할 수 있지만, 개발자가 항상 결과를 보장하기는 어렵습니다.

기존 객체 배열은 보통 이렇게 생각할 수 있습니다.

Point[] points
  points[0] -> Point object on heap
  points[1] -> Point object on heap
  points[2] -> Point object on heap

즉 배열은 값의 묶음이 아니라 reference의 묶음입니다. 데이터를 읽으려면 배열에서 reference를 읽고, 다시 heap 어딘가의 객체로 이동한 뒤 필드를 읽습니다.

작은 데이터가 아주 많다면 이 간접 접근이 누적됩니다. 단순한 계산 코드나 이벤트 처리 코드에서 primitive 배열이 객체 배열보다 빠르게 느껴지는 이유도 여기에 있습니다.

object header와 작은 객체 문제

객체 하나가 아주 큰 데이터를 담고 있다면 객체 관리 비용은 상대적으로 작게 느껴질 수 있습니다. 하지만 작은 객체는 다릅니다.

Money: long cents 하나
Point: int x, int y 두 개
UserId: long value 하나
MetricPoint: long timestamp + double value

이런 타입은 필드 자체보다 객체로 존재하기 위한 부가 비용이 상대적으로 커질 수 있습니다. 도메인 모델에는 이런 작은 타입이 많습니다. 그래서 코드의 의미를 살리면 객체가 늘고, 객체가 늘면 메모리와 GC 부담이 늘어날 수 있습니다.

B2B 시스템에서는 도메인 의미가 중요해서 이런 타입을 더 적극적으로 만들게 됩니다. 유지보수에는 좋지만, 대량 데이터 처리나 고성능 경로에서는 부담이 될 수 있습니다.

GC는 왜 작은 객체가 많을 때 부담이 되는가

Java의 GC는 매우 발전했습니다. G1, ZGC, Shenandoah 같은 collector 덕분에 예전보다 훨씬 안정적으로 큰 heap을 다룰 수 있습니다. 하지만 GC가 좋아졌다고 해서 객체 생성 비용이 사라지는 것은 아닙니다.

작은 객체가 많이 생기면 다음 문제가 생깁니다.

  • allocation rate가 높아집니다.
  • young generation collection이 자주 발생할 수 있습니다.
  • 살아남는 객체가 많으면 promotion 비용이 생깁니다.
  • reference graph를 추적해야 합니다.
  • pause time은 줄어도 CPU 사용량은 늘 수 있습니다.

대부분의 웹 API에서는 큰 문제가 아닐 수 있습니다. 하지만 이벤트 처리, 로그 처리, 메트릭 집계, 랭킹, 추천, 벡터 처리처럼 많은 데이터를 계속 만지는 코드에서는 차이가 커질 수 있습니다.

CPU cache locality 문제

성능은 CPU 연산 속도만의 문제가 아닙니다. CPU가 데이터를 얼마나 연속적으로 잘 읽을 수 있는지도 중요합니다.

primitive 배열은 데이터가 비교적 연속적으로 배치됩니다.

int[] values = [1, 2, 3, 4, 5]

반면 객체 배열은 reference가 연속적으로 있을 뿐, 실제 객체는 heap 여러 곳에 흩어질 수 있습니다.

Point[] -> ref, ref, ref
             ↓    ↓    ↓
           obj  obj  obj

이렇게 데이터가 흩어지면 CPU cache 효율이 떨어질 수 있습니다. Go나 Rust의 struct 배열이 성능상 유리하게 느껴지는 이유 중 하나가 여기에 있습니다.

Project Valhalla가 바라보는 개선 방향

Project Valhalla는 이 문제를 Java답게 해결하려고 합니다. Java의 객체지향 모델을 버리는 것이 아니라, identity가 필요 없는 값 객체를 JVM이 더 효율적으로 다룰 수 있게 만드는 방향입니다.

JEP 401의 value class는 identity-free object를 도입합니다. identity가 없으면 JVM은 해당 값을 특정 heap 객체로 고정해서 생각하지 않아도 됩니다. 이 덕분에 flattening, scalarization, compact layout 같은 최적화 가능성이 생깁니다.

쉽게 말하면 이런 기대입니다.

의미 있는 타입을 유지하면서
런타임에서는 primitive에 가까운 layout을 일부 얻을 수 있게 하자

물론 모든 객체가 이렇게 되는 것은 아닙니다. identity가 필요한 객체는 여전히 일반 객체가 맞습니다. 하지만 Money, Point, Range, MetricPoint 같은 타입에서는 기대할 만합니다.

실무에서 지금 할 수 있는 판단

Valhalla가 안정화되기 전이라도, 지금 코드에서 생각해볼 점은 있습니다.

첫째, 모든 작은 타입을 성능 문제로 미리 primitive로 풀어버릴 필요는 없습니다. 대부분의 업무 시스템에서는 도메인 의미와 유지보수성이 더 중요합니다.

둘째, 정말 성능이 중요한 경로는 따로 봐야 합니다. API 전체가 아니라 특정 배치, 집계, ranking, log processing, metric pipeline 같은 구간에서 문제가 생깁니다.

셋째, 성능 문제는 측정 후 판단해야 합니다. 객체가 많아 보인다고 무조건 문제가 되는 것은 아닙니다. allocation rate, GC log, profiler, async-profiler 같은 도구로 확인해야 합니다.

넷째, Valhalla가 들어오면 이런 작은 value object를 다시 점검할 가치가 있습니다. 지금은 성능 때문에 primitive로 내렸던 코드도 나중에는 더 의미 있는 타입으로 되돌릴 수 있을지 모릅니다.

결론

Java 객체가 무겁게 느껴지는 이유는 Java가 나쁜 언어라서가 아닙니다. Java 객체 모델이 identity, reference, heap allocation, GC 관리 같은 풍부한 의미를 함께 제공하기 때문입니다. 이 모델은 큰 시스템을 안정적으로 만들기 좋지만, 작은 값 객체가 대량으로 생기는 곳에서는 비용이 됩니다.

Project Valhalla가 흥미로운 이유는 바로 이 지점을 건드리기 때문입니다. Java가 도메인 모델의 표현력을 유지하면서도, 일부 값 객체를 더 compact하고 효율적으로 다룰 수 있다면 오래된 타협이 줄어들 수 있습니다.

아직은 실제로 나온 뒤 테스트를 해봐야 합니다. 하지만 Java를 오래 써온 입장에서는 이런 방향의 개선이 반갑습니다. Java가 익숙해서 좋은 것도 있지만, 익숙한 언어가 계속 좋아지는 모습을 보는 것도 꽤 기대되는 일입니다.

참고 자료