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 여성게
:

템플릿 메소드 패턴(template method pattern)은 소프트웨어 공학에서 동작 상의 알고리즘의 프로그램 뼈대를 정의하는 행위 디자인 패턴이다. 알고리즘의 구조를 변경하지 않고 알고리즘의 특정 단계들을 다시 정의할 수 있게 해준다.

 

템플릿(template)은 하나의 '틀'을 의미한다. 하나의 틀에서 만들어진 것들은 형태가 다 같다. 이런 틀 기능을 구현할 때는 template method 패턴을 이용할 수 있다. 이는 상속의 개념이 있는 상위 클래스와 하위 클래스의 구조에서 표현할 수 있다. 일반적으로 상위 클래스(추상 클래스)에는 추상 메서드를 통해 기능의 골격을 제공하고, 하위 클래스(구체 클래스)의 메서드에서는 세부 처리를 구체화한다.

이처럼 상위 클래스에서는 추상적으로 표현하고 그 구체적인 내용은 하위 클래스에서 결정되는 디자인 패턴을 template method 패턴이라고 한다. 상속의 개념처럼 template method 패턴도 코드 양을 줄이고 유지보수를 용이하게 만드는 역할을 한다. 따라서 유사한 서브 클래스가 존재할 때 template method 패턴을 사용하면 매우 유용하다.

 

예를 한번 들어보자. 음료를 만들기 위한 클래스가 하나 있으면 음료수를 만들기 위해서는 1)컵을 준비한다. 2)물을 붓는다 3)첨가물을 넣는다. 4)음료를 내어드린다. 이렇게 4가지의 음료 만드는 과정이 있다. 1,2,4번 과정은 모든 음료를 만드는 데 공통적인 과정이라면 3번은 어떤 음료인가에 따라 첨가물이 달라질 것이다. 예를 들면 커피면 커피 가루, 홍차면 홍차 가루를 넣을 것이다.

 

이렇게 변경되는 로직부분을 추상 메소드로 만들어 놓고 상위 클래스에서는 알고리즘의 전체적인 틀을 만들고 하위 클래스에서는 구체적인 알고리즘의 일부를 구현하는 것이다.

 

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
/**
 * 공통 기능을 구현하고 세부 기능은 추상화한 추상클래스(음료제작)
 * @author yun-yeoseong
 * 
 */
public abstract class TemplateMethodPattern {
    
    public final void makeBeverage() {
        prepareCup();
        prepareWater();
        additive();
        finish();
    }
    
    /**
     * 공통 메소드
     */
    private void prepareCup() {
        System.out.println("컵을 준비한다.");
    }
    
    /**
     * 공통 메소드
     */
    private void prepareWater() {
        System.out.println("물을 붓는다.");
    }
    
    /**
     * 실제 구현이 필요한 부분
     */
    abstract void additive();
    
    /**
     * Hook 메소드, 서브클래스에서 구현이 필요하다면 오버라이드 해도된다.
     * 하지만 꼭 오버라이드가 강제는 아니다.
     */
    private void hookMethod() {
        System.out.println("hook method");
    }
    
    /**
     * 공통 메소드
     */
    private void finish() {
        System.out.println("음료를 내어드린다.");
    }
    
}
 
/**
 * 템플릿 추상 클래스를 상속하는 서브 클래스
 * 세부내용을 구현한다.
 * @author yun-yeoseong
 *
 */
public class SubClassA extends TemplateMethodPattern{
 
    @Override
    void additive() {
        System.out.println("커피가루를 넣는다.");
    }
    
    
}
 
/**
 * 템플릿 추상 클래스를 상속하는 서브 클래스
 * 세부내용을 구현한다.
 * @author yun-yeoseong
 *
 */
public class SubClassB extends TemplateMethodPattern{
 
    @Override
    void additive() {
        System.out.println("홍차가루를 넣는다.");
    }
    
    
}
 
cs

 

위와 같이 템플릿 메소드 패턴을 구현할 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestMain {
 
    public static void main(String[] args) {
        
        TemplateMethodPattern template1 = new SubClassA();
        TemplateMethodPattern template2 = new SubClassB();
        
        template1.makeBeverage();
        System.out.println("=========================");
        template2.makeBeverage();
        
    }
 
}
cs

 

posted by 여성게
:

싱글톤(singleton)은 '단독 개체', '독신자'라는 뜻 말고도 '정확히 하나의 요소만 갖는 집합' 등의 의미가 있다. singleton 패턴은 객체의 생성과 관련된 패턴으로서 특정 클래스의 객체가 오직 한 개만 존재하도록 보장한다. 즉 클래스의 객체를 하나로 제한한다. 프로그램에서 이런 개념이 필요할 때는 언제일까? 프린터 드라이버의 예를 들어보자.

 

여러 컴퓨터에서 프린터 한 대를 공유하는 경우, 한 대의 컴퓨터에서 프린트하고 있을 때 다른 컴퓨터가 프린트 명령을 내려도 현재 프린트하는 작업을 마치고 그다음 프린트를 해야지 두 작업이 섞여 나오면 문제가 될 것이다. 즉 여러 클라이언트(컴퓨터)가 동일 객체(공유 프린터)를 사용하지만 한 개의 객체(프린트 명령을 받은 출력물)가 유일하도록 상위 객체가 보장하지 못한다면 singleton 패턴을 적용해야 한다. 이처럼 동일한 자원이나 데이터를 처리하는 객체가 불필요하게 여러 개 만들어질 필요가 없는 경우에 주로 사용한다.

 

사실 싱글톤을 구현하는 방법은 몇 가지 존재한다. 하지만 오늘 소개할 싱글톤 패턴 코드는 가장 많이 사용하고 안전한 싱글톤 객체를 만드는 코드 한가지만 다루어 볼것이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {
    
    private Singleton() {}
    
    private static class LazyHolder{
        public static final Singleton SINGLETON=new Singleton();
    }
    
    public static Singleton getInstance() {
        return LazyHolder.SINGLETON;
    }
    
}
cs

 

위 코드는 싱글톤으로 생성될 객체를 Lazy Loading 하는 코드이다. 클래스가 로딩될때 LazyHolder라는 내부 클래스는 로딩되지 않는다. 이유는 LazyHolder라는 객체를 참조하여 호출하는 코드가 존재하지 않기 때문이다. 그 말은 진짜 Singleton 객체가 필요하여 getInstance()를 호출할때 LazyHolder 클래스가 로딩되는 것이다. 바로 필요할때 객체를 생성한다는 장점이 있다.

 

또 하나의 장점이 있다. 해당 코드는 다른 싱글톤 생성 코드와 달리 싱글톤 인스턴스가 NULL인지 체크하는 것과 synchronized 블럭을 사용하여 동기화하는 부분도 존재하지 않는다. 왜 일까? 그러면 과연 멀티 스레드 환경에서 안전할까? 안전하다! JVM에서 클래스를 로딩하는 과정에서는 멀티 스레드 환경에서도 안전한 동기화 환경을 제공하기 때문이다! 즉, LazyHolder 클래스가 로딩되는 과정에 static 한 SINGLETON 인스턴스를 생성하기 때문에 싱글톤 객체 생성과정에서 별도의 동기화 처리를 하지 않아도 클래스 로딩되는 과정 자체가 JVM에서 동기화 환경을 제공하기 때문에 멀티 스레드 환경에서도 안전한 것이다!

 

LazyHolder를 이용한 싱글톤 객체 생성 시 장점

  1. 싱글톤 객체가 진짜 필요할 때까지 초기화를 늦춘다.(Lazy Loading)
  2. JVM 클래스 로딩 타임때 싱글톤 객체를 생성하므로 멀티 스레드 환경에 안전한 동기화 이슈를 JVM이 직접 해결해준다.

싱글톤 객체를 생성할 때는 위의 방법을 이용하자!

 

 

기타 싱글톤 객체 생성 코드

나머지 방법들은 멀티 스레드에 안전하지 않으면서도 불필요한 Lock을 잡고 있는 방법들이기 때문에 사용을 권장하지 않는다.

 

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
public class Singleton_methodSync {
    
    private static Singleton_methodSync instance;
 
    private Singleton_methodSync() {}
    
    public static synchronized Singleton_methodSync getInstance() {
        if(instance == null) {
            instance = new Singleton_methodSync();
        }
        return instance;
    }
    
}
 
//Double Checked Lock
public class Singleton_methodSync2 {
    
    private static Singleton_methodSync2 instance;
    
    private Singleton_methodSync2() {}
    
    public static Singleton_methodSync2 getInstance() {
        if(instance == null) {
            synchronized (Singleton_methodSync2.class) {
                if(instance==null) instance = new Singleton_methodSync2();
            }
        }
        return instance;
    }
}
 
//이른 초기화
public class Singleton_precreate {
    
    private static Singleton_precreate instance = new Singleton_precreate();
    
    private Singleton_precreate() {    }
    
    public static Singleton_precreate getInstance() {
        return instance;
    }
    
}
 

public class Singleton_notsafety {        

private static Singleton_methodSync instance;

    private Singleton_methodSync() {}    

    public static Singleton_methodSync getInstance() {

        if(instance == null) {

            instance = new Singleton_methodSync();

        }

        return instance;

    }    

}

 cs
posted by 여성게
:
인프라/운영체제 2019. 7. 28. 18:55

2019/07/28 - [운영체제] - 운영체제 - 병행 프로세스란?

 

운영체제 - 병행 프로세스란?

2019/07/27 - [운영체제] - 운영체제 - 프로세스(Process)란? 프로세스상태,Context Switching 운영체제 - 프로세스(Process)란? 프로세스상태,Context Switching 프로세스의 개념 프로세스는 다양한 정의가 있다...

coding-start.tistory.com

 

이전 포스팅에서 병행 프로세스에 대해 간단히 개념을 다루어보았는데, 병행 프로세스에서 꼭 해결해야할 것 중 하나가 공유 자원에 대한 상호배제(동기화)였다. 오늘은 이런 상호배제에 대한 내용을 다루어볼 것이다.

 

상호배제의 개념

상호배제는 병행 프로세스에서 프로세스 하나가 공유 자원을 사용할 때 다른 프로세스들이 동일한 일을 할 수 없도록 하는 방법이다. 즉, 공유 자원에 있는 데이터에 접근하는 다른 프로세스를 이미 사용중인 프로세스 하나가 해당 데이터에 접근할 수 없게 하는 것을 상호배제(Mutual exclustion,Mutex)라고 한다. 물론 읽기 연산은 공유 데이터에 동시에 접근해도 문제가 발생하지 않지만, 변수나 파일은 프로세스 별로 하나씩 차례로 읽거나 쓰도록 해야한다. 예를 들면 하나의 프로세스가 순차적으로 파일을 읽는 작업을 하는 도중에 다른 프로세스가 파일의 내용을 변경해버리면 읽어오는 값이 예상과 다를 수 있기에 이러한 상황을 제어하는 동기화 작업이 필요한 것이다.

 

 

예를 들어 위의 그림에서 Thread A가 파일 쓰기 작업을 진행 중이다. 하지만 작업 도중에 Thread B가 파일을 읽기 위해 접근한다면 Thread B는 읽기 작업을 하지 못하고 대기하게 된다. 여기서 해당 파일(공유자원)을 가지고 주무르는 작업(코드)을 임계영역이라고 하고 먼저 해당 자원을 사용하는 임계코드를 실행중인 Thread A는 Lock(열쇠)을 손에 쥐게 된다. 그리고 그 이후에 Thread B가 접근하면 해당 스레드는 이미 Thread A가 Lock(열쇠)을 손에 쥐고 있기 때문에 임계영역에 접근할 수 없고 대기하게 되며 추후에 Thread A가 Lock(열쇠)를 반환하면 그때서야 Thread B가 임계영역에 진입할 수 있게 되는 것이다.

 

여기서 특징은 상호배제(Mutex)의 특징이 나온다. 바로 해당 자원에는 한순간 하나의 프로세스만 접근할 수 있기 때문에 Lock은 딱 한 프로세스에게만 쥐어주는 것이다. 또한 공유 자원을 사용하는 프로세스만 다른 프로세스를 차단하는 것을 알 수 있다.

 

<상호배제의 조건>

  1. 두 프로세스는 동시에 공유 자원에 진입할 수 없다.
  2. 프로세스의 속도나 프로세서 수에 영향을 받지 않는다.
  3. 공유 자원을 사용하는 프로세스만 다른 프로세스를 차단할 수 있다.
  4. 프로세스가 공유 자원을 사용하려고 너무 오래 기다려서는 안 된다.

 

여기서 임계영역은 어떤 전역 변수일 수도 있고 입출력을 위한 버퍼가 될 수도 있다. 이러한 임계영역을 이용하여 효과적으로 상호배제를 구현할 수 있는 것이다.

 

상호배제 방법들

상호배제를 해결하는 다양한 방법이 있다. 아래 표에는 상호배제를 구현하는 방법들을 정리해 놓은 것이다.

 

수준 방법 종류
고급 소프트웨어로 해결
  • 데커 알고리즘
  • 크누스 알고리즘
  • 램포트의 베이커리 알고리즘
  • 핸슨 알고리즘
  • 다익스트라 알고리즘
소프트웨어가 제공 : 프로그래밍 언어와 운영체제 수준에서 제공
  • 세마포어
  • 모니터
저급 하드웨어로 해결
  • TestAndSet(TAS)

 

모든 것을 다 다루어 보지는 않을 것이다. 몇 가지만 살펴보자.

 

<데커 알고리즘>

데커 알고리즘은 병행 프로그래밍의 상호배제 문제를 풀 수 있는 첫 번째 해결책으로 알려졌다. 두 프로세스가 동시에 임계영역에 진입하려고 시도하면 순서에 따라 오직 하나만 임계영역에 들어가도록 허용한다. 데커 알고리즘에서 각 프로세스는 플래그를 설정할 수 있고, 다른 프로세스를 확인한 후 플래그를 재설정할 수도 있다. 프로세스가 임계 영역에 진입하고 싶으면 플래그를 설정하고 차례를 기다린다. 즉, 임계 영역에 다른 프로세스가 이미 있으면 해당 프로세스를 종료할 때까지 while 문에서 순환한다. 여기서는 임계 영역 진입, 두 프로세스 간의 순서를 나타내는 turn 변수를 입력했다는 의미로 flag[0] 플래그와 flag[1] 플래그를 사용한다. 간단한 코드로 살펴보자.

 

======================데커 알고리즘 간단한 코드=========================
flag[0]=false;
flag[1]=false;
turn=0;

===================프로세스 P0 임계 영역 진입 코드=======================
//프로세스 P0의 임계 영역 진입 절차
flag[0]=true; //P0의 임계 영역 진입 표시
while(flag[1]==true){ //P1의 임계 영역 진입 여부 확인
	if(turn==1){ //P1이 임계영역에 진입할 차례가 되면
    	flag[0]=false; //플래그를 재설정하여 P1에 진입 순서 양보
        while(turn==1){ //P0이 임계영역에 진입할 차례가 될 때까지
        	//프로세스 P0의 바쁜 대기
        }
        flag[0]=true; //P1이 임계 영역에 진입할 차례가 되면 플래그 값 변경
    }
}

/*임계 영역 코드*/
turn=1; //임계 영역 코드 수행 이후 P1에게 진입 turn을 양보.
flag[0]=false;
/*나머지 코드 수행부분*/

===================프로세스 P1 임계 영역 진입 코드=======================
//프로세스 P1의 임계 영역 진입 절차
flag[1]=true;
while(flag[0]==true){
	if(turn==0){
    	flag[1]=false;
        while(turn==0){
        	//프로세스 P1의 바쁜 대기
        }
        flag[1]=true;
    }
}

/*임계 영역 코드*/
turn=0; //임계 영역 코드 수행 이후 P0에게 진입 turn을 양보.
flag[1]=false;
/*나머지 코드 수행부분*/
======================데커 알고리즘 간단한 코드=========================

 

상호배제 문제를 소프트웨어적으로 해결하는 데커 알고리즘의 특징

  • 특별한 하드웨어 명령문이 필요 없다.
  • 임계 영역 바깥에서 수행 중인 프로세스가 다른 프로세스들이 임계 영역에 들어가려는 것을 막지 않는다.
  • 임계 영역에 들어가기를 원하는 프로세서를 무한정 기다리게 하지 않는다.

 

<TestAndSet(TAS) 명령어>

공유 변수를 수정하는 동안 인터럽트 발생을 억제하여 임계 영역 문제를 간단하게 해결할 수 있지만, 이 방법은 항상 적용할 수 없고 실행 효율이 현저히 떨어진다. 또 소프트웨어적인 해결책은 더 복잡하고 프로세스가 2개 이상일 때는 더 많이 대기할 수 있다. 메모리 영역의 값에 대해 검사와 수정을 원자적으로 수행할 수 있는 하드웨어 명령이 TAS를 이용하여 간단한 방법으로 임계 영역 문제를 해결할 수 있다.

 

//target을 검사하고 target 값을 true로 설정
boolean TestAndSet(boolean *target){
	boolean temp=*target;
    *target=true;
    return temp;
}

//전역변수 영역(프로세스들의 공유변수들)
boolean waiting[n]; //배열을 선언함으로써 프로세스가 2개 이상와서 대기할 수 있도록 한다.
boolean lock=false;
int j; //0..n-1
boolean key;

do{ //프로세스 Pi의 진입 영역
	waiting[i]=true
    key=true;
    while(waiting[i]&&key){
    	key=TestAndSet(&lock);
    }
    waiting[i]=false;
    /*임계영역*/
    /*탈출영역*/
    
    j=(i+1)%n;
    while((j!=i)&&!waiting[j]){ //대기 중인 프로세스를 찾음
    	j=(j+1)%n;
    }
    if(j==i){ //대기 중인 프로세스가 없다면
    	lock=false; //다른 프로세스의 진입 허용
    }else{ //대기 중인 프로세스가 있으면 다음 순서로 임계 영역에 진입
    	waiting[j]=false; //Pj가 임계 영역에 진입할 수 있도록
    }
    //나머지 영역
}while(true);

 

프로세스 Pi의 진입 영역에서 waiting[i]가 true이므로 Pi는 임계 영역에 들어가려고 시도한다. 처음에 lock을 false로 초기화했다. 그러므로 임계 영역에 들어가는 첫 번째 Pi 프로세스는 TestAndSet(&lock)으로 key가 false가 되어 while문을 벗어 나게 되어 임계 영역을 진행한다. lock은 TestAndSet(&lock)으로 true가 되므로 다른 프로세스의 임계 영역 진입 코드의 while 문에서는 key가 true이기에 계속 while문에서 대기하게 된다. Pi가 임계 영역에 들어가기 전에 waiting[i]는 false로 설정하고 임계 영역으로 진입한다. 여기서 중요한 것은 lock이 true가 되어 다른 프로세스의 임계 영역 진입 코드의 while문에서 key가 true로 계속 반환되어 while 문에 머물고 있는 것을 기억해야한다.

Pi가 임계 영역을 떠날 때는 대기 프로세스 중에서 다음으로 진입할 수 있는 프로세스를 선택해야 한다. j=(i+1)%n; 코드로 차례가 높은 프로세스를 선택한 후 다음 while 문에서 각 프로세스를 검사한다. waiting 배열을 i+1,i+2,...n-1,0 순서로 조사하여 waiting 값이 true인 첫 번째 프로세스가 임계 영역으로 진입할 다음 프로세스가 된다.(임계 영역에 진입하기 위해 대기하는 프로세스는 임계 영역 초반 waiting[i]가 true가 된 상태로 while 문에서 대기중) 만약 대기 중인 프로세스가 없다면 lock을 false로 해제하고, 다음 프로세스가 Pj이면 임계 영역에 진입할 수 있도록 Pi는 waiting[j]를 false로 변경한다.(waiting[j]를 false로 변경하면 임계 영역을 진입하기 위해 대기중인 Pj가 while(waiting[j]&&key)에서 벗어 나게 되고 임계영역으로 진입한다.)

 

TestAndSet 명령어의 장단점

 

장점

사용자 수준에서 가능하다.

  • 메인 메모리를 공유하는 다중 프로세서나 단일 프로세서에서 프로세스 수에 관계없이 적용할 수 있다.
  • lock 변수 수에 상관없이 구현할 수 있다.
  • 구현이 단순하고 확인이 용이하다.
  • 다중 임계 영역을 지원한다.
단점

-바쁜 대기 발생

  • 프로세서 시산 소모가 크다.
  • 대기 프로세스는 비생산적, 자원이 소모되는 대기 루프에 남는다.

-기아 상태 발생 : 프로세스가 임계 영역을 떠날 때 프로세스 하나 이상을 대기하는 경우가 가능하다.

-교착 상태 발생 : 플래그는 우선순위가 낮은 프로세스가 재설정할 수 있지만, 우선순위가 높은 프로세스가 선점한다. 따라서 우선순위가 낮은 프로세스는 lock을 가지고, 우선순위가 높은 프로세스가 이것을 얻으려 시도할 때 높은 우선순위 프로세스는 무한정 바쁜 대기가 될 것이다.

 

<세모포어,semaphore>

앞서 제시한 상호배제의 해결 방법들은 좀 더 복잡한 문제에서는 일반화하기 어렵다. 또 프로세스가 임계 영역에 진입할 수 없을 때는 진입 조건이 true가 될 때까지 반복적으로 조사하고 바쁜 대기를 하게 되어 프로세스를 낭비한다. 진입 조건을 반복 조사하지 않고 true일 때 프로세스 상태를 확인한다면 프로세서 사이클을 낭비하지 않을 것이다. 다익스트라가 제안한 세모포어라는 동기화 도구는 상호배제 이외에도 다양한 연산의 순서도 제공한다.

 

세모포어는 값이 음이 아닌 정수인 플래그 변수이다.(음수 값을 가질 수 있는 세마포어는 음수 값을 할당하여 대기 중인 프로세스 갯수를 알고 처리하는 방법이 있다고는 하는데..음수가 되는 순간 해당 프로세스는 대기큐에 넣은 후에 S가 0보다 커지는 순간 대기큐에서 가져와 임계영역 코드를 수행시키는 원리.) 또한 P와 V 연산과 관련되어 있고 세마포어를 의미하는 S라는 변수를 갖는다. 임계 영역에 진입하는 프로세스는 P연산(wait)을 수행하여 S>=0이라면 S값을 하나 감소시키고 임계영역에 들어가고 만약 S<=0이라면 S값을 하나 감소시키고(S값이 음수로 된다) 대기큐로 들어간다.(지속적으로 S>=0일때가지 반복문을 도는 것이 아니라 대기 큐에 들어가 멈춰있는 상태-sleep가 된다. 바쁜 대기 문제 해결) 그리고 임계영역의 코드를 모두 수행하면 V연산(signal)로 S값을 하나 증가시키고 S값이 0보다 커지면 대기큐에서 sleep 중인 프로세스를 깨우는 행동을 하게 된다.

 

<Info>

음수 값을 가질 수 없는 세마포어는 뮤텍스와 같이 바쁜대기가 발생한다. 하지만 음수를 가지는 세마포어는 대기큐에 프로세스를 중단시킨 상태로 넣어놓으니 바쁜 대기가 발생하지 않는다.

바쁜 대기 : 자원을 사용할 수 있는 상태인지 반복해서 체크

 

P,V 연산은 운영체제가 실행하고, 임의의 프로세스가 시스템 호출을 하는 것이다.

 

P(S) : wait(S){
		S -> count--;
	    if(S -> count < 0) {
        	add this process to S -> queue; //프로세스를 준비 큐에 추가
            block(); //프로세스 중단(일시정지)
		}
	   }
       
V(S) : signal(S){
       		S -> count++;
            if(S -> count > 0){
            	remove a process P from S -> queue; // 준비 큐에서 P 프로세스 제거
                wakeup(P); //신호를 보내 프로세스를 실행
            }
	   }

 

P와 V 연산에 있는 세마포어 S의 정수 값 변경은 개별적으로 실행하고, 누군가가 이 연산을 수행하고 있다면 다른 프로세스는 해당 연산을 수행할 수 없다. 즉, P,V 연산이 다른 프로세스들이 동시에 할 수 없도록 조정해야한다. 여기서 일반 상호배제(뮤텍스)와는 조금 다른 것이 S값을 1보다 큰 값으로 초기화하여 여러 프로세스가 동시에 임계 영역을 진입하게 할 수 있다는 것이다.(S값만큼 공유 영역을 만들어서 각각의 공유 영역에 서로 다른 프로세스를 통과시킬 수 있다.)

 

즉, S가 1로 초기화된다면 바이너리 세마포어, S가 1보다 크다면 계수형 세마포어가 된다. 밑의 그림은 계수형 세마포어가 된다.

 

그렇다면 세마포어와 뮤텍스의 차이점을 무엇일까? 기본적인 차이점을 세마포어는 시그널링 메커니즘이라는 것이다. 즉, 프로세스는 wait() 및 signal() 작업을 수행하여 자원 획득 또는 해제여부를 나타낸다. 뮤텍스는 잠금 메커니즘이며, 프로세는 Lock을 획득해야한다. 아래는 세마포어와 뮤텍스의 차이점을 정리해 놓은 표이다.

 

세마포어 뮤텍스
세마포어는 시그널링 메커니즘이다. 뮤텍스는 잠금 메커니즘이다.
세마포어는 정수 변수이다. 뮤텍스는 Object이다.
세마포어는 여러 프로세스가 여러 유한한 자원에 액세스할 수 있게 한다. 뮤텍스는 여러 프로세스가 단일 리소스에 액세스할 수 있지만 동시에 수행할 수 없게 한다.
세마포어 값은 자원을 얻거나 해제하는 프로세스에 의해 변경 될 수 있다. 뮤텍스 Lock은 반드시 획득한 프로세스에 의해서만 해제된다.
세마포어는 계수형(count) 세마포어와 바이너리 세마포어로 분류된다. 더 이상의 분류는 없다.
세마포어 값은 wait() 및 signal() 연산을 사용하여 수정된다. 리소스를 요청하거나 해제하는 프로세스에 의해 Lock&Unlock이 된다.
모든 리소스가 사용 중이면 리소스를 요청하는 프로세스는 wait() 작업을 수행하고 대기큐에 들어가 있으면서 세마포어 값이 1이상이 될때 다른 프로세스에 의해 wakeup한다. Lock이 걸려있으면 Lock의 소유 프로세스가 잠금을 풀때까지 프로세스가 대기하고 있는다(바쁜 대기)

 

<모니터>

세마포어는 상호배제와 프로세스 사이를 조정하는 유연성 있고 강력한 도구이지만 wait&signal 연산 순서를 바꿔 실행하거나 둘 중 하나 이상을 생략하면 상호배제를 위반하거나 교착 상태가 발생한다. wait과 signal 연산이 프로그램 전체에 퍼져 있고 이들 연산이 각 세마포어에 주는 영향을 전체적으로 파악하기가 쉽지 않기에 세마포어를 잘못 사용하면 여러 가지 오류가 쉽게 발생하여 프로그램을 작성하기가 어렵다. (즉, 타이밍 문제가 발생할 수 있다.)모니터는 이러한 단점을 극복하려고 등장하였다.

 

모니터의 개념과 구조

모니터는 프로그래밍 언어 수준에서 제공해준다. 모니터를 사용하여 상호배제를 하는 예제로는 Java 언어가 있다.

 

 

프로세스들은 모니터의 프로시저를 호출하여 모니터 안에 진입한 후 지역(공유) 데이터에 접근할 수 있다. 무엇보다 언제나 한 번에 프로세스 하나만 모니터에 진입할 수 있도록 제한하여 상호배제를 실현한다는 것이 중요하다. 만약 다른 프로세스가 모니터를 점유하고 있으면 프로세스는 외부의 모니터 준비 큐에서 진입을 기다리게 되어 상호배제를 실현한다. 위에서 초기화 코드는 모니터를 생성할 때 한번 사용된다.

 

또한 중요한 개념중 하나가 조건 변수이다. 특정 조건이 부합하지 않아 모니터 실행 도중 cwait(c1)을 호출한 프로세스는 모니터 내부의 조건 c1 준비큐에 들어가 대기한다. 그리고 새로운 프로세스가 모니터 안에서 수행을 진행하고 해당 프로세스가 c1.signal을 호출하면 c1 대기 큐에 있던 프로세스가 중단되어 있다 다시 실행하러 들어온다. 즉, 단순히 세마포어처럼 signal 연산을 보내는 것이 아니라 특정 조건 대기큐에 대한 signal을 보내 작업을 시작시키는 것이다.

 

예를 들어 프로세스 하나가 모니터 내부에서 임계영역 코드를 수행하고 c1에 시그널을 보내면 모니터 내부에 있는 c1 준비큐에서 프로세스 하나가 나와 임계영역 코드에 진입하고, 만약 조건 signal을 보내지 않고 빠져 나온다면 외부에 있는 큐중에 한 프로세스를 꺼내어 임계영역에 진입시킨다. 물론 c1 시그널을 보냈는 데 c1에 대기하고 있는 프로세스가 없다면 아무런 효과가 없어 외부에 있는 대기 큐에서 프로세스를 꺼내온다.

 

Java의 wait(),notify(),notifyAll()이 모니터를 사용하기 위한 조건 변수라고 볼 수 있다. 모니터 내부에서 wait()을 호출하면 모니터 내부에 있는 WaitSet에 들어가 중단된 상태로 대기하고 있는 상태가 되는 것이고 누군가가 notify(),notifyAll()을 호출하면 모니터 내부에 있는 WaitSet에 있는 프로세스중 하나를 실행상태로 만들어주는 것이다. 물론 synchronized가 걸려 모니터 내부에 들어오지 못한 프로세스(스레드)들은 EntrySet이라는 외부 준비큐에 들어가 있는 상태가 되는 것이다.

 

 

 

-참조

 

Difference Between Semaphore and Mutex (with Comaprison Chart) - Tech Differences

The basic difference between semaphore and mutex is that semaphore is a signalling mechanism i.e. processes perform wait() and signal() operation to indicate whether they are acquiring or releasing the resource, while Mutex is locking mechanism, the proces

techdifferences.com

 

 

Difference Between Semaphore and Monitor in OS (with Comparison Chart) - Tech Differences

Semaphore and Monitor both allow processes to access the shared resources in mutual exclusion. Both are the process synchronization tool.

techdifferences.com

 

posted by 여성게
:
인프라/운영체제 2019. 7. 28. 16:07

2019/07/27 - [운영체제] - 운영체제 - 프로세스(Process)란? 프로세스상태,Context Switching

 

운영체제 - 프로세스(Process)란? 프로세스상태,Context Switching

프로세스의 개념 프로세스는 다양한 정의가 있다. 실행 중인 프로그램 비동기적 행위 실행 중인 프로시저 실행 중인 프로시저의 제어 추적 운영체제에 들어 있는 프로세스 제어 블록(PCB) 프로세서에 할당하여 실..

coding-start.tistory.com

2019/07/27 - [운영체제] - 운영체제 - 쓰레드란?(Thread,사용자 수준 쓰레드, 커널 수준 쓰레드, 혼합형 쓰레드)

 

운영체제 - 쓰레드란?(Thread,사용자 수준 쓰레드, 커널 수준 쓰레드, 혼합형 쓰레드)

2019/07/27 - [운영체제] - 운영체제 - 프로세스(Process)란? 프로세스상태,Context Switching 운영체제 - 프로세스(Process)란? 프로세스상태,Context Switching 프로세스의 개념 프로세스는 다양한 정의가 있다...

coding-start.tistory.com

 

컴퓨터는 프로그램 작업을 수행하는 데 사용할 수 있는 여러 자원으로 구성된다. 예를 들면, 명령을 실행하는 프로세서, 데이터를 저장하는 메인 메모리, 프로세서의 임시 저장소인 레지스터, 프로세서와 메인 메모리 사이의 속도 차를 조절해주는 캐시 등이 있다. 이 중에서 메모리 같은 자원은 공유 영역을 모든 프로세스가 동시에 공유한다. 즉, 이 메모리 자원은 공유 영역을 프로세스들이 병렬로 사용한다. 반면에 입출력장치 일부나 프로세서는 한 번에 프로세스 하나만 사용할 수 있는 공유자원이다.

 

프로세서 하나는 한 번에 프로세스 하나만 실행할 수 있다. 하지만 운영체제가 프로세서를 빠르게 전환하여 프로세서 시간을 나눠서 마치 프로세스 여러 개를 동시에 실행하는 것처럼 보이게 하는 것을 병행 프로세스라고 한다. 그리고 하나의 프로세스 내에 스레드들을 커널 수준의 스레드로 구현하면 각 스레드에 별도의 프로세서를 할당하여 병렬로 하나의 프로그램을 수행할 수도 있다.

 

  • 단일 프로그래밍 : 프로세서를 사용 중이던 프로세스를 완료한 후 다른 프로세스를 실행한다.
  • 다중 프로그래밍 : 프로세스 여러 개가 프로세서 하나를 공유한다.
  • 다중 처리 : 프로세서를 2개 이상 사용하여 동시에 프로그램 여러 개를 병렬로 실행한다. 프로세스는 한 번에 프로세서 하나에서 실행하지만, 동일한 시스템에서는 서로 다른 시간에 서로 다른 프로세서에서 실행할 수 있다.

이러한 병행 프로세스를 사용하기 위해서는 꼭 해결해야할 과제가 있다.

 

  • 공유 자원을 상호 배타적으로 사용해야 한다. 예를 들어, 프린터, 통신망 등은 한순간에 프로세스 하나만 사용해야 한다.
  • 병행 프로세스 간에는 협력이나 동기화가 되어야 한다. 상호배제도 동기화의 한 형태이다.
  • 두 프로세스 사이에서는 데이터를 교환할 수 있도록 통신이 되어야 한다.
  • 프로세스는 동시에 수행하는 다른 프로세스의 실행 속도와 관계없이 항상 일정한 실행 결과를 보장하도록 해야한다.
  • 교착 상태를 해결하고 병행 프로세스들의 병렬 처리 능력을 극대화해야 한다.
  • 실행 검증 문제를 해결해야 한다.
  • 병행 프로세스를 수행하는 과정에서 발생하는 상호배제, 즉 어떤 프로세스가 작업을 실행 중일때 나머지 프로세스는 그것과 관련된 작업을 수행할 수 없도록 보장해야 한다.

다중 처리 시스템에서는 프로세서들이 모든 입출력장치와 메모리를 참조할 수 있어 동시에 동일한 자원에 접근할 때 충돌이 발생할 수 있다. 따라서 프로세서 간의 충돌을 해결하는 방법이 필요하다.

posted by 여성게
:
인프라/운영체제 2019. 7. 27. 22:29

2019/07/27 - [운영체제] - 운영체제 - 프로세스(Process)란? 프로세스상태,Context Switching

 

운영체제 - 프로세스(Process)란? 프로세스상태,Context Switching

프로세스의 개념 프로세스는 다양한 정의가 있다. 실행 중인 프로그램 비동기적 행위 실행 중인 프로시저 실행 중인 프로시저의 제어 추적 운영체제에 들어 있는 프로세스 제어 블록(PCB) 프로세서에 할당하여 실..

coding-start.tistory.com

 

쓰레드(Thread)란 간단히 말해 프로세스 내에서 실행되는 실행 단위이다. 프로세스는 이러한 쓰레드를 한 개 이상으로 나눌 수 있다. 쓰레드는 프로그램 카운터와 스택 포인터 등을 비롯한 쓰레드 실행 환경 정보(Context 정보), 지역 데이터, 스택을 독립적으로 가지면서 코드, 전역 데이터, 힙을 다른 쓰레드와 공유한다.

그림을 보면 프로세스 내에서 쓰레드는 별도의 Stack(+ 스레드 실행 환경 정보, 지역데이터, 레지스터 등)을 할당받고 Code, Data, Heap 영역은 같은 프로세스 내의 다른 쓰레드와 공유한다.

 

프로세스 하나에 포함된 쓰레드들은 공동의 목적을 달성하려고 병렬로 수행한다. 이러한 쓰레드를 이용하면 아래와 같은 이점들이 있다.

  • 사용자 응답성 증가
  • 프로세스 자원과 메모리 공유가능
  • 경제성이 좋음(프로세스 컨텍스트 스위칭보다 쓰레드 컨텍스트 스위칭이 오버헤드가 더 적다.)
  • 다중 처리로 성능과 효율 향상

현대 시스템은 대부분 다중 쓰레드 운영체제이다. 다중 쓰레드는 프로그램 하나를 여러 실행 단위로 쪼개어 실행한다는 측면에서 다중 처리(다중 프로세싱)와 의미가 비슷하다. 하지만 동일 프로세스의 쓰레드는 자원을 공유하므로 자원 생성과 관리의 중복성을 최소화하여 실행 능력을 향상시킬 수 있다. 그리고 각 쓰레드는 커널이 개입하지 않고도 독립적으로 실행할 수 있어 서버에서 많은 요청을 효과적으로 처리할 수 있다.

 

이러한 쓰레드의 특성 때문에 프로세스보다 쓰레드를 생성하는 것이 더 빠르고, 동일한 프로세스에 있는 쓰레드 간의 교환이나 쓰레드 종료도 훨씬 빠르다.

 

보통 프로세스를 생성하면 해당 프로세스의 쓰레드도 함께 생성한다. 단, 쓰레드 생성에서는 운영체제가 부모 프로세스와 공유할 자원을 초기화할 필요가 없다. 해당 프로세스가 스택과 레지스터를 직접 제공하기 때문이다. 그러므로 프로세스의 생성과 종료보다는 오버헤드가 훨씬 적다. 여기서 스레드의 장점 하나는 쓰레드 한개가 대기 상태로 변할 때 전체 프로세스를 대기 상태로 바꾸지 않는다는 것이다. 실행 상태의 쓰레드가 대기 상태가 되면 다른 쓰레드를 실행할 수 있다. 그러나 프로세스와 달리 서로 독립적이지는 않다. 프로세스 하나에 있는 전체 쓰레드는 프로세스의 모든 주소에 접근할 수 있으므로 쓰레드 한 개가 다른 쓰레드의 스택을 읽거나 덮어쓸 수 있다.

 

쓰레드 제어 블록(TCB)

프로세스가 프로세스 제어 블록에 정보를 저장하듯이 쓰레드도 쓰레드 제어 블록에 정보를 저장한다. 그런데 프로세스는 쓰레드를 한 개 이상 가질수 있으므로, 결국 프로세스 제어블록은 스레드 제어 블록의 리스트를 가리킨다.

 

쓰레드의 구현

쓰레드는 운영체제에 따라 다양하게 구현할 수 있는데, 대부분 다음 세 가지 형태로 구현한다. 사용자 수준 쓰레드는 쓰레드 라이브러리를 이용하여 작동하는 형태이고, 커널 수준 쓰레드는 커널(운영체제)에서 지원하는 형태이다. 그리고 이 둘을 혼합한 형태가 혼합형 쓰레드이다.

 

  1. 사용자 수준 쓰레드 - 다대일 매핑
  2. 커널 수준 쓰레드 - 일대일 매핑
  3. 혼합형 쓰레드 - 다대다 매핑

 

<사용자 수준 쓰레드>

사용자 수준 쓰레드는 사용자 영역의 쓰레드 라이브러리로 구현하고, 쓰레드와 관련된 모든 행위를 사용자 영역에서 하므로 커널이 쓰레드의 존재를 알지 못한다. 여기서 쓰레드 라이브러리는 쓰레드의 생성과 종료, 쓰레드 간의 메시지 전달, 쓰레드의 스케줄링과 컨텍스트 등 정보를 보관한다. 

사용자 수준 쓰레드에서는 쓰레드 교환에 커널이 개입하지 않아 커널에서 사용자 영역으로 전환할 필요가 없다. 그리고 커널은 쓰레드가 아닌 프로세스를 한 단위로 인식하고 프로세서를 할당한다. 다수의 사용자 수준 쓰레드가 커널 수준 쓰레드 한 개에 매핑되므로 다대일 쓰레드 매핑이라고 한다. 

 

 

사용자 수준 쓰레드의 장점

  • 이식성이 높음 : 커널에 독립적으로 스케쥴링을 할 수 있어 모든 운영체제에 적용할 수 있다.
  • 오버헤드가 적음 : 스케쥴링이나 동기화를 하려고 커널을 호출하지 않으므로 커널 영역으로 전환하는 오버헤드가 줄어든다.
  • 유연한 스케쥴링이 가능 : 커널이 아닌 쓰레드 라이브러리에서 쓰레드 스케줄링을 제어하므로 응용 프로그램에 맞게 스케줄링할 수 있다.

사용자 수준 쓰레드의 단점

  • 시스템의 동시성을 지원하지 않음 : 쓰레드가 아닌 프로세스 단위로 프로세서를 할당하여 다중 처리 환경을 갖춰도 쓰레드 단위로 다중 처리를 하지 못한다. 동일한 프로세스의 쓰레드 한개가 대기 상태가 되면 이 중 어떤 쓰레드도 실행하지 못한다.
  • 확장에 제약이 따름 : 커널이 한 프로세스에 속한 여러 쓰레드에 프로세서를 동시에 할당할 수 없어 다중 처리 시스템에서 규모를 확장하기가 어렵다.
  • 쓰레드 간 보호 불가능 : 쓰레드 간 보호에 커널의 보호 방법을 사용할 수 없다. 쓰레드 라이브러리에서 쓰레드 간 보호를 제공해야 프로세스 수준에서 보호가 가능하다.

 

<커널 수준 쓰레드>

커널 수준 쓰레드는 사용자 수준 쓰레드의 한계를 극복하는 방법으로, 커널이 쓰레드와 관련된 모든 작업을 관리한다. 한 프로세스에서 다수의 쓰레드가 프로세서를 할당받아 병행으로 수행하고, 쓰레드 한 개가 대기 상태가 되면 동일한 프로세스에 속한 다른 쓰레드로 교환이 가능하다. 이때도 커널이 개입하므로 사용자 영역에서 커널 영역으로 전환이 필요하다. 커널 수준 쓰레드는 사용자 수준 쓰레드와 커널 수준 쓰레드가 일대일로 매핑된다. 따라서 사용자 수준 쓰레드를 생성하면 이에 대응하는 커널 쓰레드를 자동으로 생성한다.

 

커널 수준 쓰레드는 커널이 직접 스케줄링하고 실행하기에 사용자 수준 쓰레드의 커널 지원이 부족한 문제를 해결할 수 있지만, 커널이 전체 프로세스와 쓰레드 정보를 유지하여 오버헤드가 커진다. 대신 커널이 각 쓰레드를 개별적으로 관리할 수 있어 동일한 프로세스의 쓰레드들을 병행으로 수행할 수 있다. 동일한 프로세스에 있는 쓰레드 중 한 개가 대기 상태가 되더라도 다른 쓰레드를 실행할 수 있다는 장점이 있다. 하지만 이 과정에서도 커널 영역으로 전환하는 오버헤드가 발생하고 스케쥴링과 동기화를 하려면 더 많은 자원이 필요하다.

 

<혼합형 쓰레드>

혼합형 쓰레드는 사용자 수준 쓰레드와 커널 수준 쓰레드를 혼합한 구조이다. 이는 시스템 호출을 할 때 다른 쓰레드를 중단하는 다대일 매핑의 사용자 수준 쓰레드와 쓰레드 수를 제한하는 일대일 매핑의 커널 수준 쓰레드 문제를 극복하는 방법이다. 즉, 사용자 수준 쓰레드는 커널 수준 쓰레드와 비슷한 경량 프로세스에 다대다로 매핑되고, 경량 프로세스는 커널 수준 쓰레드와 일대일로 매핑된다. 결국 다수의 사용자 수준 쓰레드에 다수의 커널 쓰레드가 다대다로 매핑된다. 

 

 

프로세스 하나에는 경량 프로세스가 하나 이상 있고, 경량 프로세스에는 이에 대응하는 커널 쓰레드가 한 개 있다. 그리고 자원과 입출력 대기를 하려고 경량 프로세스 단위로 대기하므로 프로세스는 입출력을 완료할 때까지 대기할 필요가 없다.(다른 경량 프로세스는 대기상태가 아니기 때문) 어떤 경량 프로세스가 입출력 완료를 기다리더라도 동일한 프로세스에서 다른 경량 프로세스를 실행할 수 있기 때문이다.

 

[쓰레드 풀링]

시스템이 관리하는 쓰레드의 풀을 응용 프로그램에 제공하여 스레드를 효율적으로 사용할 수 있게 하는 방법이다. 즉, 미리 생성한 스레드를 재사용하도록 하여 스레드를 생성하는 시간을 줄여 시스템의 부담을 덜어준다. 또 동시에 생성할 수 있는 스레드수를 제한하여 시스템의 자원 소비를 줄여서 응용 프로그램의 전체 성능을 일정 수준으로 유지한다.

posted by 여성게
:
인프라/운영체제 2019. 7. 27. 14:29

프로세스의 개념

프로세스는 다양한 정의가 있다.

  • 실행 중인 프로그램
  • 비동기적 행위
  • 실행 중인 프로시저
  • 실행 중인 프로시저의 제어 추적
  • 운영체제에 들어 있는 프로세스 제어 블록(PCB)
  • 프로세서에 할당하여 실행할 수 있는 개체 디스패치가 가능한 대상

이 중 가장 일반적인 프로세스 정의는 "실행 중인 프로그램"이다. 프로그램이 실행 중이라는 의미는 디스크에 있던 프로그램을 메모리에 적재하여 운영체제의 제어를 받는 상태가 되었다는 것이다. 이는 자신만의 메모리 영역(주소 공간)이 있음을 의미한다.

 

프로세스가 실행 중인 프로그램이 되려면 프로세서 점유 시간, 메모리, 파일, 입출력장치 같은 자원이 필요한데, 프로세스를 생성하거나 실행할 때 이 자원을 할당한다. 그리고 프로세스는 현재의 활동 상태를 나타내는 프로그램 카운터(PC), 프로세서의 현재 활동(레지스터 내용)도 포함한다. 프로그램은 컴파일한 코드와 초기화 전역변수, 문자열과 문자열 상수 등 정적 데이터를 포함하는 정적인 개체이다. 반면에 프로세스는 아래와 같이 메모리 구조를 이루고, 프로그램 카운터나 레지스터처럼 현재 어떤 자원을 사용하는지 관련 정보가 들어 있는 동적인 개체이다.

스택은 데이터를 일시적으로 저장하는 영역이다. 지역변수에 사용하고, 변수가 범위 밖으로 이동하면 공간을 해제한다. 호출한 함수의 반환 주소, 반환 값, 매개변수 등에 사용하고, 함수를 호출할수록 커지고 반환하면 줄어든다. 보통 힙과 인접한 방향으로 점점 커져 스택 포인터와 힙 포인터를 만나면 메모리가 소진되었다는 의미이다.

 

힙은 코드 영역과는 별도로 유지되는 자유 영역이다. 동적으로 메모리를 할당하려고 프로그램 실행 중 시스템 호출을 사용했다가 해제하는 방법으로 활용한다. 프로세스의 공유 라이브러리와 동적으로 적재된 모듈이 서로 공유하는데, 동적 메모리 할당이 발생하면 스택영역 쪽인 위쪽으로 커진다.

 

데이터는 프로그램의 가상 주소 공간이다. 전역변수나 정적변수를 저장하거나 할당하고 실행하기 전에 초기화한다. 읽기&쓰기가 가능한 영역이다.

 

코드는 실행 명령을 포함하는 메모리이거나 목적 파일에 있는 프로그램 영역이다. 프로그램을 시작할 때 프로세서가 디스크에서 읽어 실행하는 컴파일한 프로그램을 저장한다. 프로세스로 변경할 수 없고, 읽기 전용이므로 프로그램이 코드 영역을 침범하여 쓰기를 시도하면 오류가 발생해서 프로그램을 종료한다.

 

이러한 프로세스는 별도의 주소 공간에서 실행되며, 한 프로세스는 다른 프로세스의 자원 영역에 접근할 수 없다. 만약 한 프로세스가 다른 프로세스의 자원에 접근하려면 프로세스 간의 통신(IPC)을 사용해야한다. (파이프, 파일, 소켓 등)

 

프로세스는 수행하는 역할에 따라 시스템(커널) 프로세스와 사용자 프로세스로 구분하고, 병행 수행 방법에 따라 독립 프로세스와 협력 프로세스로 구분한다.

구분 종류 설명
역할 시스템(커널)프로세스 모든 시스템 메모리와 프로세서의 명령에 액세스할 수 있는 프로세스이다. 프로세스 실행 순서를 제어하거나 다른 사용자 및 커널 영역을 침범하지 못하게 감시하고, 사용자 프로세스를 생성하는 기능을한다.
사용자 프로세스 사용자 코드를 수행하는 프로세스이다.
병행 수행 방법 독립 프로세스 다른 프로세스에 영향을 주지 않거나 다른 프로세스의 영향을 받지 않으면서 수행하는 병행 프로세스이다.
협력 프로세스 다른 프로세스에 영향을 주거나 다른 프로세스에서 영향을 받는 병행 프로세스이다.

 

프로세스의 상태 변화와 상태 정보

운영체제는 프로세스의 실행을 제어한다. 그리고 프로세스는 실행하면서 상태가 변하므로 운영체제는 프로세스 제어에 필요한 상태를 점검하고 프로세스를 제어한다.

 

프로세스의 상태는 크게 실행 상태와 비실행 상태로 구분할 수 있다. 운영체제가 프로세스를 생성하면 비실행 상태로 초기화해서 실행을 기다린다. 실행 중인 프로세스를 종료하거나 인터럽트가 발생하면 비실행 프로세스 중에서 선택한 프로세스를 실행 상태로 바꾼다.(디스패치)이때 인터럽트된 프로세스를 비실행 상태가 된다. 실행 중인 프로세스는 새로운 자원을 할당받으려고 프로세서를 기다리는 비실행 상태로 바뀌기도 한다.

 

프로세스의 상태 변화는 운영체제가 작업 스케쥴러와 프로세스 스케쥴러를 이용하여 관리한다. 작업 스케쥴러는 스풀러가 디스크에 저장한 작업 중 실행할 작업을 선정하고 준비 리스트에 삽입하여 다중 프로그래밍의 정도를 결정하며, 프로세스 스케쥴러는 선정한 작업의 상태를 변화시키며 프로세스의 생성에서 종료까지의 과정을 수행한다.

 

실행 상태의 프로세스가 프로세서를 자발적으로 반환하기 전에 할당된 CPU점유 시간이 지나면 이 프로세스는 준비 상태가 된다. 그리고 프로세스를 실행하다 입출력 명령이 발생하면 대기 상태가 된다. 대기 상태인 프로세스는 대기 원인을 제거하면 준비 상태로 바뀌고, 디스패처가 준비 상태인 프로세스에 프로세서를 할당하면 다시 실행 상태로 바뀐다. 여기서 디스패처는 스케쥴러가 선택한 프로세스에 프로세서를 할당하는 모듈이다.

 

상태변화 표기방법
1.준비->실행 dispatch(프로세스 이름)
2.실행->준비 timeout(프로세스 이름)
3.실행->대기 block(프로세스 이름)
4.대기->준비 wakeup(프로세스 이름)

 

준비->실행
준비 큐 맨 앞에 있던 프로세스가 프로세서를 점유하는 것을 디스패치라고 한다.(스케쥴 알고리즘에 따라 다름) 다중 프로그래밍 운영체제에서는 실행 상태인 프로세스가 할당된 CPU점유 시간만큼만 프로세서를 사용하도록 하여 특정 프로세스가 프로세서를 계속 독점하는 것을 방지한다.

실행->준비
운영체제는 실행 상태의 프로세스가 프로세서를 계속 독점하지 않도록 인터럽트 클록을 두어 특정 프로세스가 할당된 CPU점유 시간동안만 프로세서를 점유하게 한다. 프로세스가 일정 시간이 지나도 프로세서를 반환하지 않으면 클록이 인터럽트를 발생시켜 운영체제에 프로세서 제어권을 부여하는 것이다. 그러면 실행 상태의 프로세스는 준비 상태가 되고, 준비리스트의 첫 번째 프로세스가 실행 상태가 된다.(스케쥴 알고리즘에 따라 다름)

실행->대기
할당된 시간 이전에 실행 상태의 프로세스에 입출력 연산 등이 필요하거나 새로운 자원 요청 등의 문제로 프로세서를 스스로 양도하면 대기 상태가 된다.

대기->준비
프로세스는 입출력 작업이 끝나면 wake up으로 대기에서 준비 상태가 된다.

 

프로세스 제어 블록(PCB,Process Control Block)

운영체제가 프로세스를 제어할 때 필요한 프로세스 상태 정보는 프로세스 제어 블록에 저장된다. 프로세스 제어 블록은 특정 프로세스가 생성될 때 마다 생성되는 것이며 특정 프로세스 정보를 저장하는 데이터 블록이나 레코드로, 작업 제어 블록(TCB,Task Controll Block)이라고도 한다. 프로세스가 생성되면 메모리에 PCB를 생성하고, 프로세스가 실행을 종료하면 PCB도 삭제한다. PCB에 들어있는 정보들을 아래와 같다.

 

정보 설명
프로세스 식별자 각 프로세스의 고유 식별자
프로세스 상태 생성,준비,실행,대기,중단 등 상태표시
프로그램 카운터(PC) 프로세스를 실행하는 다음 명령어의 주소
레지스터 저장 영역 누산기, 인덱스 레지스터, 스택 포인터, 범용 레지스터, 조건 코드 등 정보로 컴퓨터 구조에 따라 수나 형태가 다르다. 인터럽트가 발생하면 프로그램 카운터와 함께 저장하여 재실행할 때 원래대로 복귀할 수 있게 한다.
프로세서 스케줄링 정보 프로세스의 우선순위, 스케줄링 큐의 포인터, 기타 스케줄 매개변수
계정정보 프로세서 사용 시간, 실제 사용시간, 사용 상한 시간, 계정 번호, 작업이나 프로세스 번호 등

입출력 상태 정보

메모리 관리 정보

...

특별한 입출력 요구 프로세스에 할당된 입출력장치, 열린 파일 리스트 등

운영체제가 사용하는 메모리 시스템에 따른 상한,하한 레지스터(경계 레지스터), 페이지 테이블이나 세그먼트 테이블 값 등

 

프로세스 Context Switching

인터럽트나 시스템 호출 등으로 실행 중인 프로세스의 제어를 다른 프로세스에 넘겨 실행 상태가 되도록 하는 것을 프로세스 Context Switching이라고 한다. 프로세스 문맥 교환이 일어나면 프로세서의 레지스터에 있던 내용을 나중에 사용할 수 있도록 저장한다.

 

예) 프로세스 A가 실행 상태에 있던 도중 프로세스 B에 제어권을 넘겨주는 Context Switching 가정

  1. 인터럽트 신호
  2. PCB_A에 프로세스 A 상태 저장
  3. PCB_B에 저장되어 있던 프로세스 B 상태를 다시 적재
  4. 인터럽트 신호
  5. PCB_B에 프로세스 B 상태 저장
  6. PCB_A에 저장되어 있던 프로세스 A 상태를 다시 적재

Context Switching에서는 오버헤드가 발생하는데, 이는 메모리 속도, 레지스터 수, 특수 명령어의 유무에 따라 다르다. 그리고 Context Switching은 프로세스가 "준비->실행" 상태로 바뀌거나 "실행->준비, 실행->대기" 상태로 바뀔 때 발생한다.

 

현재 실행 중인 프로세스에서 Context Switching을 요청하면, 우선 사용자 모드에서 커널 모드로 프로세스 제어가 넘어가면서 프로세스를 종료한다. 그리고 다시 시작할 수 있도록 프로세스의 현재 상태를 PCB에 저장하고 다음에 실행할 프로세스를 선택한다. 새로 실행될 프로세스 정보를 PCB에서 얻어와 프로세서에 재저장하고는 사용자 모드로 돌아와서 새로운 프로세스를 실행한다.

 

이러한 Context Switching은 시간 비용이 들어가는 오버헤드이고, 이 오버헤드는 메모리 속도, 레지스터 수, 특수 명령어의 유무에 따라 시스템마다 다르다. 그래서 운영체제를 설계할 때, 최대한 불필요한 Context Switching을 줄이는 것을 목표로 정한다. Context Switching은 레지스터 Context 교환, 작업 Context 교환, 쓰레드 Context 교환, 프로세스 Context 교환이 가능하다. 

 

<프로세스 구조>

프로세스는 실행 중에 프로세스 생성 시스템 호출을 이용하여 새로운 프로세스를 생성할 수 있다. 이때 프로세스 생성 순서를 저장하고 부모-자식 관계를 유지하여 계층적으로 생성한다. 

 

프로세스의 생성

운영체제나 응용 프로그램에서 요청을 받아 프로세스를 생성하면, 운영체제는 해당 프로세스에서 프로세스 제어 블록을 만들어 주소 공간을 할당한다. 

  1. 새로운 프로세스에 프로세스 식별자를 할당
  2. 프로세스의 모든 구성 요소를 포함할 수 있는 주소 공간과 프로세스 제어 블록 공간을 할당한다.
  3. 프로세스 제어 블록을 초기화한다. 프로세스 상태, 프로그램 카운터 등 초기화, 자원 요청, 프로세스 제어 정보(우선순위) 등을 포함한다.
  4. 준비큐에 삽입

프로세스가 작업을 수행하려면 프로세서 점유 시간, 메모리, 파일, 입출력장치 등 자원이 필요하다. 자식 프로세스는 운영체제에서 직접 필요한 자원을 얻거나 부모 프로세스의 자원을 일부 사용할 수 있다. 이때 부모 프로세스는 자식 프로세스가 사용하는 자원을 제한해서 특정 프로세스가 자식 프로세스를 너무 많이 생성하여 시스템에 부담을 주는 것을 막을 수 있다.

 

프로세스의 종료

프로세스가 마지막 명령을 실행하면 종료하여 운영체제에 프로세스의 삭제를 요청한다. 또한 부모 프로세스는 자식 프로세스가 할당된 자원을 초과하여 자원을 사용할 때나 자식 프로세스에 할당한 작업이 더는 없을 때 자식 프로세스를 종료한다.

 

프로세스의 제거

프로세스 제거는 프로세스를 파괴하는 것이다. 프로세스를 제거하면 사용하던 자원을 시스템에 돌려주고, 해당 프로세스는 시스템 리스트나 테이블에서 사라져 프로세스 제어 블록을 회수한 후 디스크에 저장한다.

 

프로세스의 중단과 재시작

프로세스의 준비, 실행, 대기 상태만 이용하면 입출력 동작이 일반 연산보다 느려 시스템이 대부분 유휴 상태이다. 다중 프로그래밍 환경에서도 프로세서의 동작시간이 입출력보다 짧아 프로세스 문맥 교환이 일어난 후에도 기다리게 되므로 대부분 유휴시간이 된다.

 

시스템의 유휴시간 문제는 프로세스 중단(일시중단) 상태를 이용하여 해결할 수 있다. 운영체제는 새로운 프로세스를 생성하여 실행하거나 실행 중인 프로세스를 중단했다가 다시 실행하여 사용할 수 있다. 후자의 방법을 이용하면 시스템 전체의 부하를 증기시키지 않으면서 프로세스에 서비스를 제공할 수 있다. 중단 상태를 추가하면 실행에서 대기가 아닌 중단 상태로 전환하여 특정 이벤트의 발생을 기다리면서 대기 상태가 된다. 그리고 해당 이벤트가 발생할 때 준비상태로 가서 실행을 기다리는 것이 아닌 즉시 실행 상태로 바꿀 수 있는 이점이 있다.

 

프로세스를 중단한 원인을 제거하여 다시 실행하는 것을 재시작이라고 한다.

 

프로세스 중단과 재시작은 시스템 부하를 조절하는 데 상당히 중요하고, 다음 상황에서 주로 발생한다.

  • 시스템에 장애가 발생하면 실행 중인 프로세스는 잠시 중단했다가 시스템이 기능을 회복할 때 다시 재시작할 수 있다.
  • 프로세스에 의심스러운 부분이 있으면 실행 중인 프로세스를 중단하여 확인한 후 재시작하거나 종료할 수 있다.
  • 처리할 작업이 너무 많아 시스템에 부담이 되면 프로세스 몇 개를 중단했다가 시스템이 정상 상태로 다시 돌아왔을 때 재시작할 수 있다.

대다수 시스템은 프로세스를 실행하기 전에 자원을 할당받고 실행을 시작하지만, 다중 프로그래밍 환경에서는 지원의 이용률과 시스템 효율을 높이려고 자원을 동적으로 할당한다. 이때 자원을 할당받으려고 기다리는 상태가 "대기"이고, 할당받은 자원을 기다리는 상태가 "중단"이다.

 

중단된 상태는 프로세스가 보조 메모리에 있고 이벤트를 대기 중인 상태이다. 중단 상태는 프로세스가 보조 메모리에 있지만 즉시 메인 메모리로 적재하여 실행할 수 있는 상태이다.

posted by 여성게
: