재미있는 프로그램을 몇 개 만들어 보겠습니다. 제가 정의한 MObj는 아래와 같습니다.

 

 

 평범한 getter 메소드가 있습니다.

 

 

 그리고 더 평범한 equals 메소드가 있습니다.

 

 

 그리고 hashCode가 있는데요. 리턴값이 0부터 1999까지의 정수 중에서 Random하게 뽑은 값입니다. 딱 봐도 뭔가 이상하다는 것을 알 수 있습니다. 이 이상해 보이는 프로그램을 왜 예시로 들었는지는 천천히 설명해 보도록 하겠습니다.

 

 


 저번에 가비지는, 도달 불가능한 객체라고 하였습니다. 이것은 gc가 알아서 회수를 할 겁니다. 문제는, 필요 없는 객체인데, 참조 (흔히, Strong reference)가 되는 경우가 있을 수 있습니다. 이 경우에, 그 객체는 gc의 수거 대상이 되지 않습니다. 예를 하나 들어보겠습니다.

 

 

 L이 arrayList이고, Main 함수 내에 있는 지역 변수라고 해 봅시다. 현재, L의 size는 4라고 해 봅시다. 그러면, Obj1, 2, 3, 4 사실은 쓸모 있으니까 들고 있을 겁니다. 문제는, 여기서, 가장 뒤에 있는 원소를 제거한 경우를 생각해 봅시다.

 

 

 데이터를 미는 연산이 수행이 된 다음에, 505번째 줄이 수행되는데요. 이 줄이 없다면 어떻게 되는지부터 보겠습니다.

 

 

 뒤에 원소를 제거했다면, L의 size는 3이 됩니다. 그러면, Obj4는 쓸모 없는 객체가 되어 버릴 겁니다. 문제는 Root로 표시된 L에서 Obj4까지 접근 가능한 경로가 있다는 겁니다. 따라서 gc의 대상이 되지 않습니다. 그러면, 이런 관점에서 생각을 하면 어떨까요? 쓸모가 없어졌는데, 정작 gc의 대상이 되지 않는다.

 

 이제, 제 프로그램을 보도록 하겠습니다.

 

 


 hashCode 값을 Random하게 뽑아버렸습니다.

 

 

 그러면, 612번째 줄의 putVal을 호출할 겁니다. hash(key)가 어떻게 수행 되는지가 중요한데요.

 

 

 339번째 줄의 key.hashCode()를 호출합니다. key는 MObj였고, 이것은 hashCode를 오버라이딩 했으니, Random하게 해시값을 뽑는 그 알고리즘이 수행될 겁니다.

 

 

 그러면, 625번째에 넘어오는 hash값은 다를 겁니다. 630번째의 i는, (n-1) & hash로 계산이 된다는 것을 알 수 있는데요. n값이 같아도, hash가 다르게 나오면, (n-1)&hash가 다르게 나올 수도 있다는 것이 중요한 포인트입니다. 예를 들어서, n = 16이라고 해 봅시다. 그러면, n - 1은 15일 겁니다. hash값이 한 번은 7이 들어왔고, 한 번은 3이 들어왔다고 한다면, i값은 15 & 7 = 7, 2번째 i값은 15 & 3 = 3일 겁니다.

 

 

 이런 상황을 생각해 보겠습니다. "gahui"라는 이름을 가진 개의 정보에 대해서 계속 업데이트 해야 한다고 해 봅시다. 예를 들자면, gahui가 올 때 마다 나이를 업데이트 할 수 있을 겁니다. 그러면, hm.put 메서드를 이용해서 update를 할 수 있을 겁니다. equals랑 hashCode가 잘 구현되었다면, 그렇게 해도 큰 문제는 없습니다.

 

 문제는 key 역할을 하는 MObj의 hashCode 메서드의 리턴값이 Random 값이라는 겁니다. 그 값이 서로 다르게 나온다면, 같은 동물인 "gahui"라도 서로 다른 슬롯에 들어갈 수 있을 겁니다.

 

 

 만약에, 2라는 정보가 최근에 업데이트 된 정보라면, 1이라는 정보는 가진 주황색 객체는 필요 없는 정보입니다. 그런데도 Root로부터 참조되고 있습니다. 그렇기 때문에, gc의 정리 대상이 되지 않습니다. 이를 Memory leak 이라고 합니다. 실제로, hm의 size 값을 출력해 보면, 1이 아닌 더 큰 수가 출력이 됩니다.

 

 

 이는, for loop가 끝난 시점에, 1개를 제외한 나머지 1258개의 쓸모 없는 데이터가 hm 안에 있다는 것입니다.

 


 이를 해결하기 위해서, 쓸모 없는 데이터인, ["gahui", 1]을 제거하고, ["gahui", 2]를 추가할 수 있습니다.

 

 

 그런데, remove도, hashCode 값을 기반으로 동작합니다.

 

 

 1067번째 줄에 보면 hash(key)를 기반으로 넘긴다는 것을 알 수 있습니다. 동등한 key에 대해서 다른 hash 값이 나온다면, 엉뚱한 위치의 버킷을 찾을 수도 있습니다.

 

 

 예를 들자면, 0번 버킷에 있는데, 실제로는 3번 버킷에서 제거할려고 "gahui", 1을 찾는다던지. 그러면 한 번에 제거가 되지 않을 겁니다. 그렇다면, 제가 고친 프로그램의 결과도 1이 안 나올 수도 있다는 것을 알 수 있습니다.

 

 

 1259가 나왔습니다. 어딘가에 메모리 릭이 있다는 이야기입니다. 이는, 쓸모 없는 정보도 버킷의 어딘가에 들어갔기 때문입니다. 이를 해결하려면, while loop를 돌려서, 해당 K, V가 제거가 될 때 까지 remove를 돌리면 됩니다.

 

 

 그러면, 제거가 되기는 될 겁니다. 문제는, while loop를 돌리는 경우에, 평균적으로 2000여개의 버킷을 뒤져야 하는 게 문제입니다. 이는, 효율성과 연결이 되는 부분입니다. Key가 적절히 쌓였을 때, 랜덤을 돌려가면서 처리하는 방법을 생각할 수도 있습니다. 메모리 릭을 없애기 위해서. 그런데 확실한 것은, 해시코드를 제대로 정의해 주기만 하면 되는데, 제대로 정의를 안 해서 복잡한 트릭을 써야 한다는 것이죠. 해시코드를 제대로 재정의 하지 않은 것의 패널티가 생각보다 큽니다. 이는 좋지 못합니다.

 

 


 이제 문제를 바꿔 봅시다. equals를 구현하지 않고, hashCode만 구현했다면 어떻게 될까요?

 

 

  equals만 빠졌습니다. Main의 코드는 아래와 같습니다.

 

 

 결과는 어떻게 나올까요? temp의 equals가 오버라이딩 되지 않았다면, 객체 두 개가 같은지를 비교합니다. 24번째 줄의 temp를 계속 넣기 때문에, equals는 같습니다. 그리고, hashCode 값은 "gahui"의 hashCode 값으로 비교하기 때문에 같습니다.

 

 

 따라서 635번째 줄에 걸리게 되고, 637번째 else if문이나, 639번째 else 문을 수행하지 않습니다. 따라서, 1이 출력될 겁니다.

 

 

 문제를 바꿔 봅시다. 위 코드는 어떻게 수행이 될까요? new MObj("gahui")를 loop가 돌 때 마다 수행하고 있습니다.

 

 

 그러면 다른 객체 여러개가 Key값으로 생성이 될 겁니다. 그러면, 오래된 객체였고, value 값이 2인 것이 있고, 새로운 객체였고, 값이 5인 객체가 있었다고 해 봅시다. 그러면 우리에게 필요한 정보는 "gahui"의 value가 5라는 것이지, "gahui"의 값이 2라는 정보가 아닙니다.

 

 

 그러므로, 637번째 줄이나 639번째 줄을 타게 될 겁니다. 대신 hash 값은 같기 때문에 같은 bucket에 넣어질 겁니다.

 

 

 그런데, 필요 없어진 <K, V>쌍인 ["gahui", 2]는 root인 hm으로부터 도달할 수 있는 객체입니다. 따라서 gc의 정리 대상이 되지 않습니다. memory leak이 발생했습니다. 트로부터 도달할 수 있지만, 쓸모 없어진 객체 라는 키워드는 이 포스트에서 제일 중요합니다. 기술 블로그에서도 Out Of Memory 예외와 힙 덤프와 함께 많이 언급되는 키워드이니 알고 넘어가시는 것도 나쁘지 않을 듯 싶습니다. 추후에 제가 또 이 키워드를 언급할 수도 있고요.