JVM이란? , OutOfMemoryError: Java heap space

리브리버·2023년 5월 21일
0

대용량 DB

목록 보기
3/4
post-thumbnail

에러 설명

이제야 예상했던 제대로된 에러를 만나게 되었다 !

저번의 에러는 뭔가 찜찜했지만 이 에러를 만나고는 뿌듯했다

하고자 했던 테스트 코드는 Post 객체 List를 만들고 해당 리스트에 100만개의 Post 객체를 만들어 List에 넣은 뒤 JPA의 saveAll() 을 통해 Bulk Insert를 하는 것 이었다.

이 과정에서 Java의 Heap 공간이 부족하여 위 에러가 발생하였다
(숫자 1,000,000 는 리스트.size() 이다)

Java의 힙이 무엇인지 부터 JVM이 어떻게 동작하는지에 대한 궁금증이 생겼다

JVM

로컬 컴퓨터에서 자바 코드를 실행하면 어떤일이 발생하는지 간략하게 알아보면서 JVM에 대해 설명해보겠다

JVM ?

JVM을 얘기하기 전에 왜 JVM이 생겼는지부터 알아보았다

C/C++ 는 컴파일 플랫폼과 타겟 플랫폼이 다를 경우, 프로그램이 동작하지 않는다

플랫폼 : 운영체제 + CPU 아키텍처 / linux + arm64, window + intel core i7 등을 플랫폼이라 한다

이유는 OS마다 지원하는 시스템 콜 인터페이스가 다르고 CPU 아키텍처마다 지원하는 명령어 아키텍처가 다르기 때문이다

동일한 플랫폼에서 컴파일과 실행을 같이 한다면 문제가 없다

하지만 linux에서 컴파일한 실행파일을 window와 같이 다른 플랫폼에서 실행하면 동작하지 않는다

크로스 컴파일 - 타겟 플랫폼에 맞춰 컴파일

JVM이란 ?

Java Virtual Machine의 줄임말.

직역하면 '자바를 실행하기 위한 가상 기계(컴퓨터)'라고 할 수 있다.

Java 는 OS에 종속적이지 않다는 특징을 가지고 있다. OS에 종속받지 않고 실행되기 위해선 OS 위에서 Java 를 실행시킬 무언가가 필요하다. 그게 바로 JVM이다.

즉, OS에 종속받지 않고 CPU 가 Java를 인식, 실행할 수 있게 하는 가상 컴퓨터이다.

JVM은 근본적으로 문제를 해결함
자바 바이트 코드는 타겟 플랫폼에 상관없이 JVM 위에서 동작함
자바 소스코드가 javac 라는 컴파일러를 거치고 나면 자바 바이트코드가 됨
이 자바 바이트코드는 JVM이 설치된 플랫폼이라면 어디에서든 동작함

WORA
"Write Once, Run Anywhere - Sun microsystems"

"네가 짠 자바 코드를 컴파일 해서 배포하면, 어떤 플랫폼이든 다시 컴파일할 필요 없이 실행할 수 있다."

근데 실행하려면 그 플랫폼에 맞는 JVM이 설치되어 있어야 한다

자바의 야심

A.java b.java 자바 파일이 java compiler를 거처 a.class 파일이 됨 (a.class 파일은 바이트 코드)
위 클래스 파일을 네트워크를 통해 전달하면 웹 브라우저에 JVM이 설치되어 있어서 어디서든지 실행만 하면 되겠다

자바는 실패하였지만 이 방식이 자바 스크립트에서 정확히 동일하게 이대로 동작한다

자바 코드가 실행되기까지

컴파일러에도 프론트와 백엔드가 존재한다

웹에서는 백엔드는 크게 바뀌지 않고 프론트 엔드가 클라이언트 종류에 따라 바뀐다
컴파일러는 반대이다 컴파일러에서 프론트엔드는 바뀌지 않는다
왜냐하면 프론트엔드가 하는것이 우리가 작성한 소스코드를 분석해서 의미를 파악하는 것이다
하지만 백엔드는 바뀐다
자바 바이트코드를 어셈블리 언어로 바꾸는 과정에서
어셈블리 언어들은 운영체제나 기기에 의존적이다
따라서 백엔드에서는 window용, linux용, mac용이 있는 것이다

자바에서는 프론트에서는 javac(자바 컴파일러)가 해주고 백엔드는 JVM이 한다

하는 일을 분리한다

일을 분리함으로써 좋은 점은

Runtime에 어떤일이 벌어질지 컴파일을 하기 전에 모든 정보들을 다 알 수 없기때문에
Runtime에서만 발생하는 소중한 정보들이 있기 때문 이 정보들을 이용해서 최적화를 하는것이 JIT 컴파일러이다

JVM 내부구조

JVM의 내부 구조를 살펴보자

주목할 부분은 runtime area를 살펴봐야한다

이 중 스레드가 공유하는 부분은 method area, heap을 공유하며 Java stack, Program counter register, name method stack은 스레드별로 가진다

method area는 클래스 로더가 클래스 파일을 읽어보면 클래스 정보를 파싱해서 method area에 저장함

여기에는 변수, 메소드, 정적변수가 어떤것이 있는가 바이트 코드는 어떤가 와 같은 부분을 저장하는 것이 method area이다

heap은 프로그램을 실제로 실행하면서 생성한 모든 객체를 heap에 저장

스레드마다 존재하는 부분은
java stack, pc register, native method stacks이다

pc는 program counter이다

각 스레드는 항상 어떤 메서드를 실행하고 있다

그때 PC는 그 메서드 안에서 바이트코드 몇번째줄을 실행하고 있는지를 나타내는 역할

스택은 스레드별로 한개만 존재하고 스택 프레임은 메서드가 호출될 떄마다 생성된다
메서드 실행이 끝나면 스택 프레임은 pop이 되어 스택에서 제거된다

thread1을 예시로 들면 이 스택은 아래로 성장하는 것임
이때 최상단의 스택프레임이 메인 함수이고 그 아래는 메인함수에서 호출된 메서드이며 그 아래는 호출된 메서드가 다시 호출한 형태로 이어진다

스택프레임은 local variables array, operand stack, frame data를 갖는다

native method stack은 java bytecode가 아닌 다른 언어로 작성된 메서드를 의미한다


이렇게 자바 내부가 어떻게 동작하는지 알아보았다

그렇다면 나의 문제는 무엇이었지는지를 다시 살펴본다면

Java heap에서 메모리가 부족했다는 에러이다

자바는 JVM 위에서 동작한다
가상 공간이 있고 그곳에는 메모리도 할당된다
(아마 IDE가 할당해주는 것으로 알고 있다)
다시 말해, 나의 로컬 컴퓨터의 사용 가능한 모든 메모리가 필요할때마다 Java로 할당되는 것이 아니다. JVM에서 사용할 수 있는 메모리는 한정되어 있다

이는 위에서 살펴본 runtime data area의 heap 부분이 부족한 것 같다

왜냐하면 heap은 프로그램을 실행하면서 생성한 모든 객체 인스턴스를 Heap에 저장하기 때문에 1,000,000개의 Post 객체들을 만들때 JVM 메모리가 초과되었기 때문이다

이제 원인을 알았으니 해결해보자

해결

위 에러 사항을 gpt에게 물어보니 아래 5가지 해결방법을 알려주었다

  1. Heap 공간 증가 : JVM힙 공간을 늘려 메모리 할당량을 늘림
  2. 메모리 누수 확인
  3. 객체 크기 최적화 : 불필요한 객체 속성을 제거
  4. 데이터베이스 연결 관리 : 사용하지 않는 연결을 제대로 닫고 반환하는 코드 작성
  5. 대용량 데이터 처리 : bulk insert나 페이지네이션을 사용하여 묶음 단위로 데이터를 처리

위 요소중 JVM힙 공간을 늘리고, batch를 이용하니 쉽게 해결되었다

아래는 데이터 생성 테스트 코드이다.

@Test
    void bulkPostList() {
        List<Post> postList = new ArrayList<>();
        Faker faker = new Faker(new Locale("ko"));
        String input = "2022-01-01 11:22:33";
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        LocalDateTime createdDate = LocalDateTime.parse(input, formatter);

        for (int i=0; i < 1000000; i++) {
            Post post = new Post();
            post.setSubject("Poster : " + faker.name().fullName());
            post.setContent("Poster ip 주소 : " + faker.internet().ipV4Address());
            post.setLikes(255);
            post.setCreatedDate(createdDate);
            post.setModifiedDate(LocalDateTime.now());
            postList.add(post);
        }
        System.out.println("postList = " + postList.size());

        int batchSize = 1000; // 배치 단위 크기
        int listSize = postList.size();; // 전체 데이터 크기
        for (int i=0; i<listSize; i += batchSize) {
            int endIndex = i + batchSize; // 최소 index를 구한다 save를 할때 batch 단위만큼 subList로 저장하기 위해서
            List<Post> batchList = postList.subList(i, endIndex);
            jpaPostRepository.saveAll(batchList);
            jpaPostRepository.flush();
        }

    }

참고

0개의 댓글