출처 : http://d2.naver.com/helloworld/329631

Java의 가비지 컬렉터(Garbage Collector)는 그 동작 방식에 따라 매우 다양한 종류가 있지만 공통적으로 크게 다음 2가지 작업을 수행한다고 볼 수 있습니다.

  1. 힙(heap) 내의 객체 중에서 가비지(garbage)를 찾아낸다.
  2. 찾아낸 가비지를 처리해서 힙의 메모리를 회수한다.

최초의 Java에서는 이들 가비지 컬렉션(Garbage Collection, 이하 GC) 작업에 애플리케이션의 사용자 코드가 관여하지 않도록 구현되어 있었습니다. 그러나 위 2가지 작업에서 좀 더 다양한 방법으로 객체를 처리하려는 요구가 있었습니다. 이에 따라 JDK 1.2부터는 java.lang.ref 패키지를 추가해 제한적이나마 사용자 코드와 GC가 상호작용할 수 있게 하고 있습니다.

java.lang.ref 패키지는 전형적인 객체 참조인 strong reference 외에도 soft, weak, phantom 3가지의 새로운 참조 방식을 각각의 Reference 클래스로 제공합니다. 이 3가지 Reference 클래스를 애플리케이션에 사용하면 앞서 설명하였듯이 GC에 일정 부분 관여할 수 있고, LRU(Least Recently Used) 캐시 같이 특별한 작업을 하는 애플리케이션을 더 쉽게 작성할 수 있습니다. 이를 위해서는 GC에 대해서도 잘 이해해야 할 뿐 아니라, 이들 참조 방식의 동작도 잘 이해할 필요가 있습니다.

GC와 Reachability

Java GC는 객체가 가비지인지 판별하기 위해서 reachability라는 개념을 사용한다. 어떤 객체에 유효한 참조가 있으면 'reachable'로, 없으면 'unreachable'로 구별하고, unreachable 객체를 가비지로 간주해 GC를 수행한다. 한 객체는 여러 다른 객체를 참조하고, 참조된 다른 객체들도 마찬가지로 또 다른 객체들을 참조할 수 있으므로 객체들은 참조 사슬을 이룬다. 이런 상황에서 유효한 참조 여부를 파악하려면 항상 유효한 최초의 참조가 있어야 하는데 이를 객체 참조의 root set이라고 한다.

JVM에서 메모리 영역인 런타임 데이터 영역(runtime data area)의 구조를 그림으로 그리면 다음과 같다.

javareference1

그림 1 런타임 데이터 영역(Oracle HotSpot VM 기준)

런타임 데이터 영역은 위와 같이 스레드가 차지하는 영역들과, 객체를 생성 및 보관하는 하나의 큰 힙, 클래스 정보가 차지하는 영역인 메서드 영역, 크게 세 부분으로 나눌 수 있다. 위 그림에서 객체에 대한 참조는 화살표로 표시되어 있다.

힙에 있는 객체들에 대한 참조는 다음 4가지 종류 중 하나이다.

  • 힙 내의 다른 객체에 의한 참조
  • Java 스택, 즉 Java 메서드 실행 시에 사용하는 지역 변수와 파라미터들에 의한 참조
  • 네이티브 스택, 즉 JNI(Java Native Interface)에 의해 생성된 객체에 대한 참조
  • 메서드 영역의 정적 변수에 의한 참조

이들 중 힙 내의 다른 객체에 의한 참조를 제외한 나머지 3개가 root set으로, reachability를 판가름하는 기준이 된다.

reachability를 더 자세히 설명하기 위해 root set과 힙 내의 객체를 중심으로 다시 그리면 다음과 같다.

javareference2

그림 2 Reachable 객체와 Unreachable 객체

위 그림에서 보듯, root set으로부터 시작한 참조 사슬에 속한 객체들은 reachable 객체이고, 이 참조 사슬과 무관한 객체들이 unreachable 객체로 GC 대상이다. 오른쪽 아래 객체처럼 reachable 객체를 참조하더라도, 다른 reachable 객체가 이 객체를 참조하지 않는다면 이 객체는 unreachable 객체이다.

이 그림에서 참조는 모두 java.lang.ref 패키지를 사용하지 않은 일반적인 참조이며, 이를 흔히 strong reference라 부른다.

Soft, Weak, Phantom Reference

java.lang.ref는 soft reference와 weak reference, phantom reference를 클래스 형태로 제공한다. 예를 들면, java.lang.ref.WeakReference 클래스는 참조 대상인 객체를 캡슐화(encapsulate)한 WeakReference 객체를 생성한다. 이렇게 생성된 WeakReference 객체는 다른 객체와 달리 Java GC가 특별하게 취급한다(이에 대한 내용은 뒤에서 다룬다). 캡슐화된 내부 객체는 weak reference에 의해 참조된다.

다음은 WeakReference 클래스가 객체를 생성하는 예이다.

WeakReference<Sample> wr = new WeakReference<Sample>( new Sample());  
Sample ex = wr.get();  
...
ex = null;  

위 코드의 첫 번째 줄에서 생성한 WeakReference 클래스의 객체는 new() 메서드로 생성된 Sample 객체를 캡슐화한 객체이다. 참조된 Sample 객체는 두 번째 줄에서 get() 메서드를 통해 다른 참조에 대입된다. 이 시점에서는 WeakReference 객체 내의 참조와 ex 참조, 두 개의 참조가 처음 생성한 Sample 객체를 가리킨다.

javareference3

그림 3 Weak Reference 예 1

위 코드의 마지막 줄에서 ex 참조에 null을 대입하면 처음 생성한 Sample 객체는 오직 WeakReference 내부에서만 참조된다. 이 상태의 객체를 weakly reachable 객체라고 하는데, 이에 대한 자세한 내용은 뒤에서 다룬다.

javareference4

그림 4 Weak Reference 예 2

Java 스펙에서는 SoftReference, WeakReference, PhantomReference 3가지 클래스에 의해 생성된 객체를 "reference object"라고 부른다. 이는 흔히 strong reference로 표현되는 일반적인 참조나 다른 클래스의 객체와는 달리 3가지 Reference 클래스의 객체에 대해서만 사용하는 용어이다. 또한 이들 reference object에 의해 참조된 객체는 "referent"라고 부른다. Java 스펙 문서를 참조할 때 이들 용어를 명확히 알면 좀 더 이해하기 쉽다. 위의 소스 코드에서 new WeakReference() 생성자로 생성된 객체는 reference object이고, new Sample() 생성자로 생성된 객체는 referent이다.

Reference와 Reachability

앞에서 설명한 것처럼, 원래 GC 대상 여부는 reachable인가 unreachable인가로만 구분하였고 이를 사용자 코드에서는 관여할 수 없었다. 그러나 java.lang.ref 패키지를 이용하여 reachable 객체들을 strongly reachable, softly reachable, weakly reachable, phantomly reachable로 더 자세히 구별하여 GC 때의 동작을 다르게 지정할 수 있게 되었다. 다시 말해, GC 대상 여부를 판별하는 부분에 사용자 코드가 개입할 수 있게 되었다.

두 번째 그림에서 몇몇 객체들을 WeakReference로 바꾸어서 예를 들어보면 다음과 같다.

javareference5

그림 5 Reachable, Unreachable, Weakly Reachable 예제

녹색으로 표시한 중간의 두 객체는 WeakReference로만 참조된 weakly reachable 객체이고, 파란색 객체는 strongly reachable 객체이다. GC가 동작할 때, unreachable 객체뿐만 아니라 weakly reachable 객체도 가비지 객체로 간주되어 메모리에서 회수된다. root set으로부터 시작된 참조 사슬에 포함되어 있음에도 불구하고 GC가 동작할 때 회수되므로, 참조는 가능하지만 반드시 항상 유효할 필요는 없는 LRU 캐시와 같은 임시 객체들을 저장하는 구조를 쉽게 만들 수 있다.

위 그림에서 WeakReference 객체 자체는 weakly reachable 객체가 아니라 strongly reachable 객체이다. 또한, 그림에서 A로 표시한 객체와 같이 WeakReference에 의해 참조되고 있으면서 동시에 root set에서 시작한 참조 사슬에 포함되어 있는 경우에는 weakly reachable 객체가 아니라 strongly reachable 객체이다.

GC가 동작하여 어떤 객체를 weakly reachable 객체로 판명하면, GC는 WeakReference 객체에 있는 weakly reachable 객체에 대한 참조를 null로 설정한다. 이에 따라 weakly reachable 객체는 unreachable 객체와 마찬가지 상태가 되고, 가비지로 판명된 다른 객체들과 함께 메모리 회수 대상이 된다.

Strengths of Reachability

앞에서 설명한 것처럼 reachability는 총 5종류가 있고 이는 GC가 객체를 처리하는 기준이 된다. Java 스펙에서는 이들 5종류의 reachability를 "Strengths of Reachability"라 부른다. 앞의 예제 그림에서는 weakly reachable만 예를 들었기 때문에 WeakReference만 표시하였으나, SoftReference, PhantomReference 등을 이용하여 여러 가지 방식으로 reachability를 지정할 수 있고 이에 따라 각 객체들의 GC 여부는 다양하게 달라지게 된다. 하나의 객체에 대한 참조의 개수나 참조 형태에는 아무런 제한이 없으므로, 하나의 객체는 여러 strong reference, soft reference, weak reference, phantom reference의 다양한 조합으로 참조될 수 있다.

Java GC는 root set으로부터 시작해서 객체에 대한 모든 경로를 탐색하고 그 경로에 있는 reference object들을 조사하여 그 객체에 대한 reachability를 결정한다. 다양한 참조 관계의 결과, 하나의 객체는 다음 5가지 reachability 중 하나가 될 수 있다.

  • strongly reachable: root set으로부터 시작해서 어떤 reference object도 중간에 끼지 않은 상태로 참조 가능한 객체, 다시 말해, 객체까지 도달하는 여러 참조 사슬 중 reference object가 없는 사슬이 하나라도 있는 객체
  • softly reachable: strongly reachable 객체가 아닌 객체 중에서 weak reference, phantom reference 없이 soft reference만 통과하는 참조 사슬이 하나라도 있는 객체
  • weakly reachable: strongly reachable 객체도 softly reachable 객체도 아닌 객체 중에서, phantom reference 없이 weak reference만 통과하는 참조 사슬이 하나라도 있는 객체
  • phantomly reachable: strongly reachable 객체, softly reachable 객체, weakly reachable 객체 모두 해당되지 않는 객체. 이 객체는 파이널라이즈(finalize)되었지만 아직 메모리가 회수되지 않은 상태이다.
  • unreachable: root set으로부터 시작되는 참조 사슬로 참조되지 않는 객체

다음 예의 경우 객체 B의 reachability는 softly reachable이다.

javareference6

그림 6 Softly Reachable

root set으로부터 바로 SoftReference를 통해서 B를 참조할 수 있기 때문이다. 만약 root set의 SoftReference에 대한 참조가 없다면(즉, 왼쪽 아래 화살표를 삭제한다면), 객체 B는 phantomly reachable이 된다.

Softly Reachable과 SoftReference

softly reachable 객체, 즉 strong reachable이 아니면서 오직 SoftReferencce 객체로만 참조된 객체는 힙에 남아 있는 메모리의 크기와 해당 객체의 사용 빈도에 따라 GC 여부가 결정된다. 그래서 softly reachable 객체는 weakly reachable 객체와는 달리 GC가 동작할 때마다 회수되지 않으며 자주 사용될수록 더 오래 살아남게 된다. Oracle HotSpot VM에서는 softly reachable 객체의 GC를 조절하기 위해 다음 JVM 옵션을 제공한다.

-XX:SoftRefLRUPolicyMSPerMB=<N>

이 옵션의 기본값은 1000이다.

softly reachable 객체의 GC 여부는 위 옵션의 에 설정한 숫자에 따라 다음 수식에 의해 결정된다.

(마지막 strong reference가 GC된 때로부터 지금까지의 시간) > (옵션 설정값 N) * (힙에 남아있는 메모리 크기)

어떤 객체가 사용된다는 것은 strong reference에 의해 참조되는 것이므로 위 수식의 좌변은 해당 객체가 얼마나 자주 사용되는지를 의미한다. 옵션 설정값이 1000이고 남아 있는 메모리가 100MB이면, 수식의 우변은 1,000ms/MB * 100MB = 100,000ms = 100sec, 즉 100초가 된다(옵션 이름 마지막이 MSPerMB로 끝나므로 옵션 설정값의 단위는 ms/MB임을 알 수 있다). 따라서 softly reachable 객체가 100초 이상 사용되지 않으면 GC에 의해 회수 대상이 된다. 힙에 남아있는 메모리가 작을수록 우변의 값이 작아지므로, 힙이 거의 소진되면 대부분의 softly reachable 객체는 모두 메모리에서 회수되어 OutOfMemoryError를 막게 될 것이다.

softly reachable 객체를 GC하기로 결정되면 앞서 설명한 WeakReference 경우와 마찬가지로 참조 사슬에 존재하는 SoftReference 객체 내의 softly reachable 객체에 대한 참조가 null로 설정되며, 이후 이 softly reachable객체는 unreachable 객체와 마찬가지가 되어 GC의해 메모리가 회수된다.

Weakly Reachable과 WeakReference

weakly reachable 객체는 특별한 정책에 의해 GC 여부가 결정되는 softly reachable 객체와는 달리 GC를 수행할 때마다 회수 대상이 된다. 앞서 설명한 것처럼 WeakReference 내의 참조가 null로 설정되고 weakly reachable 객체는 unreachable 객체와 마찬가지 상태가 되어 GC에 의해 메모리가 회수된다. 그러나 GC가 실제로 언제 객체를 회수할지는 GC 알고리즘에 따라 모두 다르므로, GC가 수행될 때마다 반드시 메모리까지 회수된다고 보장하지는 않는다. 이는 softly reachable 객체는 물론 unreachable 객체도 마찬가지이다. GC가 GC 대상인 객체를 찾는 작업과 GC 대상인 객체를 처리하여 메모리를 회수하는 작업은 즉각적인 연속 작업이 아니며, GC 대상 객체의 메모리를 한 번에 모두 회수하지도 않는다.

LRU 캐시와 같은 애플리케이션에서는 softly reachable 객체보다는 weakly reachable 객체가 유리하므로 LRU 캐시를 구현할 때에는 대체로 WeakReference를 사용한다. softly reachable 객체는 힙에 남아 있는 메모리가 많을수록 회수 가능성이 낮기 때문에, 다른 비즈니스 로직 객체들을 위해 어느 정도 비워두어야 할 힙 공간이 softly reachable 객체에 의해 일정 부분 점유된다. 따라서 전체 메모리 사용량이 높아지고 GC가 더 자주 일어나며 GC에 걸리는 시간도 상대적으로 길어지는 문제가 있다.

ReferenceQueue

phantomly reachable 객체의 동작과 PhantomReference를 설명하기 전에 java.lang.ref 패키지에서 제공하는 ReferenceQueue 클래스에 대해 설명할 필요가 있다.

SoftReference 객체나 WeakReference 객체가 참조하는 객체가 GC 대상이 되면 SoftReference 객체, WeakReference 객체 내의 참조는 null로 설정되고 SoftReference 객체, WeakReference 객체 자체는 ReferenceQueue에 enqueue된다. ReferenceQueue에 enqueue하는 작업은 GC에 의해 자동으로 수행된다. ReferenceQueue의 poll() 메서드나 remove() 메서드를 이용해 ReferenceQueue에 이들 reference object가 enqueue되었는지 확인하면 softly reachable 객체나 weakly reachable 객체가 GC되었는지를 파악할 수 있고, 이에 따라 관련된 리소스나 객체에 대한 후처리 작업을 할 수 있다. 어떤 객체가 더 이상 필요 없게 되었을 때 관련된 후처리를 해야 하는 애플리케이션에서 이 ReferenceQueue를 유용하게 사용할 수 있다. Java Collections 클래스 중에서 간단한 캐시를 구현하는 용도로 자주 사용되는 WeakHashMap 클래스는 이 ReferenceQueue와 WeakReference를 사용하여 구현되어 있다.

SoftReference와 WeakReference는 ReferenceQueue를 사용할 수도 있고 사용하지 않을 수도 있다. 이는 이들 클래스의 생성자 중에서 ReferenceQueue를 인자로 받는 생성자를 사용하느냐 아니냐로 결정한다. 그러나 PhantomReference는 반드시 ReferenceQueue를 사용해야만 한다. PhantomReference의 생성자는 단 하나이며 항상 ReferenceQueue를 인자로 받는다.

ReferenceQueue<Object> rq = new ReferenceQueue<Object>(); PhantomReference<Object> pr = new PhantomReference<Object>(referent, rq);  

SoftReference, WeakReference는 객체 내부의 참조가 null로 설정된 이후에 ReferenceQueue에 enqueue되지만, PhantomReference는 객체 내부의 참조를 null로 설정하지 않고 참조된 객체를 phantomly reachable 객체로 만든 이후에 ReferenceQueue에 enqueue된다. 이를 통해 애플리케이션은 객체의 파이널라이즈 이후에 필요한 작업들을 처리할 수 있게 된다. 더 자세한 내용은 다음 절에서 설명한다.

Phantomly Reachable과 PhantomReference

softly reachable과 weakly reachable, phantomly reachable은 많이 다르다. 이를 설명하기 위해서는 먼저 GC 동작을 설명해야 한다. GC 대상 객체를 찾는 작업과 GC 대상 객체를 처리하는 작업이 연속적이지 않 듯이, GC 대상 객체를 처리하는 작업과 할당된 메모리를 회수하는 작업도 연속된 작업이 아니다. GC 대상 객체를 처리하는 작업, 즉 객체의 파이널라이즈 작업이 이루어진 후에 GC 알고리즘에 따라 할당된 메모리를 회수한다.

GC 대상 여부를 결정하는 부분에 관여하는 softly reachable, weakly reachable과는 달리, phantomly reachable은 파이널라이즈와 메모리 회수 사이에 관여한다. strongly reachable, softly reachable, weakly reachable에 해당하지 않고 PhantomReference로만 참조되는 객체는 먼저 파이널라이즈된 이후에 phantomly reachable로 간주된다. 다시 말해, 객체에 대한 참조가 PhantomReference만 남게 되면 해당 객체는 바로 파이널라이즈된다. GC가 객체를 처리하는 순서는 항상 다음과 같다.

  1. soft references
  2. weak references
  3. 파이널라이즈
  4. phantom references
  5. 메모리 회수

즉, 어떤 객체에 대해 GC 여부를 판별하는 작업은 이 객체의 reachability를 strongly, softly, weakly 순서로 먼저 판별하고, 모두 아니면 phantomly reachable 여부를 판별하기 전에 파이널라이즈를 진행한다. 그리고 대상 객체를 참조하는 PhantomReference가 있다면 phantomly reachable로 간주하여 PhantomReference를 ReferenceQueue에 넣고 파이널라이즈 이후 작업을 애플리케이션이 수행하게 하고 메모리 회수는 지연시킨다.

앞서 설명한 것처럼 PhatomReference는 항상 ReferenceQueue를 필요로 한다. 그리고 PhantomReference의 get() 메서드는 SoftReference, WeakReference와 달리 항상 null을 반환한다. 따라서 한 번 phantomly reachable로 판명된 객체는 더 이상 사용될 수 없게 된다. 그리고 phantomly reachable로 판명된 객체에 대한 참조를 GC가 자동으로 null로 설정하지 않으므로, 후처리 작업 후에 사용자 코드에서 명시적으로 clear() 메서드를 실행하여 null로 설정해야 메모리 회수가 진행된다.

이와 같이, PhantomReference를 사용하면 어떤 객체가 파이널라이즈된 이후에 할당된 메모리가 회수되는 시점에 사용자 코드가 관여할 수 있게 된다. 파이널라이즈 이후에 처리해야 하는 리소스 정리 등의 작업이 있다면 유용하게 사용할 수 있다. 그러나 개인적으로는 PhantomReference를 사용하는 코드를 거의 본 적이 없으며, 그 효용성에 대해서는 의문이 있다.

마치며

Java의 Reference는 그 선후 관계와 용어가 복잡해서 글로 쉽게 풀어쓰기가 어려워 본문이 꽤 장황해졌다. 본문의 내용을 간단히 요약하면 다음과 같다.

  • Java GC는 GC 대상 객체를 찾고, 대상 객체를 처리(finalization)하고, 할당된 메모리를 회수하는 작업으로 구성된다.
  • 애플리케이션은 사용자 코드에서 객체의 reachability를 조절하여 Java GC에 일부 관여할 수 있다.
  • 객체의 reachability를 조절하기 위해서 java.lang.ref 패키지의 SoftReference, WeakReference, PhantomReference, ReferenceQueue 등을 사용한다.

개인적으로는 내부 캐시 등을 구현하고자 하는 대부분의 애플리케이션에서는 WeakReference 혹은 이를 이용한 WeakHashMap만으로도 충분하다고 생각한다. 다른 애플리케이션에서는 가끔 SoftReference를 사용하는 경우도 있지만, PhantomReference는 거의 예제가 없으며 그만큼 불필요할 것이다. 이들 Java Reference들과 관련된 GC 동작을 잘 이해하면 Java의 heap 메모리 문제에서 더욱 유연한 애플리케이션 작성에 크게 도움이 될 것이다.

'Programing > Java' 카테고리의 다른 글

Junit 사용하기  (0) 2016.03.03
Effective Java - 객체 생성  (0) 2016.03.03
Java Reference와 GC  (0) 2016.02.29
Effective Java - Reference  (0) 2016.02.29
Garbage Collection  (0) 2015.11.15
Wrapper클래스,박싱(boxing),언박싱(unboxing)  (0) 2015.11.15
  • JAVA의 객체가 garbage collection 대상이 되는 순간은?
    1. reference가 영원히 영역을 벗어남 (ex. 지역 변수로 선언한 객체의 메소드의 영역을 벗어남)
    2. reference에 다른 객체를 대입
    3. reference를 직접 'null'로 설정
    but, 위 방법 외에 참조를 약하게 만들어 GC 대상이 되도록 만들 수 있다.

Strong Reference > Soft Reference > Weak Reference > Phantom Reference

  • Strong Reference
    : 일반적으로 new를 통해서 객체를 생성하게 되면 생기게 되는 참조.
    : 참조에 대해서 해지하지 않는다면 그 메모리는 절대 수거되지 않는다.

  • Soft Reference
    : GC에 의해 수거될 수도 있고, 수거되지 않을 수도 있다.
    : 메모리 상태에 따라 결정

  • Weak Reference
    : GC가 발생하기 전까지는 참조를 유지한다.
    : GC가 발생하는 순간 무조건 수거된다.
    : 객체 캐시에 유용 (안드로이드에서 그림이 포함된 listView 등)

  • Phantom Reference
    : 가장 약한 참조
    : GC가 발생 전 메모리에서 정리된다. (finalize() 호출 후)
    : 내부적으로 유지하고 있지만, 객체를 다시 꺼내오면 null 이다 

    - ReferenceTest.java
    package kr.co.ioacademy; //iocademy 윤찬식 강사님
    
    import java.lang.ref.SoftReference;
    import java.lang.ref.WeakReference;
    import java.util.LinkedList;
    import java.util.List;
    
    // 강한 참조(Strong Reference)
    // 일반적으로 new를 통해서 객체를 생성하게 되면 생기게 되는 참조.
    
    // 강한 참조를 통해 참조되고 있는 객체는 절대 가비지 컬렉션의 대상에서
    // 제외된다.
    // 자바에서 아무리 자동적으로 메모리가 수거된다고 하지만, 
    // 참조에 대해서 해지하지 않는 다면 그 메모리는 절대 수거되지 않는다.
    //  (참조 = null , 참조 = 다른객체)
    
    // OutOfMemory를 방지하기 위해서는 약한 형태의 참조를 사용해야 한다.
    //  SoftReference<>, WeakReference<>
    
    // Soft Reference
    // 생성 방법 : SoftReference<Data> r
                // = new SoftReference<Data>(new Data);
    // 동작 : 강한 참조와 다르게 GC에 의해 수거될 수도 있고, 수거되지 않을 수도 있다.
    //  메모리에 충분한 여유가 있다면 GC가 수행되고 있다고 하더라도 수거되지 않는다.
    //  하지만 out of memory의 시점이 가깝다면, 수거될 확률이 높다.
    
    // Weak Reference
    //생성 방법 : WeakReference<Data> r
    // = new WeakReference<Data>(new Data);
    
    // WeakReference에 의해 참조된 객체는 가비지 컬렉션이 발생하기 전까지는 참조를 유지
    // 하지만 GC가 발생하는 순간 무조건 수거된다.
    // WeakReference가 사라지는 시점이 GC의 실행 주기와 일치한다.
    // 이를 이용하면 짧은 주기에 자주 사용되는 객체를 캐시할 때 유용하다.
    // => WeakHashMap
    // WeakHashMap<K,V>
    
    // PhantomReference
    
    class BigData {
    	private int[] array = new int[5000]; // 20000byte, 20K
    }
    
    public class ReferenceTest {
    
    	private List<WeakReference<BigData>> refs = new LinkedList<>();
    
    	public void referenceTest() {
    		try {
    			for (int i = 0; true; i++) {
    				refs.add(new WeakReference<BigData>(new BigData()));
    			}
    		} catch (OutOfMemoryError ofm) { // Strong일 경우 out of memory 발생
    			System.out.println("out of memory!");
    		}
    	}
    
    	public static void main(String[] args) {
    		System.out.println("run");
    		// Thread.sleep(10 * 1000);
    		ReferenceTest test = new ReferenceTest();
    		test.referenceTest();
    		System.out.println("finish");
    	}
    	
    }
    

    - WeakSingleton.java
    package kr.co.ioacademy; //iocademy 윤찬식 강사님
    import java.lang.ref.WeakReference;
    
    public class WeakSingleton {
    	private static WeakReference<WeakSingleton> instance
    	 = new WeakReference<WeakSingleton>(null);
    
    	public static WeakSingleton getInstance()
    	{
    		WeakSingleton m = instance.get();
    		if (m != null)
    			return m;
    		
    		synchronized (WeakSingleton.class) {	
    			
    			System.out.println("Create new instance");
    			
    			m = new WeakSingleton();
    			instance = new WeakReference<WeakSingleton>(m);		
    		}
    		
    		return m;
    	}
    	
    	public static void main(String[] args) {
    		WeakSingleton w = WeakSingleton.getInstance();
    		
    		// System.gc();
    		
    		w = WeakSingleton.getInstance();
    	}
    }
    


'Programing > Java' 카테고리의 다른 글

Effective Java - 객체 생성  (0) 2016.03.03
Java Reference와 GC  (0) 2016.02.29
Effective Java - Reference  (0) 2016.02.29
Garbage Collection  (0) 2015.11.15
Wrapper클래스,박싱(boxing),언박싱(unboxing)  (0) 2015.11.15
Collection  (0) 2015.11.15

Java Garbage Collection

지극히 개인적이고 주관적인 판단 기준을 먼저 밝힌다면, 가비지 컬렉션(Garbage Collection, 이하 GC)에 대해 잘 알고 있을수록 실력이 좋은 Java 개발자라고 생각합니다. GC 과정에 관심을 가질 정도라면 규모가 일정 이상인 애플리케이션을 제작해 본 경험이 있을 것입니다. 또, 어떤 GC 알고리즘을 선택할 것인지 고민할 정도면 스스로 제작한 애플리케이션의 특징을 정확히 이해하고 있다고 볼 수 있습니다. 이러한 판단 기준이 보편적이지는 않지만, GC에 대한 이해는 훌륭한 Java 개발자가 되기 위한 필수 조건이라는 데에는 별다른 이견이 없을 것입니다. 이 글에서는 GC 이론을 되도록 쉽게 소개하겠습니다. 피가 되고 살이 되는 글이 되기를 바랍니다.

가비지 컬렉션 과정 - Generational Garbage Collection

GC에 대해서 알아보기 전에 알아야 할 용어가 있다. 바로 'stop-the-world'이다. stop-the-world란, GC을 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 것이다. stop-the-world가 발생하면 GC를 실행하는 쓰레드를 제외한 나머지 쓰레드는 모두 작업을 멈춘다. GC 작업을 완료한 이후에야 중단했던 작업을 다시 시작한다. 어떤 GC 알고리즘을 사용하더라도 stop-the-world는 발생한다. 대개의 경우 GC 튜닝이란 이 stop-the-world 시간을 줄이는 것이다.

Java는 프로그램 코드에서 메모리를 명시적으로 지정하여 해제하지 않는다. 가끔 명시적으로 해제하려고 해당 객체를 null로 지정하거나 System.gc() 메서드를 호출하는 개발자가 있다. null로 지정하는 것은 큰 문제가 안 되지만, System.gc() 메서드를 호출하는 것은 시스템의 성능에 매우 큰 영향을 끼치므로 System.gc() 메서드는 절대로 사용하면 안 된다(다행히도 NHN에서 System.gc() 메서드를 호출하는 개발자를 보진 못했다).

Java에서는 개발자가 프로그램 코드로 메모리를 명시적으로 해제하지 않기 때문에 가비지 컬렉터(Garbage Collector)가 더 이상 필요 없는 (쓰레기) 객체를 찾아 지우는 작업을 한다. 이 가비지 컬렉터는 두 가지 가설 하에 만들어졌다(사실 가설이라기보다는 가정 또는 전제 조건이라 표현하는 것이 맞다).

  • 대부분의 객체는 금방 접근 불가능 상태(unreachable)가 된다.
  • 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다.

이러한 가설을 'weak generational hypothesis'라 한다. 이 가설의 장점을 최대한 살리기 위해서 HotSpot VM에서는 크게 2개로 물리적 공간을 나누었다. 둘로 나눈 공간이 Young 영역과 Old 영역이다.

  • Young 영역(Yong Generation 영역): 새롭게 생성한 객체의 대부분이 여기에 위치한다. 대부분의 객체가 금방 접근 불가능 상태가 되기 때문에 매우 많은 객체가 Young 영역에 생성되었다가 사라진다. 이 영역에서 객체가 사라질때 Minor GC가 발생한다고 말한다.
  • Old 영역(Old Generation 영역): 접근 불가능 상태로 되지 않아 Young 영역에서 살아남은 객체가 여기로 복사된다. 대부분 Young 영역보다 크게 할당하며, 크기가 큰 만큼 Young 영역보다 GC는 적게 발생한다. 이 영역에서 객체가 사라질 때 Major GC(혹은 Full GC)가 발생한다고 말한다.

영역별 데이터 흐름을 그림으로 살펴보면 다음과 같다.

JavaGarbage1

그림 1 GC 영역 및 데이터 흐름도

위 그림의 Permanent Generation 영역(이하 Perm 영역)은 Method Area라고도 한다. 객체나 억류(intern)된 문자열 정보를 저장하는 곳이며, Old 영역에서 살아남은 객체가 영원히 남아 있는 곳은 절대 아니다. 이 영역에서 GC가 발생할 수도 있는데, 여기서 GC가 발생해도 Major GC의 횟수에 포함된다.

그렇다면 "Old 영역에 있는 객체가 Young 영역의 객체를 참조하는 경우가 있을 때에는 어떻게 처리될까?"라고 궁금해 하는 분도 더러 있을 것이다. 이러한 경우를 처리하기 위해서 Old 영역에는 512바이트의 덩어리(chunk)로 되어 있는 카드 테이블(card table)이 존재한다.

카드 테이블에는 Old 영역에 있는 객체가 Young 영역의 객체를 참조할 때마다 정보가 표시된다. Young 영역의 GC를 실행할 때에는 Old 영역에 있는 모든 객체의 참조를 확인하지 않고, 이 카드 테이블만 뒤져서 GC 대상인지 식별한다.

JavaGarbage2

그림 2 카드 테이블 구조

카드 테이블은 write barrier를 사용하여 관리한다. write barrier는 Minor GC를 빠르게 할 수 있도록 하는 장치이다. write barrirer때문에 약간의 오버헤드는 발생하지만 전반적인 GC 시간은 줄어들게 된다.

Young 영역의 구성

GC를 이해하기 위해서 객체가 제일 먼저 생성되는 Young 영역부터 알아보자. Young 영역은 3개의 영역으로 나뉜다.

  • Eden 영역
  • Survivor 영역(2개)

Survivor 영역이 2개이기 때문에 총 3개의 영역으로 나뉘는 것이다. 각 영역의 처리 절차를 순서에 따라서 기술하면 다음과 같다.

  • 새로 생성한 대부분의 객체는 Eden 영역에 위치한다.
  • Eden 영역에서 GC가 한 번 발생한 후 살아남은 객체는 Survivor 영역 중 하나로 이동된다.
  • Eden 영역에서 GC가 발생하면 이미 살아남은 객체가 존재하는 Survivor 영역으로 객체가 계속 쌓인다.
  • 하나의 Survivor 영역이 가득 차게 되면 그 중에서 살아남은 객체를 다른 Survivor 영역으로 이동한다. 그리고 가득 찬 Survivor 영역은 아무 데이터도 없는 상태로 된다.
  • 이 과정을 반복하다가 계속해서 살아남아 있는 객체는 Old 영역으로 이동하게 된다.

이 절차를 확인해 보면 알겠지만 Survivor 영역 중 하나는 반드시 비어 있는 상태로 남아 있어야 한다. 만약 두 Survivor 영역에 모두 데이터가 존재하거나, 두 영역 모두 사용량이 0이라면 여러분의 시스템은 정상적인 상황이 아니라고 생각하면 된다.

이렇게 Minor GC를 통해서 Old 영역까지 데이터가 쌓인 것을 간단히 나타내면 다음과 같다.

JavaGarbage3

그림 3 GC 전과 후의 비교

참고로, HotSpot VM에서는 보다 빠른 메모리 할당을 위해서 두 가지 기술을 사용한다. 하나는 bump-the-pointer라는 기술이며, 다른 하나는 TLABs(Thread-Local Allocation Buffers)라는 기술이다.

bump-the-pointer는 Eden 영역에 할당된 마지막 객체를 추적한다. 마지막 객체는 Eden 영역의 맨 위(top)에 있다. 그리고 그 다음에 생성되는 객체가 있으면, 해당 객체의 크기가 Eden 영역에 넣기 적당한지만 확인한다. 만약 해당 객체의 크기가 적당하다고 판정되면 Eden 영역에 넣게 되고, 새로 생성된 객체가 맨 위에 있게 된다. 따라서, 새로운 객체를 생성할 때 마지막에 추가된 객체만 점검하면 되므로 매우 빠르게 메모리 할당이 이루어진다.

그러나 멀티 스레드 환경을 고려하면 이야기가 달라진다. Thread-Safe하기 위해서 만약 여러 스레드에서 사용하는 객체를 Eden 영역에 저장하려면 락(lock)이 발생할 수 밖에 없고, lock-contention 때문에 성능은 매우 떨어지게 될 것이다. HotSpot VM에서 이를 해결한 것이 TLABs이다.

각각의 스레드가 각각의 몫에 해당하는 Eden 영역의 작은 덩어리를 가질 수 있도록 하는 것이다. 각 쓰레드에는 자기가 갖고 있는 TLAB에만 접근할 수 있기 때문에, bump-the-pointer라는 기술을 사용하더라도 아무런 락이 없이 메모리 할당이 가능하다.

간단하게 Young 영역에 대한 GC에 대해서 알아보았다. 위에서 이야기한 두 가지 기술(bump-the-pointer, TLABs)을 반드시 기억하고 있을 필요는 없다. 몰라도 쇠고랑 안차고 경찰 출동 안한다. 그러나 Eden 영역에 최초로 객체가 만들어지고, Survivor 영역을 통해서 Old 영역으로 오래 살아남은 객체가 이동한다는 사실은 꼭 기억하기 바란다.

Old 영역에 대한 GC

Old 영역은 기본적으로 데이터가 가득 차면 GC를 실행한다. GC 방식에 따라서 처리 절차가 달라지므로, 어떤 GC 방식이 있는지 살펴보면 이해가 쉬울 것이다. GC 방식은 JDK 7을 기준으로 5가지 방식이 있다.

  • Serial GC
  • Parallel GC
  • Parallel Old GC(Parallel Compacting GC)
  • Concurrent Mark & Sweep GC(이하 CMS)
  • G1(Garbage First) GC

이 중에서 운영 서버에서 절대 사용하면 안 되는 방식이 Serial GC다. Serial GC는 데스크톱의 CPU 코어가 하나만 있을 때 사용하기 위해서 만든 방식이다. Serial GC를 사용하면 애플리케이션의 성능이 많이 떨어진다.

그럼 각 GC 방식에 대해서 살펴보자.

Serial GC (-XX:+UseSerialGC)

Young 영역에서의 GC는 앞 절에서 설명한 방식을 사용한다. Old 영역의 GC는 mark-sweep-compact이라는 알고리즘을 사용한다. 이 알고리즘의 첫 단계는 Old 영역에 살아 있는 객체를 식별(Mark)하는 것이다. 그 다음에는 힙(heap)의 앞 부분부터 확인하여 살아 있는 것만 남긴다(Sweep). 마지막 단계에서는 각 객체들이 연속되게 쌓이도록 힙의 가장 앞 부분부터 채워서 객체가 존재하는 부분과 객체가 없는 부분으로 나눈다(Compaction).

Serial GC는 적은 메모리와 CPU 코어 개수가 적을 때 적합한 방식이다.

Parallel GC (-XX:+UseParallelGC)

Parallel GC는 Serial GC와 기본적인 알고리즘은 같지다. 그러나 Serial GC는 GC를 처리하는 스레드가 하나인 것에 비해, Parallel GC는 GC를 처리하는 쓰레드가 여러 개이다. 그렇기 때문에 Serial GC보다 빠른게 객체를 처리할 수 있다. Parallel GC는 메모리가 충분하고 코어의 개수가 많을 때 유리하다. Parallel GC는 Throughput GC라고도 부른다.

다음 그림은 Serial GC와 Parallel GC의 스레드를 비교한 그림이다.JavaGarbage4

그림 4 Serial GC와 Parallel GC의 차이 (이미지 출처: "Java Performance", p. 86)

Parallel Old GC(-XX:+UseParallelOldGC)

Parallel Old GC는 JDK 5 update 6부터 제공한 GC 방식이다. 앞서 설명한 Parallel GC와 비교하여 Old 영역의 GC 알고리즘만 다르다. 이 방식은 Mark-Summary-Compaction 단계를 거친다. Summary 단계는 앞서 GC를 수행한 영역에 대해서 별도로 살아 있는 객체를 식별한다는 점에서 Mark-Sweep-Compaction 알고리즘의 Sweep 단계와 다르며, 약간 더 복잡한 단계를 거친다.

CMS GC (-XX:+UseConcMarkSweepGC)

다음 그림은 Serial GC와 CMS GC의 절차를 비교한 그림이다. 그림에서 보듯이 CMS GC는 지금까지 설명한 GC 방식보다 더 복잡하다.

JavaGarbage5

그림 5 Serial GC와 CMS GC(이미지 출처)

초기 Initial Mark 단계에서는 클래스 로더에서 가장 가까운 객체 중 살아 있는 객체만 찾는 것으로 끝낸다. 따라서, 멈추는 시간은 매우 짧다. 그리고 Concurrent Mark 단계에서는 방금 살아있다고 확인한 객체에서 참조하고 있는 객체들을 따라가면서 확인한다. 이 단계의 특징은 다른 스레드가 실행 중인 상태에서 동시에 진행된다는 것이다.

그 다음 Remark 단계에서는 Concurrent Mark 단계에서 새로 추가되거나 참조가 끊긴 객체를 확인한다. 마지막으로 Concurrent Sweep 단계에서는 쓰레기를 정리하는 작업을 실행한다. 이 작업도 다른 스레드가 실행되고 있는 상황에서 진행한다.

이러한 단계로 진행되는 GC 방식이기 때문에 stop-the-world 시간이 매우 짧다. 모든 애플리케이션의 응답 속도가 매우 중요할 때 CMS GC를 사용하며, Low Latency GC라고도 부른다.

그런데 CMS GC는 stop-the-world 시간이 짧다는 장점에 반해 다음과 같은 단점이 존재한다.

  • 다른 GC 방식보다 메모리와 CPU를 더 많이 사용한다.
  • Compaction 단계가 기본적으로 제공되지 않는다.

따라서, CMS GC를 사용할 때에는 신중히 검토한 후에 사용해야 한다. 그리고 조각난 메모리가 많아 Compaction 작업을 실행하면 다른 GC 방식의 stop-the-world 시간보다 stop-the-world 시간이 더 길기 때문에 Compaction 작업이 얼마나 자주, 오랫동안 수행되는지 확인해야 한다.

G1 GC

마지막으로 G1(Garbage First) GC에 대해서 알아보자. G1 GC를 이해하려면 지금까지의 Young 영역과 Old 영역에 대해서는 잊는 것이 좋다.

다음 그림에서 보다시피, G1 GC는 바둑판의 각 영역에 객체를 할당하고 GC를 실행한다. 그러다가, 해당 영역이 꽉 차면 다른 영역에서 객체를 할당하고 GC를 실행한다. 즉, 지금까지 설명한 Young의 세가지 영역에서 데이터가 Old 영역으로 이동하는 단계가 사라진 GC 방식이라고 이해하면 된다. G1 GC는 장기적으로 말도 많고 탈도 많은 CMS GC를 대체하기 위해서 만들어 졌다.JavaGarbage6

그림 6 G1 GC의 레이아웃(이미지 출처: "The Garbage-First Garbage Collector" (TS-5419), JavaOne 2008, p. 19)

G1 GC의 가장 큰 장점은 성능이다. 지금까지 설명한 어떤 GC 방식보다도 빠르다. 하지만, JDK 6에서는 G1 GC를 early access라고 부르며 그냥 시험삼아 사용할 수만 있도록 한다. 그리고 JDK 7에서 정식으로 G1 GC를 포함하여 제공한다.

그러나 JDK 7을 실서비스에서 사용하려면 많은 검증 기간(1년은 필요하다는 생각이다)을 거쳐야 할 것으로 보이기 때문에, G1 GC를 당장 사용하고 싶어도 더 기다리는 것이 좋다는 것이 개인적인 생각이다. JDK 6에서 G1 GC를 적용했다가 JVM Crash가 발생했다는 말도 몇 번 들었기에 더더욱 안정화될 때까지 기다리는 것이 좋겠다.

마치며

이번 글에서는 Java의 GC에 대해서 아주 간단하게(?) 살펴보았다. 다음 글에서는 Java의 GC 상황을 모니터링하는 방법과 GC 튜닝 방법을 알아볼 예정이다.

마지막으로 한 가지 더 말하고 싶은 것이 있다. 어떤 서비스에서 A라는 GC 옵션을 적용해서 잘 동작한다고 그 GC 옵션이 다른 서비스에서도 훌륭하게 적용되어 최적의 효과를 볼 수 있다고 생각하지 말라는 것이다.

만약 애플리케이션에서 만들어지는 모든 객체의 크기와 종류가 같다면 회사에서 사용하는 모든 WAS의 GC 옵션을 동일하게 설정할 수 있다. 하지만, 각 서비스의 WAS에서 생성하는 객체의 크기와 생존 주기가 모두 다르고, 장비의 종류도 다양하다. WAS의 스레드 개수와 장비당 WAS 인스턴스 개수, GC 옵션 등은 지속적인 튜닝과 모니터링을 통해서 해당 서비스에 가장 적합한 값을 찾아야 한다. 이 이야기는 필자의 경험에서 나온 이야기가 아니고, 2010년 JavaOne에서 Oracle JVM을 만드는 엔지니어들이 한 말이다.

참고 자료

이 글의 내용과 그림은 다음의 자료를 참고했다.

출처: http://d2.naver.com/helloworld/1329


'Programing > Java' 카테고리의 다른 글

Java Reference와 GC  (0) 2016.02.29
Effective Java - Reference  (0) 2016.02.29
Garbage Collection  (0) 2015.11.15
Wrapper클래스,박싱(boxing),언박싱(unboxing)  (0) 2015.11.15
Collection  (0) 2015.11.15
JAVA 실행 과정  (0) 2015.11.14

 자바에는 int, double과 같은 기본형 자료형(primitive type)의 포장 클래스(wrapper class)가 있어서 기본형을 객체로 다루어야 할 경우에 사용할 수 있다. 다음 표에서와 같이 포장 클래스명은 기본형의 첫 문자를 대문자로 바꾸면 된다.( char형과 int형만 이름이 다르다.)


[표 1] 포장 클래스

기본형

포장 클래스

생성 예

boolean

Boolean

Boolean bA = new Boolean(true);

Boolean bB = new Boolean(“false”);

char

Character

Character cA = new Character(‘a’);

byte

Byte

Byte byA = new Byte(10);

Byte byB = new Byte(“127”);

short

Short

Short sA = new Short(1234);

Short sB = new Short(“1234”);

int

Integer

Integer iA = new Integer(1234);

Integer iB = new Integer(“1234”);

long

Long

Long lA = new Long(1234);

Long lB = new Long(“1234”);

float

Float

Float fA = new Float(12.34f);

Float fB = new Float(“12.34f”);

double

Double

Double dA = new Double(12.34);

Double dB = new Double(“12.34”);


생성자는 위와 같이 해당하는 기본형 값을 줄 수도 있고 문자열로 줄 수도 있다. 문자열로 주는 경우 해당 데이터형의 형식에 맞아야 한다.


 박싱(boxing)이란 기본형을 참조형으로 변환하는 것이고 언박싱(unboxing)이란 반대로 참조형을 기본형으로 바꾸는 것이다. 그리고 JDK 1.5부터는 이것을 자동으로 해주는 기능이 추가되었다.


package tut02;
public class Tut02 {
   public static void main(String[] args) {
       Integer iA = new Integer(123);
       Integer iB = new Integer(123);
       
       int ia = (int)iA; //(1) 언박싱(unboxing)
       int ib = iB; //(2) 오토언박싱(auto unboxing)
       Integer iC = (Integer)456; //(3)박싱(boxing)
       Integer iD = ia; //(4)오토 박싱(auto boxing)
   }
}


위 예세서 (1)은 명시적으로 언박싱을 해주는 것이고 (2)는 자동으로 수행해 주는 것이다. 박싱의 경우도 자동으로 처리해 준다. 오토박싱과 오토언박싱은 대응되는 자료형 사이에만 일어난다는 점도 유의해야 한다.


 포장 클래스 객체간 연산도 가능하며 기본형과 포장형 간 연산도 가능하다.


package tut02;
public class Tut02 {
   public static void main(String[] args) {
       Integer iA = new Integer(123);
       Integer iB = new Integer(123);
       
       int ia = (int)iA; //(1) 언박싱(unboxing)
       int ib = iB; //(2) 오토언박싱(auto unboxing)
       Integer iC = (Integer)456; //(3)박생(boxing)
       Integer iD = ia; //(4)오토 박싱(auto boxing)
       
       Integer iE = iA + iB; // 포장형끼리의 연산
       Integer iF = ia - ib; // 기본형끼리의 연산 결과를 오토박싱
       int ic = ia * iB; // 기본형과 포장형 간 연산
   }
}


자바에서는 연산자가 오버로딩되지 않는다는 점을 생각하면 의아할 것이다. 하지만 이 코드는 내부적으로 포장형인 피연산자가 오토언박싱 되어서 기본형 끼리의 연산으로 수행되는 것이라고 이해할 수 있다.


 비교 연산도 가능하지만 내용물의 동치 여부를 검사할 때 ==기호대신 equals() 메소드를 이용해야 한다.


package tut02;
public class Tut02 {
   public static void main(String[] args) {
       Integer ia = new Integer(123);
       Integer ib = new Integer(123);
       Integer ic = new Integer(456);
       System.out.println("ia>=ib:"+(ia>=ib));
       System.out.println("ib>=ic:"+(ib>=ic));
       System.out.println("ia==ib:"+(ia==ib));
       System.out.println("ia.equals(ib):"+ia.equals(ib));
   }
}


ia>=ib:true
ib>=ic:false
ia==ib:false
ia.equals(ib):true


왜냐면 포장형고 객체이기 때문에 ==연산은 두 객체 인스턴스의 참조(주소값)을 비교하게 되는 것이다. 내용물의 동치 여부는 문자열 객체와 마찬가지로 equals() 메소드를 이용해야 한다.


출처: http://studymake.tistory.com/420

'Programing > Java' 카테고리의 다른 글

Effective Java - Reference  (0) 2016.02.29
Garbage Collection  (0) 2015.11.15
Wrapper클래스,박싱(boxing),언박싱(unboxing)  (0) 2015.11.15
Collection  (0) 2015.11.15
JAVA 실행 과정  (0) 2015.11.14
JVM, JRE, JDK의 차이  (0) 2015.10.11

  1. Collection

    Collection 인터페이스를 상속받아 List와 Set 인터페이스가 된다. List는 순서가 있는 Collection, 그리고 List는 Data 중복을 허락한다. 하지만 Set은 순서의 의미가 없으며 Data를 중복해서 포함할 수 없다.

  • List 인터페이스의 특징
    • 순서가 있는 Collection.(이 순서는 삽입된 순서를 의미한다.)
    • Data를 중복해서 포함할 수 있다.
    • Stack의 특징
      • Data의 삽입과 추출이 후입선출(Last-In First-Out) 구조로 되어 있다.
      • push() method : Data 삽입할 때 사용
      • pop() method : Data 추출할 때 사용
      • peek() method : 추출할 Data를 삭제하지 않고 Data만을 가져 올 때 사용
      • search() method : Stack으로부터 Data를 검색할 때 사용
    • Vector의 특징
      • 자동으로 동기화를 보장해준다.
      • ArrayList에 동기화가 보장되도록 최적화한 클래스이다.
      • JAVA 5.0 이 후로는 AutoBoxing/AutoUnBoxing을 지원한다.
        • AutoBoxing이란? 기본 Data 타입을 Wrapper 클래스형의 객체로 자동으로 변환해주는 기능. AutoUnBoxing은 AutoBoxing의 반대 개념
        • JAVA 1.4까지

Vector v = new Vector();
v.addElement(new Integer(100));

  • JAVA 5.0이후

Vector v = new Vector();
v.addElement(100); // AutoBoxing 발생, 자동으로 Wrapper class인 Integer로 변경

  • addElement() method : Data를 삽입할 때 사용
  • elementAt() method : Data를 추출할 때 사용, Index에 해당하는 객체를 얻어냄
  • size() method : Vector 내에 존재하는 객체의 수를 얻어낼 대 사용
  • insertElementAt() method : Vector 내에 중간 삽입할 때 사용
  • setElementAt() method : Vector 내에 존재하는 Data를 수정할 때 사용
  • indexOf() method : Vector 내에 Data를 검색할 때 사용, Index를 반환
  • contains() method : Data의 존재 유무를 알기 위해 사용.
  • ArrayList의 특징
    • 동기화를 보장해주지 않는다.
    • 배열에 동적 메모리 증가 기능을 구현한 클래스이다.
    • 동기화 지원 방법 : List list = Collections.synchronizeList(new ArrayList(…));
    • add() method : Data 삽입할 때 사용
    • get(0 method : Data 추출할 때 사용
    • toArray() method : ArrayList로부터 배열 얻어낼 때 사용
    • contains() method : Data의 존재 유무를 알기 위해 사용
    • size() method : ArrayList의 요소 개수를 얻어낼 때 사용
  • Set 인터페이스의 특징
    • 집합적인 개념의 Collection
    • 순서의 의미가 없다.
    • Data를 중복해서 포함할 수 없다.
    • HashSet의 특징
      • add() method : Data 삽입할 때 사용
      • next() method : Data 추출할 때 사용
        • HashSet의 Data 추출은 Iterator을 이용하면 된다. Iterator는 Collection내의 모든 Data에 접근할 수 있는 특징이 있다. 그리고 Data의 마지막에 상관하지 않고 검색하기 위한 인터페이스이다. Set의 Iterator() method로 Iterator를 얻어 낼 수 있으며, Iterator의 hasNext() method를 이용해서 Data 끝을 만날 때까지 next() method를 호출해서 Data를 추출할 수 있다.

Iterator<String iter = set.iterator();
while(iter.hasNext()) {
String temp = iter.next();

System.out.print(temp + ", ");
}

  • remove() method : Data를 삭제할 때 사용
  • contains() method : Data의 포함여부를 알기 위해 사용
  • size() method : HashSet의 요소 개수를 얻어낼 때 사용

       

  1. Map

    List와 Set이 순서나 집합적인 개념의 인터페이스라면 Map은 검색의 개념이 가미된 인터페이스이다. Map 인터페이스는 데이터를 삽입할 때 Key와 Value의 형태로 삽입되며, Key를 이용해서 Value를 얻을 수 있다.

  • Hashtable, HashMap의 공통점
    • 내부적으로 모두 Hash 기법을 이용한다.
    • Map 인터페이스를 구현하고 있다.
    • Key와 Value를 이용해서 Data를 관리한다.
  • Hashtable, HashMap의 차이점
    • Hashtable은 동기화가 보장된다.
    • HashMap은 동기화가 보장되지 않는다.
    • HashMap의 동기화 지원 방법 : Map m = Collections.synchronizedMap(New HashMap(…));
  • Hashtable, HashMap과 HashSet과의 관계
    • Hashtable과 HashMap은 둘 다 Map 인터페이스를 구현하고 있다.
    • HashSet은 내부적으로 Hash기법을 사용하지만 Set인터페이스를 구현하고 있다.
  • HashMap
    • 객체 생성 : Map<String, Integer> map = new HashMap<String, Integer>();
    • put() method : Data 삽입할 때 사용
    • get() method : Data를 추출할 때 사용, argument값은 Key를 사용
  • Hashtable
    • 객체 생성 : Hashtable<String, Object> h = new Hashtable<String, Object>();
    • put() method : Data 삽입할 때 사용
    • get() method : Data를 추출할 때 사용, argument값은 Key를 사용

   

  1. Sorted

    Set과 Map 인터페이스를 상속받아 정렬 기능이 추가된 SortedSet과 SortedMap 인터페이스가 된다. 그리고 이들은 각각 TreeSet 클래스와 TreeMap 클래스로 구성된다. TreeSet과 TreeMap은 Set과 Map의 기능을 가지고 있으면서 정렬 기능이 가미되었다는 것이 특징이다.

  • Sorted를 지원하지 않는 클래스
    • HashSet, HashMap
  • Sorted를 지원하는 클래스
    • TreeSet, TreeMap
  • TreeMap
    • Key와 Value로 Data를 관리
    • Key를 기준으로 오름차순으로 정렬된다.
    • Map 인터페이스를 상속한 SortedMap 인터페이스를 구현한 클래스
  • TreeSet
    • Set 인터페이스를 상속한 SortedSet 인터페이스를 구현한 클래스
    • 데이터들이 자동으로 오름차순으로 정렬된다.
  • Comparator
    • TreeSet과 TreeMap은 사용자가 직접 정렬의 방식을 지정할 수 있다.
    • TreeSet과 TreeMap은 정렬을 위한 Comparator 인터페이스를 구현하면 된다.
    • TreeSet에 Data를 집어 넣으면 기본적으로 오름차순(Ascending) 정렬이 되지만 그것도 문자열이나 기본 데이터 타입과 같은 단순한 것에만 해당된다. 이에 사용자가 직접 비교법을 넣어주기 위해 사용하는 것이 Comparator 인터페이스이다.
    • Comparator의 구현 방법 : Comparator 내부에 compare() method를 구현하면 된다.

class Mycomparator<T> implements Comparator<T> {
public int compare(T o1, T o2) {
// 비교방법 구현
}

  • Comparator가 추가된 TreeSet의 생성

TreeSet<Score> tset = new TreeSet<Score>(new MyComparator<Score>());

  • Comparator가 추가된 TreeMap의 생성

TreeMap<Score, String> tset = new TreeMap<Score, String>(new MyComparator<Score>());

  • 일반적인 정렬기능의 사용
    • HashSet이나 HashMap을 정렬 기능이 지원되는 TreeSet이나 TreeMap으로 변환해서 사용
    • HashSet을 이용한 TreeSet 생성

Set<String> set = new HashSet<String>();
...
TreeSet<String> ts = new TreeSet<String>();
ts.addAll(set);

  • HashMap을 이용한 TreeMap 생성

Map<String, Integer> map = new HashMap<String, Integer>();
...
Map<String, Integer> sortedMap = new TreeMap<String, Integer>();
sortedMap.putAll(map);


출처: http://withwani.tistory.com/150

'Programing > Java' 카테고리의 다른 글

Effective Java - Reference  (0) 2016.02.29
Garbage Collection  (0) 2015.11.15
Wrapper클래스,박싱(boxing),언박싱(unboxing)  (0) 2015.11.15
Collection  (0) 2015.11.15
JAVA 실행 과정  (0) 2015.11.14
JVM, JRE, JDK의 차이  (0) 2015.10.11

출처: http://loveinsky.tistory.com/entry/%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-1-2


앞에서 JAVA 에 대한 개략적인 특징들을 알아보았다. 그 개략적인 특징들을 중심으로 이제부터는 JAVA 의 세심한 부분에서의 특징들을 분석할 것인데, 우선 첫번째로 JAVA Development 과정을 알아볼 것이다. 어떻게 작성하고, 어떤 방식으로 '소스 코드'가 해석되고, 그 해석된 코드는 어떤 방식으로 실행되는지 전반적인 과정을 이번 1 (2) 에서 알아보고, 다음 글인 2 장에서 부터는 다른 언어와 차별화를 두는 JAVA의 특성인 '객체 지향성(Object-Oriented)'의 특성들에 대해 알아보자.

 

우선 컴퓨터 언어는 3 가지 종류로 분류된다.

   저수준 언어 (Low-Level)

 

1) 기계어 (Machine Language) : 0101 와 같이 오직 컴퓨터만 이해하는 언어로 CPU에 내장된 명령들을 직접 사용하는 것으로, 수행시간이 빠르다. 기종마다 기계어가 다르기 때문에 언어의 호환성이 없다. 반면에 Java의 경우는 위에서 설명하였듯이 JVM을 이용해 '바이너리 코드'를 각 기계에 맞게 실행시켜준다. 

 

2) 어셈블리어 (Assembly Language) 단순한 언어로 기계어와 고수준 언어 중간지점 개념의 언어. 기계어와 1:1로 대응되는 기호로 이루어진 언어로 이도 기계어와 마찬가지로 언어의 호환성이 없다.

 

   고수준 언어 (High-Level)

 

3) 고수준 언어 (High-Level) - C, C++, C#, Java 등 인간이 실생활에서 사용하는 자연어와 비슷한 형태와 구조를 갖는다. 실행을 하기 위해서는 컴퓨터가 이해할 수 있는 기계어로 반드시 해석(Translate)되야하는데, Translate 하는 프로그램에는 컴파일러나 인터프리터가 사용된다.

 

   + 추가. JAVA 언어에 대한 개략적인 역사

 

- C  C++ 의 역사 : 진화 과정 : C à C++ à JAVA

 현대에는 일반적인 목적을 지닌 운영 시스템들을 위한 거의 모든 코드들은 C나 C++로 작성된다.

 

C

Originally implemented in 1972 

UNIX 운영 체제 개발 언어로 널리 알려져있다.

 

C++

An extension of C Developed by Bjarne Stroustrup in the early 1980s at Bell Laboratories

객체 지향 프로그래밍의 능력을 제공해준다.

 

 

- Java 의 역사 

 

1991

썬 마이크로시스템의 James Gosling 는 C++을 기반으로한 Java를 만들었다.

 

1993

Web이 급 성장함과 동시에, Web에서 많이 사용하는 Java가 유명해졌다.

이렇게 해서 Java 가 우선적으로 웹 애플리케이션 작성도구로 유명해졌다.

 

– 인터넷 & 웹 개발

– 모바일 애플리케이션과 운영체제 (안드로이드)

– 임베디드 소프트웨어

– 거대한 소프트웨어 (Enterprise Software)
 

 

컴파일러와 인터프리터는 무엇인가? 간단하게 설명하자면, 고수준 언어를 저수준 언어인 기계어로 바꾸어주는 것이다. 이렇게 번역된 프로그램은 0101과 같은 기계어로 구성되어있으며, 일반적으로는 Object Program (목적 프로그램 / .obj 또는 .o) 이라고 부르며, Java에서는 Class Program (클래스 프로그램 / .class) 이라고 부른다. 컴파일러와 인터프리터는 이렇게 기계어로 바꾸어주는 똑같은 목적을 지녔지만, 방법면에서는 이 두 방법을 따로 분류시킬 정도의 상이한 차이를 보인다.

 

1. 컴파일러 Compiler
- 컴파일러는 '고수준 언어'를 읽어내어 '목적 프로그램'으로 번역한 후, Linking 작업을 통해 컴퓨터에서 실행 가능한 실행 프로그램인 ‘기계어 프로그램’을 만들어낸다. 아래 그림에서 점선 박스 안에 있는 과정이 컴파일러의 '컴파일' 과정이다.

 

Source ( 고수준언어로 작성된 소스 프로그램 ) ]

|

<Compiler (컴파일러) : Sourse를 Compile 한다.>

|

Object ( 기계어(저수준언어)로 작성된 목적 프로그램) ]

|

<Linker (링커) : Object들을 Link 한다.>

|

Execute ( 기계어들을 각자 의미에 맞게 Link시킨 최종 실행 프로그램 ) ]

 

 

 

2. 인터프리터 Interpreter

- 인터프리터는 ‘고수준 언어 프로그램’을 직접적으로 바로 실행 한다. 그렇기 때문에 목적 프로그램을 만들지 않는다.프로그램을 '인터프리트'하는 것은 그 프로그램의 ‘기계어(기계 코드) 프로그램’을 실행하는 것 보다 느리다. 아래 점선 박스 안에 있는 것이 인터프리터의 과정이다.

 

Source ( 소스 프로그램에서 '한 줄'만 ) ]

|

<Interpreter (인터프리터) : Sourse에서 '한 줄'의 고수준 언어를 Interpret 한다.>

< Object. 목적 프로그램을 거치지 않고, paser에 의해 자동 parsing 된 후 바로 실행된다. >

|

Execute* ( *작은 의미에서의 실행. 즉 '한 줄'에 대한 실행만을 뜻한다. ) ]

|

< 이 과정을 계속해서 반복한다. C 언어에서의 디버깅과 유사 >

 

인터프리터가 컴파일러에 비해 실행 속도가 느린 이유는 '기계어 프로그램'는 모든 순서 처리와 기타 다른 요소들을 다 정리한 후에 '목적 파일'이라는 하나의 통 파일로 제작된 것이기 때문이다. 기계어들이 처음부터 끝까지 정리된 상태에서 실행하는 것과, 기계어를 처음부터 끝까지 한 줄, 한 줄 해석해 나가는 것이 상식적으로 느리다. 추가적으로 컴파일러는 번역속도 면에서는 인터프리터 보다 느리다. 이유는 위와 같다. 한 줄을 번역하는 것이, 전체를 번역하는 것보다 빠른것은 당연하다.

 

 

Java 도 역시 '고수준 언어' 중 하나로, 해석 과정을 거쳐야지 비로소 실행을 할 수 있게된다.

' 그럼, Java 는 컴파일러를 사용하는 컴파일러 언어일까? 인터프리터를 이용하는 인터프리터 언어일까? '

 

 

Java 는 '고수준 언어'이기 때문에 Java 프로그램을 개발하려고 하는 프로그래머들은 어떠한 종류의 프로그램을 개발 하려고하던간에 우선 맨 처음 소스 프로그램을 Java 코드로 작성해야 한다. 이를 프로그래밍의 가장 초기 과정 Edit라 하며 컴파일러와 인터프리터의 사용은 그 다음 과정에서 사용된다. 전체 프로그래밍 과정은 아래와 같다.

 

1. Edit

2. Compile

3. Run ( Interpret )

   - Load

   - Verify

   - Execute  

 

Edit 다음 이 소스 프로그램을 기계어로 바꾸어야 한다. 위 2. 번 Compile을 보면 알 수 있듯이, 이 바꾸는 과정을 Compile 이라고 부르고 이 과정은 Compiler 를 통해 하게된다.

 

Compile 을 하면 .class 라는 확장자를 지닌 'Bytecode 프로그램'이 생겨난다. 그 다음 3. Run 과정에서는 2. Compile해서 나온 'Bytecode 프로그램'을 실행해서 원하는 결과를 출력하기 위하여, JVM 이라는 프로그램을 사용하게 된다. JVM 은 Bytecode 들을 C 언어에서와 같이 따로 Link 같은 결과를 거쳐 .exe 확장자를 가진 '실행 프로그램'과 같은 '결과 프로그램'을 만들지 않고, Bytecode 프로그램에 있는 코드 그대로 한 줄, 한 줄 씩 읽어서 Interpret 방법으로 프로그램을 직접 바로 실행한다. 한 줄 한 줄 씩 Interpret 하는 과정에서 JVM은 사용자 User 가 사용하고 있는 H/W 와 OS 들에 맞추어 Bytecode를 '맞춤 실행' 해준다. 이 '맞춤 실행'은 Java의 큰 특징이기도 하다.

 

 Compile 과정 ( 2. Compile 과정 )

Source ( Java 언어로 짜여진 소스 프로그램 ) ]

|

<Compile : 컴파일, 고수준 언어를 저수준 언어인 기계어로>

|

Class ( Source 프로그램을 기계어로 해석한 Bytecode 프로그램 ]

|

 

 

 Interpret 과정 ( 3. Run 과정 ) 

Class ( Source 프로그램을 기계어로 해석한 Bytecode 프로그램 ]

|

<Interpret : 인터프리트, Bytecode 프로그램인 Class 프로그램을 '한 줄' 단위로 실행>

|

Execute* ( *.exe 확장자와 같은 개별적인 '실행 프로그램'을 만들지 않는다. ]

이유는 .class 확장자를 지닌 Bytecode 프로그램들만 갖고,

어느 OS 든지 JVM 을 이용해 그때 그때 Interpret (즉흥적인 프로그램 실행) 을 하기 때문이다.

 

 

 

Java의 특징 중 하나인 JVM 가 수행하는 Interpret 과정( Run 과정 )은 단순하게 하나의 과정으로 보이지만, 사실은 하나의 과정속에 여러 추가 과정들이 포함되어 있다. Interpret ( Run ) 의 자세한 과정은 다음과 같다.

 

   - Load

Class loader 가 .class 파일들을 primary memory 로 load 하고, 또한 실행하려는 프로그램에 필요한 다른 .class 파일들( C 언어에서 '라이브러리' 처럼 미리 정의 되어있는 클래스들 )도 load 한다.

( 이 .class 파일들은 당신의 시스템에 있는 disk 에서 load 될 수 도있고, 네트워크를 통해서도 load 될 수 있다. )

 

   - Verify

 

.class 파일들이 load 되면, bytecode verifier 가 load 된 class 파일들의 bytecode들을 보안을 위해 검사한다.

( Java 가 서버, 클라이언트 등의 네트워크 애플리케이션에 많이 사용되기 때문에 Verify 기능을 넣었다. Java가 처음 개발될 때, 인터넷에 대한 언어로 개발했었기 때문. 위에 '파란 박스' Java의 역사 참조 )

Java 는 네트워크를 통해 전달된 Java 프로그램들이 컴퓨터, 웜 바이러스등의 이유로 당신의 시스템이나 파일들을 망가트리지 않는다는 점에 대해 강력한 보안을 확신시켜준다.


   - Execute

 

JVM 가 특정 프로그램의 bytecode들을 읽어내 실행한다. JVM 은 가장 초기 버전에는 Interpreter 로써의 역할만을 수행하였지만, 후에 여러 실행 효율에 대한 이야기가 나오자, 성능 향상을 위해 Compiler 를 추가시켰는데, 그것이 Just-In-Time Compiler 이다. 그렇게 해서 최종적으로 JVM 은 Interpreter + JIT Compiler 이들을 혼합하여 사용한다.

 

Interpret 를 수행함과 동시에 bytecode 들을 분석하면서, hot spots ( bytecodes 중에서 자주 실행하는 부분 ) 을 찾는다. hot spots 을 찾으면 JIT Compiler 가 프로그램내에서 반복 실행되는 부분에 대해서 실행중에 기계어 명령으로 변환하여 처리한다. 이런 이유로 JIT Compiler 를 Java HotSpot Compiler 라고도 부르며, bytecodes 들을 제일 컴퓨터에서 하드웨어적인 기계어(underlying computer's machine language)로 컴파일한다. ( bytecode 도 기계어로 컴파일 된 결과였다. 컴파일된 bytecode 를 한번 더 underlying computer's machine language, 아주 하위 단계의 기계어로 컴파일 하는것 ) JVM이 이렇게 가장 하위 단계의 기계어로 컴파일된 부분들을 Interpret 도중에 다시 또 다른 부분에서 맞닥뜨리게 되면, 이미 컴파일 되어있는 (가장 하위 단계의)기계어가 이 부분을 매우 빠르게 실행시켜준다. 이렇게 함으로써 '반복 처리되는 명령어들을 매 명령어 마다 반복 Interpret 해서 발생하는 커다란 비효율성'을 줄일 수 있게 된다.

 

Java 개발에서 Compiler 와 JVM 은 JDK에 포함되어있고, 이는 Oracle 홈페이지에서 언제든지 최신버전을 받을 수 있다. 이 '전체 프로그래밍 과정'을 수행하기 위해서 우리는

 

1. Edit 을 위해 메모장을 사용하고,

2. Compile 을 위한 JDK 를 다운받아서 DOS-MS (cmd.exe) 내에서 컴파일을 수행하고,

3. JDK 에 포함되어있는 JVM 을 이용하여 컴파일되어 새로 생긴 Bytecode 들을 실행해야한다.

 

이렇게 3 가지 과정을 순서대로 각자 수행하기에는 귀찮은 것이 사실이다. 이 과정들을 하나의 프로그램에서 간편하게 처리할 수 있게 해주기 위해, 예전 C 언어의 등장에서 부터 사람들이 쉽게 개발을 할 수 있게 도와주는 많은 '통합 Tool' 들이 많이 존재해 왔다. 에디터, 컴파일러, 실행기, 디버거 등의 개발에 필요한 모든 환경들을 하나로 모아준 것을 Integrated Development Environments (통합 개발 환경, IDEs) 이라고 부른다. 필자가 어렸을 때 읽었던 '터보 C'책에 나온 '터보 C' 라는것도 이 IDE 이며, 현재 사람들이 많이들 사용하는 Visual Studio 등의 Visual 시리즈들도 이 IDE 이다. Java는 현재 시중에 나와 있는 많은 언어 IDE 와는 달리 free IDE 인 Eclipse 가 존재한다. 무료임에도 매우 강력한 환경을 제공해 주기에 사람들이 많이 애용하고, 앞으로 작성할 모든 Java 관련 포스트도 다 Eclipse 를 기반으로 할 것이다.  

 

다음은 JAVA 의 가장 중요한 특징인 객체 지향성 에 대해 살펴볼 것이다. Class 에 대한 내용은 단순히 객체 개념 에서 끝나는 것이 아니라 그에 관련된 기타 다른 내용들도 많기 때문에, 1 장 처럼 2장 - (1), (2) 이런 식으로 나누어서 설명할 생각이다.

'Programing > Java' 카테고리의 다른 글

Effective Java - Reference  (0) 2016.02.29
Garbage Collection  (0) 2015.11.15
Wrapper클래스,박싱(boxing),언박싱(unboxing)  (0) 2015.11.15
Collection  (0) 2015.11.15
JAVA 실행 과정  (0) 2015.11.14
JVM, JRE, JDK의 차이  (0) 2015.10.11

JVM

JVM은 자바 가상머신(Java Virtual Machine)의 약자이다.

JVM은 자바 소스코드로부터 만들어지는 자바 바이너리 파일(.class)을 실행할 수 있다. 또한 JVM은 플랫폼에 의존적이다. 즉 리눅스의 JVM과 윈도우즈의 JVM은 다르다. 단, 컴파일된 바이너리 코드는 어떤 JVM에서도 동작시킬 수 있다.

JVM은 다음과 같은 역할을 한다.

  • 바이너리 코드를 읽는다.
  • 바이너리 코드를 검증한다.
  • 바이너리 코드를 실행한다.
  • 실행환경(Runtime Environment)의 규격을 제공한다. (필요한 라이브러리 및 기타파일)

JRE

JRE는 자바 실행환경(Java Runtime Environment)의 약자이다.

JRE는 JVM 이 자바 프로그램을 동작시킬 때 필요한 라이브러리 파일들과 기타 파일들을 가지고 있다. JRE는 JVM의 실행환경을 구현했다고 할 수 있다.

JDK

JDK는 자바 개발도구(Java Development Kit)의 약자이다.

JDK는 JRE + 개발을 위해 필요한 도구(javac, java등)들을 포함한다.


출처: https://wikidocs.net/257

'Programing > Java' 카테고리의 다른 글

Effective Java - Reference  (0) 2016.02.29
Garbage Collection  (0) 2015.11.15
Wrapper클래스,박싱(boxing),언박싱(unboxing)  (0) 2015.11.15
Collection  (0) 2015.11.15
JAVA 실행 과정  (0) 2015.11.14
JVM, JRE, JDK의 차이  (0) 2015.10.11

+ Recent posts