Java - JVM이란? JVM 메모리 구조

초심으로 돌아갈 때가 된 것 같다. 오늘 포스팅할 내용은 JVM(Java Virtual Machine)이다. 사실 지금까지는 스킬 베이스의 공부만 해왔었다. 하지만 점점 개발을 하다보니 성능이라는 것에 굉장히 큰 관심이 생겼..

coding-start.tistory.com

이전 포스팅에서는 초심으로 돌아가 JVM에 대해 포스팅을 하였습니다. 이번 포스팅은 JVM의 Heap 영역에서 이루어지는 Garbage Collection에 다루어 보려고 합니다. 본 포스팅에서 Hotspot JVM의 GC를 다룰 것입니다.

 

Garbage Collection은 무엇일까? GC란 이미 할당된 메모리에서 더 이상 사용하지 않는 메모리를 해제하는 행동을 의미한다. 사용되지 않는 메모리의 대상은 Heap과 Method Area에서 사용되지 않는 Object를 의미한다. 그리고 소스상의 close()는 Object 사용중지 의사표현일 뿐 Object를 메모리에서 삭제하겠다는 의미가 아니다. 물론 개발자는 System.GC()를 명시적으로 호출할 수 있지만 주의해야 할 것이 있다면 해당 메소드 호출은 Full GC를 수행시키는 메소드이기 때문에 Stop-the-world 시간이 길고 무거운 작업이며 또한 반드시 즉시 수행한다는 보장도 없는 메소드이기에 사용을 지향하는 메소드이다.

 

GC에 대해 자세히 다루어보기 전에 과연 GC로 인한 문제점이 무엇이 있을까?

 

GC라는 자동화 메커니즘으로 인해 프로그래머는 직접 메모리를 핸들링 할 필요가 없게 되었다. 더불어 잘못된 메모리 접근으로 인한 Crash 현상의 소지도 없어지게 되었으니 더 없이 편리한 기능이 아닐 수 없다. 그러나 GC는 명시적인 메모리 해제보다 느리며 GC 순간 발생하는 Suspend Time(Stop-the-world) 시간으로 인해 다양한 문제를 야기시킨다.(애플리케이션 동작이 멈춰 클라이언트는 한순간 애플리케이션이 멈춰보인다.)

 

Root Set과 Garbage

Garbage Collection은 말 그대로 Garbage를 모으는 작업인데 여기서 Garbage란 사용되지 않는 Object, 즉 더 이상 참조되지 않는 Object를 뜻한다. 좀 더 자세히 설명하면 Object의 사용 여부는 Root Set과의 관계로 판단하게 되는데 Root Set에서 어떤 식으로든 참조 관계가 있다면 Reachable Object라고 하며 이를 현재 사용하고 있는 Object로 간주하며 그 밖의 Object는 Unreachable Object가 된다.

 

물론 위 그림에는 없지만 참조되지만 더 이상 사용되지 않는 Object인 Memory Leak Object도 존재한다. 뒤에서 정확히 다루어볼 것이다.

 

Root Set은 더 구체적으로 아래와 같이 3가지 참조 형태를 통해 Reachable Object를 판별한다.

  1. Local variable Section, Operand Stack에 Object의 참조 정보가 있다면 Reachable Object이다.
  2. Method Area에 로딩된 클래스 중 constant pool에 있는 참조 정보를 토대로 Thread에서 직접 참조하지 않지만 constant pool을 통해 간접으로 참조되고 있는 Object는 Reachable Object이다.
  3. 아직 메모리에 남아 있으며 Native Method Area로 넘겨진 Object의 참조가 JNI 형태로 참조 관계가 있는 Object는 Reachable Object이다.

위의 3가지 경우를 제외하면 모두 GC 대상이 된다.

 

위에서 설명한 Memory Leak 객체의 구체적인 사례를 살펴 볼 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Main{
    public static void main(String args[]){
        Leak lk = new Leak();
        for(int i=0;i<9000000;i++){
            lk.addList(a);
            lk.removeStr(a);
        }
    }
}
 
class Leak{
    ArrayList lst = new ArrayList();
 
    public void addList(int a){
        lst.add("abcdefg");
    }
 
    public void removeStr(int i){
        Object obj = lst.get(i);
        obj=nulll;
    }
}
cs

위의 코드는 어떻게 될까? 아마 OutOfMemoryException이 발생할 것이다. removeStr()을 통해 참조를 null로 바꾸고 있는 객체는 List에 들어있는 객체의 참조이지만 문자열 객체 그 자체가 아니기에 List 안에 있는 문자열 객체는 그대로 남아 있을 것이다. 하지만 사용자 입장에서는 삭제했다고 생각하고 더 이상 List에 들어있는 문자열을 사용하지 않을 것이다. 이것을 바로 Memory Leak한 Object라고 한다. 다른 표현으로는 Reachable but not Live라고 표현한다.

 

GC가 수행된 메모리의 해지는 할당된 그 자리에서 이루어지며 그로 인해 Heap 메모리 단편화 문제가 발생한다.(객체를 할당할 자리는 충분하지만 자리가 듬성듬성 나있고 연속적이지 않아 객체를 할당하지 못하는 현상) JVM에서는 이러한 단편화를 해소하기 위해 compaction 같은 알고리즘을 사용한다. 하지만 compaction은 비용이 큰 작업이기에 TLAB(Thread Local Allocation Block)을 사용하여 단편화 문제를 해결하기도 한다. 이것은 뒤에서 더 자세히 다룰 것이다. 여기까지 GC는 Root Set에서 참조되지 않는 메모리 자원을 반납하는 행위. 즉, 한정된 자원인 Heap이라는 메모리 공간을 재활용하려는 목적인 것이다. 

 

Hotspot JVM의 Garbage Collection

Hotspot JVM은 기본적으로 Generational Collection 방식을 사용한다. 즉, Heap을 Object Generation 별로 Young Area와 Old Area로 구분하며 Young Area는 다시 Eden Area와 Survivor Area 두개로 구분하여 사용된다.

 

GC 메커니즘은 경험적 지식(Weak Generational Hypothesis)으로 두 가지 가설을 두고 있는데 첫째로 대부분의 객체는 생성된 후 금방 Garbage가 된다. 둘째로 Old 객체가 Young 객체를 참조할 일은 드물다라는 것이다.

 

첫번째 가설을 보면 새로 할당되는 객체가 모인 곳은 메모리 단편화가 발생할 확률이 높다고 간주한다. 메모리 할당은 기존 객체 다음 주소에 할당하게되며 Garbage는 먼저 할당된 부분에서 많이 생길 것이다. Sweep(Mark되지 않은 객체 제거)을 하게되면 메모리 단편화가 발생하게 되며 이후 Compaction처럼 비용이 높은 작업을 해야한다. 이 때문에 객체 할당만을 위한 전용공간인 Eden Area를 만들게 된 것이고 Minor GC 당시 Live한 객체를 옮기는 Survivor Area를 따로 구성한 것이다.

 

Garbage를 추적하는 부분은 Tracing 알고리즘을 사용한다. 위쪽 그림에서도 설명한 RootSet에서 참조 관계를 추적하고 참조되고 있는 객체는 Marking 처리한다. 이러한 Marking 작업도 Young 영역에 국한 되는데 Marking 작업은 메모리 Suspend 상황에서 수행되기 때문이다. 만약 Old 영역까지 Marking 작업을 수행한다면 그만큼 Suspend(Stop-the-world)시간이 길어질 것이다. 하지만 만약 Old 영역에서 Young 영역의 객체를 참조하고 있다면 어떻게 해야할까? 분명 Marking 처리를 하지 않는다고 했는데? 이런 경우를 처리하기 위하여 Card Table이라는 것이 존재한다.

 

Card Table

Card Table이란 Old 영역의 메모리를 대표하는 별도의 메모리구조이다.(실제로 Old 영역의 일부는 Card Table로 구성됨) 만약 Young 영역의 객체를 참조하는 Old 영역의 객체가 있다면 해당 Old 영역의 객체의 시작주소에 카드를 Dirty로 표시하고 해당 내용을 Card Table에 기록한다.

 

 

이후 해당 참조가 해제되면 표시한 Dirty Card도 사라지게끔 하여 참조 관계를 쉽게 파악할 수 있게 한다. 이러한 Card Table 덕분에

Minor GC 과정에서 Card Table의 Dirty Card만 검색하면 빠르게 Old 영역에서 참조되는 Young 영역의 객체를 알 수 있다. 참고로 Card Table은 Old 영역 512byte당 1byte의 공간을 차지한다고 한다.

 

TLAB(Thread-Local Allocation Buffers)

 

Garbage Collection이 발생하거나 객체가 각 영역에서 다른 영역으로 이동할 때 애플리케이션의 병목이 발생하면서 성능에 영향을 주게 된다. 즉, T1이 할당하기 전까지 T2~T5는 락이 걸려 대기하게 될 것이다. 그래서 Hotspot JVM에서는 스레드 로컬 할당 버퍼(TLAB)라는 것을 사용한다. 이를 통해 각 스레드별 메모리 버퍼를 사용하면 다른 스레드에 영향을 주지 않는 메모리 할당 작업이 가능하게 된다. 하지만 TLAB도 최초 할당 때와 할당된 TLAB이 부족하여 새로이 할당을 받을 때에는 동기화 이슈가 발생한다. 하지만 객체 Allocation 작업 횟수에 비하면 동기화 이슈가 대폭 줄기 때문에 Allocation에 걸리는 시간은 상당히 줄어든다.

 

여기서 하나 더 그림의 특징이 있다. 스레드가 객체를 할당할때 메모리 주소의 맨 뒷 공간에 할당하는 것이 보인다. 이것은 Bump-the-pointer라는 기술로써 어딘가에 주소 맨 뒷 자리 정보를 캐싱해 놓고 매번 마지막 주소에 할당하는 방법인 것이다.

 

GC 대상 및 범위

GC 대상 Area는 Young, Old Generation과 Java 1.7 기준 Permanent Area인데 Hotspot JVM은 각 Generation 별로 각각 GC를 수행한다.(Java 1.8에서도 Static 변수, 상수 등은 Heap 메모리에 할당되므로 GC 대상이 된다.) Young Generation에서 발생하는 GC를 Minor GC라고 한다. Young Generation은 빈번하게 GC가 수행되는 영역이며 성숙된 Object는 Old Generation으로 Promotion된다. 여기서 성숙된 Object라 함은 특정 횟수 이상 참조되어 기준 Age를 초과한 Object를 말한다. Promotion 과정 중 Old Generation의 메모리도 충분하지 않은 경우면 GC를 수행하는 데, 이는 Full GC(Major GC)라고 한다. 해당 GC는 Young 영역보다 비교적 큰 메모리를 GC하는 과정이므로 Suspend(Stop the world) 시간이 긴 GC 과정이다. Perm 영역에서도 메모리가 부족할 수 있는 데, 이는 너무 많은 수의 클래스가 로딩되어 여유 공간이 없어진 경우이다. 이 경우에도 Young+Old에 여유공간이 많다고 해도 Full GC가 발생한다.

 

GC 관련 옵션

 

옵션 상세 설명
-Client Client Hotspot VM으로 구동
-Server Server Hotspot VM으로 구동

-Xms<Size>

-Xmx<Size>

Yound Generation의 최소,최대 크기를 설정
-Xss<Size> Stack 사이즈 설정
-XX:MaxNewSize=<Size>

Young Generation의 최대 크기 설정

1.4 버전 이후 NewRatio에 따라 자동 계산

<Java 1.7>

-XX:PermSize

-XX:MaxPermSize

Perm 영역의 초기 크기와 최대 크기(Default 64MB)를 설정

<Java 1.8>

-XX:MetaspaceSize

-XX:MaxMetaspaceSize

Metaspace 영역의 초기 크기와 최대 크기(Default OS 최대 자원 사용) 설정
-XX:SurvivorRatio=<value> 값이 n 이면 n:1:1(Eden:Survivor1:Survivor2=n:1:1) default 8
-XX:NewRatio=<value>

값이 n 이면 Young:Old 비율이 1:n

Client VM은 기본값이 8이며, Server VM은 2이다. 

Intel 계열 CPU를 사용하면 기본값은 12이다.

-XX:TargetSurvivorRatio=<value>

Minor GC는 Eden 영역 뿐만 아니라 Survivor 영역이 Full나도 발생한다. 해당 설정은 Survivor가 Minor GC를 유발하는 비율을 말하며 default=50이다.

즉, Survivor 영역이 50%만 차도 Minor GC가 발생한다.

높게 지정한다면 Survivor 영역 활용도가 높아져 Minor GC 횟수를 조금 줄일 수 있다.

-XX:MinHeapFreeRatio=<percent> 전체 Heap 대비 Free space가 지정된 수치 이하면 Heap을 -Xmx로 지정된 메모리까지 확장한다.(default=40)
-XX:MaxHeapFreeRatio=<percent> 전체 Heap 대비 Free space가 지정된 수치 이상이면 Heap을 -Xms까지 축소한다.(default=70)
-XX:MaxTenuringThreshold=<value> Value만큼 Survivor 영역을 이동하면 그 다음 Minor GC 시간때 Old 영역으로 Promotion한다.
-XX:+DisableExplicitGC System.gc() 함수를 통한 수동 Full GC를 방지한다.

 

Garbage Collector 종류

업무 요건이 복잡해지고 시스템이 점점 거대해지면서 최근에는 자연스럽게 JVM Heap Size 설정이 커지게 되었다. 그러나 그로 인해 GC로 인한 Suspend 현상도 이슈로 대두되게 되었다. Hotspot JVM은 기본적으로 Generation 별로 다른 Garbage Collection 알고리즘을 선택할 수 있다. 아래는 Java 6까지의 Hotspot JVM의 Garbage Collector를 정리한 표이다.

 

Garbage Collector Option

Young Generation 

Collector 알고리즘

Old Generation

Collector 알고리즘

Serial Collector -XX:+UseSerialGC Serial Serial Mark-Sweep-Compact
Parallel Collector -XX:+UseParallelGC Parallel Scavenge Serial Mark-Sweep-Compact
Parallel Compacting Collector -XX:+UseParallelOldGC Parallel Scavenge Parallel Mark-Sweep-Compact
CMS Collector -XX:+UseConcMarkSweepGC Parallel Concurrent Mark-Sweep
G1 Collector -XX:+UseG1GC

Young GC(Evacuation Pause)

Concurrent Mark - Marking

Current Mark - Remarking

Old Region Reclaim - Remarking

Old Region Reclaim - Evacuation Pause

Compaction Phase

 

  • Serial Collector : Client VM의 기본 collector로 한 개의 스레드가 serial로 수행한다.
  • Parallel Collector : 모든 자원을 투입하여 Garbage Collection을 빨리 끝내는 전략으로 대용량 Heap에 적합하다.
  • CMS Collector : Suspend Time을 분산시켜 체감 Pause Time을 줄이고자 할 때 사용한다.
  • G1 Collector : Generation을 물리적으로 구분하지 않고 Region이라는 단위로 쪼개서 관리를 한다. G1 Collector는 이론적으로 거의 Real Time에 가깝게 Suspend Time을 감소시킬 수 있다고 한다.

다양한 GC 알고리즘이 있다. 하지만 여기서 중요한 것은 뭐가 좋다 나쁘다 할 수 없다는 것이다. 애플리케이션의 성격 그리고 현 시스템이 어떠한 시스템이냐에 따라 각 알고리즘의 성능이 달라지기 때문이다. 많은 테스트를 통해 나의 애플리케이션이 어떠한 GC 알고리즘에 더 잘 맞을 지를 선택하는 것이 중요하다. 각 GC 알고리즘에 대해 살펴보자.

 

Serial Collector

Young / Old Generation 모두 Serial로 Single CPU를 사용한다. 즉 1 개의 스레드를 가지고 GC를 수행한다. Client VM의 기본 컬렉터이며 현재는 거의 사용되지 않는 컬렉터이다.

 

Parallel Collector

이 방식은 throughput collector로도 알려진 방식이다. 이 방식의 목표는 다른 CPU가 대기 상태로 남아 있는 것을 최소화 하자는 것이다. Serial Collector와 달리 Young 영역에서의 컬렉션을 병렬로 처리한다. Old 영역은 Serial Collector와 동일한 방식이다. 이는 많은 CPU 자원을 사용하는 반면에 GC의 부하를 줄이고 애플리케이션의 처리량을 증가시킬 수 있다.

 

이는 다수의 스레드가 동시에 GC를 수행하며 적용범위는 Young Area에 국한된다. Old Area는 Serial Mark-Sweep-Compact 알고리즘이 사용된다. 이 컬렉터는 Server VM의 JVM에서 기본 Collector이며 Client VM에서도 옵션 설정으로 사용이 가능하다. 단 CPU가 1개라면 해당 옵션은 무시된다.

 

Mark-Sweep-Compact : 우선 참조관계에 있는 Object를 마킹처리한다.(Mark) 그 이후 마킹처리되지 않은 Object를 자원 해제한다.(Sweep) 자원 해제 후 Heap 메모리의 단편화 해결을 위하여 Compact 작업을 통해 사용되고 있는 메모리와 사용되지 않는 메모리로 구분하여 몰아준다.

 

 

Eden, Survivor Area의 Live Object Copy 작업을 다수의 스레드가 동시에 수행한다.(Suspend 현상 발생) 하지만 투입한 자원만큼 Suspend 시간을 단축할 수 있다. 하지만 여기서 생각해야할 문제가 있다. 다수의 스레드가 Minor GC를 수행하다가 두 개이상의 스레드가 Old Generation 영역에 Promotion 작업을 하게 될 때가 있다. 시간의 간격이 있다면 괜찮지만 만약 동시에 Promotion을 한다면? Corruption이 발생할 수 있다. 하지만 Hotspot JVM은 이러한 문제를 해결하기 위하여 PLAB(Parallel Allocation Buffer)라는 Promotion Buffer를 두어 이러한 문제를 해결하고 있다.

 

 

PLAB(Parallel Allocation Buffer)

위의 그림을 보면 Old Generation으로 Promotion 작업을 두 개의 스레드가 동시에 수행하여 충돌이 나는 상황이 발생하였다. 이러한 문제를 해결하기 위하여 Minor GC를 수행하는 스레드마다 Old Generation의 일정부분을 PLAB이라는 버퍼를 할당하여 충돌상황을 피하고 있다. 하지만 이것도 문제가 아예 없는 것은 아니다. PLAB 때문에 어쩔 수 없이 Old Generation에 메모리 단편화가 생기곤 한다.

 

Parallel Collector 옵션

Parallel Collector의 기본 동작 방식은 애플리케이션 수행시간 대비 GC 수행시간은 default 1%이며, 애플리케이션 수행시간의 최대 90%를 넘지 못한다. 또한 Young Area는 확장할 때 20%씩 증가하고 5%씩 감소한다. GC를 수행하게 되면 최대 Heap Size 대비 5%의 여유 공간을 확보해야 한다.

 

옵션 상세 설명
-XX:+UseParallelGC Parallel Collector 사용 옵션이다.
-XX:ParallelGCThreads=개수 Parallel GC Thread 개수를 설정한다.(기본 값은 CPU 개수)
-XX:+AlwaysTenure

설정하면 Eden Aread의 Reachable Object는 바로 Old Generation으로 Promotion 된다. Parallel Collector 사용시에만 유효하다.

-XX:MaxTenuringThreadshold=0과 같은 설정이다.

-XX:+NeverTenure Survivor Area가 꽉 차는 경우를 제외하고 Object를 Old 영역으로 Promotion 안되게 설정하는 옵션이다. Parallel Collector에서만 유효한 설정이다.(age 자체를 없애서 승격이 안되게 한다.)
-XX:MaxGCPauseMillis=값 설정값 이하로 GC의 Pause Time을 유지하라는 설정이다.
-XX:GCTimeRatio=값 애플리케이션 수행시간 대비 전체 GC의 시간비율을 말한다. 19로 설정하면 1/(1+19)=5% default는 99(1%의 GC Time Ratio)
-XX:+UseAdaptiveSizePolicy GC 회수, Generation의 각 Area 비율, free Heap Size 등 통계 정보를 바탕으로 Young, Old Generation 크기를 자동으로 설정하는 옵션이다.
-XX:YoungGenerationSizeIncrement=값 Young Generation의 확장 비율 설정 옵션(기본값은 20,20%씩 증가)
-XX:AdaptiveSizeDecrementScaleFactor=값

Heap의 Growing Percent 대비 Shrink를 수행할 Ratio 의미 값이 4이고 Heap growing percent가 20 이면 20/4=5%가 된다.

기본값은 4이므로 20/4=5%가 된다.

-XX:GCHeapFreeLimit=값 GC로 확보해야 하는 최소 Free Space를 설정한다. 값은 최대 Heap Size에 대한 비율을 의미한다. Default는 5(%) Out Of Memory Exception 방지를 위한 설정이다.
-XX:MaxHeapFreeRatio=<percent> 전체 Heap 대비 Free Space가 지정 수치 이상이면 -Xms까지 축소하는 옵션이다(기본값 70)

 

CMS Collector

이 방식은 low-latency-collector로도 알려져 있으며, 힙 메모리 영역의 크기가 클 때 적합하다. CMS Collector는 Suspend Time을 분산하여 응답시간을 개선한다. 비교적 자원이 여유 있는 상태에서 GC의 Pause Time을 줄이는 목적이며 Size가 큰 Long Live Object가 있는 경우 가장 적합하다. Young Area에는 Parallel Copy 알고리즘, Old Area에는 Concurrent Mark-Sweep 알고리즘이 사용된다. Young 영역의 GC 알고리즘은 크게 다른 방식과 차이가 없다. 크게 차이가 나는 부분은 Old 영역의 알고리즘이다.

 

Old Area의 Concurrent Mark-Sweep 알고리즘

 

 

위의 그림을 보면 Serial Mark-Sweep-Compact Collector의 알고리즘과 비교하여 Suspend 시간을 분산하고 있는 것이 보인다. 

 

  • Initial Mark Phase 단계 : Single Thread만 사용한다. Suspend 시간이 있으며 애플리케이션에서 직접 참조되는 Live Object만 구별한다. 여기서 직접 참조되는 관계란 RootSet에서 한 단계의 참조관계를 말한다. 깊이가 있는 탐색이 아니라 Suspend 시간이 짧다.
  • Concurrent Mark Phase 단계 : Single Thread만 사용한다. 애플리케이션의 Working Thread와 GC Thread가 같이 수행된다. 즉, 애플리케이션 수행이 가능하다. Initial Mark phase에서 선별된 Live Object가 참조하고 있는 모든 Object를 추적해 Live 여부를 구별한다.
  • Remark Phase 단계 : Multi Thread가 사용되며 애플리케이션이 중지된다. 이미 Marking 된 Object를 다시 추적, Live 여부 확정한다. 이 단계에서는 모든 자원을 투입한다.(많은 자원을 투자한만큼 Suspend 시간이 적다.)
  • Concurrent Sweep Phase 단계 : Single Thread만 사용한다. 애플리케이션은 수행되고 최종 Live로 판명된 Object를 제외한 Dead Object를 지운다. 단 Sweep 작업만 하고 Compaction 작업은 수행 안 한다. 항상 Compaction 작업은 Heap의 Suspend를 전제로 하는데 반복된 Sweep은 단편화를 유발한다. 때문에 Free List를 사용하여 단편화를 줄이는 노력을 한다.

 

CMS Collector를 수행하면 아래와 같은 메모리 단편화 문제가 발생한다. 해당 문제를 해결하기 위해 Free List를 이용한다.

Free List?

Promotion 할당을 할 때 Young Area에서 승격된 Object와 크기가 비슷한 Old Area의 Free space를 Free List에서 탐색하게 된다. 또한 Promotion되는 Object Size를 계속 통계화하여 Free Memory 블록들을 붙이거나 쪼개서 적절한 Size의 Free Memory Chunk에 Object를 할당한다. 그러나 이러한 작업은 GC 수행 중 Young 영역에 부담을 준다. 그 이유는 Free List에서 적절한 Chunk 크기를 찾아 Allocation 해야 되기 때문이며 시간도 오래 걸린다. 즉 이말은 Young 영역에서의 체류 시간이 길어진다는 의미이다. 그렇다고 Compaction을 적용하기에도 해당 연산은 비용이 높다. 그때그때 상황에 맞게 사용하는 것이 좋을 듯 하다.

 

CMS Collector의 Floating Garbage 문제 : Old Area

CMS Collector는 Floating Garbage 문제가 있다. 만약 Initial 단계가 아니라 Concurrent Mark 단계에서 새롭게 Old 영역에 Promotion된 Object가 있다고 생각하자. 여기에서 Concurrent Mark 단계가 끝나기 전에 새로 Promotion된 Object의 참조관계가 끊어졌다. 이러면 과연 이 Object가 GC 대상이 될까? 아니다. CMS Collector는 initial mark 단계에서 마킹된 Object만 GC 대상이 되기 때문에 Concurrent Mark 단계에 Promotion되고 참조가 끊기 Object는 다음 GC 시간때 자원 해제된다. 이러한 문제를 Floating Garbage문제라고 한다. 이러한 문제를 해결하기 위해 CMS Collector는 통계정보를 계속 수집하여 적당한 GC Scheduling 시간을 잡는다. 가장 적당한 스케줄링 시간이 Remark 단계가 Minor GC 중간 지점인 스케줄링 시간이다.

 

CMS Collector 옵션

 

-XX:+UseConcMarkSweepGC

이 플래그는 애초에 CMS 컬렉터를 활성화하기 위해 필요하다. 기본적으로, HotSpot 은 처리율 컬렉터를 대신 사용한다.

-XX:+UseParNewGC

CMS 컬렉터를 사용할때, 이 플래그는 다중 쓰레드를 사용해 young generation GC의 parallel 실행을 활성화한다. 아마도 컨셉상 young generation GC 알고리즘에서 사용되는 것과 같기때문에 처리량 컬렉터에서 봤던 -XX:+UseParallelGC 플래그를 다시 사용하지 않는다는데 놀랄 것이다. 하지만, young generation GC 알고리즘과 old generation GC 알고리즘의 상호작용이 CMS 컬렉터에서 다르며 young generation 에서의 구현은 서로 달라 결국 이 둘 플래그는 다른 것이다.

주목할 것은 현재 JVM 버전에서 -XX:+UseConcMarkSweepGC를 지정하면 자동적으로 -XX:+UseParNewGC가 활성화 된다. 결과적으로, 만일 parallel young generation GC가 매력적이지 않다면 -XX:-UseParNewGC 세팅함으로써 비활성화할 필요가 있다.

-XX:+CMSConcurrentMTEnabled

이 플래그를 지정하면, 동시적 CMS 단계는 다중 쓰레드로 동작한다. 따라서 다중 GC 쓰레드는 모든 애플리케이션 쓰레들에서 parallel에서 작동한다. 이 플래그는 이미 기본값으로 활성화된다. 만약 serial 실행이 더 바람직하다면, 하드웨어 사양을 고려했을때, 다중 쓰레드 실행은 -XX:-CMSConcurrentMTEnabled 를 통해서 비활성화될 수 있다.

-XX:ConcGCThreads

-XX:ConcGCThreads=<value> 플래그는 (이전 버전에서 -XX:ParallelCMSThreads 으로 알려진) 동시적 CMS 단계가 동작할때에 사용할 쓰레드 개수를 정의한다. 예를들어, 값이 4면 CMS 주기의 모든 동시적 단계는 4개의 쓰레드를 사용해 동작한다는 뜻이다. 비록 높은 쓰레드의 개수가 동시적 CMS 단계의 속도를 높여줄수 있지만 추가적으로 동기화 오버헤드를 발생시킨다. 따라서 특정 애플리케이션을 다룰때, CMS 쓰레드의 수의 증가가 실제로 성능향상을 가지고 오는지 그렇지 않는지를 측정해야만 한다.

만일 이 플래그를 명시적으로 지정해주지 않으면 JVM은 처리량 컬렉터에서 알려진 -XX: ParallelGCThreads 플래그 값에 기반에 기본값 parallel CMS 쓰레드 수를 계산한다. 사용된 계산 공식은 ConcGCThreads = (ParallelGCThreads + 3)/4 이다. 따라서 CMS 컬렉터에서, -XX:ParallelGCThreads 플래그는 stop-to-world GC 단계에서만 영향을 주는게 아니라 동시성 단계에서도 영향을 준다.

요약을 하자면, CMS 컬렉터 실생에 다중쓰레드 설정을 위한 몇가지 방법이 있다. 이렇게 말하는 이유는, 먼저 CMS 컬렉터의 기본 세팅값을 사용해보고 튜닝이 필요하면 먼저 측정을하도록 권고하기 때문이다. 프로덕트 시스템에서(혹은 프로덕트와 유사한 테스트 시스템에서) 측정이 애플리케이션의 목표로하는 일시 정지 시간에 도달하지 않았다면 이러한 플래그를 통한 GC 튜닝은 고려해볼만 하다.

-XX:CMSInitiatingOccupancyFraction

처리량 컬렉터는 힙이 꽉 찼을때만 GC 주기를 시작한다. 예를들어, 새로운 객체를 할당할 공간이 없거나 객체를 old generation으로 승격시킬 공간이 없을때. CMS 컬렉터에서는 동시적 GC 동안에도 애플리케이션이 동작중여야하기 때문에 오랜시간을 기다리면 안된다. 따라서, 애플리케이션이 out of memory 되기전에 GC 주기를 끝내기 위해서 CMS 컬렉터는 처리량 컬렉터보다 일찍 GC 주기를 시작할 필요가 있다.

서로 다른 애플리케이션이면 객체 할당 패턴도 서로 다르며, JVM은 실제 객체 할당 및 비할당에 대한 런타임 통계를 수집하고 CMS GC 주기를 언제 시작할지 결정할때 사용한다. 이 과정을 부트스랩기, JVM은 첫번째 CMS 실행을 시작할때 힌트를 획득한다. 그 힌트는 -XX:CMSInitiatingOccupancyFraction=<value> 통해서 퍼센트로 old generation 힙 공간의 사용량으로 표시되어 지정될 수 있다. 예를들어 값이 75면 old generation의 75%를 획득했을때에 첫번째 CMS 주기를 시작하라는 의미다. 전통적으로 CMSInitiatingOccupancyFraction 의 기본값은 68 이다. (오랫동안 경험을 통해 얻은 수치다)

-XX+UseCMSInitiatingOccupancyOnly

우리는 런타임 통계에 기반해 CMS 주기를 시작할지 결정하지 않도록 JVM 에게 지시하기 위해 -XX+UseCMSInitiatingOccupancyOnly 를 사용할 수 있다. 대신, 이 플래그가 활성화된 때에, JVM은 첫음 한번만이 아닌 매번 CMS주기에서 CMSInitiatingOccupancyFraction 값을 사용한다. 그러나, 대부분의 경우 JVM이 우리 인간보다 GC 의사결정을 좀 더 잘한다는 것을 염두에 둬야 한다. 따라서, 이것은 합당한 이유가(ex, 측정을 통해 데이터를 얻었을때에) 있을때 뿐만아니라 애플리케이션에 의해서 생성된 객체의 생명주기에대해서 실제로 좋은 지식을 가지고 있는 경우에 이 플래그를 사용해야 한다.

-XX:+CMSClassUnloadingEnabled

처리량 컬렉터와 비교해, CMS 컬렉터는 기본적으로 permanent generation 에 GC를 수행하지 않는다. 만약 permanent generation GC가 필요하다면, -XX:+CMSClassUnloadingEnabled 통해서 활성화될 수 있다. 이전버전의 JVM에서는 추가적으로 -XX:+CMSPermGenSweepingEnabled 플래그 지정이 필요했었다. 주의할 것은, 이 플래그를 지정하지 않는다하더라도, 공간이 부족하게 되면 permanent generation 의 가비지 컬렉트를 시도할 것이지만 컬렉션은 동시적으로 수행되지 않을 것이다. – 대신, 다시 한번, 전체 GC가 동작할 것이다.

-XX:+CMSIncrementalMode

이 플래그는 CMS 컬렉터의 점진적 모드(incremental mode)를 활성화 한다. 점진적 모드는 애플리케이션 쓰레드에 완전히 양도(yield)되도록 동시적 단계를 주기적으로 잠시 멈추게 한다. 결론적으로, 컬렉터는 전체 CMS 주기를 완료시키기 위해서 아주 오랜시간을 가질 것이다. 따라서, 점진적 모드 사용은 일반적인 CMS 사이클에서 동작중인 컬렉터 쓰레드가 애플리케이션 쓰레드와 아주 많이 간섭이 발생되고 있다고 측정되어질경우에 유효하다. 이것은 동시적 GC 수용을 위해 충분한 프로세스를 활용하는 현대 서버 하드웨어에서 아주 드물게 발생된다.

-XX:+ExplicitGCInvokesConcurrent 와 -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses

오늘날, 가장 폭 널게 받아들여지는 최고의 기술은 애플리케이션에서 System.gc() 호출에 의해서 명시적으로 호출되는 GC를 차단하는 것이다. 이러한 조언이 사용되어지는 GC 알고리즘과 관련이 없다고 생각하고 있는데, CMS 컬렉터를 사용하고 있을때에 시스템 GC는 기본적으로 전체 GC를 발생시키는 아주 않좋은 이벤트이기 때문에 언급하고 넘어간다. 운좋게도, 기본 행동을 바꿀수 있는 방법이 있다. -XX:+ExplicitGCInvokesConcurrent 플래그는 JVM이 시스템 GC 요청이 있을때마다 전체GC 대신 CMS GC를 실행하도록 지시한다. 두번째 플래그인 -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses 는 시스템 GC가 요청될 경우에 추가로 permanent generation 을 CMSGC에 포함하도록 해준다. 따라서, 이러한 플래그의 사용으로, 우리는 예기치못한 시스템GC의 stop-to-world에 대해서 우리 스스로 보호할 수 있다.

-XX:+DisableExplicitGC

CMS 컬렉터 주제를 다루는 동안, 어떤 타입의 컬렉터를 사용하던지간에 시스템 GC 요청을 완벽하게 무시하도록 JVM에게 지시하게하는 -XX:+DisableExplicitGC 플래그를 언급할 좋은 기회다. 필자에게, 이 플래그는 더 이상 생각할 필요도 없이 모든 JVM 운영에서 안전하게 지정되어질수 있도록 기본 플래그 세트에 포함된다.

 

 

Parallel Compaction Collector

Parallel Collector의 Old Area에 새로운 알고리즘이 추가된 개념으로 multi CPU에서 유리하다. Old Area의 Collection 시간을 감소시켜 효율이 증가하나 몇몇 애플리케이션이 Large System을 공유해 CPU를 확실히 점유 못하면 오히려 제 성능을 발휘하지 못한다. 이 경우 Collection의 스레드 개수를 줄여서 사용한다.(-XX:ParallelGCThreads=n 옵션으로 조정) 다시 이야기하면 Young GC는 Parallel Collector와 동일하지만 Old 영역의 GC는 다음의 3단계를 거쳐 수행된다.

 

 

 

 

  • Mark Phase 단계 : 살아 있는 객체를 식별하여 표시해 놓는 단계
  • Summary Phase 단계 : 이전에 GC를 수행하여 컴팩션된 영역에 살아 있는 객체의 위치를 조사하는 단계
  • Compact Phase 단계 : 컴팩션을 수행하는 단계로 수행 이후에는 컴팩션된 영역과 비어 있는 영역으로 나뉜다.

Mark Phase 단계

Reachable Object를 체크하며 병렬작업으로 수행된다. Old Area의 Region(논리적 구역,2KB 정도의 Chunk)이라는 단위로 균등하게 나눈다. Region이 구분되면 Collection Thread들은 각각 Region 별로 Live Object를 Marking 한다. 이 때 Live Object의 Size, 위치 정보 등 Region 정보가 갱신된다. 이 정보들은 각 Region별 통계를 낼 때 사용한다.

 

 

Summary Phase 단계

하나의 스레드가 GC를 수행, 나머지는 애플리케이션을 수행, Summary 단계가 Mark 단계 결과를 대상으로 작업을 수행한다. Region 단위이며 GC 스레드는 Region의 통계 정보로 각 Region의 Density(밀도)를 평가한다. Density는 각 Region마다 Reachable Object의 밀도를 나타내는데 Density를 바탕으로 Dense prefix를 설정하게 된다. Dense prefix는 Reachable Object의 대부분을 차지한다고 판단되는 Region이 어디까지인가 구분하는 선이며 더불어 다음 단계의 대상을 구분해 준다. Dense prefix 왼편(메모리 번지가 작은 쪽)에 있는 Region은 이 후 GC 과정에서 제외, 나머지만 Compaction이 수행된다.

 

 

Hotspot JVM에서 Compaction은 보통 Sliding Compaction을 의미하는 데 이는 Live Object를 한쪽 방향으로 이동시킨다. 오랜 기간 참조된 Object는 더 오래 Heap에 머무를 가능성이 큰데 이를 전제로 어느 지점까지의 Object는 거의 Garbage되지 않을 확률이 높고 그게 왼쪽으로 갈 수록 높아진다. 즉 Parallel Compaction Collector는 Region별 Density를 평가하고 GC를 수행할 범위를 정하는(Dense prefix) 작업을 해 Compaction의 범위를 줄여 결과적으로 GC 소요시간을 줄인다. Dense prefix 설정 작업이 끝나면 Compaction의 대상이 되는 Region의 First Live Object 주소를 찾아 저장 작업을 수행한다.

 

Compaction Phase 단계

 

 

Heap을 잠시 Suspend 상태로 만들고 Thread들이 각 Region을 할당 받아 Compaction을 수행한다. Compaction 작업은 Garbage Object를 Sweep하고 Reachable Object를 왼편으로 몰아넣는 것이다. 작업은 Destination Region, Source Region을 각각 구별하여 작업을 수행한다. 위 그림에서 Dense prefix(3번째 Region의 시작이 Dense prefix였다.-summary 단계 그림 참조) 이후 Region 들이 Destination과 Source로 구분된다. 마지막 Region은 모두 Garbage Object 들로만 되어서 별도로 구분되지 않았다. Region마다 배정된 스레드가 Garbage Object를 Sweep 한다. Source Region 외 Region에는 Garbage Object가 사라지며 Destination Region에는 Live Object만 남게 되고 스레드-1이 Source Region의 Garbage들을 수거한다. 마지막으로 Source Region에 남겨진 Live Object는 Destination Region으로 이동한다.(Compact)

 

Parallel Compaction Collector 옵션

 

옵션 상세 설명
-XX:+UseParallelOldGC Parallel Compact Collector를 사용한다는 설정
-XX:+UseParallelOldGCCompacting

Parallel Compaction Collector 사용 시 설정한다.

Parallel Compaction 사용 여부를 설정한다.(기본값 true)

-XX:+UseParallelDensePrefixUpdate Summary phase 단계에서 Dense prefix를 매번 갱신 할지에 대한 설정이다.(기본값 true)

 

Garbage First Collector(G1GC)-(추후 정리)

CMS Collector에 비해 Pause 시간이 개선되었고 예측 가능한 게 장점이다. Young Area에 집중하면 효율이 좋으나 CMS처럼 Old Area의 단편화가 있으며 Free List 사용의 문제점과 Suspend 시간이 길어지는 현상등이 있다. G1은 물리적 Generation 구분을 없애고 전체 Heap을 1M 단위 Region으로 재편한다. Parallel Compaction Collector의 Region 단위 작업과 Incremental의 Train 알고리즘을 섞어 놓은 느낌이다. Mark 단계에서 최소한의 Suspend 시간으로 Live Object를 골라내는 것은 Parallel Compaction Collector와 비슷하다. Region 별로 순차적인 작업이 진행되고 Remember set을 이용한다. Garbage First 라는 의미는 Garbage로만 꽉 찬 Region부터 Collection을 시작한다는 의미로 발견 되자 마자 즉각 Collection한다. Garbage Object가 대부분인 Region의 경우 Live Object는 Old Region에서 다른 Old Region으로 Compaction이 이루어진다. G1 Collector에서 Young, Old Area는 물리적 개념이 아니고 Object가 Allocation되는 Region의 집합을 Young Generation, Promotion되는 Region의 집합을 Old Generation이라고 한다.

 

G1 Collector가 Region 내에 참조를 관리하는 방법은 Remember set을 이용한다. 전체 Heap의 5% 미만 공간을 각 Region의 참조정보를 저장하는 Remember set으로 할당된다. Remember set은 Region 외부에서 들어오는 참조 정보를 가지고 있다. 이로 인해 Marking 작업 시 trace 일량을 줄여줘 GC 효율을 높히게 된다.

 

 

기본적으로 Allocation 때 메모리 압박이 생기거나 임계값이 초과될 때마다 GC가 발생한다.

 

Young GC(Evacuation Pause)

GC는 Young Area부터 시작하며 Minor GC와 동일한 개념이다. Suspend Time이 존재하며 Multi Thread가 작업한다. Live Object는 Age에 맞게 Survivor Region, Old Region으로 Copy되며 기존 공간은 해지된다. 이후 새로운 Object가 할당되는 Young Region은 Survivor Region과 그 근처에 비어있는 Region이 된다.

 

Concurrent Mark phase(Mark -> Remark) : Old Area GC 시작

 

Marking 단계 : Single Thread로 수행하며 이전 단계 때 변경된 정보를 바탕으로 빠르게 Initial Mark를 진행한다.

Remarking 단계 : Suspend가 있고 전체 스레드가 동시에 작업한다. 각 Region마다 Reachable Object의 Density를 계산 한 후에 Garbage Region은 다음 단계로 안 넘어가고 바로 해지된다.

 

Concurrent Mark phase에서는 snapshot-at-the-beginning(SATB) Marking 알고리즘을 사용한다. 이는 GC를 시작할 당시 참조를 기준으로 모든 Live Object의 참조를 추적하는 방식이다. 단 Mark 작업은 Concurrent phase이므로 참조가 계속 변경된다. G1 Collector는 위 작업을 위해 write barrier를 이용, 각 참조의 생성/분리를 기록한다. 이 기록은 GC 시작 시 만들어진 snapshot과 비교되어 Marking 작업을 빠르게 수행한다.

 

Old Region reclaim phase(Remark -> Evacuation)

 

 

Remark 단계 : concurrent 작업, Multi Thread 방식으로 수행한다. GC를 위해 Live Object의 비율이 낮은 몇 개의 Region을 골라낸다.

Evacuation Pause 단계 : 독특하게 Young Area의 GC를 포함, 앞의 Remark 단계에서 골라낸 Old Region은 

 

아직 작성중 ㅠㅠ..

posted by 여성게
:

초심으로 돌아갈 때가 된 것 같다. 오늘 포스팅할 내용은 JVM(Java Virtual Machine)이다. 사실 지금까지는 스킬 베이스의 공부만 해왔었다. 하지만 점점 개발을 하다보니 성능이라는 것에 굉장히 큰 관심이 생겼다. Java의 성능에 핵심인 JVM 튜닝을 다루어 보기 전에 우선 JVM이 무엇인지 알아보자.

 

JVM이란?

JVM(Java Virtual Machine)을 어떻게 정의할 것인가? 우선 원어를 먼저 알아보자. 처음의 "Java"는 프로그램 언어인 Java를 의미한다. 그리고 Virtual Machine은 물리적인 형태가 아닌 소프트웨어로서의 개념을 의미한다. 즉, 소프트웨어적으로 Java 언어를 실행시키기 위한 Machine을 말하는 것이다. 결국 JVM은 정의된 스펙(벤더사마다 다른 JVM 구현을 가진다. 하지만 표준은 존재.)을 구현한 하나의 독자적인 프로세스 형태로 구동되는 Runtime Instance라고 할 수 있다. 따라서 JVM의 역할은 개발자들이 작성한 Java 프로그램을 실행시키는 하나의 데몬이라고 볼 수 있는 것이다.

 

아래그림은 JVM의 내부구조를 보여준다.

 

 

  • Java Source : 사용자가 작성한 Java 코드이다(.java)
  • Java Compiler : Java Source 파일을 JVM이 해석할 수 있는 Java Byte Code로 변경한다.
  • Java Byte Code : Java Compiler에 의해 컴파일된 결과물이다.(.class)
  • Class Loader : JVM 내로 .class 파일들을 로드하여 Runtime Data Areas에 배치한다.
  • Execution Engine : 로드된 클래스의 Bytecode를 해석(Interpret)한다.
  • Runtime Data Area : JVM이라는 프로세스가 프로그램을 수행하기 위해 OS에서 할당 받은 메모리 공간이다.

 

 

Java코드를 JVM으로 실행시키는 과정을 간단하게 설명하면, 자바 컴파일러에 의해 자바코드가 클래스 파일로 컴파일된 후 클래스로더로 해당 클래스 파일들을 로드 시킨다. 이후 로딩된 바이트코드를 실행엔진을 이용하여 해석(Interpret)하여 런타임 데이터 영역에 배치시킨다. 하지만 매번 인터프리터하여 한줄씩 해석하는 데에는 시간이 오래 걸리기 때문에 JIT(Just In Time) 컴파일러를 이용한다. JIT 컴파일러를 이용하지 않는다면 바이트코드를 사용할 때마다 인터프리터해서 사용해야 하기 때문에 실행시간이 굉장히 느리다. 하지만 적절한 시점에 JIT 컴파일러가 바이트코드를 컴파일하여 네이트브 코드로 캐싱해놓는 것이다. 여기서 컴파일한다는 것은 인터프리터처럼 한줄 단위로 컴파일하는 것이 아니라 실행하는 전체 코드를 컴파일하고 캐시하므로 이 또한 시간이 꽤 걸린다. 하지만 한번 컴파일된 이후 캐싱되니 그 이후로는 빠른 실행을 보장할 것이다.(한번 실행되고 말 것들은 인터프리터 하고 주기적으로 자주 수행되는 것들은 체킹을 통해 일정 횟수가 넘으면 JIT 컴파일러로 전체 컴파일한다.) 이렇게 해석된 녀석들은 Runtime Data Areas에 배치한다.

 

그렇다면 Runtime Data Areas는 어떠한 메모리 구조를 갖고 있을까? 한번 살펴보자!

 

 

  • Method Areas : 클래스, 변수, Method, static 변수, 상수 정보 등이 저장되는 영역이다.(모든 Thread가 공유한다.)
  • Heap Area : new 명령어로 생성된 인스턴스와 객체가 저장되는 구역이다.(Garbage Collection 이슈는 이 영역에서 일어나며, 모든 Thread가 공유한다.)
  • Stack Area : Java Method 내에서 사용되는 값들(매개변수, 지역변수, 리턴값 등)이 저장되는 구역으로 메소드가 호출될 때 LIFO로 하나씩 생성되고, 메소드 실행이 완료되면 LIFO로 하나씩 지워진다.(각 Thread별로 하나씩 생성된다.)
  • PC Register : CPU의 Register와 역할이 비슷하다. 현재 수행 중인 JVM 명령의 주소값이 저장된다.(각 Thread별로 하나씩 생성된다.)
  • Native Method Stack : 다른 언어(C/C++ 등)의 메소드 호출을 위해 할당되는 구역으로 언어에 맞게 Stack이 형성되는 구역이다.

Java Heap(Hotspot JVM의 Heap)

Java의 메모리 구조는 Heap이 전부는 아니다. Thread 공유의 정보는 Stack에 저장이 되고 Class나 Method 정보, Bytecode등은 Method Areas에 저장된다. Java Heap은 Instance, Array 객체 두 가지 종류만 저장되는 공간이다. 모든 Thread들에 의해 공유되는 영역이다. 같은 애플리케이션을 사용하는 Thread 사이에서는 공유된 Heap Data를 이용할 때 동기화 이슈가 발생할 수 있다.

JVM은 Java Heap에 메모리를 할당하는 메소드(ex.new 연산)만 존재하고 메모리 해제를 위한 어떤 Java Code를 직접적으로 프로그래머가 명시하지 않는다.(물론 있긴하지만 사용하지 않는 편이 낫다.) Java Heap의 메모리 해제는 오로지 Garbage Collection을 통해서만 수행한다.

 

지금 다루어볼 Heap의 구조는 Hotspot JVM의 Heap 구조를 다루어 볼 것이다. 하지만 Hotspot JVM Heap 구조는 Java 8 버전 전후로 구조가 바뀌었다. 해당 내용은 기술 면접에서도 나올 만한 내용이다. 왜냐? 지금은 대부분 Java 1.8 이상을 사용하고 있기 때문이다. 아래 그림을 참조해보자. 

 

Java 1.7 이전

 

Java 1.8 이후

 

달라진 부분이 Perm 영역이다. Metaspace 영역으로 대체된 것이 보인다. 이것은 뒤에서 설명할 것이다.

 

Hotspot JVM은 크게 Young Generation과 Old Generation으로 나누어져 있다. Young Generation은 Eden 영역과 Suvivor 영역으로 구성되는데 Eden 영역은  Object가 Heap에 최초로 할당되는 장소이며 Eden 영역이 꽉 차게 되면 Object의 참조 여부를 따져 만약 참조가 되어 있는 Live Object이면 Suvivor 영역으로 넘기고, 참조가 끊어진 Garbage Object 이면 그냥 남겨 놓는다. 모든 Live Object가 Survivor 영역으로 넘어가면 Eden 영역을 모두 청소한다.

 

Suvivor 영역은 말 그대로 Eden 영역에서 살아남은 Object들이 잠시 머무르는 곳이다. 이 Suvivor 영역은 두 개로 구성되는데 Live Object를 대피시킬 때는 하나의 Survivor 영역만 사용하게 된다. 이러한 전반의 과정을 Minor GC라고 한다.

 

Young Generation에서 Live Object로 오래 살아남아 성숙된 Object는 Old Generation으로 이동하게 된다. 여기서 성숙된 Object란 의미는 애플리케이션에서 특정 회수 이상 참조되어 기준 Age를 초과한 Object를 말한다. 즉, Old Generation 영역은 새로 Heap에 할당되는 Object가 들어오는 것이 아니라, 비교적 오랫동안 참조되어 이용되고 있고 앞으로도 계속 사용될 확률이 높은 Object들이 저장되는 영역이다. 이러한 Promotion 과정 중 Old Generation의 메모리도 충분하지 않으면 해당 영역에서도 GC가 발생하는데 이를 Full GC(Major GC)라고 한다.

 

Java 1.7 기준의 Perm 영역은 보통 Class의 Meta 정보나 Method의 Meta 정보, Static 변수와 상수 정보들이 저장되는 공간이다. 이 영역은 Java 1.8부터는 Native 영역으로 이동하여 Metaspace 영역으로 변경되었다.(다만, 기존 Perm 영역에 존재하던 Static Object는 Heap 영역으로 옮겨져 최대한 GC 대상이 될 수 있도록 하였다.)

 

조금 더 1.7과 1.8 메모리 구조의 변경에 대해 설명하자면, Java 1.8에서 JVM 메모리의 구조적인 개선 사항으로 Perm 영역이 Metaspace 영역으로 전환되고 기존 Perm 영역은 사라지게 되었다. Metaspace 영역은 Heap이 아닌 Native 메모리 영역으로 취급하게 된다. (이 말은 기존의 Perm은 Heap 안에 존재하는 것으로 알 수 있다.) Native 메모리는 Heap과 달리 OS레벨에서 관리 하는 영역이다.

 

Perm과 Metaspace를 표로 구분해 보았다.

구분 상세 구분 Perm Metaspace
저장 정보 클래스의 메타 정보 저장 저장
메소드의 메타 정보 저장 저장
Static 변수, 상수 저장 Heap 영역으로 이동
관리 포인트 메모리 관리(튜닝)

Heap 영역 튜닝

Perm 영역도 별도로 튜닝해야함

Heap 영역 튜닝

Native 영역 동적 조정

(별도 옵션으로 조절 가능)

GC 측면 GC 수행 대상 Full GC 수행 대상 Full GC 수행 대상
메모리 측면 메모리 크기(옵션)

-XX:PermSize

-XX:MaxPermSize

-XX:MetaspaceSize

-XX:MaxMetaspaceSize

 

Java 1.8로 넘어오면서 주의해야 할 점이 있다. 아래 명령어 결과를 보자.

Metaspace가 잡고 있는 Max Size가 약 16Exabyte이다. 이 수치는 64bit 프로세서가 취급할 수 있는 메모리 상한치라 할 수 있다. Metaspace 영역은 Native 메모리로 다루기 때문에 기본적으로 JVM에 의해 크기가 강제되지 않고 프로세스가 이용할 수 있는 메모리 자원을 최대한 활용할 수 있다고 본다. 만약 Classloader에 메모리 누수가 의심되는 경우 -XX:MaxMetaspaceSize를 지정할 필요성이 있다. 왜냐하면  최대 값을 정해놓지 않는다면 Metaspace 영역의 메모리가 계속해서 커질 것이고 결국은 서버가 뻑나는 경우가 생길 수 있다.

 

오늘은 여기까지 간단히 JVM 구조에 대해 다루어보았다. 다음 포스팅은 Heap에서 이루어지는 GC에 대한 포스팅을 할 예정이다. 자바 성능에 아주 중요한 요인중 하나인 GC는 튜닝 관점에서 아주 중요한 관리 포인트이기도 하다.

posted by 여성게
:

 

Archive for required library 와 같은 메시지와 함께 이클립스 빌드패스 문제가 있을 때가 있다. Update Project를 해보고, Maven clean & install을 해보아도 문제가 해결되지 않았다.(여기까지로 해결되는 경우도 종종있음)

해당 에러 메시지를 보면 특정 메이븐 경로에 있는 특정 라이브러리가 눈에 띌 것이다. 해당 경로를 들어가서 폴더를 싹 지운 후에 해당 프로젝트 우클릭->Maven->Update Project 하면 문제가 말끔히 해결된다.

posted by 여성게
:

 

RestTemplate으로 다른 API를 호출하고 특정 객체 타입으로 JSON을 parsing 하는 상황이었다. 그런데 해당 특정 객체는 내부적으로 Inner Class를 가지고 있는 상황이었는데, 아래와 같은 예외가 발생하였다.

 

예외:can only instantiate non-static inner class by using default no-argument constructor

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Getter
@Setter
@ToString
public class Outer {
    
    @Getter
    @Setter
    @ToString
    @NoArgsConstructor
    public class Inner1{
        
        private String[] a;
        private double[] b;
    }
    
    @Getter
    @Setter
    @ToString
    @NoArgsConstructor
    public class Inner2{
        
        private String[] a;
    }
    
    private long id;
    private Inner1[] a;
    private Inner2[] b;
    private int status;
    
}
cs

 

무슨 일 일까? 분명 매개변수 없는 생성자로 생성하였는데, 위와 같은 예외가 발생했다니... 원인은 바로 Inner Class가 static(정적)으로 선언되지 않는 한 단독(Outer 클래스를 참조하지 않고)으로 Inner Class의 디폴트 생성자를 호출해 인스턴스를 생성할 수 없는 것이다. 즉, 위와 같은 예외를 피하려면 Inner Class를 별도의 클래스로 생성하던가, 아니면 static Inner Class로 선언해주어야 한다. 

posted by 여성게
:

Java - lambda(람다) 간단한 사용법 !


람다란 무엇일까? 람다(lambda)란 간단하게 표현하면 메서드에 전달 가능한 함수형 인터페이스의 구현체이다.

그럼 여기서 함수형 인터페이스란 무엇인가? 함수형 인터페이스는 하나의 메소드만 선언되어 있는 인터페이스를 함수형 인터페이스라고 부른다.

이것의 예제 소스코드를 보면,

1
2
3
4
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}
cs

이것은 java.util.concurrent의 Callable 인터페이스이다. 이러한 인터페이스를 함수형 인터페이스라고 부른다.

(@FunctionalInterface 어노테이션을 붙이면 이 인터페이스가 함수형 인터페이스 조건에 어긋나면 예외가 발생)


여기서 이런 생각이 들 수 있다. 하나의 함수만 선언된 인터페이스가 무슨 소용인가..? 함수형 인터페이스는 무조건 우리가 매개변수로 전달한 람다식만 사용가능한 인터페이스인가? 아니다! 자바 1.8의 혁신적인 변화중 람다,스트림등등 이외에도 인터페이스 디폴트 메소드가 존재한다.


1
2
3
4
5
6
7
8
9
10
11
12
@FunctionalInterface
public interface Function<T, R> {
 
    R apply(T t);
 
    default <V> Function<V, R> compose(Function<super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
 
    ....
}
cs


위는 자바의 Function 인터페이스 코드의 일부를 발췌한 것이다. 위의 코드를 보면 이상한 생각이 들수도 있다.

분명 함수형 인터페이스는 하나의 메소드 선언만 갖는다고 했는데, 위의 인터페이스는 default라는 선언이 된 메소드를 갖고 있다.


사실 위에서 정의한 바를 정확히 얘기하면 함수형 인터페이스란 구현 해야될 메소드가 하나인 인터페이스를 말하는 것이다. 위의 default메소드의 기능이란 무엇인가 하면, 이전 인터페이스는 메소드 선언만 가능했다. 그런데 지금은 모든 인터페이스 구현체가 동일한 기능을 쓴다면 계속 반복해서 오버라이드하는 것이 아닌, 인터페이스에 구현 메소드를 아예 선언해 버리는 기능인 것이다. 이것이 default 메소드의 기능이다. 


그렇다면 위의 Function 인터페이스는 사용할때 구현해야하는 메소드를 하나만 갖는 함수형 인터페이스인 것이다.



Lambda(람다)에는 아주 다양한 함수형 인터페이스가 있다. 여기서는 몇가지의 함수형 인터페이스를 이용하여

사용법을 다루어볼 것이다.




BiConsumer<T,U>

Represents an operation that accepts two input arguments and returns no result.

BiFunction<T,U,R>

Represents a function that accepts two arguments and produces a result.

BinaryOperator<T>

Represents an operation upon two operands of the same type, producing a result of the same type as the operands.

BiPredicate<T,U>

Represents a predicate (boolean-valued function) of two arguments.

BooleanSupplier

Represents a supplier of boolean-valued results.

Consumer<T>

Represents an operation that accepts a single input argument and returns no result.

DoubleBinaryOperator

Represents an operation upon two double-valued operands and producing a double-valued result.

DoubleConsumer

Represents an operation that accepts a single double-valued argument and returns no result.

DoubleFunction<R>

Represents a function that accepts a double-valued argument and produces a result.

DoublePredicate

Represents a predicate (boolean-valued function) of one double-valued argument.

DoubleSupplier

Represents a supplier of double-valued results.

DoubleToIntFunction

Represents a function that accepts a double-valued argument and produces an int-valued result.

DoubleToLongFunction

Represents a function that accepts a double-valued argument and produces a long-valued result.

DoubleUnaryOperator

Represents an operation on a single double-valued operand that produces a double-valued result.

Function<T,R>

Represents a function that accepts one argument and produces a result.

IntBinaryOperator

Represents an operation upon two int-valued operands and producing an int-valued result.

IntConsumer

Represents an operation that accepts a single int-valued argument and returns no result.

IntFunction<R>

Represents a function that accepts an int-valued argument and produces a result.

IntPredicate

Represents a predicate (boolean-valued function) of one int-valued argument.

IntSupplier

Represents a supplier of int-valued results.

IntToDoubleFunction

Represents a function that accepts an int-valued argument and produces a double-valued result.

IntToLongFunction

Represents a function that accepts an int-valued argument and produces a long-valued result.

IntUnaryOperator

Represents an operation on a single int-valued operand that produces an int-valued result.

LongBinaryOperator

Represents an operation upon two long-valued operands and producing a long-valued result.

LongConsumer

Represents an operation that accepts a single long-valued argument and returns no result.

LongFunction<R>

Represents a function that accepts a long-valued argument and produces a result.

LongPredicate

Represents a predicate (boolean-valued function) of one long-valued argument.

LongSupplier

Represents a supplier of long-valued results.

LongToDoubleFunction

Represents a function that accepts a long-valued argument and produces a double-valued result.

LongToIntFunction

Represents a function that accepts a long-valued argument and produces an int-valued result.

LongUnaryOperator

Represents an operation on a single long-valued operand that produces a long-valued result.

ObjDoubleConsumer<T>

Represents an operation that accepts an object-valued and a double-valued argument, and returns no result.

ObjIntConsumer<T>

Represents an operation that accepts an object-valued and a int-valued argument, and returns no result.

ObjLongConsumer<T>

Represents an operation that accepts an object-valued and a long-valued argument, and returns no result.

Predicate<T>

Represents a predicate (boolean-valued function) of one argument.

Supplier<T>

Represents a supplier of results.

ToDoubleBiFunction<T,U>

Represents a function that accepts two arguments and produces a double-valued result.

ToDoubleFunction<T>

Represents a function that produces a double-valued result.

ToIntBiFunction<T,U>

Represents a function that accepts two arguments and produces an int-valued result.

ToIntFunction<T>

Represents a function that produces an int-valued result.

ToLongBiFunction<T,U>

Represents a function that accepts two arguments and produces a long-valued result.

ToLongFunction<T>

Represents a function that produces a long-valued result.

UnaryOperator<T>

Represents an operation on a single operand that produces a result of the same type as its operand.


위의 표는 자바 api가 제공하는 많은 함수형 인터페이스이다. 사실 위의 표 이외에도 Callable,Runnable 등의 많은 함수형 인터페이스가 존재한다.


이중 몇가지만 예제로 구현해 보았다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
public class LambdaTest {
 
    public static void main(String[] args) {
        /*
         * Predicate
         * 문자열을 받아서 해당 문자열이 빈 문자열인지를 반환
         */
        String str = "asd";
        System.out.println(lambdaIsEqual(p->p.isEmpty(), str));
        
        /*
         * Cunsumer
         * 인수를 각각 +1 해줌.
         */
        List<Integer> list = Arrays.asList(1,2,3,4,5);
        lambdaProcessParam( (Integer c)->{
                                        list.set(c-1,c+1);
                                        } , list);
        System.out.println(Arrays.toString(list.toArray()));
        
        /*
         * Function
         * 문자열 리스트를 받아서 각각의 문자열의 길이를 담은 리스트로 반환
         */
        List<String> list2 = Arrays.asList("ab","cds","ewwqd","a");
        List<Integer> result = lambdaConvertParam(list2, f->f.length());
        System.out.println(Arrays.toString(result.toArray()));
        
        /*
         * Supplier
         * 랜덤한 숫자를 반환한다.
         */
        System.out.println(lambdaSupplier(()->{
            return (int)(Math.random()*10)+1;
        }));
        
        /*
         * UnaryOperator
         * 숫자 리스트를 받아 각 숫자에 +2한 값을 담은 리스트로 반환
         */
        List<Integer> ele = Arrays.asList(1,2,3,4,5);
        List<Integer> result2 = lambdaUnaryOper(ele, uo->uo+2);
        System.out.println(Arrays.toString(result2.toArray()));
        
        /*
         * BinaryOperator
         * 두개의 문자열을 받아서 공백을 기준으로 두개의 문자열을 합하여 반환
         */
        String str1 = "yeoseong";
        String str2 = "yoon";
        System.out.println(lambdaBinaryOper(str1, str2, (bo1,bo2)->bo1+" "+bo2));
        
        /*
         * 하나의 정수와 문자열을 입력받아 정수와 문자열의 길이가 같은지 검사
         */
        System.out.println(lambdaBiPred(4"yoon", (bp1,bp2)->bp1 == bp2.length()));
        
        /*
         * 문자열 리스트에 인덱스한 숫자 번호를 붙여준다.
         */
        List<String> list3 = Arrays.asList("a","b","c","d");
        List<String> result4 = new ArrayList<>();
        lambdaBiConsumer(result4,list3, ()->(int)(Math.random()*10)+1, (bc1,bc2)->{
            bc1 = bc1.concat("-"+bc2+"");
            result4.add(bc1);
        });
        System.out.println(Arrays.toString(result4.toArray()));
        
        /*
         * lambda를 포함하면서 길이가 5이상인 문자열인가?
         */
        System.out.println(lambdaPredAnd("It's lambda", (String p1)->p1.contains("lambda"), (String p2)->p2.length()>5));
        
        /*
         * 숫자를 입력받아서 +1한 후에 *2를 수행한 숫자를 반환
         */
        System.out.println(lambdaFuncAndThen(1, f1->f1+1, f2->f2*2));
    }
    
    /**
     * boolean Predicate<T>
     * 하나의 인수를 받아서 적절한 로직을 처리한 후에 boolean을 반환한다.
     */
    public static <T> boolean lambdaIsEqual(Predicate<T> predicate,T t) {
        return predicate.test(t);
    }
    
    /**
     * void Consumer<T>
     * 하나의 인수를 받아서 인수를 소모한후 void를 반환.
     */
    public static <T> void lambdaProcessParam(Consumer<T> consumer,List<T> list) {
        list.forEach(e->consumer.accept(e));
    }
    
    /**
     * R Function<T>
     * <T,R>의 인수를 받아서 T타입을 R타입으로 변환후 반환한다.
     */
    public static <T,R> List<R> lambdaConvertParam(List<T> list, Function<T, R> f) {
        List<R> result = new ArrayList<>();
        list.forEach(c->{
            result.add(f.apply(c));
        });
        return result;
    }
    
    /**
     * T Supplier
     * 매개변수는 없고 T타입을 반환한다.
     * @return 
     */
    public static <T> T lambdaSupplier(Supplier<T> s) {
        return s.get();
    }
    /**
     * T타입의 매개변수를 받아 같은 타입의 T타입의 값을 반환
     */
    public static <T> List<T> lambdaUnaryOper(List<T> list,UnaryOperator<T> uo){
        List<T> result = new ArrayList<>();
        list.forEach(c->{
            result.add(uo.apply(c));
        });
        return result;
    }
    
    /**
     * T타입의 매개변수를 2개 받아서, T타입의 값으로 반환
     */
    public static <T> T lambdaBinaryOper(T a,T b,BinaryOperator<T> bo) {
        return bo.apply(a, b);
    }
    
    /**
     * T,R타입의 매개변수를 받아서 boolean 값을 반환
     */
    public static <T,R> boolean lambdaBiPred(T t,R r,BiPredicate<T, R> bp) {
        return bp.test(t, r);
    }
    
    /**
     * T,R타입의 매개변수를 받아서 적절한 처리를 한다. void 반환
     */
    public static <T,R> void lambdaBiConsumer(List<T> result,List<T> t , Supplier<R> r , BiConsumer<T, R> bc) {
        t.forEach(c->{
            bc.accept(c,r.get());
        });
    }
    
    /**
     * Predicate and/or/negate
     * 두개 이상의 Predicate를 and로 묶을 수 있다.
     */
    public static <T> boolean lambdaPredAnd(T t,Predicate<T> p1,Predicate<T> p2) {
        return p1.and(p2).test(t);
    }
    
    /**
     * Function andThen,compose
     * andThen a.andThen(b) a를 먼저 수행한 결과를 b의 함수의 입력으로 가져간다.
     * compose a.compose(b) b를 먼저 수행한 결과를 a의 함수의 입력으로 가져간다.
     */
    public static <T> T lambdaFuncAndThen(T t, Function<T, T> f1, Function<T, T> f2) {
        return f1.andThen(f2).apply(t);
    }
}
 
cs




이제는 람다에서 알아야할 몇가지 정보가 있다. 

1) 예외, 람다, 함수형 인터페이스의 관계 - 함수형 인터페이스는 확인된 예외를 던지는 동작을 허용하지 않는다. 즉, 예외를 던지는 람다 표현식을 만드려면 확인된 예외를 선언하는 함수형 인터페이스를 직접 정의하거나 람다를 try/catch 블록으로 감싸야한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
@FunctionalInterface
public interface Lamda{
    T process(T t) throws Exception;
}
 
 
Function<BufferedReader, String> f = 
    (BufferedReader b)-> {
        try{
            return b.readLine();
        }catch(IOException e){
            throw new RuntimeException(e);
        }
    };
cs


2)형식 추론 - 위의 예제를 보면 매개변수로 전달하는 람다의 인자에는 인자의 형식을 캐스팅하지 않는다. 인자의 형식은 선언된 메소드에서 내부적으로 추론해낸다.


3)람다의 조합 - Predicate(and,or,negate),Function(andThen,compose).... 람다식끼리 조합해서 더 제한적인 조건 혹은, 더 많은 처리를 파이프라인처럼

묶을 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    /**
     * Predicate and/or/negate
     * 두개 이상의 Predicate를 and로 묶을 수 있다.
     */
    public static <T> boolean lambdaPredAnd(T t,Predicate<T> p1,Predicate<T> p2) {
        return p1.and(p2).test(t);
    }
    
    /**
     * Function andThen,compose
     * andThen a.andThen(b) a를 먼저 수행한 결과를 b의 함수의 입력으로 가져간다.
     * compose a.compose(b) b를 먼저 수행한 결과를 a의 함수의 입력으로 가져간다.
     */
    public static <T> T lambdaFuncAndThen(T t, Function<T, T> f1, Function<T, T> f2) {
        return f1.andThen(f2).apply(t);
    }
cs


위의 예제에도 나왔지만 다시 한번 설명하기 위해 발췌했다. 이렇게 람다식끼리 조건부로 연결가능하다. 


이상 자바8의 람다에 대한 설명이었다. 사실 람다에 대해 설명하지 못한 것이 훨씬 많다. 내가 설명한 것은 람다의 일부일 것이다. 하지만 이번 포스팅에서는

람다란 무엇이고 어떤 식으로 접근하여 어떻게 사용할 것인가를 설명하기 위한 포스팅이었기에 이 글을 읽고 간단한 사용법을 익혀

나중에 응용해 나갔으면 하는 생각에 더 복잡하고 많은 설명을 하지 않았다.

posted by 여성게
:

Java - Collections.rotate() 란?



만약 List 객체에 [1,2,3,4,5] 요소들이 들어있다고 생각해보자. 

여기에서 Collections.rotate(list,2) 메소드를 호출한다면 맨뒤의 요소를 하나씩 두번꺼내서 맨앞 요소자리에 넣고

다른 요소들은 뒤로 한칸씩밀리게 되는 것이다.


이것을 Step으로 표현하면


1
2
3
4
5
6
7
8
9
10
@Test
public void CollectionsRotate(){
    List<Integer> list = new ArrayList<>();
    list.addAll(Arrays.asList(new Integer[] {1,2,3,4,5}));
    
    Collections.rotate(list, 1);
    System.out.println(Arrays.toString(list.toArray()));
    Collections.rotate(list, 1);
    System.out.println(Arrays.toString(list.toArray()));
}
cs




Collections.rotate(list,2)를 step을 두단계로 나누어서 결과를 출력하기 위해 Collections.rotate(list,1)을 두번 출력했다.

이것들의 결과는

>[5,1,2,3,4]

>[4,5,1,2,3]


이라는 결과를 출력하게된다. 이것은 쉽게 말하면 마지막 요소를 꺼내서 맨앞 요소자리로 넣는 것이고, 나머지 요소들은 방금꺼낸 요소의

자리까지 한칸 밀리게 되는 것이다.

posted by 여성게
:

Java - ThreadLocal 이란? 쓰레드로컬 사용법!



ThreadLocal(쓰레드로컬)이란?





쓰레드로컬이란 간단히 얘기하면 하나의 스레드의 작업 흐름동안에 전역변수처럼 무엇인가를 저장하여 사용할수 있다.
일반 변수의 수명은 특정 코드 블록(예, 메서드 범위, for 블록 범위 등) 범위 내에서만 유효하다.

{
    int a = 10;
    ...
   // 블록 내에서 a 변수 사용 가능
}
// 변수 a는 위 코드 블록이 끝나면 더 이상 유효하지 않다. (즉, 수명을 다한다.)


반면에 ThreadLocal을 이용하면 쓰레드 영역에 변수를 설정할 수 있기 때문에, 특정 쓰레드가 실행하는 모든 코드에서 그 쓰레드에 설정된 변수 값을 사용할 수 있게 된다. 아래 그림은 쓰레드 로컬 변수가 어떻게 동작하는 지를 간단하게 보여주고 있다. 



위 그림에서 주목할 점은 동일한 코드를 실행하는 데, 쓰레드1에서 실행할 경우 관련 값이 쓰레드1에 저장되고 쓰레드2에서 실행할 경우 쓰레드2에 저장된다는 점이다.



ThreadLocal의 기본 사용법

ThreadLocal의 사용방법은 너무 쉽다. 단지 다음의 네 가지만 해 주면 된다.
  1. ThreadLocal 객체를 생성한다.
  2. ThreadLocal.set() 메서드를 이용해서 현재 쓰레드의 로컬 변수에 값을 저장한다.
  3. ThreadLocal.get() 메서드를 이용해서 현재 쓰레드의 로컬 변수 값을 읽어온다.
  4. ThreadLocal.remove() 메서드를 이용해서 현재 쓰레드의 로컬 변수 값을 삭제한다.

아래 코드는 ThreadLocal의 기본적인 사용방법을 보여주고 있다.

// 현재 쓰레드와 관련된 로컬 변수를 하나 생성한다.
ThreadLocal<UserInfo> local = new ThreadLocal<UserInfo>();

// 로컬 변수에 값 할당
local.set(currentUser);

// 이후 실행되는 코드는 쓰레드 로컬 변수 값을 사용
UserInfo userInfo = local.get();


위 코드만으로는 ThreadLocal이 어떻게 동작하는 지 잘 이해가 되지 않을테니, 구체적인 예제를 이용해서 ThreadLocal의 동작 방식을 살펴보도록 하겠다. 먼저 ThreadLocal 타입의 static 필드를 갖는 클래스를 하나 작성해보자.

public class Context {
    public static ThreadLocal<Date> local = new ThreadLocal<Date>();
}

이제 Context 클래스를 사용해서 쓰레드 로컬 변수를 설정하고 사용하는 코드를 작성할 차례이다. 아래는 코드의 예이다.

class A {
    public void a() {
        Context.local.set(new Date());
        
        B b = new B();
        b.b();

        Context.local.remove();
    }
}

class B {
    public void b() {
        Date date = Context.local.get();

        C c = new C();
        c.c();
    }
}

class C {
    public void c() {
        Date date = Context.local.get();
    }
}

위 코드를 보면 A, B, C 세 개의 클래스가 존재하는데, A.a() 메서드를 호출하면 다음 그림과 같은 순서로 메서드가 실행된다.


위 그림에서 1~10은 모두 하나의 쓰레드에서 실행된다. ThreadLocal과 관련된 부분을 정리하면 다음과 같다.

  • 2 - A.a() 메서드에서 현재 쓰레드의 로컬 변수에 Date 객체를 저장한다.
  • 4 - B.b() 메서드에서 현재 쓰레드의 로컬 변수에 저장된 Date 객체를 읽어와 사용한다.
  • 6 - C.c() 메서드에서 현재 쓰레드의 로컬 변수에 저장된 Date 객체를 읽어와 사용한다.
  • 9 - A.a() 메서드에서 현재 쓰레드의 로컬 변수를 삭제한다.

위 코드에서 중요한 건 A.a()에서 생성한 Date 객체를 B.b() 메서드나 C.c() 메서드에 파라미터로 전달하지 않는다는 것이다. 즉, 파라미터로 객체를 전달하지 않아도 한 쓰레드로 실행되는 코드가 동일한 객체를 참조할 수 있게 된다.



ThreadLocal의 활용

ThreadLocal은 한 쓰레드에서 실행되는 코드가 동일한 객체를 사용할 수 있도록 해 주기 때문에 쓰레드와 관련된 코드에서 파라미터를 사용하지 않고 객체를 전파하기 위한 용도로 주로 사용되며, 주요 용도는 다음과 같다.

  • 사용자 인증정보 전파 - Spring Security에서는 ThreadLocal을 이용해서 사용자 인증 정보를 전파한다.
  • 트랜잭션 컨텍스트 전파 - 트랜잭션 매니저는 트랜잭션 컨텍스트를 전파하는 데 ThreadLocal을 사용한다.
  • 쓰레드에 안전해야 하는 데이터 보관

이 외에도 쓰레드 기준으로 동작해야 하는 기능을 구현할 때 ThreadLocal을 유용하게 사용할 수 있다.

ThreadLocal 사용시 주의 사항


쓰레드 풀 환경에서 ThreadLocal을 사용하는 경우 ThreadLocal 변수에 보관된 데이터의 사용이 끝나면 반드시 해당 데이터를 삭제해 주어야 한다. 그렇지 않을 경우 재사용되는 쓰레드가 올바르지 않은 데이터를 참조할 수 있다.


▶︎▶︎▶︎자바캔


posted by 여성게
:

Java - You need to run build with JDK or have tools.jar on the classpath 오류




You need to run build with JDK or have tools.jar on the classpath. 

If this occures during eclipse build make sure you run eclipse under JDK as well 

(com.myseam.maven:apt-maven-plugin:1.1.3:process:default:generate-sources)


만약 위와 같은 에러가 pom.xml에 났다면?






project folder 우클릭 > Properties > Java Build Path > Libraries Tab 에서 JRE System Library를 현재 로컬에 

설치되어 있는 JDK의 패스로 잡아줍니다.


그리고 이클립스 eclipse.ini 파일을 열고 -vm 옵션을 사용해서 실제 JDK가 설치된 경로를 추가합니다.

여기서 -vm 위치가 중요한데 다음과 같이 -vmargs 위에 해당 경로를 추가합니다.


....

-vm

$JDKPATH/bin/javaw.exe

-vmargs

...


이클립스를 재실행하고 Maven> Update Project...를 선택해서 해당 프로젝트를 클릭하고

Update 해줍니다.



posted by 여성게
: