발단

오랜만에 네이버 카페 남궁성의 코드 초보 스터디에 갔는데, 재미난 질문이 있어서 정리를 해 본다.

게시물 및 책 내용

책 내용

  • 책의 일부
    • append를 연결해서 사용하면 메서드 체인이 발생해 메모리 누수가 발생할 가능성이 있다.
  • 위 내용에 대한 설명
    • 모든 메서드가 하나의 체인으로 연결되며 이 메서드에 사용된 모든 인자도 연결되므로 비효율적인 메모리 점유가 발생
    • 하지만 메소드 체인으로 서로 연결된 메서드는 결국 연결된 모든 메소드의 스택이 종료되기 전까지 점유한 메모리를 반환하지 않으며, 메서드에 전달된 인자 또한 모든 메서드가 종료되기 전까지 메모리 상에 유지된다.
    • 나머지 연관된 메서드와 인자가 모두 생존하면서 메모리를 점유할 가능성이 높다.

논란?

  • 우리가 일반적으로 아는 지식을 기반으로 하면, append 메소드가 호출되고, return 되면, append 메소드 안에서 사용한 모든 메모리를 해제 된다.
  • append 메소드 내에서 사용된 Stack 영역의 메모리는 당연히 해제되고, append 메소드 내에서 사용된 heap 영역의 메모리 역시 return 값으로 할당되고 해제 된다.
  • 그런데 왜 책에서는 저렇게 어려운 말을 적었을까? 라는 궁금함이 생겼다.

진짜 다를까? 진짜 확인해 보자.

  • 정확한것을 확인해 보려면, bytecode를 까 보면 되지 않을까? 라는 생각이 들어 실제로 bytecode를 비교해 보기로 했다.

메소드 체인 형태의 bytecode 확인

  • 코드

    Object o1 = new Object();
    Object o2 = new Object();
    // ....
    sb.append("A").append("B").append(o1.toString()).append(o2.toString())... 
  • bytecode

  • bytecode를 분석해 보자.

    1. local variable로 되어 있는 o1 aload로 "메모리의 stack"영역으로 load
    2. invokevirtual로 toString() 메소드를 호출
    3. 그리고 다시 o2에 대해서 위의 1과 2를 반복
    4. 그리고 o3 에 대해서 위의 1과 2를 반복
    5. 그리고 제일 마지막에 pop 를 단 한번만 호출
  • 즉, pop을 딱 한번만 호출

일일이 호출한 경우 bytecode 확인

  • 코드

    Object o1 = new Object();
    Object o2 = new Object();
    // ....
    sb.append("A").append("B")
    sb.append(o1.toString());
    sb.append(o2.toString());
  • bytecode

  • bytecode를 분석해 보자.

    1. aload_1
    2. aload_2
    3. invokevirtual
    4. 로 메모리를 stack에 데이터를 올린뒤, append()가 끝나면 pop으로 데이터 날림
    5. 위 1, 2, 3, 4를 반복
  • 즉, append() 명령이 종료된 시점에 매번 pop을 호출

결론

  • 위 두가지 경우로만 확인해 본다면, 결과적으로 bytecode 상으로는 확연히 다르게 동작한다는것을 알 수 있다.
  • 문제는 메모리의 Stack 영역에 n개의 데이터가 올려져 있다는 것이다.

메소드 체인 형태의 호출에서 비효율적인 메모리 점유가 발생할 수 있나?

  • 맞다. 비효율적인 메모리 점유가 발생할 수 있다.
  • 필요 없는 variable을 stack에 올려 두는것은 비효율적인 메모리 점유가 발생할 수 있다.
  • 우리가 일반적으로 알기에는 method내에 정의된 variable들만 stack 영역에 들어 가는것으로 알고 있다.
  • 하지만, method내에서 호출된 또 다른 메소드의 return값(invokevirtual)도 stack 영역에 보관해야 한다. 그래야 호출한 method에서 그 값을 사용할 수 있을 것이기 때문이다.
  • 그렇기 때문에, 메소드 체인 형태의 호출 방법에서는 pop을 단 한번만 호출하기 때문에, 메소드 체인이 끝나기 전까지, return 값들의 reference count가 여전히 1일 것이다.
  • reference count 가 1이기 때문에 당연히 GC를 할 수 없는 상태가 될 것이므로,
  • 비효율적인 메모리 점유가 발생할 수 있다.

메소드 체인 형태의 호출에서 누수가 발생할 수 있나?

  • 정확하게 말하면 아니다. 누수란 회수 하지 못한 메모리라고 생각한다.
  • 하지만, bytecode에서 발견할 수 있다시피, 해당 Line이 종료되면 pop되므로 메모리는 반드시 회수된다.
  • 누수가 발생할 수 있냐는 질문에는, 아니라고 말하는게 맞을 듯 하다.

메소드 체인 형태의 호출은 성능상 문제가 발생 할 수 있나?

  • 발생할 수도 있다.
  • 코드의 처리 속도
    • 메소드 체인 형태의 호출은 bytecode가 훨씬 더 간결하다.
    • 그렇기 때문에 메소드 체인 형태의 호출이 속도면에서는 훨씬 빠를 것이다.
  • 하지만, GC가 문제가 될 수 있다.
    • pop을 매 함수마다 호출하나, 최종에 한번만 호출하나 어차피 할당되어야 할 객체와 풀여야할 객체다.
    • 하지만, pop이 제일 마지막에 호출 된다면
    • 그 전까지 할당된 메모리를 풀지 않기 때문에
    • 메모리가 부족할 확률이 조금 더 높아진다.
    • 메모리가 부족하면, GC가 동작하게 된다
    • 이로 인해서 성능상 문제가 발생할 수 있다.

메소드 체인 형태의 호출은 "반드시" 성능상 문제가 발생할 수 있나?

  • 아닐 수 있다.
  • 성능에 문제가 발생할 수는 있지만, 반드시 성능에 영향을 미친다고 볼 수 없다.
  • 위의 bytecode만을 본다면, 반드시 성능에 영향을 미친다고 볼 수 있다.
  • 문제는 컴파일러다.
    • JVM은 bytecode를 검토/실행하는 스펙이다.
    • bytecode를 생성하는것은 컴파일러의 몫이다.
    • 컴파일러의 구현은 스펙이 없다.
    • 그러므로 모든 컴파일러가 같은 bytecode를 생성한다는 보장이 없다.
    • 컴파일러의 구현에 따라 어떠한 bytecode가 나올지 알 수 없으므로
    • 반드시 성능 문제가 발생한다고 볼 수는 없다.

너무 오랜만에 Java쪽 자료를 찾아봐서.. 정확한 결론인지는 모르겠다. 하지만 오랜만에 이런거 찾아보고 정리하니 재미있었다. ㅋ.

혹시나 잘못 된 정보가 있다면, 언제든지 지적 부탁드립니다. ^_^

+ Recent posts