2022-09-13-TIL
2022-09-13-TIL
Today I Learned
OOM and Memory Leak by JPA
java.lang.OutOfMemoryError: Java heap space가 났다고 해서 곧바로 메모리 누수라고 볼 수는 없다. Oracle 문서는 이 에러가 힙에 객체를 할당하지 못했다는 뜻일 뿐이고, 원인은 두 갈래라고 설명한다. 하나는 힙 크기 자체가 애플리케이션에 비해 작은 설정 문제이고, 다른 하나는 애플리케이션이 의도치 않게 참조를 계속 쥐고 있어서 GC가 객체를 회수하지 못하는 경우다. 후자가 자바에서 말하는 메모리 누수다. 원인을 보려면 -XX:+HeapDumpOnOutOfMemoryError로 OOM 순간의 힙 덤프를 남기고, Full GC 이후에도 살아남는 객체(live set)가 시간이 갈수록 늘어나는지 보면 된다(Troubleshoot Memory Leaks).
JPA(Hibernate)를 쓸 때 힙을 키우는 대표적인 지점은 두 가지다.
첫째는 영속성 컨텍스트(1차 캐시)다. 영속성 컨텍스트는 조회하거나 저장한 엔티티를 트랜잭션 동안 들고 있다. Hibernate 문서는 세션을 오래 열어 두거나 데이터를 너무 많이 읽으면 영속성 컨텍스트가 끝없이 커져 OOM에 이를 수 있다고 경고한다. 그래서 대량 저장이나 배치 처리에서는 일정 개수마다 flush()와 clear()를 호출해 1차 캐시 크기를 관리하라고 권한다(Hibernate User Guide 5.6, Batching).
1
2
3
4
5
6
7
for (int i = 0; i < items.size(); i++) {
entityManager.persist(items.get(i));
if (i % 50 == 0) {
entityManager.flush();
entityManager.clear();
}
}
둘째는 쿼리 계획 캐시다. Hibernate는 HQL을 파싱한 결과를 캐시하고, 그 최대 항목 수는 hibernate.query.plan_cache_max_size(기본값 2048)로 정한다. IN 절에 넘기는 파라미터 개수가 매번 다르면 쿼리 문자열이 매번 달라진다. hibernate.query.in_clause_parameter_padding을 켜면 바인딩 파라미터 수를 4, 8, 16처럼 2의 거듭제곱으로 맞춰서 서로 다른 쿼리의 가짓수를 줄인다(기본값은 false).
읽은 글 중 우아한형제들 기술블로그 글은 JPA가 아니라 RestTemplate 이야기였다. URL에 ID를 문자열로 이어 붙였더니, 의존성으로 딸려 온 메트릭 라이브러리가 URL마다 새 키를 만들어 계속 쌓았다는 내용이다. 겉모습은 달라도 “값이 계속 바뀌는 문자열을 캐시 키로 쓰면 캐시가 끝없이 자란다”는 점에서 쿼리 계획 캐시 문제와 같은 구조다.
마지막 링크는 계층형 댓글을 RDB에 저장하는 방법(인접 리스트, 중첩 집합, 경로 열거, 클로저 테이블)을 비교한 글이다. 계층 데이터를 엔티티 연관관계로 한꺼번에 읽으면 한 번에 메모리에 올라오는 엔티티 수가 커지기 때문에 같이 읽어 두었다.
- https://techblog.woowahan.com/2628/
- https://inside.getyourguide.com/blog/find-and-fix-oom-and-memory-leaks-in-java-services
- https://www.baeldung.com/java-memory-leaks
- https://stackoverflow.com/questions/706238/entitymanager-causing-memory-leak
- https://www.ibm.com/support/pages/outofmemory-caused-leak-open-jpa
- https://tecoble.techcourse.co.kr/post/2021-09-03-hierarchy-data-with-rdb/
댓글
아직 댓글이 없습니다