공부/컴퓨터
[Java] 자바코딩, 이럴 땐 이렇게 - 메소드 체인 형태의 호출에서의 bytecode동작과 메모리 반환 관계
찬
2019. 1. 8. 23:05
반응형
발단
오랜만에 네이버 카페 남궁성의 코드 초보 스터디에 갔는데, 재미난 질문이 있어서 정리를 해 본다.
게시물 및 책 내용
- 카페 게시물 링크 : Stringbuffer 클래스의 append메소드 관련
- 책이름 : 자바코딩, 이럴 땐 이렇게 - PMD로 배우는 올바른 자바코딩 방법
책 내용
- 책의 일부
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를 분석해 보자.
- local variable로 되어 있는 o1 aload로 "메모리의 stack"영역으로 load
- invokevirtual로 toString() 메소드를 호출
- 그리고 다시 o2에 대해서 위의 1과 2를 반복
- 그리고 o3 에 대해서 위의 1과 2를 반복
- 그리고 제일 마지막에 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를 분석해 보자.
- aload_1
- aload_2
- invokevirtual
- 로 메모리를 stack에 데이터를 올린뒤, append()가 끝나면 pop으로 데이터 날림
- 위 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쪽 자료를 찾아봐서.. 정확한 결론인지는 모르겠다. 하지만 오랜만에 이런거 찾아보고 정리하니 재미있었다. ㅋ.
혹시나 잘못 된 정보가 있다면, 언제든지 지적 부탁드립니다. ^_^
반응형