Skip to content

Commit

Permalink
Merge pull request #33 from meengi07/mingi
Browse files Browse the repository at this point in the history
Chapter12 ,13
  • Loading branch information
choihuk authored Apr 2, 2024
2 parents 181f868 + 7102dd5 commit 50e78b9
Show file tree
Hide file tree
Showing 18 changed files with 312 additions and 4 deletions.
8 changes: 4 additions & 4 deletions ch06/김민기.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

가장 초보적인 마크 앤 스위프 알고리즘은 할당됐지만, 아직 회수되지 않은 객체를 가리키는 포인터를 포함한 할당 리스트(allocated list)를 사용한다. 전체적인 GC알고리즘은 다음과 같다.

![markandswift.png](/images/markandswift.png)
![markandswift.png](../images/markandswift.png)

1. 할당 리스트를 순회하면서 마크 비트를 지운다
1. GC루트부터 살아 있는 객체를 찾는다.
Expand Down Expand Up @@ -63,7 +63,7 @@ Klass 워드 : 객체의 클래스 정보를 나타내는 포인터, 객체가

이전 7버전까지는 메모리 레이아웃은 Klass 워드가 자바 힙의 일부인 펌젠(perm gen)이라는 메모리 영역을 가리켰다. 8부터는 Klass가 자바 힙의 주 영역 밖으로 빠지게 됐다(Meta space로 변경). 그래서 객체 헤더가 필요 없어졌다.

![memory.png](/images/memory.png)
![memory.png](../images/memory.png)

oop는 대부분 기계어 워드라 이전엔 32비트, 요즘은 64비트다. 64비트가 되면서 메모리를 절약할 수 있게 압축 oop라는 기법을 옵션으로 제공하며, 7버전 이상,64비트 힙에서 디폴트로 적용되어 있다.

Expand All @@ -82,7 +82,7 @@ oop는 대부분 기계어 워드라 이전엔 32비트, 요즘은 64비트다.

객체 인스턴스 필드는 헤더 바로 다음에 나열된다. klassOop는 klass 워드 다음에 메서드 vtable이 나온다.

![instance_vtable.png](/images/instance_vtable.png)
![instance_vtable.png](../images/instance_vtable.png)

자바에서 배열은 객체다. JVM 배열도 oop로 표시되며 배열은 Mark 워드, Klass 워드 다음에 배열 길이를 나타내는 Length 워드가 붙는다. 자바 배열 인덱스가 32비트 값으로 제한되는 건 이 때문이다.

Expand Down Expand Up @@ -167,7 +167,7 @@ JVM은 에덴 여러 버퍼로 나눠 각 애플리케이션 스레드가 새

아래 그림을 보면 각 애플리케이션 스레드가 새 객체를 할당할 버퍼를 갖고 있다. 애플리케이션 스레드가 버퍼를 다 채우면 JVM은 새 에덴 영역을 가리키는 포인터를 내어준다.

![thread-local.png](/images/thread-local.png)
![thread-local.png](../images/thread-local.png)

### 4.2 반구형 수집

Expand Down
120 changes: 120 additions & 0 deletions ch06/김민기_발표_GC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# GC의 종류와 특징

Garbage Collector 는 Java 개발자가 프로그램 코드로 메모리는 명시적으로 해제하지 않기 때문에 GC가 더 이상 필요없는 객체(쓰레기)를 찾아 지우는 작업을 한다. 그리고 이 GC는 두가지 전제 조건하에 만들어 졌다.

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

<aside>
💡 **STW (Stop the world)** : GC가 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 행위, STW가 발생하면 GC를 실행하는 스레드를 제외한 나머지 스레드는 모두 작업을 멈춘다. GC가 작업이 완료된 후 중단했던 작업을 다시 시작한다.

</aside>

이 전제 조건을 weak generational hypothesis (약한 세대 가설)이라고 한다. 이를 위해 두가지로 물리적 공간을 나눴다.

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

![gc_flow](../images/gc_flow.png)

GC의 영역 및 데이터 흐름도


# Serial GC

단일 스레드 환경에서 사용되며, GC 작업을 수행하는 동안 애플리케이션의 다른 스레드는 일시 중지 된다. 지금도 서버의 CPU코어가 1개라면 `Serial GC`가 사용된다. Minor GC에는 Mark-Sweep 을 사용하고 Major GC에는 Mark-Sweep-Compact 알고리즘을 사용한다.

- Mark : Old 영역에 살아있는 객체를 식별하는 것
- Sweep : Heap 의 앞 부분부터 확인하여 살아있는 것만 남긴다
- Compact : 각 객체들이 연속되게 쌓이도록 힙의 앞 부분부터 채워서 객체가 존재하는 부분과 객체가 없는 부분으로 나눈다.

# Parallel GC

Serial GC와 알고리즘은 같다. 하지만 Minor GC를 처리하는 스레드를 여러 개로 늘려 더 빠른 동작이 가능하게 하는 방식으로 메모리가 충분하고 코어의 개수가 많을 때 유리하다. Throughput GC라고도 부른다.

# Parallel Old(Compacting) GC

Parallel GC의 개선된 버전으로 Old 영역에 알고리즘만 다르며, Mark-Summary-Compaction 단계를 거치는데, Summary 단계는 앞서 GC를 수행한 영역에 대해서 별도로 살아있는 객체를 식별한다는 점에서 다르며, 더 복잡한 단계를 거친다.

# CMS(Concurrent Mark Sweep) GC

Heap 메모리의 크기가 클 때 사용하며, 다수의 스레드를 사용하면서 최소한의 pause 타임을 가지게 설계되었습니다. 애플리케이션이 실행되는 동안 GC와 프로세서의 리소스를 공유할 수 있습니다. 즉, 애플리케이션 스레드와 GC스레드가 동시에 실행되어 STW 시간을 최대한 줄이기 위한 GC입니다.

간단히 말하면 애플리케이션의 평균 응답 시간이 느려질 수 있지만 GC에 의해 애플리케이션이 정지되지 않는다는 장점이 있습니다.

Serial GC와 Parallel GC는 Compact라는 Sweep 후 남은 데이터를 정리하는 과정이 있지만 CMS GC는 Compact 과정을 사용하지 않고 다양한 Mark 과정을 거칩니다.

- Initial Mark : GC Root 에서 참조하는 객체들만 식별
- Concurrent Mark : 이전 단계에서 식별한 객체들이 참조하는 모든 객체를 추적
- Remark : 이전 단계에서 식별한 객체를 다시 추적, 추가되거나 참조가 끊긴 객체 확정
- Concurrent Sweep : 식별한 객체를 삭제

![cms_gc](../images/cms_gc.png)

Serial GC 와 CMS GC


# G1(Garbage First) GC

CMS GC를 대체하기 위해 고안된 GC로 JDK9 버전부터 디폴트 GC로 지정되었으며, 기존 GC의 알고리즘에서 Heap 영역을 물리적으로 고정된 Young / Old 영역으로 나눴지만, G1 GC는 이런 구조가 아닌 `Region`이라는 개념을 새로 도입했다.

![g1_gc](../images/g1_gc.png)

전체 Heap 영역을 Region이라는 영역으로 체스같이 분할하여 상황에 따라 Eden, Survivor, Old등 역할을 고정이 아닌 동적으로 부여하여 유연하게 Garbage의 공간을 확보하므로 GC의 빈도가 줄어들게 됩니다.

# ZGC

JDK 15에서 정식 채택되었으며 17에 반영되었다. ZGC 는 메모리를 ZPage라는 Region을 재정의한 논리적인 단위로 구분한다.

![ZGC](../images/zgc.png)

ZGC Heap 영역의 메모리 구조

동적으로 생성/삭제 되며, 2MB의 배수 형태로 관리되며, ZPage는 세 가지 타입이 있다.

```java
const uint8_t _type; // zpage type

enum class ZPageType : uint8_t {
// type size object size limit allignment
small, 2M <= 265K <MinObjAlignmentInBytes>
medium, 32M <= 4M 4K
large X*M > 4M 2M
};
```

여기서 주의해야 할 점은 Large 타입의 ZPage에는 단 하나의 객체만 할당할 수 있다는 것이다. 이는 곧 1~5MB 의 작은 크기의 객체를 할당해도 해당 ZPage는 더 이상 할당할 수 없어진다.

### Compact 개념

GC가 수행되면 힙 영역을 비우기 위해 살아있는 객체를 이동시키는데, 이 때 객체를 위치시키기 위해 빈 공간을 찾아야 하는데 이것은 새로운 영역을 할당하고 채우는 것보다 비싼 비용의 작업이다. 그래서 ZGCCompact의 과정을 기존 region의 빈 곳을 찾아 넣는게 아니라 새로운 region을 생성 후 살아있는 객체들을 채운다.

기존의 region 의 GC가 대상이 되어 처리 한다고 가정한다면, 기존 region 을 참조하던 region은 새로운 region으로 이동한 객체를 가리키게(remapping) 된다. 만약 이 때 remapping이 완료되기 전에 기존 region 의 객체 값을 변경하면 새로운 region과 기존 region의 값이 불일치 하게 된다.

이 문제를 해결하기 위해 ZGC는 몇 가지 전략을 사용한다.

- Concurrent GC를 사용해서 객체의 GC 메타데이터를 객체 주소에 저장 (Reference coloring, Colored Pointers) : GC 메타데이터를 객체의 메모리 주소에 표시하는데 ZGC는 64비트만 지원하는데 메모리의 주소 파트로 42비트(4TB)를 사용하고 다른 4비트를 GC metadata(finalizable, remap, mark1, mark0)를 저장하는 용도로 사용
- 애플리케이션 스레드는 힙 메모리에 있는 객체를 참조할 때 JIT를 사용해 GC를 돕기 위해 작은 코드를 만나게 된다. 이코드의 주소는 colored point가 bad color 인지 체크하고, 만약 bad color 라면 객체를 상황에 따라서 mark/relocate/remapping 한다. (GC load Barriers)
- Multi-mapping 으로 reference coloring에 의해 메모리 offset(주소) x 에 대해서 특정 시점에는 mark0, mark1, remap 비트 중 하나가 1이 되기에 offset x 에 페이지(ZPage)를 할당할 때 ZGC는 동일한 페이지를 3개의 다른 주소영역에도 할당한다.

### GC Phase

GC는 크게 marking, relocating이라는 두가지 중요한 단계를 가진다.

1. Mark Start 단계 : 모든 애플리케이션 스레드를 멈추고 각 스레드마다 가지고 있는 local variable들을 스캔한다. thread local variable에서 힙으로의 참조를 GC Root라고 하며 GC Root set을 만든다. 일반적으로 gc root개수는 적은 편이라 mark start 단계의 STW는 **극히 짧은 시간**이다.
2. Concurrent Marking 진행 : root set에서 시작해 객체 그래프를 탐색하며 도달할 수 있는 객체를 살아있는 것으로 표시한다. ZGC는 각 page의 livemap 이라고 부르는 곳에 살아있는 객체 정보를 저장한다. livemap 은 주어진 인덱스의 객체가 strongly-reachable하거나 final-reachable 한지 등의 정보를 비트맵 형태로 저장하고 있다. 이 단계에서 애플리케이션 스레드의 경우 load barrier를 통해 객체의 참조에 대해 테스팅을 진행하며, 참조가 bad color라면 slow_path로 진입한 후 Marking을 위해 thread-local marking buffer(queue)에 추가한다. 이 버퍼가 가득차면 GC 스레드가 이 버퍼의 소유권을 가져오고 이 버퍼에서 도달할 수 있는 모든 객체를 재귀적으로 탐색한다. 즉 애플리케이션 스레드에서의 marking은 주소를 버퍼로 넣기만 할 뿐 GC 스레드가 객체 그래프를 탐색하고 live map을 업데이트 하는 역할이다. 이 단계가 끝나면 살아있는 객체와 가비지 객체로 나뉜다.
3. Mark End : 모든 애플리케이션이 멈추고 Thread-local marking buffer를 탐색하며 비운다. 이때 marking 하지 않은 참조들 중 큰 하위 객체 그래프를 발견하면 처리해야하는 시간이 많아 STW가 길어질 수 있으므로 1ms 후 이 단계를 끝내고 Concurrent Mark 단계로 돌아간 다음 다시 Mark End단계로 진입한다.
4. Concurrent Processing : Concurrent Reset Relocation Set, Concurrent Destroy Detached Pages (비어있는 page는 메모리를 해제, 불필요한 클래스는 unload), Concurrent Select Relocation Set, Prepare Relocation Set(비워야하는 page들의 Set으로 relocation set 에 들어있는 page의 객체들을 대상으로 forwarding table을 할당한다. forwarding table은 기본적으로 객체가 재배치된 주소를 저장하는 hash map이다.)
5. Relocation Start : 모든 애플리케이션 스레드를 멈추고 relocation set의 page 객체 중 GC Root에 참조되는 것들은 모두 일괄 relocation/remapping 한다.
6. Concurrent Relocation : GC 스레드는 살아있는 객체를 탐색하고 아직 재배치되지 않은 모든 객체를 새로운 ZPage로 재배치하며 이 재배치는 애플리케이션 스레드를 통해 일어날 수도 있다.
7. Concurrent Remapping : 모든 재배치가 끝나면 old 객체가 아닌 새로운 객체로 참조를 변경한다. remapping은 애플리케이션 스레드의 load barrier에 의해서 진행되며 다음 GC cycle전까지 모두 완료되지 않을 수도 있다.

# 정리

ZGC의 목적 중 하나는 STW 상태를 10ms 이하로 가져가는 것을 목표로 한다. G1 GC 이전 GC들의 문제점인 Major GC 시 STW문제를 다시 겪지 않기위해서다. referenct 할당마다 실행되는 load barrier를 통해 GC에 필요한 작업들을 멀티 스레드로 애플리케이션 스레드와 동시에 진행한다.

참조

- [Naver Garbage Collection](https://d2.naver.com/helloworld/1329)
- [Naver ZGC의 기본 개념 이해하기](https://d2.naver.com/helloworld/0128759)
- [드림어스컴퍼니 ZGC에 대해서](https://www.blog-dreamus.com/post/zgc%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C)
59 changes: 59 additions & 0 deletions ch12/김민기.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 자바 동시성

# 자바에서 사용할 수 있는 동시성 문제를 해결하는 방법 몇가지

## 1. synchronized

특정 메서드나 블럭에 lock을 걸어 동시성 문제를 해결합니다. 하지만 범위가 큰 경우가 많기 때문에 성능문제(데드락)가 발생할 여지가 높습니다.

보통 메서드, 객체에 `synchronized` 키워드를 사용해 잠금을 겁니다. 접근에 성공한 스레드는 잠금을 획득하고 해당 메서드나 블락이 종료될 때 까지 다른 스레드에서 접근이 불가능하게 됩니다.

```java
int count = 0;
public synchronized void increment() {
count++;
}
```

## 2. Volatile

Java에서 가시성(visibility)을 보장하며 변수단위로 키워드(`volatile`)를 선언해 값을 CPU 캐시가 아닌 메인 메모리에서 직접 읽고 씁니다. 이를 통해 변경 내용을 즉시 반영하는 원리입니다.

```java
private volatile int count = 0;
```

## 3. Atomic

Java에서 원자성을 보장하는 클래스로 값을 읽고 쓰는 연산을 원자적으로 수행, 다른 스레드가 동시에 접근해도 이전 스레드의 연산이 완전히 종료된 이후 접근을 허용합니다.

CAS(Compare-And-Swap)알고리즘을 사용하며, 이 알고리즘은 변수의 값을 읽고 쓰는 동안 다른 스레드가 해당 변수의 값을 변경하지 않았는지 확인하고 변경이 없는 경우에만 값을 설정합니다.

```java
private AtomicInteger count = new AtomicInteger(0);
```

## 4. Semaphore

개수를 나타내는 값을 가지며, 스레드가 접근하는 경우 값을 -1 씩 줄여 0 이되면 접근을 차단하며, 다른 스레드들은 세마포어가 릴리즈될 때까지 대기합니다.

Java 에서는 `Semaphore` 클래스로 지원합니다.
`acquire()` : 접근을 시도하며 접근에 성공한 경우 세마포어의 값을 감소하고 리소스를 획득합니다.

`release()` : 스레드의 연산이 종료된 후 허용된 개수를 증가시키며 다른 스레드가 리소스에 접근할 수 있도록 허용합니다.

```java
private Semaphore semaphore = new Semaphore(1);
```

## 5. Mutex

공유 자원에 대한 동시 접근을 막고, 한번에 하나의 스레드만 허용하는 기능(상호배제)을 제공하는 기법으로 Java 에서는 `ReentrantLock` 클래스로 구현할 수 있습니다.

`lock()` : 뮤텍스를 획득하고 잠금처리

`unlock()` : 뮤텍스를 반환하고 잠금해제

```java
private Lock mutex = new ReentrantLock();
```
69 changes: 69 additions & 0 deletions ch13/김민기.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# IntelliJ 프로파일링

# 개요

IntelliJ 프로파일러를 통해서 jvm 애플리케이션의 프로파일링 확인해보기

# 왜 IntelliJ Profiler 인가?

다른 에이전트나 프로그램을 설치할 필요없이, 개발환경에서 바로 실행하고 확인해 볼 수 있는 접근성이 뛰어나며, 결과를 프로파일링 종료 후 바로 확인이 가능함

물론 운영 환경에선 다른 상용 프로파일러를 사용해야 하지만 프로파일링을 간편히 확인해볼 수 있다는 점에서 토이프로젝트나 개발환경에서 간단한 디버깅용으로 쓰기 좋을거 같다.

## 시작하기

먼저 IntelliJ → View → Tool Windows → Profiler 로 콘솔창에서 조회할 수 있다.

실행은 프로그램 실행하는 run → Profile ‘애플리케이션명’ with IntelliJ Profiler 를 선택해서 실행하게 되면 프로파일링을 시작하며 Stop으로 중지하면 프로파일링을 종료하고 바로 show result로 볼 수 있다.

![스크린샷 2024-03-10 오후 8.47.24.png](../images/스크린샷 2024-03-10 오후 8.47.24.png)

## 현재 프로세스 조회하기

먼저 콘솔창 위치에 있는 Profiler 창이 있다면 Home → Process → CPU and Memory Live Charts 로 현재 실행중인 프로세스의 cpu, memory, threads 상태들을 확인해 볼 수 있다.

![스크린샷 2024-03-10 오후 8.53.12.png](../images/스크린샷 2024-03-10 오후 8.53.12.png)

## Heap Dump

콘솔창 옆에 카메라 모양 아이콘을 보면

- Capture Memory Snapshot,

![스크린샷 2024-03-10 오후 9.24.00.png](../images/스크린샷 2024-03-10 오후 9.24.00.png)

- Get Thread Dump

![스크린샷 2024-03-10 오후 9.24.18.png](../images/스크린샷 2024-03-10 오후 9.24.18.png)


두가지 정보를 얻을 수 있다.

## 프로파일링 결과보기

프로파일링 결과를 확인하게 되면 애플리케이션명과 프로파일링한 일시가 적힌 탭을 눌러서 확인해 볼 수 있다.

- Flame Graph : 전체적으로 프로파일링된 클래스, 메서드와 메모리 상태, 호출된 스택을 확인해 볼 수 있다.

![스크린샷 2024-03-10 오후 8.54.23.png](../images/스크린샷 2024-03-10 오후 8.54.23.png)


- Call Tree : 샘플링된 데이터를 트리 구조로 구성하며 실행 시간, 재귀 호출, 필퍼링된 호출들을 확인해 볼 수 있다.

![스크린샷 2024-03-10 오후 9.29.09.png](../images/스크린샷 2024-03-10 오후 9.29.09.png)

- Method List : 프로필 데이터의 모든 분석법을 수집하고 누적 샘플 시간을 기준으로 정렬하며, 호출자의 계층 구조, 호출 수신자, 호출 수신자 리스트를 볼 수 있다.

![스크린샷 2024-03-10 오후 9.29.03.png](../images/스크린샷 2024-03-10 오후 9.29.03.png)

- Time line : 개별 스레드의 활동을 모니터링하는 기능으로 시간 경과에 따른 CPU 활동과 메모리 할당을 시각화해서 보여준다. 가장 활동적인 스레드, 동시성 문제 확인, IO 프로파일등을 확인할 수 있다.

![스크린샷 2024-03-10 오후 9.28.56.png](../images/스크린샷 2024-03-10 오후 9.28.56.png)

- Events : 애플리케이션, JVM, OS등 프로세스와 연관된 이벤트 처리목록을 조회할 수 있다.


참조

- https://www.jetbrains.com/help/idea/read-the-profiling-report.html#profiler-flame-chart
- https://gomnezip.tistory.com/396
Loading

0 comments on commit 50e78b9

Please sign in to comment.