<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>jimmy_neutron.log</title>
        <link>https://velog.io/</link>
        <description>안녕하세요!</description>
        <lastBuildDate>Tue, 10 Feb 2026 13:13:17 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>jimmy_neutron.log</title>
            <url>https://velog.velcdn.com/images/jimmy_neutron/profile/88d13deb-5c95-4b6a-80e1-293fcd151dd4/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. jimmy_neutron.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jimmy_neutron" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[스레드 안전성과 락 최적화]]></title>
            <link>https://velog.io/@jimmy_neutron/%EC%8A%A4%EB%A0%88%EB%93%9C-%EC%95%88%EC%A0%84%EC%84%B1%EA%B3%BC-%EB%9D%BD-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@jimmy_neutron/%EC%8A%A4%EB%A0%88%EB%93%9C-%EC%95%88%EC%A0%84%EC%84%B1%EA%B3%BC-%EB%9D%BD-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Tue, 10 Feb 2026 13:13:17 GMT</pubDate>
            <description><![CDATA[<h1 id="스레드-안전">스레드 안전</h1>
<p>여러 스레드가 한 객체에 동시에 접근할 때 어떤 런타임 환경에서든 다음 두 조건을 모두 충족하면서 객체를 호출하는 행위가 올바른 결과를 얻을 수 있다면 <code>그 객체는 스레드 안전하다</code> 라고 한다.</p>
<ul>
<li>특별한 스레드 스케줄링이나 대체 실행 수단을 고려할 필요가 없다</li>
<li>추가적인 동기화 수단이나 호출자 측에서 조율이 필요 없다</li>
</ul>
<h1 id="스레드-안전성-단계">스레드 안전성 단계</h1>
<p>자바 스레드 안전성은 안전하다/안전하지 않다 두개로 나뉘지 않는다.
5가지 단계로 나눠 생각할 수 있어야 한다. (불변, 절대적 스레드 안전, 조건부 스레드 안전, 스레드 호환, 스레드 적대적)</p>
<p><strong>불변</strong></p>
<ul>
<li>자바 final 키워드가 붙은 변수들이다.</li>
<li>이 변수들은 불변성이 지켜지므로, 스레드 안전하다.</li>
<li>final 이 붙은 변수의 불변성은 자바 컴파일러와 JVM 이 지켜준다.</li>
<li>Enum 타입, Long, Double, String, BigInteger, BigDecimal, java.lang.Number 하위 등 클래스들이 불변하다.</li>
</ul>
<p><strong>절대적 스레드 안전</strong></p>
<ul>
<li>위의 스레드 안전 정의를 만족하는 경우</li>
<li>자바 API 는 대부분 조건부 안전을 만족한다. 따라서 자바 API 를 사용하여 애플리케이션을 작성할 때 절대적 스레드 안전을 만들기 위해 synchronized, volatile 등 동기화 작업을 해야 한다.</li>
<li>동기화 작업이 완벽하게 된 메서드는 절대적 스레드 안전이라 할 수 있을 것이다.</li>
</ul>
<p><strong>조건부 스레드 안전</strong></p>
<ul>
<li>단일 작업은 스레드 안전하지만, 스레드 안전한 단일 작업을 n개 묶어서 사용할때에는 추가적인 동기화 처리가 필요한 경우<ul>
<li>메서드 자체는 synchronized 로 감싸 메서드 자체는 절대적 스레드 안전이지만, 절대적 스레드 안전 메서드 두개를 썼을때 스레드 불안전한 경우를 말한다.</li>
</ul>
</li>
<li>자바에선 Vector, HashTable, SynchronizedCollections 등이 있다.</li>
</ul>
<pre><code class="language-java">public class ThreadSafe {

    private static List&lt;Integer&gt; list = Collections.synchronizedList(new ArrayList&lt;Integer&gt;());

    static void main(String[] args) {
        while (true) {
            for (int i = 0; i &lt; 10; i++) {
                list.add(i);
            }

            Thread removeThread = new Thread(() -&gt; {
                for (int i = 0; i &lt; list.size(); i++) {
                    list.remove(i);
                }
            });

            Thread printThread = new Thread(() -&gt; {
                for (Integer integer : list) {
                    System.out.println(integer);
                }
            });

            removeThread.start();
            printThread.start();

            while (Thread.activeCount() &gt; 20) ;
        }
    }
}</code></pre>
<p>위 예시 코드에서 사용한 <code>Collections.synchronizedList</code> 는 add, remove 등 리스트에 가하는 메서드에 모두 synchronized 를 적용한다. 즉, 요소 수정 메서드 자체는 모두 절대적 스레드 안전이다.</p>
<p>하지만 위 예시는 <code>ConcurrentModificationException</code> 를 발생시키는 스레드 불안전 코드다.
위처럼 절대적 스레드 안전 메서드를 잘못 조합하면 문제가 생겨, 추가적인 동기화 처리가 필요한 경우를 <code>조건부 스레드 안전</code> 이라 한다.</p>
<p>추가적인 동기화 처리를 적용한 코드는 다음과 같다.</p>
<pre><code class="language-java">public class ThreadSafe {

    private static List&lt;Integer&gt; list = Collections.synchronizedList(new ArrayList&lt;Integer&gt;());

    static void main(String[] args) {
        while (true) {
            for (int i = 0; i &lt; 10; i++) {
                list.add(i);
            }

            Thread removeThread = new Thread(() -&gt; {
                synchronized(list) { // 추가됨
                    for (int i = 0; i &lt; list.size(); i++) {
                        list.remove(i);
                    }
                }
            });

            Thread printThread = new Thread(() -&gt; {
                synchronized (list) { // 추가됨
                    for (Integer integer : list) {
                        System.out.println(integer);
                    }
                }
            });

            removeThread.start();
            printThread.start();

            while (Thread.activeCount() &gt; 20) ;
        }
    }
}
</code></pre>
<p><strong>스레드 호환</strong></p>
<ul>
<li>객체 자체가 스레드 안전하지 않아 안전하게 사용하려면 동기화 처리를 해야 하는 클래스들</li>
<li>일반적인 ArrayList, HashMap 등등</li>
</ul>
<p><strong>스레드 적대적</strong></p>
<ul>
<li>동기화 조치를 취해도 멀티 스레드 환경에서 안전하게 사용할 수 없는 것들을 스레드 적대적이라 한다.</li>
<li>대표적으로 Thread.suspend(), Thread.resume()<ul>
<li>JDK23 에서 사라졌다.</li>
<li>동기화 블럭에서 Thread.suspend() 하면 락을 유지한 채로 스레드가 멈춘다. 스레드가 재게되어 로직을 모두 실행하고 락을 해제하기 전까지는 락을 필요로 하는 다른 스레드 모두 무기한 대기하게 된다.</li>
<li><a href="https://medium.com/@sruthy.g.jayan/why-java-23-removes-the-deprecated-thread-suspend-3ddb32c98b13">참고</a></li>
</ul>
</li>
<li>이외에도 System.setIn(), System.setOut(), System.runFinalizersOnExit()</li>
</ul>
<h1 id="스레드-안전성-구현">스레드 안전성 구현</h1>
<p>자바에서 스레드 안전성을 실제로 구현하는 방법은 크게 3가지로 나눌 수 있다.</p>
<ol>
<li>상호 배제 동기화(Blocking 동기화)</li>
<li>Non-Blocking 동기화</li>
<li>동기화 불필요 매커니즘</li>
</ol>
<h2 id="상호-배제-동기화blocking-동기화">상호 배제 동기화(Blocking 동기화)</h2>
<ul>
<li><code>synchronized</code></li>
<li><code>ReentrantLock</code></li>
<li><code>java.util.concurrent.Semaphore</code></li>
</ul>
<blockquote>
<p>synchronized, ReentrantLock 은 뮤텍스다.
뮤텍스는 공유 자원 하나에 대한 락이며, 락 해제는 락을 건 스레드만 가능하다.
세마포어는 n개의 리소스가 사용할 수 있는 잠금이며, 락의 소유 개념이 없어 다른 스레드가 허용 count 를 줄일 수 있다.</p>
</blockquote>
<h3 id="상호-배제-동기화의-비용">상호 배제 동기화의 비용</h3>
<p>자바는 커널 스레드와 연결되는 1:1 스레드 모델이다. 상호 배제 락 때문에 스레드를 멈추거나 재개할 때에는 커널 모드로 전환하여 커널 스레드를 정지/재개해야 한다. 이는 상당히 비싼 비용의 작업이다.</p>
<h3 id="synchronized">synchronized</h3>
<p>synchronzied 는 바이트 코드 명령어 <code>monitorenter</code>, <code>monitorexit</code> 를 사용한다.</p>
<p><code>monitorenter</code> 는 객체의 모니터를 획득하려 시도하며, 다른 스레드에서 이미 락을 가지고 있다면 대기한다. 모니터를 획득한 스레드가 <code>monitorexit</code> 를 실행하면 대기중이던 스레드는 모니터를 획득하며 <code>monitorenter</code> 를 완료한다.</p>
<p>또한 같은 스레드라면 이미 모니터를 소유하고 있기 때문에 synchronized 블록에 다시 진입할 수 있다.</p>
<h3 id="reentrantlock">ReentrantLock</h3>
<p>재진입 가능 락으로 synchronized 와 비슷하다.
JDK 5 에 생긴 <code>java.util.concurrent.locks.Lock</code> 의 대표적 구현체다.</p>
<p>synchronized 에 비해 몇가지 추가 기능을 제공한다.</p>
<ol>
<li>대기중 인터럽트 : 대기 중 스레드가 너무 오래 걸리면 락 획득을 포기하고 다른 일을 할 수 있다.</li>
<li>페어 락 : 같은 락을 얻기 위해 대기하는 여러 스레드에 대해 획득 시도 순서로 락을 획득하도록 할 수 있다.<ul>
<li>처리량에 큰 악영향이 있다. 사용에 주의</li>
<li>대기 중인 스레드 중 우선순위 없이 랜덤으로 획득하는 것을 un-fair 락이라 한다. synchronized 는 un-fair 이며, ReentrantLock 는 선택할 수 있다.</li>
</ul>
</li>
<li>둘 이상의 조건 지정 : newCondition() 객체를 여러번 호출하여 여러 조건을 만들 수 있다.<ul>
<li>synchronized 는 wait(), notifiy(), notifiyAll() 에 하나의 조건만 줄 수 있다.</li>
</ul>
</li>
</ol>
<h3 id="synchronized-vs-reentrantlock">synchronized vs ReentrantLock</h3>
<p>현재는 두 키워드의 성능이 비슷하다.</p>
<p>ReentrantLock 는 finally 블록에서 락 해제를 반드시 해줘야 한다.</p>
<p>ReentrantLock 만의 기능이 필요한게 아니라면 락 획득과 해제를 자동으로 해주는 synchronized 가 더 좋을 것이다.</p>
<h2 id="논블로킹-동기화">논블로킹 동기화</h2>
<p>상호 배제 동기화는 상당이 큰 비용을 지불한다고 했다.
상호 배제 동기화는 비관적 동기화로서, 동기화하지 않으면 문제가 생김을 전제로 동작한다.</p>
<p>이 반대는 낙관적 동기화로, 문제가 생기지 않을 것 같으니 일단 실행한다.</p>
<ol>
<li>일단 작업 실행</li>
<li>충돌 감지</li>
<li>적절한 처리</li>
</ol>
<p>대기를 할 필요 없으니 Non-Blocking 동기화라고도 한다.</p>
<blockquote>
<p>Lock-Free 라고도 한다.</p>
</blockquote>
<h3 id="논블로킹-동기화의-하드웨어-지원">논블로킹 동기화의 하드웨어 지원</h3>
<p><strong>중요한 것은 1번 작업 실행과 2번 충돌 감지는 원자적으로 일어나야 한다는 것이다.</strong>
<strong>따라서 이는 하드웨어 명령어 지원이 필요하다.</strong></p>
<p>현대의 프로세서들이 지원하는 논블로킹 동기화 명령어는 다음 것들이 있다.</p>
<ul>
<li>TAS(Test-and-Set</li>
<li>FAA(Fetch-and-Add)</li>
<li>Swap</li>
<li>CAS(Compare-and-Swap)</li>
<li>LL/SC(Load-Linked/Store-Conditinal)</li>
</ul>
<h3 id="cas">CAS</h3>
<p>위 명령어 중 하나인 CAS 를 알아보자.
이는 자바에서 지원하는 논블로킹 동기화다.</p>
<p>공유 자원 변수 X 에 대해</p>
<ol>
<li>X 의 기존값이라고 예상하는 A</li>
<li>X 값으로 세팅할 B</li>
<li>메모리 주소</li>
</ol>
<p>세가지를 CAS 연산의 인자로 넣는다. 만약 실제 값이 A 와 같다면 B 로 값을 변경한다.
또한 이 작업은 하드웨어의 도움을 받아 <strong>원자적</strong>으로 일어난다.</p>
<h4 id="자바의-cas">자바의 CAS</h4>
<ul>
<li><code>java.util.concurrent.AtomicInteger</code> 외 Atomic 시리즈는 CAS 를 이용하여 원자적으로 연산하는 숫자를 만들 수 있다.</li>
<li>JDK 9 부터 제공되는 <code>VarHandle</code> 을 사용하면 직접 CAS 를 사용할 수 있다.</li>
</ul>
<blockquote>
<p>Unsafe 의 cas 연산은 유저 사용 용도가 아니니 사용하지 말자. VarHandle 쓰자.</p>
</blockquote>
<h4 id="cas-의-문제">CAS 의 문제</h4>
<p>대표적인 CAS 의 문제는 ABA 문제가 있다.</p>
<p>예상하는 값 A 가 맞다는건 X 를 읽어온 뒤 변경되지 않았다고 판단하는 것이다.
하지만 A 가 B 로 바뀌었다가 다시 A 로 돌아온 경우 CAS 는 변경되지 않았다고 판단할 수 있다.</p>
<p><code>java.util.concurrent.atomic</code> 의 <code>AtomicStampedReference</code> 는 변수값의 버전을 관리하며 ABA 를 보완한다.</p>
<p>하지만 대부분 ABA 문제는 동시성 해결에 별로 중요치 않다.
ABA 가 중요하다면 기존의 상호 배제 동기화를 고려해볼 수 있을 것이다.</p>
<h2 id="동기화가-필요없는-메커니즘">동기화가 필요없는 메커니즘</h2>
<p>다음 두가지는 동기화 자체가 필요 없는 안전한 메커니즘이다.</p>
<ol>
<li><p>재진입 가능한 코드 : 전역 변수, 힙 데이터, 공유 시스템 자원을 전혀 사용하지 않고 항상 같은 입력에 같은 출력을 반환하는 재진입 가능 코드는 스레드 안전하다.</p>
</li>
<li><p>스레드 로컬 저장소 : 코드 조각에서 사용하는 데이터를 다른 코드와 공유해야하지만, <strong>두 코드조각이 같은 스레드에서 동작함</strong> 이 보장되면 스레드 로컬 저장소를 사용할 수 있다. 스레드 로컬 저장소는 하나의 스레드만이 접근하기 때문에, 스레드 안전하다.</p>
<ul>
<li>자바에서는 ThreadLocal </li>
<li>웹 서버에는 인증정보나 파라미터 등 요청당 정보들을 공유 변수가 아닌 스레드 로컬을 사용하여 동기화 문제를 피한다.</li>
</ul>
</li>
</ol>
<h1 id="락-최적화">락 최적화</h1>
<h2 id="스핀락-적응형-스핀락">스핀락, 적응형 스핀락</h2>
<ul>
<li>컨텍스트 스위칭은 커널 스레드 정지 비용이 너무 높다. 하지만 대부분 락은 금방 끝난다.</li>
<li>커널 모드로 전환하여 커널 스레드를 정지하지 않고 않고 while 루프를 돌려서 기다린다.</li>
<li>다만 프로세서를 계속 사용한다는 단점 때문에, 일정 횟수 스핀 후 원래처럼 커널 스레드 대기한다.</li>
<li>적응형 스핀락 : 자바는 스핀 횟수를 적응형으로 그때그때 바꾼다.<ul>
<li>스핀락 시점에서 락 획득에 성공한다면 다음 번에는 횟수 이내에 락을 획득하지 못하더라도 스핀 횟수를 늘려 조금 더 기다려본다.</li>
<li>만약 충분히 늘린 스핀에서도 계속해서 실패한다면 오래 걸리는 락이라 판단하여 아예 스핀락을 없앨수도 있다.</li>
</ul>
</li>
</ul>
<h2 id="락-제거">락 제거</h2>
<ul>
<li>JIT 컴파일러의 최적화이다.</li>
<li>탈출 분석시 메서드의 힙 변수가 메서드를 탈출하지 않고 다른 스레드가 사용하지 않는다고 분석되면 스택 변수처럼 취급할 수 있다. -&gt; 동기화도 제거할 수 있다.</li>
</ul>
<p>탈출 분석은 비싼 분석이다. 프로시저간 분석을 해야 하기 때문이다.
개발자라면 꼭 필요한 부분에 락을 추가했을텐데, 이를 제거하기 위해 분석 하는것은 너무 비효율적이지 않을까?</p>
<p>그 이유는 우리 코드가 아닌 자바 API 들 때문이다.
자바는 보수적으로 작성한 동시성 코드가 상당히 많다. 필요하지 않다면 이 코드들을 없애는 최적화가 주 목적이다.</p>
<p>예를 들어 다음 코드를 보자</p>
<pre><code class="language-java">public class Test {

    public static void main(String[] args) {
        System.out.println(test(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;));
    }

    static String test(String s1, String s2, String s3) {
        return s1 + s2 + s3;
    }
}</code></pre>
<p>위의 문자열 합치기는 JDK 1.4 까지는 javac 컴파일 타임에 StringBuffer 를 사용하는 코드로 바뀐다.</p>
<p>StringBuffer::append() 는 기본적으로 synchronized 블록이다.
하지만 JIT 는 위 코드를 실행할 때에는 필요없는 동기화 블럭을 제거한다.
물론 인터프리터 시점에는 락이 걸리겠지만 말이다.</p>
<blockquote>
<p>참고로 String 더하기 연산은 JDK 5 부터 StringBuffer 가 아닌 StringBuilder 로 바뀐다. 다음은 JDK 8 로 컴파일한 클래스 파일</p>
</blockquote>
<pre><code> 0: new           #8                  // class java/lang/StringBuilder
 3: dup
 4: invokespecial #9                  // Method java/lang/StringBuilder.&quot;&lt;init&gt;&quot;:()V
 7: aload_0
 8: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
11: aload_1
12: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: aload_2
16: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #11                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;</code></pre><blockquote>
<p>또한 JDK 9 부터는 invokedynamic 을 활용한다. 다음은 JDK 11 로 컴파일한 클래스 파일</p>
</blockquote>
<pre><code>3: invokedynamic #8,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;</code></pre><h2 id="락-범위-확장">락 범위 확장</h2>
<p>이 또한 최적화의 한 종류로, 크게 락 한번 잡을 수 있는데 자잘하게 여러번 잡은 것들을 자동으로 큰 동기화 구문으로 바꿔준다.</p>
<p>경합이 없는 상황에서 불필요하게 상호 베제 동기화가 발생하는것을 막는 것이다.</p>
<p>예를 들어 StringBuffer 의 append() 를 연달아 쓰면 synchronized 블럭이 연속으로 몇번 왔다갔다 한다. 이를 큰 synchronized 블록 하나로 바꾸면 성능이 좋아질 것이다.</p>
<h2 id="경량-락">경량 락</h2>
<p>일반적으로 지금까지 설명한 뮤텍스를 사용하는 락들은 모두 중량 락이다.</p>
<p>경량 락이란 중량 락보다 성능저하가 덜한 가벼운 락 메커니즘이다. 이는 <code>대부분의 락은 경합을 하지 않음</code>을  전제로, <code>CAS</code> 연산으로 동작한다.
따라서 경합이 없을때 동작하는 락이다.(아래 설명)</p>
<p>자바는 상호 배제 동기화를 하면 먼저 경량 락으로 객체를 동기화하고, 필요시에 중량 락으로 변경하여 동작한다.</p>
<h3 id="경량-락의-동작">경량 락의 동작</h3>
<p>경량 락은 객체 레이아웃의 마크 워드 부분을 이용한다.
객체 레이아웃은 <a href="https://velog.io/@jimmy_neutron/HotSpot-%EA%B0%9D%EC%B2%B4-%EB%A0%88%EC%9D%B4%EC%95%84%EC%9B%83">이 글</a> 을 참고하자.</p>
<ol>
<li><p>락을 획득하기전 첫 상태. 공유 객체의 마크워드 플래그는 <code>01</code> 이다.
<img src="https://velog.velcdn.com/images/jimmy_neutron/post/d2c54d59-1742-4763-bc21-0fbb34f87e7c/image.png" alt=""></p>
</li>
<li><p>락 획득을 시도하기 전 공유 객체의 마크 워드를 스택 프레임으로 복사한다.</p>
<ul>
<li>3번 단계에서 공유 객체의 마크 워드를 경량 락 동작을 위해 다른 정보로 대체하기 때문에 백업하는 용도.
<img src="https://velog.velcdn.com/images/jimmy_neutron/post/e9834d3e-a519-47f8-a61d-df23504397e9/image.png" alt=""></li>
</ul>
</li>
<li><p>락을 획득하고자 하는 스레드는 공유 객체의 마크 워드를 자신의 스택 프레임 주소로 변경을 <code>CAS</code> 로 시도한다. 성공한다면 락을 획득 한 것이고, 실패한다면 다른 스레드가 이미 락을 잡은 것이다.
<img src="https://velog.velcdn.com/images/jimmy_neutron/post/bcd91a46-b1e8-4a83-892e-6bb319a3eeaa/image.png" alt=""></p>
</li>
<li><p>만약 스레드가 공유 객체를 사용하는데 플래그가 <code>00</code> 이며(경량 락 상태), 스택 포인터 값이 본인의 스택 프레임이라면 자신이 락을 가지고 있음을 알 수 있다.</p>
</li>
</ol>
<h3 id="중량-락-변경">중량 락 변경</h3>
<p>둘 이상의 스레드가 같은 락을 두고 경합하는 상황이라면 경량 락은 더 이상 유효하지 않다.</p>
<p>위 과정의 3번에서 락 획득에 실패했다면 중량 락으로 변경한다.
경합이 있는 과정에서는 CAS 연산 + 뮤택스 까지 둘 다 하게 되어 오히려 느려지기 때문에 중량 락으로 변경하는게 좋기 때문이다.</p>
<p>이 때 플래그는 중량 락을 가리키는 <code>10</code> 로 변경되며 중량 락으로 동작한다.</p>
<h2 id="편향-락">편향 락</h2>
<p>편향 락은 경량 락에 한단계 더 가벼운 락이다.
편향 락을 획득한 스레드는 다른 스레드가 락을 획득하려 하기 전까진 CAS 연산 조차 없이 락을 획득할 수 있다.</p>
<p>마찬가지로 마크 워드를 사용하며 <code>01</code> 플래그를 사용한다.</p>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/047d544a-f8ee-4cf4-87b0-10cc9de1fc7d/image.png" alt="">
<a href="https://wiki.openjdk.org/spaces/HotSpot/pages/11829266/Synchronization">https://wiki.openjdk.org/spaces/HotSpot/pages/11829266/Synchronization</a></p>
<p>하지만 최근 도입된 동시성 API 를 사용하면 불필요한 경합이 많이 줄어듦에 비해 편향락은 복잡한 작업을 거쳐야 하는 비용이 큰 락이라 최근 JDK 에서 삭제되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스레드]]></title>
            <link>https://velog.io/@jimmy_neutron/%EC%8A%A4%EB%A0%88%EB%93%9C</link>
            <guid>https://velog.io/@jimmy_neutron/%EC%8A%A4%EB%A0%88%EB%93%9C</guid>
            <pubDate>Mon, 09 Feb 2026 19:41:52 GMT</pubDate>
            <description><![CDATA[<h1 id="멀티스레드-모델">멀티스레드 모델</h1>
<h2 id="11">1:1</h2>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/ca063739-0d1f-4c94-8acf-548e5cd3508a/image.png" alt=""></p>
<ul>
<li><p>OS 가 제공하는 커널 스레드(커널의 복제본) 한개를 고수준 인터페이스인 경량 프로세스(스레드) 하나로 맵핑하여 사용</p>
</li>
<li><p>커널의 도움으로 경량 프로세스 각각이 독립적으로 스케줄링 된다. 따라서 경량 스레드 하나가 시스템 호출에서 블록되더라도 전체 프로세스는 영향을 받지 않는다.</p>
</li>
<li><p>커널 스레드를 기반으로 구현되었기에 생성, 소멸, 동기화 등 다양한 스레드 연산이 시스템 호출로 이뤄진다. 시스템 호출은 사용자 모드와 커널 모드의 전환이 발생하여 비용이 높다.</p>
</li>
<li><p>경량 프로세스 하나가 커널 스레드 하나를 사용하기에 경량 프로세스의 수에 제한이 있다.</p>
<h2 id="1n">1:N</h2>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/86a356b3-78fe-4fce-a9e6-de4db4257487/image.png" alt=""></p>
</li>
<li><p>넓은 의미에서 1:1 의 경량 프로세스 또한 유저 스레드이지만, 좁은 의미에서는 온전히 사용자 공간에서 구현되는 스레드 라이브러리를 사용자 스레드라 한다. 1:N 관계에서는 좁은 의미의 사용자 스레드를 만들어 사용한다.</p>
</li>
<li><p>커널 모드로 전환할 필요가 없다.(고성능)</p>
</li>
<li><p>유저 스레드는 시스템 커널의 지원이 필요없다. 반대로 말하면 스레드의 생성, 소멸, 동기화, 스케줄링을 사용자 프로그램이 모두 처리해야 한다. 따라서 매우 복잡하고 어렵다.</p>
</li>
<li><p>어려운 난이도에 잘 사용하지 않는 모델이지만, go 와 Erlang 에서 사용하고 있다.</p>
<h2 id="nm">N:M</h2>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/53c948e9-3047-4061-aec2-b03fe51da0cc/image.png" alt=""></p>
</li>
<li><p>사용자 스레드와 경량 프로세스 모두 사용한다.</p>
</li>
<li><p>스레드 스케줄링, 프로세서 매핑등의 OS 기능을 사용할 수 있으면서 사용자 프로그램에서 스레드 생성, 소멸, 스케줄링 비용을 저렴하게 구현하여 감당할 수 있는 동시성 규모가 커진다.</p>
</li>
<li><p>사용자 스레드의 시스템 호출은 온전히 경량 프로세스에 의해 수행되므로 프로세스 전체가 완전히 블록될 위험이 크게 줄어든다.</p>
</li>
</ul>
<h1 id="자바-스레드">자바 스레드</h1>
<p>JVM 은 따로 스레드 모델을 규정하진 않는다.
스레드 모델은 플랫폼 독립적으로 구현하려면 1:N 유저 스레드 모델을 사용해야 하는데 이는 1.2 까지 쓰이다 폐기되었다.
대부분은 OS 의 기본 스레드 모델인 1:1을 따른다.
따라서 JVM 은 스레드 스케줄링 등을 관리하지 않고 OS 에 맡긴다.</p>
<h2 id="자바-스레드-스케줄링">자바 스레드 스케줄링</h2>
<p>자바는 기본적으로 선점형 스케줄링을 사용한다.
코틀린 코루틴을 사용하면 JVM 에서도 협력적 스케줄링을 사용할 수 있다.</p>
<h3 id="협력적-스케줄링">협력적 스케줄링</h3>
<ul>
<li>스레드 실행 시간을 스레드 스스로 결정. 스레드는 작업을 마치면 시스템에 알림.</li>
<li>장점<ul>
<li>구현이 쉽다</li>
<li>동기화 문제 발생 확률이 적다. 멈출 시점을 명확하게(코드로 지정) 알고 있기 때문이다.<ul>
<li>반면 선점형 스케줄링은 스레드가 언제 멈출지 모른다. 임계 영역에서 멈추면 동시성에 문제가 생기기 쉽다.</li>
</ul>
</li>
</ul>
</li>
<li>단점<ul>
<li>스레드 실행 시간을 제어할 수 없다. 스레드가 문제가 있어 블럭되면 제어를 프로그램에 돌려주지 못해 프로그램 자체가 멈춘다.<h3 id="선점형-스케줄링">선점형 스케줄링</h3>
</li>
</ul>
</li>
<li>각 스레드의 실행 시간을 시스템이 할당</li>
<li>스레드들은 전환 시점을 자신이 결정하지 못함. Thread::yield() 는 능동적으로 제어권을 포기하지만 제어권을 가져오는 방법은 없다.</li>
<li>장점<ul>
<li>시스템이 실행 시간을 결정하기에, 문제가 발생한 스레드를 강제로 멈출 수 있다. 프로그램 전체가 멈추는 사태는 없다.</li>
</ul>
</li>
</ul>
<blockquote>
<p>윈도우의 경우 과거 협력적 스케줄링 방식으로 프로세스 하나가 문제가 생기면 전체가 먹통이 되었다.
현재는 선점형 스케줄링 방식을 사용하므로 작업관리자로 특정 프로세스 하나를 종료할 수 있다.</p>
</blockquote>
<h2 id="자바-스케줄링-우선순위">자바 스케줄링 우선순위</h2>
<p>자바 스레드는 제어권을 가져올 순 없지만, 스레드별로 우선순위를 가지고 시스템에게 권고할 수 있다.</p>
<p><code>Thread.MIN_PRIORITY(1)</code> ~ <code>Thread.NORM_PRIORITY(5)</code> ~ <code>Thread.MAX_PRIORITY(10)</code>
의 10가지다.</p>
<p>하지만 이는 단순 권고이지 그대로 믿으면 안된다.</p>
<p>특히 자바 스레드 우선순위는 OS 스레드 우선순위와 연결되어야 하는데, 윈도우는 스레드 우선순위가 7가지다.
따라서 자바 스레드 우선순위는 윈도우 스레드 우선순위로 연결되면서 문제가 생긴다.
자바 우선순위 1, 2 는 THREAD_PRIORITY_LOWEST 하나로 연결되어 자바에는 1, 2 로 구분했지만 윈도우에서는 같은 우선순위를 가지는 일이 생긴다. 마찬가지로 3과 4,6과 7, 8과 9가 그렇다.</p>
<h2 id="스레드-상태-전이">스레드 상태 전이</h2>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/6d699be3-5353-49db-ba6d-8615b9602458/image.png" alt=""></p>
<p>자바의 스레드는 항상 위 6가지 하나의 상태를 가진다.</p>
<p><strong>Waiting</strong></p>
<ul>
<li>Object::wait(인자없음)</li>
<li>Thread::join(인자없음)</li>
<li>LockSupport:park()</li>
</ul>
<p><strong>Timed Waiting</strong></p>
<ul>
<li>Thread::sleep()</li>
<li>Object::wait(시간)</li>
<li>Thread::join(시간)</li>
<li>LockSupport::parkNanos()</li>
<li>LockSupport::parkUntil()</li>
</ul>
<p><strong>Blocked 와 Waiting</strong>
Waiting 은 스레드가 잠들어 누군가가 깨워야 한다.
반면 Blocked 는 락을 얻기 위해 기다리는 상태이며, 락을 얻으면 Running 으로 바뀐다.</p>
<h2 id="가상-스레드">가상 스레드</h2>
<p>자바는 커널 스레드와 자바 스레드를 1:1 맵핑한다.
자바 스레드는 커널 스레드의 자세한 기능을 숨기는 인터페이스를 제공하여 자바 애플리케이션 개발을 쉽게 할 수 있었다.
자바 서블릿은 하나의 커널 스레드에 하나의 Http 요청을 맵핑할 수 있었다.</p>
<p>하지만 컴퓨터가 빨라지고, 요청이 많아지고, MSA 환경에서 IO 가 많아지면서 1:1 모델의 단점인 자원의 한계가 두드러졌다.
짧고 많은 요청은 요청 시간과 컨텍스트 스위칭 시간이 거의 비슷해지는 지경에 이르렀다.</p>
<h3 id="컨텍스트-스위칭-비용">컨텍스트 스위칭 비용</h3>
<ol>
<li>스레드 전환시 문맥 저장 비용(캐시, 메모리, 레지스터 쓰기)</li>
<li>스레드 복원시 문맥 복원</li>
</ol>
<p>컨텍스트 스위칭 비용이 상대적으로 커졌다.</p>
<h3 id="코루틴">코루틴</h3>
<p>스레드 전환시 문맥에 대한 저장/복원이 비용이라 한다면, 이를 애플리케이션에서 처리하면 된다.</p>
<p>커널에서 컨텍스트 스위칭을 위한 비용보다 애플리케이션 스택에서 처리하는게 더 빠르고 좋다.
즉, 스레드 스케줄링을 각 코루틴이 하는 것이다.</p>
<ol>
<li>커널 모드로 들어가지 않아도 된다.</li>
<li>공간 자체도 적게 든다.</li>
</ol>
<p>하지만 코루틴은 협력적 스케줄링 방식으로서 애플리케이션에서 OS 가 하던 기능을 모두 구현하기는 매우 어렵다.</p>
<blockquote>
<p>코루틴 &lt;&gt; 협력적 스케줄링 &lt;&gt; 사용자 스레드 &lt;&gt; 가상 스레드 넷은 비슷비슷한 말이다.</p>
</blockquote>
<blockquote>
<p>스택풀 코루틴 : 코루틴이 각각 스택에 문맥을 위한 저장을 한다.
스택리스 코루틴 : 스택이 없고 상태 머신으로 스레드를 복구한다.</p>
</blockquote>
<blockquote>
<p>협력적 스케줄링을 사용하는 코루틴이 보통의 개념이다. 하지만 비협력적 스케줄링을 사용하는 코루틴도 있다.</p>
</blockquote>
<h3 id="가상-스레드-1">가상 스레드</h3>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/a63f7db7-0052-4a7e-aec0-af11f3b3e849/image.png" alt="">
<a href="https://waterfogsw.tistory.com/72">https://waterfogsw.tistory.com/72</a></p>
<p>Loom 프로젝트로 불리던 Java 가상 스레드는 일종의 코루틴이다.
자바가 복잡한 코루틴을 구현 해 준 것이다.
JDK21 에 정식 반영되었으며, 기존의 Thread 에 추가된 기능이기에 코드 변경이 적다.</p>
<p>가상 스레드의 애플리케이션 레벨 스케줄러는 기본적으로 ForkJoinPool 을 사용한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바 메모리 모델과 스레드]]></title>
            <link>https://velog.io/@jimmy_neutron/%EC%9E%90%EB%B0%94-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%AA%A8%EB%8D%B8%EA%B3%BC-%EC%8A%A4%EB%A0%88%EB%93%9C</link>
            <guid>https://velog.io/@jimmy_neutron/%EC%9E%90%EB%B0%94-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%AA%A8%EB%8D%B8%EA%B3%BC-%EC%8A%A4%EB%A0%88%EB%93%9C</guid>
            <pubDate>Mon, 09 Feb 2026 12:31:32 GMT</pubDate>
            <description><![CDATA[<p>컴퓨터가 점점 빨라지며 상대적으로 I/O 시간은 점점 느려지고 있다.
따라서 빠른 속도의 CPU 를 효율적으로 사용하는 멀티스레딩은 필수적인 기술이다.
하지만 프로세스, 스레드들이 공유하는 메모리의 데이터는 동시성 문제라는 것을 해결해야 한다.</p>
<h1 id="하드웨어에서의-동시성">하드웨어에서의 동시성</h1>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/e3fbca85-591d-4666-864a-0fdb933e517d/image.png" alt=""></p>
<p>CPU 의 프로세스는 매우 빠르다. 그에 비해 메모리에 값을 저장하고 읽는 속도는 느리다.
이 CPU 와 메모리의 처리속도를 보완하기 위해서, 읽고 쓰는 중간에 캐시 레이어를 둔다.
그러면 위와 같은 그림의 구조가 되는데, 이를 <code>공유 메모리 멀티 프로세스 시스템</code> 이라 한다.</p>
<p>하지만 캐시 구조는 동시성 문제를 유발하기 때문에, 캐시들 사이의 값을 일치시키기 위해 <code>캐시 일관성 프로토콜</code> 을 사용한다. 이는 캐시 일관성 프로토콜은 여러 캐시에서 읽고 수정되는 값이 일관성 있도록 처리한다.</p>
<p>또한 캐시에는 store buffer 라는 놈이 존재하는데, CPU 는 결과가 같다면 버퍼에 저장된 명령어의 순서를 변경할 수 있다. 이는 일부 시나리오에서 동시성 문제를 발생시킬 수 있다.</p>
<h1 id="자바-메모리-모델">자바 메모리 모델</h1>
<blockquote>
<p><code>메모리 모델</code> 은 특정 프로토콜을 통해 메모리나 캐시를 읽고 쓰는 절차</p>
</blockquote>
<blockquote>
<p>자바 메모리 모델의 목적은 자바 프로그램에서 변수에 접근하는 규칙을 정함으로써 여러 스레드가 메모리에 접근해도 동시성 문제가 발생하지 않도록 하는 것이다.</p>
</blockquote>
<h2 id="메인-메모리-작업-메모리">메인 메모리, 작업 메모리</h2>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/f05a4ebe-bbcc-4dda-ae29-6f3a30237004/image.png" alt="">
JVM 은 가상 머신으로 하드웨어와 비슷한 공유 메모리 멀티 프로세스 시스템을 가진다.
위 그림의 작업 메모리, 메인 메모리의 역할은 하드웨어와 비슷하다. 단, 이 메모리는 하드웨어처럼 물리 메모리를 나타내는게 아닌 JVM 개념 상의 메모리를 말한다.</p>
<blockquote>
<p>실제로 구현 상 작업 메모리 저장소를 CPU 캐시나 레지스터로 구현하기도 한다. 자주 이용하고 빨라야하기 때문이다.</p>
</blockquote>
<p>위 그림에서 작업 메모리와 메인 메모리 사이에 값을 주고 받는 프로토콜로 <code>저장, 읽기</code> 라고 나와있다. 이 부분이 <code>Java Memory Model</code> 이다. JVM 상에서 스레드, 작업 메모리, 메인 메모리간 변수를 읽고 쓰는 동작을 정의한다.</p>
<p>JVM 은 여러 하드웨어 플랫폼에서 동작해야하기 때문에 Java Memeory Model(JMM) 은 추상적으로 작성된 규칙이다. 이 덕분에 자바 프로그램은 어느 운영체제 및 메모리 모델에 상관없이 일관성 있게 메모리를 사용할 수 있다.</p>
<h2 id="jmm-연산">JMM 연산</h2>
<p>메인 메모리와 작업 메모리 사이의 프로토콜, 즉 메인 메모리에서 작업 메모리로 값을 복사하고 작업 메모리의 값을 메인 메모리로 동기화하는 방법을 JVM 은 다음 8가지로 정의했다.</p>
<p>이 8가지 연산을 <code>원자적</code> 으로 제공해야 한다.</p>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/ab244f3e-bc6d-42e1-aa4b-1a7f4cbd3155/image.png" alt=""></p>
<h2 id="jmm-연산-규칙">JMM 연산 규칙</h2>
<p>위 8가지 연산을 실행할때에는 몇가지 규칙을 지켜야한다.
어떠한 프로그램의 동시성 안전을 체크하고 싶을 때 규칙들을 적용하면 확인이 가능하다.</p>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/f0606b52-d95b-44d3-8470-db65554174ab/image.png" alt=""></p>
<p>추가로 잠금 관련해서는,</p>
<ul>
<li>변수는 한번에 한 스레드만 잠글 수 있다. 같은 스레드라면 n번 잠글 수 있으며, 잠금 해제를 위해 n번 해제해야 한다.</li>
<li>변수를 잠그면 작업 메모리의 변숫값은 지워진다. 변수를 사용할 때 적재 또는 할당을 다시 실행하여 작업 메모리에 값을 다시 초기화 해야 한다.</li>
<li>잠근 스레드만 잠금을 해제할 수 있다.</li>
<li>잠금을 해제하려면 작업 메모리 값을 메인 메모리에 동기화 해야 한다.(저장/쓰기)</li>
<li><code>volatile</code> 관련 규칙. (happens-before)<ul>
<li>작업 메모리에서 volatile 변수가 사용될 때 마다 다른 스레드에서의 변경 사항을 볼 수 있도록 메인 메모리로부터 값을 최신 값으로 고쳐야 한다.</li>
<li>작업 메모리에서 volatile 변수가 수정될 때 마다 메인 메모리로 즉시 동기화되어 다른 스레드가 변경 사항을 볼 수 있도록 보장해야 한다.</li>
<li>재정렬을 막아 프로그램에서 정의한 순서대로 실행해야 한다.</li>
</ul>
</li>
<li>long, double 은 64 비트 데이터타입으로, 32비트 시스템에서는 이 데이터 타입에 대한 모든 연산이 비원자적일 수 있기 때문에 보장해야 한다. 현재는 대부분의 CPU 레벨에서 이를 지원하므로 신경쓰지 않아도 된다.</li>
</ul>
<h2 id="volatile">Volatile</h2>
<p>동시성에는 가시성과 원자성이라는 단위가 있다.
synchronized 키워드는 가시성과 원자성을 모두 만족시키지만 비용이 비싸다.
때로는 가시성만 필요할때가 있는데, 이 때 사용하는 것이 <code>volatile</code> 이다.</p>
<h3 id="가시성">가시성</h3>
<p>가시성은 공유 변수의 값을 한 스레드가 수정했다면 다른 스레드가 즉시 수정을 알 수 있느냐이다.</p>
<p>가시성이 깨질 수 있는 케이스는 두가지이며, 하드웨어와 가상머신 모두에서 발생할 수 있다.</p>
<ol>
<li><code>공유 메모리 멀티 프로세스 시스템</code> : 캐시, 작업 메모리에 저장한 값이 메인 메모리로 동기화 되기 전 다른 스레드/프로세스가 읽어간 경우 가시성이 깨질 수 있다.</li>
<li><code>명령어 재정렬</code> : JVM, CPU 는 결과가 같은 경우에 한해 최적화의 일환으로 명령어의 순서를 변경할 수 있다. 하지만 명령어 순서 변경에 의해 다른 스레드에서 값을 잘못 읽어가는 가시성 문제가 생길 수 있다.</li>
</ol>
<h3 id="jvm-가시성-문제">JVM 가시성 문제</h3>
<p>위처럼 가시성이 깨지는 경우 때문에, 일부 시스템은 캐시 또는 작업 메모리 개념을 사용하지 않거나 명령어 재정렬을 허용하지 않을 수 있다.</p>
<p>하지만 JVM 은 성능상의 이점을 위해 작업 메모리를 사용하고, JIT 의 명령어 재정렬을 허용한다.</p>
<p>즉, 특정 시나리오의 경우 가시성 문제가 발생할 수 있으며 <code>Volatile</code> 키워드를 일일히 붙여야 함을 택했다.</p>
<h3 id="java-volatile">Java Volatile</h3>
<p>volatile 은 위 가시성 문제를 해결하기 위한 방법이다.
volatile 은 두가지 기능이 있다.</p>
<ol>
<li>모든 스레드에서 변수값을 투명하게 볼 수 있다. 이를 위해 캐시, 작업 메모리 값을 무효화해야 한다.</li>
<li>명령어 재정렬 최적화를 막는다. (happens-before 관계 성립)</li>
</ol>
<p>가시성 문제는 가상머신(인터프리터, JIT), 하드웨어(CPU) 둘 다 발생할 수 있다고 했다.
JVM 은 여러 하드웨어의 추상화 가상 머신이다. 따라서, volatile 은 <code>JVM 과 CPU 둘다의 가시성 만족</code> 을 위한 구현이다.</p>
<p>즉, volatile 은 작업 메모리와 cpu 캐시 둘 다를 무효화 시키며, JIT 의 명령어 재정렬과 CPU 의 명령어 재정렬 둘 다를 막는다.</p>
<h3 id="가상머신-레벨의-volatile">가상머신 레벨의 volatile</h3>
<p>volatile 변수는 클래스 파일에서 필드 테이블에 <code>ACC_VOLATILE</code> 플래그 정보가 있다.</p>
<p>가상머신은 이 플래그가 켜져 있으면(volatile) 위 volatile 관련 규칙을 지켜야 한다.
모든 JVM 은 이 규칙을 지키도록 되어 있다.</p>
<h3 id="하드웨어-레벨의-volatile">하드웨어 레벨의 volatile</h3>
<p>가상머신 레벨의 volatile 구현은 jvm 하나만 잘 구현되어 있으면 된다.
하지만 하드웨어는 다르다.</p>
<p>x86, amd, arm 등등 여러 CPU 는 각각 다른 가시성 구현을 가지고 있다.
따라서 JVM 은 volatile 변수 명령에 대한 네이티브 코드 생성시 CPU 에 따라 다르게 만들어야 한다.</p>
<p>따라서 volatile 이 실제로 cpu 레벨의 가시성 제어는 어떻게 되는지 알고싶다면, CPU 까지 알아야 한다.</p>
<p>JDK 동시성에 큰 기여를 한 Doung Lea 의 자료를 참고하면 좋다.
<a href="https://gee.cs.oswego.edu/dl/jmm/cookbook.html">https://gee.cs.oswego.edu/dl/jmm/cookbook.html</a></p>
<p>매모리 재정렬을 막는 경계를 설정하는 명령을 <code>barrier</code> 라 한다.
베리어에는 LoadLoad, LoadStore, StoreLoad, StoreStore 네종류로 나눌 수 있으며 StoreLoad 베리어는 Store 명령과 Load 명령 사이에 들어가는 베리어 명령어로써 Store 명령어는 베리어를 넘어서 재정렬될 수 없다는 식이다.</p>
<h4 id="x86">X86</h4>
<p>x86 CPU 는 CPU 단에서 대부분의 재정렬을 이미 막고 있다. (TSO, Total Store Order)
따라서 StoreLoad 재정렬만 막으면 된다.</p>
<p>다음 코드를 JIT 컴파일러는 어떤 네이티브로 해석하는지 보자
instance 변수를 volatile 로 하고 안하고를 비교해보겠다.</p>
<pre><code class="language-java">// double-checked-locking 코드. 대표적인 volatile 사용처
public class Singleton {
//    private static Singleton instance;
    private volatile static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

    static void main(String[] args) {
        Singleton s = Singleton.getInstance();
        System.out.println(s);
    }
}</code></pre>
<blockquote>
<p>윈도우에서 hsdis 사용하여 테스트함.</p>
</blockquote>
<pre><code>  0x000001c53532bc1f:   movsbl 0x38(%r15),%edi
  0x000001c53532bc24:   cmp    $0x0,%edi
  0x000001c53532bc27:   jne    0x000001c53532bd82
  0x000001c53532bc2d:   mov    %rax,%r10
  0x000001c53532bc30:   shr    $0x3,%r10
  0x000001c53532bc34:   mov    %r10d,0x70(%rsi)
  0x000001c53532bc38:   mov    %rsi,%rdi
  0x000001c53532bc3b:   xor    %rax,%rdi
  0x000001c53532bc3e:   shr    $0x15,%rdi
  0x000001c53532bc42:   cmp    $0x0,%rdi
  0x000001c53532bc46:   jne    0x000001c53532bda1           ;*putstatic instance {reexecute=0 rethrow=0 return_oop=0}
                                                            ; - Singleton2::getInstance@24 (line 9)</code></pre><p>위는 volatile 이 아닐때의 일부다.</p>
<pre><code>  0x000001f7c9fb3f1f:   movsbl 0x38(%r15),%edi
  0x000001f7c9fb3f24:   cmp    $0x0,%edi
  0x000001f7c9fb3f27:   jne    0x000001f7c9fb4088
  0x000001f7c9fb3f2d:   mov    %rax,%r10
  0x000001f7c9fb3f30:   shr    $0x3,%r10
  0x000001f7c9fb3f34:   mov    %r10d,0x70(%rsi)
  0x000001f7c9fb3f38:   lock addl $0x0,-0x40(%rsp) // 추가됨
  0x000001f7c9fb3f3e:   mov    %rsi,%rdi
  0x000001f7c9fb3f41:   xor    %rax,%rdi
  0x000001f7c9fb3f44:   shr    $0x15,%rdi
  0x000001f7c9fb3f48:   cmp    $0x0,%rdi
  0x000001f7c9fb3f4c:   jne    0x000001f7c9fb40a7           ;*putstatic instance {reexecute=0 rethrow=0 return_oop=0}
                                                            ; - Singleton::getInstance@24 (line 9)</code></pre><p>위는 volatile 일때의 일부다.</p>
<p>보다시피 volatile 일 때에는 <code>lock addl $0x0,-0x40(%rsp)</code> 네이티브 명령이 변수값 할당 후 추가된다.</p>
<p>lock 접두어는 IA32 메뉴얼상 캐시(정확하게는 store buffer)를 메인 메모리에 쓰는 기능을 하며 이 쓰기는 다른 프로세서가 자신의 캐시를 무효화하게 한다. </p>
<p>이 명령어가 끝나기 전에 캐시의 값을 메모리에 모두 쓰는 것을 보장하므로, 다른 스레드에서는 변수의 수정된 값을 투명하게 볼 수 있다. (과거 값을 보는게 불가능하다)</p>
<p>또한 의미상 lock 은 캐시에 들어있는 이전의 쓰기 연산을 모두 메모리에 반영 함으로서 관측 가능하게 한다는 의미이기 때문에, 이를 재정렬해버리면 다른 스레드에서의 관측 결과를 달라지게 할 수 있다. 따라서 lock 이 캐시의 명령을 모두 메모리에 동기화하는 것이 명령어 재정렬은 lock(베리어) 를 넘을 수 없다는 효과 또한 발생시키게 된다.</p>
<p>lock 명령 하나로 두마리 토끼를 잡을 수 있는 것이다.</p>
<blockquote>
<p>뒤의 addl $0x0,-0x40(%rsp) 는 별 의미 없는 코드다. 필요한건 lock 뿐이다. 다만 lock 이 쓰기 연산에만 붙을 수 있는 접두어라 한다.</p>
</blockquote>
<blockquote>
<p>cpu 는 의미(sementic) 이 바뀌지 않는 내에서 재정렬할 수 있다.</p>
</blockquote>
<h4 id="arm">arm</h4>
<p>arm 에서 위 코드를 실행해보면 dmb(Data Memory Barrier) 가 추가됨을 알 수 있다.</p>
<pre><code>  0x000000010e0caf3c:   cbnz    x0, 0x000000010e0cb164      ;*monitorenter {reexecute=0 rethrow=0 return_oop=0}
                                                            ; - Singleton::getInstance@10 (line 7)
  0x000000010e0caf40:   mov    x3, #0x8ea0                    // #36512
                                                            ;   {oop(a &#39;java/lang/Class&#39;{0x000000043f818ea0} = &#39;Singleton&#39;)}
  0x000000010e0caf44:   movk    x3, #0x3f81, lsl #16
  0x000000010e0caf48:   movk    x3, #0x4, lsl #32
  0x000000010e0caf4c:   ldr    w3, [x3, #112]
  0x000000010e0caf50:   lsl    x3, x3, #3                  ;*getstatic instance {reexecute=0 rethrow=0 return_oop=0}
                                                            ; - Singleton::getInstance@11 (line 8)</code></pre><p>위는 volatile 없을 때</p>
<pre><code>  0x000000010ee8af44:   cbnz    x0, 0x000000010ee8b184      ;*monitorenter {reexecute=0 rethrow=0 return_oop=0}
                                                            ; - Singleton::getInstance@10 (line 7)
  0x000000010ee8af48:   mov    x3, #0x8ea0                    // #36512
                                                            ;   {oop(a &#39;java/lang/Class&#39;{0x000000043f818ea0} = &#39;Singleton&#39;)}
  0x000000010ee8af4c:   movk    x3, #0x3f81, lsl #16
  0x000000010ee8af50:   movk    x3, #0x4, lsl #32
  0x000000010ee8af54:   dmb    ish
  0x000000010ee8af58:   ldr    w3, [x3, #112]
  0x000000010ee8af5c:   lsl    x3, x3, #3
  0x000000010ee8af60:   dmb    ishld                       ;*getstatic instance {reexecute=0 rethrow=0 return_oop=0}
                                                            ; - Singleton::getInstance@11 (line 8)</code></pre><p>위는 volatile 있을 때</p>
<h3 id="volatile-은-원자성을-만족시키지-않는다">volatile 은 원자성을 만족시키지 않는다.</h3>
<pre><code class="language-java">public volatile int count = 0;

public add() {
    count++;
}</code></pre>
<p>위 add() 메서드는 volatile 변수를 증가시키는게 다이다.
하지만 이 메서드는 동시성 안전하지 않다.</p>
<p>volatile 은 원자성을 만족시키지 않기 때문이다.</p>
<p>count++ 는 실제로 하나의 명령어가 아니라 여러개의 바이트 코드, 더 여러개의 네이티브 코드로 쪼개진다.</p>
<p>cpu 는 이 여러개의 명령어를 원자적으로 실행하지 않으므로 동시성 문제가 발생할 수 있다.</p>
<p>따라서 volatile 은 사용처를 잘 보고 사용해야 한다.</p>
<h3 id="volatile-을-언제-써야-하는가">volatile 을 언제 써야 하는가?</h3>
<p>기본적으로 volatile 이 synchronized 보다 성능이 좋다.</p>
<p>volatile 은 아래 두 조건이 모두 만족해야 한다.
아닐때에는 synchronized 를 쓰도록 한다.</p>
<ul>
<li>write 할 값이 write 이전 값과 관계가 없다. (싱글 스레드 동작이 보장된다면 상관없음)<ul>
<li>기존 값을 읽어서 기존 값 기반으로 바뀐 값을 write 하면 안된다는 이야기다.</li>
</ul>
</li>
<li>다른 상태 변수와 관련한 불변성 제약 조건에 관여하지 않는다.</li>
</ul>
<blockquote>
<p>두번째 조건은 다음 예와 같다.
원자적이지 않으므로 다른 변수와의 불변성 제약조건이 깨질 위험이 있다.</p>
<pre><code class="language-java">volatile int lower;
volatile int upper;

// 메서드가 원자성이 없으므로 (5, 5) -&gt; (10, 10) 으로 변경시 (10, 5) 시점에 읽으면 예외가 날 수 있다.
void set(int l, int u) {
   lower = l;
   upper = u;
}

void check() {
   if (l &gt; u) throw new Exception(); // 불변성 제약조건
}
</code></pre>
</blockquote>
<p>대표적인 사용처는 flag 값으로 쓰일때가 있다.</p>
<pre><code class="language-java">volatile boolean shoutdown;

public void shutdouwn() {
    this.shoutdown = true;
}

public void doWork() {
    while(!shutdown) {
        ...
    }
}</code></pre>
<h2 id="synchronized">synchronized</h2>
<p>synchronyzied 는 동기화 블록을 만든다.
이 블록은 가시성, 원자성을 모두 만족한다.</p>
<ul>
<li>원자성 : monitorenter, monitorexeit 바이트 코드 명령어로 확보됨</li>
<li>가시성 : JMM 규칙 중 <code>변수의 잠금을 해제하기 전 변수의 값을 메인 메모리로 다시 동기화해야 함</code> 에 의해 확보됨</li>
</ul>
<h2 id="동시성의-3요소">동시성의 3요소</h2>
<ol>
<li>가시성 : volatile, synchronized 둘 다 만족</li>
<li>원자성 : synchronized 만 만족</li>
<li>실행 순서 : volatile, synchronized 둘 다 만족</li>
</ol>
<h2 id="선-발생-원칙">선 발생 원칙</h2>
<p>먼저 작성된 프로그램의 코드는 무조건 뒤에 작성된 코드에서 볼 수 있을까?
먼저 실행된 코드의 결과는 무조건 뒤에 실행된 코드에서 볼 수 있을까?</p>
<p>아니다. 먼저 실행 되더라도 재정렬 및 캐시 구조때문에 그렇지 않다.</p>
<p>B 가 수행되기 전에 A 에서 수행한 결과를 B 에서 관찰할 수 있다면 <code>A 는 B 보다 선발생</code> 했다고 한다. 실행이 아닌 수행임을 주의하자. 먼저 실행되더라도 나중에 수행되어 볼 수 없을 수 있다는 이야기다.</p>
<p>자바는 volatile, synchronized 를 쓰지 않더라도 기본적으로 보장해주는 것들이 있다.
동시성 문제를 분석할 때에는 이를 참고해야 한다.</p>
<ol>
<li>프로그램 순서 규칙 : 한 스레드 안에서는 제어 흐름(분기, 순환문 등등) 순서에 따라 앞의 연산이 뒤따르는 연산보다 선 발생한다. 코드 순서가 아니다.</li>
<li>모니터 락 규칙 : 같은 락에 대한 시간적으로 먼저인 잠금 해제는 잠금 보다 선 발생한다.</li>
<li>휘발성 변수 규칙 : volatile 시간적으로 먼저인 변수 쓰기는 같은 변수에 대한 읽기 연산보다 선 발생한다.</li>
<li>스레드 시작 규칙 : Thread.start() 는 해당 스레드의 어떤 작업보다 선 발생한다.</li>
<li>스레드 종료 규칙 : 스레드의 모든 작업은 스레드 종료보다 먼저 발생한다.</li>
<li>스레드 인터럽트 규칙 : Thread.interrupt() 호출은 인터럽트되는 스레드의 인터럽트 이벤트 발생 감지보다 선 발생한다.</li>
<li>종료자 규칙 : 객체 초기화(생성자)는 finalize() 시작보다 선 발생한다.</li>
<li>전이성 : A 보다 B 가 선 발생하고, B 보다 C 가 선 발생하면 A 보다 C 가 선 발생한다.</li>
</ol>
<blockquote>
<p><strong>모니터 락 규칙</strong>
// Thread A
synchronized (lock) { x = 42; } // unlock
// Thread B
synchronized (lock) { System.out.println(x); }</p>
</blockquote>
<p>라면 B 의 출력은 반드시 42 이다.
이 보장이 없다면 이전 값을 출력할 수 있다. synchronized
를 보장하는 규칙이라할 수 있다.</p>
<blockquote>
<p>무조건 시간상 먼저 발생하는 작업이 선 작업을 의미하지 않는다는 것이다.
그걸 만족하는건 위 케이스밖에 없다.</p>
</blockquote>
<h1 id="참고">참고</h1>
<p><a href="https://gee.cs.oswego.edu/dl/jmm/cookbook.html">https://gee.cs.oswego.edu/dl/jmm/cookbook.html</a>
<a href="https://chriswhocodes.com/hsdis/">https://chriswhocodes.com/hsdis/</a>
논문 A Volatile-by-Default JVM for Server Applications</p>
<p><a href="https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html">https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html</a>
<a href="https://www.cs.umd.edu/users/pugh/java/memoryModel/">https://www.cs.umd.edu/users/pugh/java/memoryModel/</a></p>
<p><a href="https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html">https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html</a>
<a href="https://velog.io/@hmcck27/Double-Checked-Locking">https://velog.io/@hmcck27/Double-Checked-Locking</a></p>
<p>논문 Specifying Multithreaded Java Semantics for
Program Verification</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[GraalVM]]></title>
            <link>https://velog.io/@jimmy_neutron/GraalVM</link>
            <guid>https://velog.io/@jimmy_neutron/GraalVM</guid>
            <pubDate>Fri, 06 Feb 2026 13:00:50 GMT</pubDate>
            <description><![CDATA[<h1 id="graalvm">GraalVM</h1>
<h2 id="jaotc">jaotc</h2>
<p>Jaotc 는 JDK 9 ~ JDK 16 에 포함된 JDK AOT 컴파일러이다.
<a href="https://openjdk.org/jeps/295">JEP 295</a> 에서 제안되었으며 <a href="https://openjdk.org/jeps/410">JEP 410</a> 에서 사라졌다.</p>
<pre><code>jaotc --output jaotcResult.so jaotcSource.class</code></pre><p>와 같이 미리 AOT 컴파일러로 so 파일을 만들고,</p>
<pre><code>java -XX:+UnlockExperimentalVMOptions -XX:AOTLibrary=./jaotcResult.so HelloWorld.class</code></pre><p>와 같이 실행할 수 있다.</p>
<p>하지만 JEP 295 에도 적혀 있듯, 아무도 쓰지도 않고 사용 자체도 어려워서 JDK 에서 빠지게 되었다. 그래서 굳이 jaotc 를 공부할 필요는 없다.</p>
<p>대신 jaotc 는 내부적으로 Graal Compiler 를 사용했으며, jaotc 는 JDK 16 에서 빠지고 난 후 Graal 프로젝트로 편입되었다.</p>
<p>따라서 이제는 GraalVM 을 알아봐야 한다.</p>
<h2 id="graalvm-1">GraalVM</h2>
<p>Graal VM 은 다음과 같은 기능들을 내걸고 있다.</p>
<ul>
<li>Graal Compiler 를 사용하여 바로 실행 가능한 Native-Image 생성(AOT)</li>
<li>일반 JVM 의 C2 대신 Graal Compiler 사용 가능</li>
<li>고성능 및 메모리 사용량 감소 컴파일러</li>
<li>Polyglot(ex. 자바에서 JS 코드 사용 가능)</li>
</ul>
<p>이러한 기능들이 가능한 근본은 Graal Compiler 때문이다.</p>
<h3 id="graalvm-2">GraalVM</h3>
<p>GraalVM 은 HotSpot 을 기반으로 만들어진 일종의 JVM 이다.
C2 컴파일러 대신 Graal Compiler 가 동작한다.</p>
<p>GraalVM 은 HotSpot 처럼 일반적인 java 파일을 실행할 수 있으며,
Native Image 생성시에도 사용될 수 있다.</p>
<h3 id="graal-compiler">Graal Compiler</h3>
<p>그랄 컴파일러는 처음에는 HotSpot 의 C2 를 대체하기 위한 컴파일러였다.
여기에 다른 몇가지 요구사항들이 합쳐지면서 위의 기능을 모두 제공해주는 GraalVM 의 근본이 되었다.</p>
<p>몇가지 중요한 특징을 알아보자</p>
<h4 id="1-자바로-작성된-컴파일러다">1. 자바로 작성된 컴파일러다</h4>
<p>C++ 로 짜여진 C2 컴파일러에 반해, 그랄 컴파일러는 JEP-243 의 JVMCI 덕분에 자바로 작성될 수 있었다.</p>
<ul>
<li>자바 컴파일러 개발자들이 익숙한 언어로 개발을 더 쉽게 할 수 있다.</li>
<li>일반 개발자들이 컴파일러를 디버깅할 수 있다. (중단점 걸고 볼 수 있음)</li>
</ul>
<h4 id="2-언어-독립적인-컴파일러다">2. 언어 독립적인 컴파일러다.</h4>
<p>언어 독립성이란 말 그대로다. JS 를 GraalVM 에서 실행시킬 수 있다는 것이다.
하지만 중요한 것은 JS 의 특성 및 최적화는 적용되어야 한다.</p>
<p>Graal Compiler 개발진은 애초부터 여러 언어를 지원할 수 있는 컴파일러를 만들고 싶어 했다.</p>
<p>한편 생각해보면 JS 를 Java Class File 로 변환하는 컴파일러를 만들면 되지 않는가? 모든 언어를 Java Class File 로 만드는 컴파일러를 만들면 이미 C2 는 언어 독립적인 컴파일러 같기도 하다.</p>
<p>하지만 그렇지 않다. Java Class File 은 이미 스택 구조, 메서드 호출 규약(invokevirtual) 등 이미 자바에 맞춰진 파일이다.
JS 를 Java Class File 로 바꾼다면 JS 의 동적인 특성은 모두 사라지고 없을 것이다.</p>
<p>그렇다면 어떻게 모든 언어를 대상으로한 독립성을 지킬 수 있을까.
모든 언어는 다르게 생겼지만 하고자 하는 것은 같다. 하드웨어를 동작시켜서 원하는 동작을 하기 위해 언어를 작성하는 것이다. 즉, <code>실행 흐름</code> 은 모든 언어에서 같을 것이다.</p>
<p>Graal Compiler 는 실행 흐름을 컴파일한다.
실행 흐름을 나타내는 <a href="https://www.graalvm.org/latest/reference-manual/java/compiler/#graph-compilation">Graph</a> 라는 Graal IR 을 네이티브 코드로 변환한다. 이 방식으로 언어의 독립성을 실현했다.</p>
<p>비교하자면
C2 Compiler 는 Java Class File 을 네이티브 코드로 컴파일하지만,
Graal Compiler 는 Graph(Graal IR) 을 네이티브 코드로 컴파일한다.</p>
<h4 id="3-native-image-생성에-사용된다">3. native-image 생성에 사용된다.</h4>
<h4 id="4-3번과-더불어-hotspot-내부의-c2-대체-jit-컴파일러로도-쓰일-수-있다">4. 3번과 더불어, HotSpot 내부의 C2 대체 JIT 컴파일러로도 쓰일 수 있다.</h4>
<ul>
<li>GraalVM 은 C2 대신에 그랄 컴파일러가 내장된 JVM 이다. 우리가 작성하던 왠만한 코드는 모두 실행할 수 있다.</li>
<li>JDK 25 에서는 제거되었지만, 예를 들어 JDK 24 에서는 java 옵션으로도 C2 컴파일러 대신 Graal Compiler 를 사용할 수 있다.</li>
</ul>
<h3 id="graalvm-설치">GraalVM 설치</h3>
<p>가장 먼저 GraalVM 을 설치해야 한다.</p>
<p>Mac 에서 가장 쉽게 GraalVM 을 사용하는 방법은 <a href="https://sdkman.io/">SDKMAN</a> 을 사용하는 것이다.</p>
<p><a href="https://www.graalvm.org/latest/getting-started/macos/#sdkman">https://www.graalvm.org/latest/getting-started/macos/#sdkman</a>
<img src="https://velog.velcdn.com/images/jimmy_neutron/post/cdf6fdb1-370b-4a78-b89d-a6a898a375a0/image.png" alt=""></p>
<h3 id="native-image-만들기">Native Image 만들기</h3>
<p>GraalVM 은 <a href="https://www.graalvm.org/latest/reference-manual/native-image/">여러가지 방법으로 Native Image</a> 를 만들 수 있다.</p>
<blockquote>
<p>클래스, jar, java-module 을 gradle, maven, cli tool 을 사용하여 네이티브 이미지를 만들 수 있다.</p>
</blockquote>
<p>이번에는 간단하게 native-image cli tool 을 써볼 예정이다.
native-image cli tool 은 GraalVM 내부에서 제공하는 툴로써, GraalVM 과 내부의 Graal Compiler 를 사용하여 자바 코드를 Native Image 파일로 생성할 수 있다.</p>
<blockquote>
<p>Polyglot 을 이용하면 타 언어도 가능</p>
</blockquote>
<p>native image 에는 사전 컴파일된(AOT) 네이티브 코드와 해당 네이티브 코드를 JVM 없이 실행할 수 있는 런타임(<a href="https://docs.oracle.com/en/graalvm/enterprise/20/docs/reference-manual/native-image/SubstrateVM/">Substrate VM</a>) 이 포함되어 있다.</p>
<h4 id="native-image-특징">Native Image 특징</h4>
<ul>
<li>독립 실행 가능하다(SubStrate VM)</li>
<li>모든 코드를 분석하여 가상 메서드를 직접 호출 메서드로 만든다<ul>
<li>가상 메서드는 런타임에 리시버에 따라 호출할 메서드를 동적으로 디스패치 해야 하기 때문에 JIT 컴파일에서 섣불리 최적화(ex. inline)하지 못한다. 모든 코드를 분석하려면 시간이 많이 걸리고, 런타임에 이를 수행하기 부담스럽기 때문이다. 따라서 예측 기반 공격적 최적화를 할 수 밖에 없다.<ul>
<li>반면 AOT 방식인 Graal Native-Image 에서는 사전에 모든 코드를 분석하여 가상 메서드를 직접 호출 메서드로 바꾼다. 이는 JIT 는 할 수 없는 추가 최적화가 가능하도록 만든다.</li>
</ul>
</li>
</ul>
</li>
<li><a href="https://www.youtube.com/watch?v=9fbq3umnSeA">런타임에 동작하는 몇몇 기능을 사용하지 못한다.</a><ul>
<li>리플랙션, Dynamic Proxy, Dynamic Class Loading, Runtime Annotation Processor 를 사용하지 못한다.</li>
<li>따라서 Dynamic Class Loading 을 사용하는 Logback, 리플랙션을 사용하는 Jackson 등을 사용하지 못할 수 있다.</li>
<li>GraalVM 이 제공하는 <a href="https://www.graalvm.org/latest/reference-manual/native-image/metadata/AutomaticMetadataCollection/">Trace Agent</a> 를 사용하여 해결해야 한다.</li>
</ul>
</li>
</ul>
<h4 id="native-image-실습">Native Image 실습</h4>
<p><a href="https://www.graalvm.org/latest/reference-manual/native-image/#from-a-class">https://www.graalvm.org/latest/reference-manual/native-image/#from-a-class</a></p>
<ol>
<li><p>java 파일 생성</p>
<pre><code class="language-java">public class HelloWorld {
public static void main(String[] args) {
 System.out.println(&quot;Hello, World!&quot;);
}
}</code></pre>
</li>
<li><p>컴파일 및 네이티브 이미지 생성</p>
<pre><code>javac HelloWorld.java
native-image HelloWorld</code></pre><p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/2505d917-e4d4-4c5a-85b4-06a3671d147f/image.png" alt=""></p>
</li>
<li><p>실행</p>
<pre><code>./helloworld</code></pre><p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/44c1b2f5-884a-48ac-af03-96d76914bbac/image.png" alt=""></p>
</li>
</ol>
<h3 id="polyglot">Polyglot</h3>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/83099b17-ca97-4c4a-92b9-660a3c56b87c/image.png" alt="">
<a href="https://giljae.medium.com/graalvm-%EC%86%8C%EA%B0%9C-84ac547f8df2">https://giljae.medium.com/graalvm-%EC%86%8C%EA%B0%9C-84ac547f8df2</a>
GraalVM 을 이용하면 JS 코드를 Java 에서 사용할 수 있다.
또는 JS 파일에 작성된 함수를 Java 에서 불러와 실행할 수 있다.
JS 뿐만 아니라 C, C++, Ruby, Python 등등 많은 언어가 가능하다.</p>
<p>이를 Polyglot 이라 한다.</p>
<p><a href="https://www.graalvm.org/latest/graalvm-as-a-platform/language-implementation-framework/">Truffle</a> 은 언어를 만드는 프레임워크다.
언어를 만든다는건 사실 언어를 기계어로 바꾸는 인터프리터를 만드는 것이다.</p>
<p>Truffle 프레임워크로 만든 애플리케이션은 JS, Python, R 또는 커스텀한 언어를 AST 로 만드는 AST 인터프리터다. AST 는 원래 언어(JS, Python ..) 의 실행 흐름을 담고 있는 자바 객체 트리로서, JVM 에서 실행하면 원래 언어의 코드가 실행되는 것이다.</p>
<blockquote>
</blockquote>
<p>JS는 HotSpot JVM에서 실행된다.
다만 HotSpot이 JS를 직접 이해해서 실행하는 게 아니라,
Truffle로 구현된 JS 인터프리터(자바 코드)를 실행함으로써 실행된다.</p>
<p>Truffle 프레임워크로 만들어진 인터프리터가 변환한 AST 는 자바 객체이기 때문에 GraalVM 이 아니더라도 HotSpot 에서도 실행될 수 있다.</p>
<p>하지만 Graal Compiler 를 만났을 때 가장 높은 수준의 최적화가 이뤄질 수 있다. 다음 표를 보자.</p>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/ce2aaa3b-1ba7-4210-bcd4-49d606ec0ee5/image.png" alt="">
<a href="https://www.graalvm.org/latest/reference-manual/embed-languages/#runtime-optimization-support">https://www.graalvm.org/latest/reference-manual/embed-languages/#runtime-optimization-support</a></p>
<h4 id="polyglot실습하기">Polyglot실습하기</h4>
<ol>
<li>gradle 프로젝트를 만들고 다음 의존성을 추가한다.<pre><code class="language-gradle">// 우리 코드에서 사용할 polyglot api.
// Truffle 프레임워크를 의존성으로 따라 가져온다.
// Truffle 프레임워크는 정의된 언어 컨텍스트의 구현체로 요청을 디스패치하여 해석한 후 Graph 를 만든다
implementation(&quot;org.graalvm.polyglot:polyglot:25.0.2&quot;)
</code></pre>
</li>
</ol>
<p>// JS 폴리글랏 구현체. Truffle 에서 정의한 인터페이스에 맞게 구현되어 있으며, JS 문법을 Truffle 인터페이스로 바인딩한다고 볼 수 있다.
// 예를 들면 JS 를 해석하여 Truffle 인터페이스 A 로 바인딩하는 식.
implementation(&quot;org.graalvm.polyglot:js:25.0.2&quot;)</p>
<pre><code>
2-1. java 파일에서 js 코드 실행하기
```java
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;

public class JsFunctionCallDemo {
    public static void main(String[] args) {
        try (Context ctx = Context.newBuilder(&quot;js&quot;).build()) {
            ctx.eval(&quot;js&quot;, &quot;&quot;&quot;
                function add(a, b) { return a + b; }
            &quot;&quot;&quot;); // js code

            Value bindings = ctx.getBindings(&quot;js&quot;);
            Value add = bindings.getMember(&quot;add&quot;);

            int res = add.execute(1, 2).asInt();
            System.out.println(res); // 3
        }
    }
}</code></pre><p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/38eeaa93-acf6-443e-82b4-b48bd064d7da/image.png" alt=""></p>
<p>2-2. java 에서 js 파일 읽어서 실행하기
js file</p>
<pre><code class="language-javascript">// Hello.js
// gradle 프로젝트 최상위에 위치시키기
function add(a, b) {
    return a + b;
}</code></pre>
<p>java file</p>
<pre><code class="language-java">import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;

import java.io.File;

public class JsFileCallDemo {
    public static void main(String[] args) throws Exception {
        File jsFile = new File(&quot;hello.js&quot;);

        try (Context ctx = Context.newBuilder(&quot;js&quot;).build()) {
            Source src = Source.newBuilder(&quot;js&quot;, jsFile).build();
            ctx.eval(src);

            Value bindings = ctx.getBindings(&quot;js&quot;);
            Value add = bindings.getMember(&quot;add&quot;);

            int res = add.execute(10, 32).asInt();
            System.out.println(res); // 42
        }
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/50e56478-6812-4e2e-aaf1-2cf54fc598d6/image.png" alt="">
<a href="https://www.graalvm.org/latest/reference-manual/native-image/guides/build-polyglot-native-executable/?utm_source=chatgpt.com">https://www.graalvm.org/latest/reference-manual/native-image/guides/build-polyglot-native-executable/?utm_source=chatgpt.com</a></p>
<h3 id="graalvm-으로-공유-라이브러리-aot-컴파일">GraalVM 으로 공유 라이브러리 AOT 컴파일</h3>
<p><a href="https://www.graalvm.org/latest/reference-manual/native-image/guides/build-native-shared-library/">https://www.graalvm.org/latest/reference-manual/native-image/guides/build-native-shared-library/</a></p>
<h3 id="galahad-프로젝트">Galahad 프로젝트</h3>
<p><a href="https://openjdk.org/projects/galahad/">Galahad</a> 는 GraalVM 기술이 JDK 에 포함되도록 하는 프로젝트이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[AOT 컴파일러]]></title>
            <link>https://velog.io/@jimmy_neutron/AOT-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC</link>
            <guid>https://velog.io/@jimmy_neutron/AOT-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC</guid>
            <pubDate>Fri, 06 Feb 2026 11:18:40 GMT</pubDate>
            <description><![CDATA[<h1 id="aotahead-of-time-컴파일러">AOT(Ahead-Of-Time) 컴파일러</h1>
<h2 id="aot-compiler">AOT Compiler</h2>
<p>프로그램 실행(런타임) 이전에 최적화(C1, C2) 와 네이티브 코드 생성(인터프리터, C1, C2)를 미리 하여 런타임에는 바이트코드를 바로 실행하게 할 수 있다.</p>
<p>미리 자바 코드로 부터 네이티브 코드를 만드는 컴파일러르 AOT 컴파일러라 한다.</p>
<p>java 진영에서는 크게 두가지 컴파일러가 존재한다.</p>
<ul>
<li>jaotc 컴파일러</li>
<li>graal 컴파일러</li>
</ul>
<h2 id="aot-vs-jit">AOT vs JIT</h2>
<h3 id="jit-의-단점">JIT 의 단점</h3>
<ol>
<li>계층형 JIT 에서는 최종 단계인 C2 까지 가기 오래 걸린다. 인터프리터 -&gt; C1 -&gt; C2 순이다.</li>
<li>호출 횟수가 부족하면 C1 조차 사용하지 못할 수 있다.</li>
<li>C2 최적화는 런타임에 컴퓨팅 자원을 소모한다. 결국 유저가 쓸 컴퓨터를 뺏어쓴다.</li>
<li>유저가 쓸 컴퓨팅 자원을 뺏어 쓰기 때문에, 오래 걸리는 최적화 작업은 하지 못한다.<ul>
<li>예를 들어 전체 프로그램을 대상으로 하는 최적화인 프로시저 간 분석은 아주 오래 걸린다. JIT 는 이 분석의 일부분만 수행할 수 있다.</li>
<li>또한 확실한 증거가 아닌 모니터링을 통한 가정을 통해 공격적인 최적화를 한 후 가정이 틀리면 인터프리터로 돌아가는 형태를 띤다. 이것도 런타임에 정확한 최적화를 하기엔 시간이 너무 많이 걸리기 때문이다.</li>
</ul>
</li>
</ol>
<blockquote>
<p>최종 결과물의 코드 실행 속도가 JIT 가 AOP 보다 느린게 아니다.
결국 JIT 도 오래 실행되면 C2 컴파일의 결과로 충분히 최적화된 빠른 코드가 실행된다. 후술하겠지만 오히려 AOT 에서 할 수 없는 최적화도 가능하다.
다만 JIT 는 일정 호출 횟수가 지나야 최적화된 코드가 실행된 다는 것과 바이트 코드를 해석과 최적화를 런타임에 해야 한다는 것이 느리게 하는 요인(과정에 대한 이야기)인 것이다.</p>
</blockquote>
<h3 id="aot-의-장점">AOT 의 장점</h3>
<ol>
<li>애플리케이션 실행 전에 최적화를 실행한다. 따라서 오래 걸리는 최적화(ex. 코드 전체를 분석해야 하는 최적화) 도 부담없이 가능하다.</li>
<li>네이티브 코드를 미리 만들어 놓으므로, 애플리케이션 시작시 최적화된 코드가 바로 실행된다.</li>
<li>애플리케이션 시작 시간 자체도 줄어든다. 바이트 코드 해석 작업이 필요 없기 때문이다.</li>
<li>(JVM 에 비해) 메모리 사용량이 적다.</li>
</ol>
<h3 id="jit-의-장점">JIT 의 장점</h3>
<ol>
<li>모니터링 기반의 최적화<ul>
<li>객체의 (추상 클래스의)실제 타입, 주로 선택되는 조건 분기, 메서드 호출시 주로 호출되는 버전, 루프문의 반복 횟수 등은 런타임에 실제 호출을 분석하지 않고서는 모르는 정보들이다.</li>
<li>이런 정보들로 핫코드를 특정한뒤 최적화는 AOT 에서는 하지 못한다.</li>
</ul>
</li>
<li>예측 최적화<ul>
<li>AOT 의 경우 미리 네이티브 코드가 만들어진다. 런타임에 해석을 다시 하고 그런거 없다.</li>
<li>반면 JIT 의 경우 모니터링 결과로 예측 최적화를 한 후 예측이 틀렸다면 인터프리터로 돌아갈 수 있다. 따라서 AOT 에 비해 더 급진적인 예측 최적화가 가능하다.</li>
</ul>
</li>
<li>링크 타임 최적화<ul>
<li>예를 들어 각자 컴파일된 AOT 이미지를 조합하여 애플리케이션을 구동한다 했을때, 각자 컴파일된 AOT 이미지의 클래스들은 서로를 알지 못한다. 반면 JIT 는 동적으로 클래스를 로딩하여 구동할 때 따로 컴파일된 바이트 코드라도 JIT 는 두 클래스를 조합하여 최적화가 가능하다.</li>
</ul>
</li>
</ol>
<h3 id="aot-의-단점">AOT 의 단점</h3>
<ol>
<li>AOT 는 클래스 라이브러리를 동적으로 읽어들일 순 없다.</li>
<li>AOT 는 코드 양이 많다(바이트 팽창)</li>
<li>AOT 는 플랫폼 중립성이 없다. 하드웨어와 운영체제별로 다시 만들어야 한다.</li>
</ol>
<h2 id="aot-compile-의-형태">AOT Compile 의 형태</h2>
<ol>
<li>애플리케이션 전체를 미리 네이티브 코드로 컴파일한 후 실행하는 방법<ul>
<li>ex) 현재 GraalVM 의 native-image</li>
</ul>
</li>
<li>애플리케이션에 쓸 라이브러리를 미리 컴파일 한 후 공유<ul>
<li>ex) 과거 jaotc</li>
<li>ex) 현재 GraalVM Shared-Lib(<a href="https://www.graalvm.org/latest/reference-manual/native-image/guides/build-native-shared-library/">참고</a>)</li>
</ul>
</li>
</ol>
<h2 id="aot-실-사용-예">AOT 실 사용 예</h2>
<ol>
<li>MSA<ul>
<li>크기가 작음, 높은 빈도의 개발 및 배포</li>
<li>미리 최적화된 바이트 코드가 있다면 굳이 계층형 컴파일러의 소요 시간을 기다릴 필요 없이 최적화된 코드를 빨리 실행할 수 있음.</li>
</ul>
</li>
<li>공용 클래스 파일을 미리 로드해두는 <a href="https://medium.com/@toparvion/appcds-for-spring-boot-applications-first-contact-6216db6a4194">AppCDS</a> 와 비슷하게 Pre-Compiled Shared Library 만드는 용도.</li>
</ol>
<h2 id="뭐가-더-좋음">뭐가 더 좋음?</h2>
<p>JVM 에서 AOT 는 좀 더 최근에 개발되는 신기술 축에 속하지만 그렇다고 더 좋은건 아니다.</p>
<p>joatc 컴파일러는 JDK 9 에 들어와서 JDK 17 에 빠지고 GraalVM 에 편입된 후
최근에는 Galahad 프로젝트로 GraalVM 다시 JDK 에 들어올 시도를 하고 있다.</p>
<p>JIT, AOT 중 모든 상황에서 이게 좋다! 는 없으며, 위 장단을 잘 따져서 애플리케이션 특성에 맞게 프로파일링 후 AOT 를 도입할 수 있을 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JIT 컴파일러 최적화]]></title>
            <link>https://velog.io/@jimmy_neutron/JIT-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@jimmy_neutron/JIT-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Wed, 04 Feb 2026 11:12:41 GMT</pubDate>
            <description><![CDATA[<p>컴파일러의 이름은 네이티브 코드로 바꾼다는 것이지만, 이게 중요한게 아니다.
네이티브 코드로 바꿀 때 <code>얼마나 효율적인 코드로 최적화할 수 있냐</code> 가 컴파일러의 품질을 결정한다.</p>
<p>JIT 컴파일러는 컴파일 과정에서 LIR, HIR 을 대상으로 여러가지 최적화를 진행한다.</p>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/f81e7d5d-6c3d-4350-9df9-0e804a0bc385/image.png" alt=""></p>
<p><a href="https://wiki.openjdk.org/spaces/HotSpot/pages/11829287/PerformanceTacticIndex">https://wiki.openjdk.org/spaces/HotSpot/pages/11829287/PerformanceTacticIndex</a></p>
<p>이번 글에서는 JVM 밑바닥까지 파헤치기 책에 나오는 4가지 최적화를 알아 볼 것이다.
이 네가지는 아주 기본적인 최적화이다.</p>
<ul>
<li>전역 코드 셰이핑 - 메서드 인라인(Inlining (graph integration))</li>
<li>언어 특화 기법 - 탈출 분석</li>
<li>증명 기반 기법 - 공통 하위 표현식 제거(Common subexpression elimination)</li>
<li>배열 경계 검사 제거</li>
</ul>
<h1 id="메서드-인라인">메서드 인라인</h1>
<p>메서드 인라인은 결과가 같다면 메서드 사용처를 메서드의 구현으로 대체(inline)하는 기법이다.
메서드 인라인은 두가지 이유로 중요한 최적화다.</p>
<ul>
<li>메서드 호출 비용을 없앤다.</li>
<li>다른 최적화를 적용하는 기반이 된다.</li>
</ul>
<pre><code class="language-java">public static void foo(Object obj) {
    if (obj != null) {
        System.out.pringln(&quot;do something&quot;);
    }
}

public static void inline(String[] args) {
    Object obj = null;
    foo(obj);
}</code></pre>
<p>다른 최적화의 기반이 된다는 말을 알아보자.
위 inline() 메서드는 죽은 코드(dead code) 이다. 죽은 코드 제거(흐름 인지 재작성) 최적화를 적용할 수 있어 보인다.</p>
<p>하지만 각각의 메서드로 보았을 때에는 죽은 코드일지 알 수 없다.
foo() 입장에서는 obj 가 null 이 아닌 값이 들어올 수 있고, inline() 입장에서는 foo() 에서 의미 있는 일을 할 수도 있기 때문이다.</p>
<p>이 두 메서드를 인라인하게 되면 죽은 코드 제거 최적화를 할 수 있다.</p>
<pre><code class="language-java">public static void afterInline(String[] args) {
    Object obj = null;
    if (obj != null) { // 인라인 됨
        System.out.pringln(&quot;do something&quot;);
    }
}</code></pre>
<p>코드가 아무 동작을 하지 않음이 확실해 진다.
이처럼 인라인 최적화는 다른 최적화(이 예에서는 죽은 코드 제거)를 하기 위한 기반이 된다.</p>
<h2 id="java-에서의-인라인">java 에서의 인라인</h2>
<p>java 의 메서드는 javac 컴파일 타임에 직접 참조로 변환되는 몇몇의 케이스를 제외하고는 모두 <code>가상 메서드</code> 이다.</p>
<p>가상 메서드는 런타임에 리시버가 확정된 후 동적 디스패치를 진행해야 한다.
어떤 클래스의 메서드가 사용될지 리시버가 확정되기 전에는 모르기 때문에 인라인을 섣불리 할 수 없다.</p>
<p>반면 C++ 의 경우는 모든 메서드가 기본적으로 final(비가상) 메서드다.(다형성 필요시 virtual 키워드를 붙인다) 컴파일 타임에 리시버의 타입이 정해진다. 따라서 메서드 인라이닝이 수월하다.</p>
<h3 id="메서드-후보가-하나일-때">메서드 후보가 하나일 때</h3>
<p>자바에서도 다형성을 사용하지 않는다면 가상 메서드라도 인라이닝을 할 수 있을 것이다. 그 방법이 final 을 붙이는 것이다. 하지만 개발자가 인라이닝 최적화를 신경쓰면서 final 을 일일히 붙이는건 좋지 않다.</p>
<p>따라서 JVM 레벨에서 클래스 계층구조를 분석하여 가상 메서드가 오버라이딩을 사용하는지 판단 후 인라이닝을 진행하는 기능을 제공한다.</p>
<p>하지만 자바에서는 동적 클래스 로딩으로 클래스 계층구조는 항상 변할 수 있다. 따라서 JIT 컴파일러는 <code>현재 메서드 오버라이딩이 한개 뿐이라서, 인라이닝할 수 있다</code> 라는 <code>가정/예측</code> 을 기반으로 인라이닝한다.</p>
<p>이를 <code>guided inlining</code> 이라 하며, 따라서 자바의 인라이닝은 급진적 예측 최적화에 속한다.
동적 클래스 로딩에 의해 클래스 계층이 바뀌면 인터프리터 모드로 돌아간다.</p>
<h3 id="메서드-후보가-여러개일-때">메서드 후보가 여러개일 때</h3>
<p>클래스 계층 구조를 분석했을 때 오버라이딩 메서드가 하나면 위처럼 예측 컴파일을 하면 된다.</p>
<p>반면 만약 오버라이딩 메서드가 많아 리시버의 동적 디스패치가 필요하면 <code>인라인 캐시</code> 방식을 사용한다.</p>
<blockquote>
<p>메서드의 호출은 <code>vtable</code> 을 사용하여 메서드 영역에서 실제 메서드를 찾는 동작을 줄인다.
하지만 이 동작 자체도 이 인라인 캐시로 최적화된다.</p>
</blockquote>
<ol>
<li>메서드가 호출되면 수신자와 메서드 주소를 캐시한다.</li>
<li>다음 메서드 호출시 수신자의 버전이 같다면 메서드 주소를 통해 메서드를 바로 호출한다.<ul>
<li>monomorphic inline cache 라고 한다.</li>
<li>인라인 되지 않은 비가상 메서드와 비교하여 버전 비교 한번의 차이가 난다.</li>
</ul>
</li>
<li>다음 메서드 호출시 수신자의 버전이 다르다면 이 또한 캐시에 저장하고 모든 캐시에서 알맞은 메서드 호출 주소를 찾아 호출한다.<ul>
<li>megamorphic inline cache 라고 한다.</li>
<li>vtable 사용 방식과 부하가 같다.</li>
</ul>
</li>
</ol>
<blockquote>
<p>호출 지점(callsite) 에 캐시를 넣는다는 점에서 인라인 캐시이다.
메서드 인라이닝과 다르다. 무언가 코드가 인라인 되는 것이 아니다.</p>
</blockquote>
<h1 id="탈출-분석">탈출 분석</h1>
<p>메서드 안에서 생성한 객체가 메서드 밖으로 나가냐 안나가냐를 판단하는 것을 탈출 분석이라 한다.</p>
<p>탈출 분석 결과는 여러가지 최적화의 기반, 근거가 된다.</p>
<h2 id="탈출-수준">탈출 수준</h2>
<ul>
<li>GlobalEscape : 객체가 메서드 밖으로 탈출하는 경우. 다른 스레드가 참조할 수 있음.<ul>
<li>생성한 객체를 정적 필드에 저장하는 경우</li>
<li>리턴하는 경우</li>
<li>탈출한 객체의 필드로 넣는 경우 등</li>
</ul>
</li>
<li>ArgEscape : 인수 또는 인수의 필드 객체이지만 메서드를 탈출하지 않는 경우.</li>
<li>NoEscape : 메서드 안에서 절대 탈출하지 않는 경우. 이 때에는 객체의 힙 할당 자체가 필요없을 수 있음.</li>
</ul>
<h2 id="탈출-분석-후-최적화-예시">탈출 분석 후 최적화 예시</h2>
<h4 id="힙-할당-대신-스택-할당">힙 할당 대신 스택 할당</h4>
<p>탈출 분석 결과 <code>객체가 스레드에서 탈출하지 않는다고 확신하는 경우</code>
객체를 힙 대신 스택에 할당할 수 있다.
메서드가 끝나 스택 프레임이 없어지면 자동으로 객체 할당 메모리가 정리되므로, 가비지 컬렉팅 비용을 많이 줄일 수 있기 때문이다.</p>
<h4 id="스칼라-치환">스칼라 치환</h4>
<p>탈출 분석 결과 <code>객체가 메서드를 벗어나지 않는다고 확신하는 경우</code> 
객체를 스칼라 타입으로 분해하여 객체 필드 각각이 지역 변수 처럼 쓰일 수 있도록 한다.</p>
<p>객체 생성 비용이 줄어들며, 새로운 최적화가 가능해질 수 있다.</p>
<h4 id="동기화-제거">동기화 제거</h4>
<p>탈출 분석 결과 <code>다른 스레드에서 접근할 수 없는 변수가</code>
동기화 블럭에 사용된 경우 동기화 블럭을 제거할 수 있다.</p>
<h2 id="java-의-탈출-분석">Java 의 탈출 분석</h2>
<p>탈출 분석은 비용이 크기 때문에 Java 의 JIT 에서 적용하기 부담스럽다.
탈출 여부를 정확하게 판별하려면 복잡한 프로시저간 분석이 필요하기 때문이다.</p>
<p>따라서 C++ 이나 C# 에 비해 탈출 분석의 정확도가 조금 뒤떨어지는 알고리즘을 사용한다.
발할라 프로젝트의 값객체가 완성될 경우, 스칼라 치환이 좀 더 효율적이게 동작할 수 있다.</p>
<h2 id="탈출-분석을-통한-스칼라-치환-예시">탈출 분석을 통한 스칼라 치환 예시</h2>
<ol>
<li>최적화 수행 전<pre><code class="language-java">public int test(int x) {
 int xx = x + 2;
 Point p = new Point(xx, 42);
 return p.getX();
}</code></pre>
</li>
<li>인라인 최적화 후<pre><code class="language-java">public int test(int x) {
 int xx = x + 2;
 Point p = point_memory_alloc();
 p.x = xx; // 생성자 인라인
 p.y = 42; // 생성자 인라인
 return p.x; // Point::getX() 인라인
}</code></pre>
</li>
<li>스칼라 치환 : 객체 p 는 메서드 밖에서 사용되지 않는다는 탈출 분석 결과<pre><code class="language-java">public int test(int x) {
 int xx = x + 2;
 int px = xx; // Point 타입 객체 p 삭제 후 스칼라 치환
 int py = 42;
 return px;
}</code></pre>
</li>
<li>죽은 코드 제거 최적화 : 탈출 분석 후 스칼라 치환에 의해 바뀐 코드는 죽은 코드 제거가 추가로 가능해진다.<pre><code class="language-java">public int test(int x) {
 return x + 2;
}</code></pre>
</li>
</ol>
<h2 id="탈출-분석-로그로-보기">탈출 분석 로그로 보기</h2>
<pre><code class="language-java">// java -XX:+PrintEscapeAnalysis -XX:+PrintEliminateAllocations Escape.java
public class Escape {

    static class Point {
        int x;
        int y;
        public Point(int x, int y) { this.x = x; this.y = y; }
        public int getX() { return x; }
    }

    public static int test(int x) {
        int xx = x + 2;
        Point p = new Point(xx, 42);
        return p.getX();
    }

    public static void main(String[] args) {
        for (int i = 0; i &lt; 100000; i++) { // JIT 컴파일 촉발
            test(10);
        }
    }
}</code></pre>
<ul>
<li><code>-XX:+PrintEscapeAnalysis</code> : 탈출 분석 결과 출력<ul>
<li>NoEscape(탈출 불가) 와 같은 문구 출력</li>
</ul>
</li>
<li><code>-XX:+PrintEliminateAllocations</code> : 스칼라 치환 결과 출력<ul>
<li>Scalar, Allocate 와 같은 문구 출력</li>
</ul>
</li>
<li><code>-XX:+PrintEliminateLocks</code> : 동기화 제거 결과 출력</li>
</ul>
<blockquote>
<p>디버그 모드로 빌드된 JVM 에서 위 옵션을 사용할 수 있다.</p>
</blockquote>
<h1 id="공통-하위-표현식-제거">공통 하위 표현식 제거</h1>
<blockquote>
<p>표현식 E 가 이미 평가되었고, E 에 등장하는 모든 변수값이 평가 이후 변하지 않는다면, 이후의 등장하는 표현식 E 를 공통 하위 표현식이라 한다.</p>
</blockquote>
<pre><code class="language-java">int d = (c * b) * 12 + a + (a + b * c);</code></pre>
<p>(c<em>b) 와 (b</em>c) 는 같다.
앞의 (c<em>b) 가 계산되었다면 (b</em>c) 는 하위 표현식으로서 제거되고 이전 결과값을 사용할 수 있다.
이를 공통 하위 표현식 제거라고 한다.</p>
<p>공통 하위 표현식 제거는 클래스 파일 바이트 코드 명령어 수(연산의 수)를 줄일 수 있다.</p>
<h1 id="배열-경계-검사-제거">배열 경계 검사 제거</h1>
<p>자바는 안전한 언어이다.
배열 사용시 인덱스에 크기보다 큰 값이 들어오면 <code>IndexOfOutBoundsException</code> 이 발생한다.</p>
<p>이 체크는 배열 접근마다 발생하는 작업이다. 특히 순환문이라면 이 작업으로 인한 부하가 더 커진다.</p>
<p>컴파일 시점에 코드를 분석하여 배열 접근이 범위를 벗어나지 않음이 확실하면, 이 범위 체크(경계 검사) 로직을 제거할 수 있다.</p>
<ul>
<li>암묵적 예외 처리</li>
<li>null pointer 검사</li>
<li>0 으로 나누기</li>
</ul>
<p>같은 작업도 비슷하게 자바가 안정성을 위해 실행하는 로직이지만 JIT 컴파일시 최적화 될 수 있다.</p>
<blockquote>
<p>암묵적 예외 최적화</p>
<p>JIT 컴파일시 검사 코드를 제거하고, CPU/OS가 던지는 하드웨어 예외(트랩) 를 이용해서 자바 예외를 처리하는 최적화</p>
<p>어차피 예외가 나야 하는 상황이라면, 미리 if (x==null) throw NPE; 같은 명시적 체크를 매번 넣는 대신, 실제로 문제가 발생할 때만 하드웨어 예외가 터지게 하고 그걸 자바 예외로 바꿔치기한다.</p>
<p>숨겨진 하드웨어 예외를 사용한다는 점에서 암묵적 이란 이름이 붙음</p>
<p>배열 경계 검사 제거도 암묵적 예외 최적화 방식으로 경계 검사를 제거</p>
</blockquote>
<pre><code class="language-java">if (foo != null) {
    return foo.value;
} else {
    throw new NullPointerException();
}</code></pre>
<blockquote>
<p>를</p>
<pre><code class="language-java">try {
    return foo.value; // 체크 없이 리턴으로 변경
} catch (segment_fault) { // 하드웨어 예외 catch
    // 하드웨어 예외를 NPE 로 대체하는 핸들러
    // 스레드를 커널 모드로 변경해야 하기 때문에 느리다.
    // 따라서 JIT 모니터링 정보 상 NPE 확률이 적을때 효율적이다.
    uncommon_trap();
}</code></pre>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[인터프리터와 JIT 컴파일러]]></title>
            <link>https://velog.io/@jimmy_neutron/%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0%EC%99%80-JIT-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC</link>
            <guid>https://velog.io/@jimmy_neutron/%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0%EC%99%80-JIT-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC</guid>
            <pubDate>Wed, 04 Feb 2026 11:12:33 GMT</pubDate>
            <description><![CDATA[<p>JVM 이 클래스 파일을 컴퓨터에서 실행하기 위해서는 네이티브 코드로 해석해야 한다.
JVM 명세는 바이트 코드의 해석 및 실행의 구현을 명세하지 않는다.</p>
<p>따라서 JVM 들은 각자의 방식으로 이를 구현할 수 있는데, 가장 많이 사용되는 HotSpot 에서는 세가지 방식으로 사용할 수 있다.</p>
<ol>
<li>인터프리터</li>
<li>JIT 컴파일러</li>
<li>AOT 컴파일러</li>
</ol>
<p>인터프리터는 클래스 파일의 내용을 그대로 해석 실행하는것에 비해
JIT, AOT 컴파일러는 바이트코드에서 네이티브 코드로 해석하는 과정에서 많은 최적화가 이뤄진다.</p>
<h1 id="인터프리터">인터프리터</h1>
<ul>
<li>Hotspot 이 바이트 코드를 실행하는 기본적인 방법</li>
<li>단순히 바이트 코드를 해석하여 실행</li>
<li>사용 case<ul>
<li>빨리 끝나는 프로그램을 빨리 실행해야할 때<ul>
<li>따로의 최적화 과정이 없기에 JIT 에 비해 시작 시간이 빠르다.</li>
</ul>
</li>
<li>가용 메모리가 적을 때<ul>
<li>JIT 컴파일러는 최적화 과정에 필요한 코드 캐시 등 메모리를 사용한다.</li>
</ul>
</li>
<li>JIT 컴파일의 탈출구<ul>
<li>JIT 컴파일은 속도의 이유로 (코드가 전체적으로는 어떤 형태일 것이다라는)가정에 의해 컴파일한다.</li>
<li>새로운 클래스가 로드되고 하면서 코드 구조(상속 구조 등등)이 바뀌며 최적화 가정이 깨져 최적화를 취소해야할 경우, 인터프리터로 실행하게 된다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h1 id="jit-컴파일러">JIT 컴파일러</h1>
<blockquote>
<p>Hotspot 코드를 발견하면 그때그때 컴파일하여 실행(Just-In-Time)</p>
</blockquote>
<h2 id="hotspot-의-계층형-컴파일">Hotspot 의 계층형 컴파일</h2>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/22d4d942-c751-4f82-aac1-c842c89bdfe1/image.png" alt="">
<a href="https://www.infoq.cn/article/java-10-jit-compiler-graal">https://www.infoq.cn/article/java-10-jit-compiler-graal</a></p>
<p>HotSpot JVM 의 JIT 컴파일러는 C1, C2 가 있다.
C1, C2, 인터프리터는 각자 다른 최적화 레벨을 가지고 있다.
실행중인 바이트 코드를 시스템 상태와 호출 횟수 등에 따라 최적화 레벨을 달리하여 실행하는데, 이를 <code>계층형 컴파일</code> 이라 한다.</p>
<ol>
<li><code>인터프리터</code> : 최초 HotSpot 구동시 인터프리터 모드로 실행<ul>
<li>인터프리터로 실행 중 자주 실행되는 HotSpot 코드는 C1 으로 컴파일 실행</li>
</ul>
</li>
<li><code>C1(클라이언트 컴파일러)</code> : C2 보다 빠른 컴파일, 간단한 최적화.<ul>
<li>C1 으로 실행될 때에는 호출 횟수, 컴파일 결과등을 모니터링할 수 있다. 이는 더 높은 최적화 레벨인 C2 컴파일러로 컴파일할지를 판단하는 기준이 된다.<ul>
<li>no-profiling : 빠르게 끝나는(최적화가 딱히 필요없는) 메서드의 경우 사용할 수 있다.</li>
<li>limited-profiling : C2 가 이미 바쁠때에는 일부 제한적인 프로파일링을 하여 C2 로 넘어가는 코드를 줄일 수 있다.</li>
<li>full-profiling : 기본적으로는 할 수 있는 모든 프로파일링을 진행하여 필요한 경우 C2 로 보낸다.</li>
</ul>
</li>
</ul>
</li>
<li><code>C2(서버 컴파일러, Opto)</code> : C1 보다 느린 컴파일, 공격적인 최적화(높은 런타임 성능)<ul>
<li>C1 컴파일러가 바쁜 경우 일부 인터프리터에서 C2 컴파일러로 직행할 수 있다.</li>
</ul>
</li>
</ol>
<p>위 그림과 같이 보자.</p>
<h2 id="컴파일러-선택-관련-옵션들">컴파일러 선택 관련 옵션들</h2>
<ul>
<li><code>-XX:+TieredCompilation</code> : 계층형 컴파일 활성화(JDK 7 부터 기본값). 인터프리터, C1, C2 모두 사용</li>
<li><code>-XX:-TieredCompilation</code> : 계층형 컴파일 비활성화. 인터프리터&amp;C2 사용<ul>
<li>JDK 8 까지는 <code>-client</code> 로 C2 대신 C1 으로 고정 가능</li>
</ul>
</li>
<li><code>-XX:+TieredCompilation -XX:TieredStopAtLevel=1</code> : 인터프리터&amp;C1 사용</li>
<li><code>-XInt</code> : 인터프리터만 사용</li>
<li><code>-XComp</code> : JIT 컴파일러만 사용. 시작 시간이 오래 걸릴 수 있다.</li>
</ul>
<p><code>java -version</code>
<code>java -XInt -version</code>
<code>java -XComp -version</code></p>
<p>를 실행시켜 보자. 각각 Mixed mode, interpreted mode, compiled mode 를 볼 수 있다.</p>
<p>디버그를 위해 JIT 컴파일러 동작을 배제하고 싶은 경우 -XInt 로 인터프리터만 쓸 수 있고,
AWS lambda 같이 빨리 끝나고 가벼운 애플리케이션 실행의 경우 C2 를 배제할 수도 있고,
C2 를 대체한 Graal Compiler 를 사용하려는 경우 C1 을 배제할 수도 있다.</p>
<h1 id="jit-컴파일-대상-코드">JIT 컴파일 대상 코드</h1>
<p>JIT 컴파일러의 컴파일 대상이 되는 코드 유형은 두가지가 있다.</p>
<ol>
<li>여러 번 호출되는 메서드<ul>
<li>표준 JIT 컴파일 방식(메서드 전체를 컴파일)</li>
</ul>
</li>
<li>여러 번 실행되는 순환문의 본문(블록)<ul>
<li>촉발은 순환문의 블록이지만, 컴파일 자체는 메서드 전체를 컴파일</li>
<li>온스택 치환(OSR) : 메서드 내부의 순환문 실행 도중 메서드 자체가 컴파일 되며 스택의 메서드가 대체되기 때문에 온스택 치환이라고도 한다.</li>
</ul>
</li>
</ol>
<h1 id="핫스팟-코드-탐지-방식호출-횟수-카운팅-방식">핫스팟 코드 탐지 방식(호출 횟수 카운팅 방식)</h1>
<ul>
<li>샘플 기반 : 스레드들의 호출 상단 스택을 샘플링.<ul>
<li>쉽고 효율적임</li>
<li>스레드 스택을 보면 메서드 호출관계를 쉽게 파악 가능</li>
<li>정확한 횟수 측정 불가</li>
<li>스레드가 블로킹되면 탐지 불가</li>
<li>J9 jvm 에서 사용하는 방식</li>
</ul>
</li>
<li>카운터 기반 : 메서드와 코드 블록에 카운터 설정<ul>
<li>정확한 실행 횟수 기록 가능</li>
<li>메서드 각각 카운터 설정해야함. 어려움</li>
<li>메서드 호출 관계를 직접 얻을 수 없음</li>
<li>핫스팟 jvm 에서 사용하는 방식</li>
</ul>
</li>
</ul>
<h1 id="c1-컴파일러로-넘어가는-호출-횟수-임계점핫스팟-기준">C1 컴파일러로 넘어가는 호출 횟수 임계점(핫스팟 기준)</h1>
<blockquote>
<p>이 기준은 <code>인터프리터 -&gt; C1</code> 이다. 
<code>C1 -&gt; C2</code> 는 복잡한 모니터링 결과에 의해 전환된다.</p>
</blockquote>
<p>HotSpot 은 카운터를 기반으로 핫스팟 코드를 감지한다.
JIT 컴파일 대상 코드 유형이 2가지이므로, 카운터도 2가지가 존재한다.</p>
<h2 id="메서드-호출-카운터">메서드 호출 카운터</h2>
<ul>
<li>클라이언트 모드(Int -&gt; C1) : 1500회</li>
<li>서버 모드(C1 -&gt; C2) : 10000회</li>
<li><code>-XX:CompileThreshold</code> 로 설정 가능</li>
<li>시간당 호출 회수를 측정한다.</li>
<li>감쇠 카운터(counter decay method invocation) : 단위 시간(반감기. counter half life time)안에 임계값에 도달하지 못한다면 카운터를 절반으로 줄임.<ul>
<li><code>-XX:-UseCounterDecay</code> 감쇠 카운터 비활성화. 호출 카운터를 감쇠하지 않고 절대값으로 측정</li>
<li><code>-XX:CounterHalfLifeTime</code> 반감기를 초 단위로 설정 가능</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/b91823f4-2cc0-4c16-8372-c7072d59cd82/image.png" alt=""></p>
<h2 id="back-edge-카운터순환문-블록-카운터">back edge 카운터(순환문 블록 카운터)</h2>
<ul>
<li>클라이언트 모드(Int -&gt; C1) : 13995회<ul>
<li><code>(메서드 호출 카운터 임계값 * OSR 비율)/100</code></li>
<li>메서드 호출 카운터 임계값(-XX:CompileThreshold) : 디폴트 1500</li>
<li>OSR 비율(-XX:OnstackReplacePercentage) : 디폴트 933</li>
</ul>
</li>
<li>서버 모드(C1 -&gt; C2) : 10700회<ul>
<li><code>(메서드 호출 카운터 임계값 * OSR 비율 - 인프라 모니터링 비율)/100</code></li>
<li>메서드 호출 카운터 임계값(-XX:CompileThreshold) : 디폴트 10000</li>
<li>OSR 비율(-XX:OnstackReplacePercentage) : 디폴트 140</li>
<li>인터프리터 모니터링 비율(-XX:InterpreterProfilePercentage) : 디폴트 33</li>
</ul>
</li>
<li>감쇠 없이 절대 시행 횟수 측정</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/68136258-3c9a-498f-be3e-f88303910672/image.png" alt=""></p>
<h2 id="osr-의-특징---back-edge-카운터는-값-조정">OSR 의 특징 - back edge 카운터는 값 조정</h2>
<p>위 다이어그램 두개를 비교해 보면 back edge 카운터에 의한 OSR 컴파일 요청은 메서드 호출 카운터와 다른점이 있다.</p>
<p>back edge 카운터는 C1 컴파일이 요청된 후에 back edge 카운터 값을 줄인다는 것이다.</p>
<p>둘 다 공통적으로 임계값을 넘겼을 때 컴파일이 요청되었으면 더 이상 요청하지 않아야 할 것이다.
단 OSR 컴파일의 경우 반복문 안에서 그 동작이 발생하기 때문에, 컴파일이 요청되었는가를 체크하는 일 자체가 메서드 호출 카운터에 비해 훨씬 부담되는 일이다.</p>
<p>따라서 back edge 카운터에 의한 OSR 컴파일 요청은 호출 횟수가 임계값을 넘은 경우 호출 횟수를 일정량 줄인다. 다시 임계치에 오르는 동안 요청이 되었는지 체크를 하지 않을 수 있다.</p>
<h1 id="컴파일-과정">컴파일 과정</h1>
<h2 id="컴파일은-비동기">컴파일은 비동기</h2>
<p>컴파일은 기본적으로 메서드 컴파일, OSR 컴파일 둘 다 <code>백그라운드 스레드에서 비동기</code>로 진행.
<code>-XX:-BackgroundCompilation</code> 옵션으로 동기적으로 컴파일하도록 변경 가능. 이러면 컴파일 진행중 일시 정지됨.</p>
<h2 id="c1-컴파일-과정">C1 컴파일 과정</h2>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/0582590a-c03d-4b6d-8c90-7819fb87a01f/image.png" alt="">
<a href="https://nothingcosmos.github.io/OpenJDKOverview/src/overview.html">https://nothingcosmos.github.io/OpenJDKOverview/src/overview.html</a></p>
<p>C1 컴파일에서 바이트 코드는 다음의 과정을 거쳐 네이티브 머신 코드로 변환된다.</p>
<p>BytesCode -&gt; HIR -&gt; LIR -&gt; Native Code(Machine Code)</p>
<p>위 그림을 보면 컴파일 과정에서 HIR, LIR 각각에 적용되는 최적화들이 다름을 볼 수 있다.</p>
<blockquote>
<p>IR (Intermediate Representation)</p>
</blockquote>
<p>최적화를 위해 컴파일 과정에서 중간에 변환되는 중간 코드.</p>
<blockquote>
<ul>
<li><code>HIR(High IR)</code> 은 좀 더 바이트 코드와 비슷한 고수준의 중간 언어이며, 코드 레벨의 최적화를 하기에 적합한다.</li>
<li><code>LIR(Low IR)</code> 은 좀 더 기계어와 비슷한 중간 언어이며, 기계어 수준의 최적화를 하기 적합하다.</li>
</ul>
</blockquote>
<p>바이트코드는 네이티브 머신 코드가 되기 전 중간 코드(IR. Intermediate Representation)를 거친다. JIT 컴파일 과정의 최적화는 모두 IR 을 대상으로 이뤄진다.</p>
<h2 id="c2-컴파일-과정">C2 컴파일 과정</h2>
<p>C2 는 C1 컴파일 과정에서 수집된 모니터링 정보를 토대로, <code>공격적인 예측 최적화</code> 를 적용한다.</p>
<p>예를 들면 다음과 같은 최적화들을 진행한다. C2 컴파일의 컴파일 과정은 상당히 복잡하다.</p>
<ul>
<li>죽은 코드 제거</li>
<li>순환문 언롤링</li>
<li>순환문 표현식 호이스팅</li>
<li>공통 하위 표현 제거</li>
<li>상수 전파</li>
<li>기본 블록 재정렬</li>
</ul>
<blockquote>
<p><strong>공격적 예측 최적화</strong>
확실하지 않지만 모니터링 결과를 토대로 어떠한 상황임을 <code>예측, 가정</code> 한 후 최적화하는 것.
가정을 확실하게 알기 위해서는 비용이 많이 들기에 지금까지의 모니터링 결과로 예측하는 트레이드 오프를 적용한 결과다.</p>
<p>대부분의 상황은 자바가 가상 메서드이기 때문에 발생. 런타임에 리시버가 확정되지 않아 최적화하기 힘듦. 모니터링의 결과로 대부분이 하나의 리시버로 사용됨을 알게 되면, 미래에도 하나의 리시버일 것임을 가정한 후 최적화. 다른 리시버가 들어오면 인터프리터로 실행.</p>
<p>해당 가정이 틀린 경우는 interpreter 모드로 도망가서 일단 실행한 후 다시 컴파일한다.</p>
</blockquote>
<h2 id="컴파일-로그로-컴파일-과정-눈으로-보기">컴파일 로그로 컴파일 과정 눈으로 보기</h2>
<ul>
<li><code>-XX:+PrintCompilation</code> : 컴파일 로그</li>
<li><code>-XX:+PrintCompilation</code> -XX:+PrintInlining : 인라이닝 포함 컴파일 로그</li>
<li><code>-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly</code> : 컴파일러 어셈블리 코드</li>
<li><code>-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+PrintOptoAssembly</code> : 좀 더 최종 결과에 가까운 중간 코드 표현(서버 모드 가상 머신용)</li>
<li><code>-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+PrintLIR</code> : 좀 더 최종 결과에 가까운 중간 코드 표현(클라이언트 모드 가상 머신용)</li>
<li><code>-XX:PrintIdealGraphLevel=2 -XX:PrintIdealGraphFile=ideal.xml</code> : IGV 에서 컴파일러 보기 위함<ul>
<li>FastDebug, SlowDebug 로 빌드된 JVM 만 가능</li>
</ul>
</li>
</ul>
<h3 id="-xxprintcompilation-의-정보">-XX:+PrintCompilation 의 정보</h3>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/dd47db68-283d-49df-836e-83302618f7db/image.png" alt=""></p>
<h3 id="예시">예시</h3>
<pre><code class="language-java">// javac Test.java
// java -XX:+PrintCompilation Test
public class Test {
    public static final int NUM = 15000;

    public static int doubleValue(int i) {
        for(int j = 0; j &lt; 100000; j++);  // JIT 코드 최적화 과정 시연용 &#39;빈&#39; 순환문
        return i * 2;
    }

    public static long calcSum() {
        long sum = 0;
        for (int i = 1; i &lt;= 100; i++) {  // doubleValue()를 100번 호출
            sum += doubleValue(i);
        }
        return sum;
    }

    public static void main(String[] args) {
        for (int i = 0; i &lt; NUM; i++) {  // calcSum()을 15,000번 호출
            calcSum();
        }
    }
}
</code></pre>
<p><code>java -XX:+PrintCompilation Test</code> 결과</p>
<pre><code>190  306 %     3       servercompiler.Test::doubleValue @ 2 (18 bytes)
194  307 %     4       servercompiler.Test::doubleValue @ 2 (18 bytes)
194  306 %     3       servercompiler.Test::doubleValue @ 2 (18 bytes)   made not entrant
194  308       4       servercompiler.Test::doubleValue (18 bytes)
195  309       3       servercompiler.Test::calcSum (26 bytes)
195  310 %     4       servercompiler.Test::calcSum @ 4 (26 bytes)
196  311       4       servercompiler.Test::calcSum (26 bytes)
198  309       3       servercompiler.Test::calcSum (26 bytes)   made not entrant</code></pre><pre><code>183  303 %     3       servercompiler.Test::doubleValue @ 2 (18 bytes)
184  304       3       servercompiler.Test::doubleValue (18 bytes)
184  305 %     4       servercompiler.Test::doubleValue @ 2 (18 bytes)
185  303 %     3       servercompiler.Test::doubleValue @ 2 (18 bytes)   made not entrant
185  305 %     4       servercompiler.Test::doubleValue @ 2 (18 bytes)   made not entrant
185  306       4       servercompiler.Test::doubleValue (18 bytes)
185  304       3       servercompiler.Test::doubleValue (18 bytes)   made not entrant
185  307 %     4       servercompiler.Test::doubleValue @ 2 (18 bytes)
185  308       3       servercompiler.Test::calcSum (26 bytes)
186  308       3       servercompiler.Test::calcSum (26 bytes)   made not entrant
186  309       3       servercompiler.Test::calcSum (26 bytes)
186  310 %     4       servercompiler.Test::calcSum @ 4 (26 bytes)
187  311       4       servercompiler.Test::calcSum (26 bytes)
188  309       3       servercompiler.Test::calcSum (26 bytes)   made not entrant</code></pre><h3 id="igv">IGV</h3>
<p>Ideal Graph Viewer.</p>
<p>JIT 컴파일 과정에서 코드는 Ideal Graph 라는 형태로 표현된다.
IGV 플러그인을 사용하면 이 그래프를 컴파일 단계별로 시각적으로 볼 수 있다. 이를 이용하면 컴파일 과정에서 최적화가 어떻게 되는지 시각적으로 볼 수 있다.</p>
<p>IGV 는 JDK 소스 코드에 이미 포함되어 있다. 다만 제공되는 런타임에는 없기에, 소스코드를 받아서 직접 빌드하여 실행하도록 한다.</p>
<blockquote>
<p>IGV 는 JDK 소스코드에 포함. 빌드하여 사용
$ cd src/utils/IdealGraphVisualizer
$ mvn install
$ chmod u+x igv.sh
$ ./igv.sh</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/d5c25d78-e49c-4325-8f76-a6ad9cb796cc/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/c7d043fb-b654-46d4-90c9-1f994ca6593f/image.png" alt="">
이 그림에서는 빨간부분이 위 테스트 코드 doubleValue() 의 빈 순환문이며, 없어졌다는 뜻이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[javac 컴파일러 - 제네릭, 애너테이션 프로세서]]></title>
            <link>https://velog.io/@jimmy_neutron/javac-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC-%EC%A0%9C%EB%84%A4%EB%A6%AD-%EC%95%A0%EB%84%88%ED%85%8C%EC%9D%B4%EC%85%98-%ED%94%84%EB%A1%9C%EC%84%B8%EC%84%9C</link>
            <guid>https://velog.io/@jimmy_neutron/javac-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC-%EC%A0%9C%EB%84%A4%EB%A6%AD-%EC%95%A0%EB%84%88%ED%85%8C%EC%9D%B4%EC%85%98-%ED%94%84%EB%A1%9C%EC%84%B8%EC%84%9C</guid>
            <pubDate>Tue, 03 Feb 2026 09:52:09 GMT</pubDate>
            <description><![CDATA[<p>javac 컴파일의 편의문법 제거 대상인 제네릭과 개발자의 코드 추가 로직인 애너테이션 프로세서에 대해 알아보자.</p>
<h1 id="제네릭">제네릭</h1>
<pre><code class="language-java">Object obj = &quot;a&quot;;
Object[] strArr = new String[10]; // 공변
strArr[1] = 10; // 런타임 할당시 에러</code></pre>
<p>배열은 공변이다. String 이 Object 의 하위 타입이라면 String[] 도 Object[] 의 하위 타입이다.
따라서 Object[] 정적 타입 변수에 String 값을  추가하는 것은 컴파일 타임에 에러를 낼 수 없다.</p>
<pre><code class="language-java">ArrayList list = new ArrayList();
list.add(Integer.valueOf(10)); // 컴파일, 런타임 에러 X
list.add(&quot;b&quot;); // 컴파일, 런타임 에러 X</code></pre>
<p>제네릭이 있기 전 ArrayList(컬렉션) 은 타입이 없다. 따라서 하나의 컬렉션에는 여러 타입을 넣을 수 있다.</p>
<h3 id="제네릭-도입시-문제---하위-호환성">제네릭 도입시 문제 - 하위 호환성</h3>
<p>매개 변수화 된 타입인 제네레릭은 타입에 국한되지 않은 추상적인 코드를 만들 수 있게 한다.
하지만 위 처럼 제네릭이 있기 전의 컬렉션은 모든 타입을 받았다.</p>
<p>제네릭 컬렉션을 만들면서 기존 컬렉션 코드를 문제되게 하지 않으려면, </p>
<ol>
<li>기존 컬렉션을 두고 새로운 제네릭 컬렉션을 새로 만든다.</li>
<li>기존 컬렉션을 제네릭 버전으로 변경하고, 런타임에는 타입 소거한다. 런타임에는 적절한 형변환 코드를 넣어서 해당 타입만이 들어감을 보장한다.</li>
</ol>
<p>자바는 2번을 선택했다.
JDK 1.2 시절 <code>Vector -&gt; ArrayList</code>, <code>HashTable -&gt; HashMap</code> 과 같이 1번의 선택으로 컬렉션 라이브러리를 이미 변경한 탓에 또 1번을 선택하면 <code>Vector&lt;Int&gt;</code>, <code>ArrayList&lt;Int&gt;</code> 와 같이 너무 많은 클래스가 생겨 불편하기 때문이다.</p>
<p>이를 <code>타입 소거</code> 라고 한다.</p>
<h3 id="타입-소거의-문제1---박싱언박싱">타입 소거의 문제1 - 박싱/언박싱</h3>
<h4 id="제네릭-컬렉션에서-객체-타입을-써야하는-이유">제네릭 컬렉션에서 객체 타입을 써야하는 이유</h4>
<p>하위 호환성을 위한 타입 소거로 인해 제네릭 컬렉션의 타입 제한은 컴파일 타임에 <code>형변환 코드</code> 가 자동 생성되어 동작한다.</p>
<p>문제의 원인은 자바 컬렉션 라이브러리는 내부에서 애초에 Object[] 를 사용했다는 것이다.
ArrayList 의 add는 아래 처럼 Object[] 에 값을 저장한다.
따라서 요소는 Object 의 하위 타입이어야 한다.</p>
<pre><code>    private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }</code></pre><p><code>타입 변환</code> 으로 제네릭을 구현한 이상 제네릭 컬렉션의 모든 요소는 Object 타입의 하위 타입이어야 한다는 결론이 난다.</p>
<p>이것이 제네릭에서 기본 타입을 쓰지 못하는 이유다. primitive 타입은 object 의 하위가 아니기 때문이다. 타입 소거 방식을 택할 수 밖에 없었기 때문에 발생한 문제다.</p>
<h4 id="박싱언박싱의-문제">박싱/언박싱의 문제</h4>
<p>다행히 원소로 객체 타입만을 쓸 수 있다는 문제는 코드 레벨에서는 없다.
primitive 타입을 원소를 add 하는 코드를 작성하더라도 박싱/언박싱을 자동으로 해주기 때문이다.</p>
<p>진짜 문제는 박싱/언박싱 자체가 느리고 양도 많아서, 자바 제네릭의 주요한 성능적 문제라는 것이다.</p>
<h4 id="문제-해결의-노력---발할라-프로젝트">문제 해결의 노력 - 발할라 프로젝트</h4>
<p>박싱/언박싱을 근본적으로 없애기 위해서는 다음 중 하나가 만족되어야 한다.</p>
<ul>
<li>런타임에 제네릭 타입을 유지(reified generics) 해서 <code>List&lt;int&gt;</code>와 <code>List&lt;Object&gt;</code>를 다른 클래스로 다루기</li>
<li>primitive마다 특수화된 버전(예: ListInt, ListDouble…)을 자동 생성</li>
<li>JVM의 <code>값 타입(value type)</code> 지원</li>
</ul>
<p>이상의 방식은 모두 타입 자체를 유지하여 타입 소거가 되지 않도록 하는 방법이다. 외에도 여러 방식이 있다.</p>
<p>이 작업은 발할라 프로젝트라는 이름으로 진행중이다. 오랫동안.</p>
<h3 id="타입-소거의-문제2---런타임에-엘리먼트의-타입을-알-수-없음">타입 소거의 문제2 - 런타임에 엘리먼트의 타입을 알 수 없음</h3>
<pre><code class="language-java">class Test {
    List&lt;String&gt; list;
}</code></pre>
<p>위 list 의 제네릭으로 선언된 원소 타입 <code>&lt;String&gt;</code> 은 런타임에 알 수 없다.
타입이 소거되기 때문이다.
따라서 필요하다면 인자로 가지고 다녀야 한다.</p>
<p>다만 Signature 클래스 파일 속성에 제네릭 컬렉션 전체 타입이 적히긴 한다.
우린 리플렉션으로 이를 사용할 수 있다.</p>
<pre><code class="language-java">Field f = Test.class.getDeclaredField(&quot;list&quot;);
System.out.println(f.getGenericType());
// 결과 : java.util.List&lt;java.lang.String&gt;</code></pre>
<p>getGenericType() 의 리턴값에서 String 만 따로 추출할 방법은 없다. 정말 필요하다면 파싱을 해야 한다.</p>
<h3 id="타입-소거의-문제3---오버로딩-불가">타입 소거의 문제3 - 오버로딩 불가</h3>
<pre><code class="language-java">public void sum(List&lt;Integer&gt; a) {}
public void sum(List&lt;Double&gt; a) {}</code></pre>
<p>위 두 메서드의 시그니쳐는 분명히 다르지만 오버로딩이 불가하다.
런타임에는 둘의 타입 매개변수가 소거되므로 두 시그니쳐가 같아지기 때문이다.</p>
<h1 id="애너테이션-프로세서">애너테이션 프로세서</h1>
<p>애너테이션 프로세서는 컴파일 동작을 개발자가 간섭할 수 있는 기능이다.
간단하게는 코드를 자동으로 추가하는 기능이며
정확하게 말하면 컴파일 타임에 추상 구문 트리를 수정하는 기능이다.</p>
<ul>
<li>AbstractProcessor 를 구현하여 <code>javac -processor [구현체 클래스]</code> 로 실행한다.</li>
<li>다음 등등의 동작이 가능<ul>
<li>ProcessingEnvironment 의 Messager 로 컴파일시 에러 레벨을 지정하여 메세지를 출력</li>
<li>에너테이션을 읽어서 값을 보고 메서드 생성(롬복)</li>
<li>등등</li>
</ul>
</li>
<li>애너테이션 정보를 런타임에 사용하는 spring AOP 와 애너테이션 사용의 쌍두마차라 할 수 있다.</li>
<li>대표적인 사용처로는 롬복, 하이버네이트 validator 등이 있다.</li>
</ul>
<pre><code class="language-java">// 아래 애너테이션 둘다 필요
@SupportedAnnotationTypes(&quot;*&quot;) // 프로세서에 사용할 애너테이션 클래스 지정
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class CustomAnnotationProcessor extends AbstractProcessor {

    // 찾은 요소에 대해 실행할 클래스. 따로 적진 않을 예정
    // 예시의 구현은 ProcessingEnvironment 의 messager 를 이용한 컴파일 에러 출력
    private Validator validator;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        validator = new Validator(processingEnv.getMessager());
    }

    @Override
    public boolean process(Set&lt;? extends TypeElement&gt; annotations, RoundEnvironment roundEnv) {
        if (!roundEnv.processingOver()) {

            // 프로세서 대상을 선택한다. 이 예시에서는 루트 요소만을 검사한다.
            // 애너테이션을 대상으로 하려면 getElementsAnnotatedWith() 를 사용한다.
            Set&lt;? extends Element&gt; rootElements = roundEnv.getRootElements();

            for (Element element : rootElements) {
                validator.validate(element);
            }
        }
        return false;
    }
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[javac 컴파일러]]></title>
            <link>https://velog.io/@jimmy_neutron/javac-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC</link>
            <guid>https://velog.io/@jimmy_neutron/javac-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC</guid>
            <pubDate>Tue, 03 Feb 2026 09:21:41 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/4a929260-e50c-4c3c-83d7-b8a8ddbf0727/image.png" alt="">
javac 명령어로 java 파일 -&gt; class 파일 작업하는 것을 컴파일이라 한다.
이 컴파일은 위와 같은 단계로 이뤄진다.
실제 코드레벨로는 <code>com.sun.tools.javac.main.JavaCompiler</code></p>
<h1 id="어휘-및-구문-분석">어휘 및 구문 분석</h1>
<p><code>어휘 분석</code> : <code>int a = b + 2</code> 와 같은 코드를 <code>int</code>, <code>a</code>, <code>=</code> , <code>b</code>, <code>+</code>, <code>2</code> 와 같이 토큰으로 분리한다.</p>
<p><code>구문 분석</code> : 토큰으로 추상 구문 트리 만든다. 추상 구문 트리는 코드를 기계가 더 이해하기 쉬운 데이터 구조이며, 이후 컴파일 레벨의 작업은 모두 추상 구문 트리를 대상으로 이뤄진다.</p>
<p>어휘 및 구문 분석에는 구조적으로 올바른지 체크하고 문제가 있다면 예외를 발생한다.</p>
<p>intellij 의 PsiViewer 플러그인을 사용하면 추상 구문 트리를 시각적으로 볼 수 있다.
<img src="https://velog.velcdn.com/images/jimmy_neutron/post/afea955a-d558-4499-8fd7-c2e2ffc09c85/image.png" alt=""></p>
<h1 id="심벌-테이블-채우기">심벌 테이블 채우기</h1>
<p><code>심벌 테이블</code> : 심벌 주소와 심벌 정보의 집합</p>
<ul>
<li>의미 분석 과정에서 이름을 원래 선언과 일치하게 사용하는지 확인 한다던가</li>
<li>주소 할당할 때 쓰임</li>
</ul>
<p>기본 생성자가 만들어지는 타이밍이기도 하다.</p>
<h1 id="애너테이션-처리">애너테이션 처리</h1>
<p>플러그인된 애너테이션 처리기의 처리를 진행한다.</p>
<p><code>애너테이션 처리기(processor)</code> : 컴파일 타임에 애너테이션을 보고 바이트 코드를 생성하는 기술</p>
<ul>
<li>자바 코드로 AbstractProcessor 를 구현하여 만들 수 있음</li>
<li>AbstractProcessor 는 컴파일 타임에 구현에 의해 추상 구문 트리를 수정</li>
<li>개발자가 컴파일 시점의 동작을 추가할 수 있다는 것</li>
</ul>
<blockquote>
<p>대표적으로 애너테이션은 런타임에 처리되는 Spring AOP 를 생각할 수 있다.
애너테이션 처리기는 컴파일 타임에 애너테이션을 활용하는 방법이다.</p>
</blockquote>
<p>이 단계에서 추상 구문 트리의 수정이 발생하면 구문 분석 및 심벌 테이블 채우기 단계를 다시 실행해야 한다.</p>
<h1 id="의미-분석과-바이트코드-생성">의미 분석과 바이트코드 생성</h1>
<p>이 단계의 추상 구문 트리는 구조적으로 올바른 상태이다.</p>
<p>의미 분석 단계에는 <code>맥락상 논리적으로 올바른지</code> 체크한다.
(구조적으로 이상하게 작성한게 아닌)보통의 컴파일 타임 에러는 이 단계에서 체크된다.</p>
<h2 id="의미-분석---특성-검사">의미 분석 - 특성 검사</h2>
<p>변수를 사용하기 앞서 선언이 되어 있는지
변수와 할당될 데이터의 타입이 일치하는지</p>
<p>상수 접기 최적화(1 + 2 -&gt; 3 으로 컴파일)</p>
<h2 id="의미-분석---데이터-흐름-분석과-제어-흐름-분석">의미 분석 - 데이터 흐름 분석과 제어 흐름 분석</h2>
<p>프로그램이 맥락상 논리적으로 올바른가를 체크</p>
<ul>
<li>지역변수가 사용되기 전에  값이 할당되었는지</li>
<li>메서드의 모든 실행 경로에서 값을 반환하는지</li>
<li>checked 예외가 모두 올바르게 처리되었는지</li>
<li>등등..</li>
</ul>
<blockquote>
</blockquote>
<p>클래스 로딩 시의 제어 흐름 분석 검증과 비슷하지만 범위가 다르다.</p>
<blockquote>
</blockquote>
<p>지역 변수의 final 로 예를 들어 보자.
필드의 final 은 필드 테이블에 플래그로 final 임이 저장된다. 따라서 런타임에 해당 필드가 변경되면 에러를 낼 수 있다.
반면에 지역 변수 테이블에 저장되는 지역 변수는 클래스 파일에는 단순한 심벌 참조만이 존재하므로 런타임에 지역 변수가 final 임을 알 수 없다.
즉, 지역 변수의 final 은 <code>컴파일 타임에만</code> 검사된다.</p>
<h2 id="편의-문법-제거">편의 문법 제거</h2>
<p>런타임에는 영향을 주지 않고 더 효율적인 코딩만을 위한 코드를 편의 문법이라 한다.
컴파일 타임의 이 단계에서는 해당 편의 문법을 제거한다.</p>
<p>편의 문법의 예는 다음과 같다.</p>
<ul>
<li>제네릭</li>
<li>가변 인자</li>
<li>오토박싱/언박싱</li>
</ul>
<h2 id="바이트코드-생성">바이트코드 생성</h2>
<p>지금까지의 정보로 바이트코드를 생성한다.</p>
<p>인스턴스 생성자 <code>&lt;init&gt;()</code>, <code>&lt;clinit&gt;()</code> 은 이 단계에서 추가된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[바이트 코드 실행 엔진 - 동적 타입 언어(invokedynamic)]]></title>
            <link>https://velog.io/@jimmy_neutron/%EB%B0%94%EC%9D%B4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%8B%A4%ED%96%89-%EC%97%94%EC%A7%84-%EB%8F%99%EC%A0%81-%ED%83%80%EC%9E%85-%EC%96%B8%EC%96%B4invokedynamic</link>
            <guid>https://velog.io/@jimmy_neutron/%EB%B0%94%EC%9D%B4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%8B%A4%ED%96%89-%EC%97%94%EC%A7%84-%EB%8F%99%EC%A0%81-%ED%83%80%EC%9E%85-%EC%96%B8%EC%96%B4invokedynamic</guid>
            <pubDate>Sun, 01 Feb 2026 06:14:38 GMT</pubDate>
            <description><![CDATA[<h1 id="동적-타입-언어">동적 타입 언어</h1>
<p>동적 타입 언어에는 대표적으로 자바스크립트, 루비 등이 있고 정적 타입 언어에는 대표적으로 자바, C++ 이 있다.</p>
<p>무슨 차이일까?</p>
<h3 id="정적-타입-언어">정적 타입 언어</h3>
<ul>
<li>정적 타입 언어<ul>
<li>변수와 값 모두 타입이 있다.</li>
<li>타입 검사 과정의 주요 단계들이 <code>컴파일 타임</code> 에 수행된다.</li>
</ul>
</li>
</ul>
<pre><code class="language-java">public class CustomPrintStream extends PrintStream {

    public CustomPrintStream(OutputStream out) {
        super(out);
    }

    public static void main(String[] args) {
        PrintStream obj = new CustomPrintStream(System.out);
        obj.print(&quot;Hello&quot;);
    }
}</code></pre>
<p>obj 변수의 정적 타입은 PrintStream 이다.
변수 obj 에 들어갈 값의(런타임에) 실제 타입은 PrintStream 또는 그 하위일 것이다.</p>
<p>print 메서드 입장에서 생각해보면, 수신 객체인 obj 의 정적 타입은 <code>컴파일 타임</code>에 PrintStream 으로 정해진다.
이는 컴파일의 결과인 클래스 파일에서도 볼 수 있다.</p>
<pre><code> 0: new           #7                  // class indy/CustomPrintStream
 3: dup
 4: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
 7: invokespecial #15                 // Method &quot;&lt;init&gt;&quot;:(Ljava/io/OutputStream;)V
10: astore_1
11: aload_1
12: ldc           #16                 // String Hello
14: invokevirtual #18                 // Method java/io/PrintStream.print:(Ljava/lang/String;)V</code></pre><p>14 인덱스 invokevirtual 을 보면 메서드의 타입이 <code>java/io/PrintStream.print:(Ljava/lang/String;)V</code> 로 정해진다.
즉, 수신 객체 변수의 정적 타입이 정확하게 정해진 것이다.</p>
<p>따라서, <strong>만약 수신 객체에 없는 메서드를 호출한다면 컴파일 타임에 에러가 발생한다.</strong></p>
<h3 id="동적-타입-언어-1">동적 타입 언어</h3>
<ul>
<li>동적 타입 언어<ul>
<li>값에만 타입이 있고 변수에는 타입이 없다.</li>
<li>타입 검사 과정의 주요 단계들이 <code>런타임</code> 에 수행된다.  </li>
</ul>
</li>
</ul>
<pre><code class="language-javascript">obj.print(&quot;Hello&quot;)</code></pre>
<p>위 코드에서 obj 변수에는 타입이 없다. 런타임에 실제 객체가 정해질 때 까지는 아무것도 알 수 없다.
따라서 컴파일 타임에 알 수 있는 메서드에 대한 정보는 메서드 이름, 매개변수의 이름, 리턴 표현식 정도 밖에 없다.
즉, 수신 객체 (this) 를 정할 수 없다.</p>
<p>그렇기에 obj 변수로 어떤 메서드를 호출하더라도 컴파일 타임에는 문제가 되지 않는다.(문제를 잡을 수 없다)
코드 흐름상 런타임에 obj 값으로 print 메서드가 없는 객체만 할당됨이 분명하더라도, 컴파일 타임에는 에러가 발생하지 않는다.</p>
<h1 id="jvm의-동적-타입-언어-지원">JVM의 동적 타입 언어 지원</h1>
<p>JVM 은 바이트코드만 만들 수 있다면 작성되는 언어가 어떤 것이든 상관 없다.
애초에 JVM 은 여러 언어를 지원하려는 계획이 있었다.</p>
<p>JDK 7 이전의 메서드 호출 명령어(invokestatic, invokevirtual, invokespecial, invokeinterface) 들은 위처럼 <strong>수신 객체의 완전한 정적 타입을 인자</strong>로 받았다.</p>
<p>하지만 동적 언어는 수신 객체의 타입을 <code>런타임</code> 에 정할 수 있어야 한다.
따라서 JDK 7 이전에는 동적 언어에 대한 지원이 시원찮았다.
억지로 동적 언어의 흉내는 낼 수 있었는데, 예를 들면 런타임에 껍데기 객체를 넣어두고 런타임에 필요한 객체 바이트 코드를 동적으로 생성해서 집어넣으면 된다. 다만 바이트 코드 생성을 런타임에 하게 되므로 성능적 부하가 있었다.</p>
<p>또한 런타임에 정적 타입이 확정되지 않으므로 JIT 컴파일러가 인라인 최적화를 하지 못했다. 수신 객체의 결정(동적 바이트코드 생성)은 당연하게도 JIT 컴파일러 인라인 최적화 이후에 이뤄지기 때문이다.
한번 실행시켰을때 타입이 나오니 그 때 타입을 캐싱해두면 또 가능하긴 하다면 캐시는 무한정이 아니다. 어찌되었든 오버헤드다.</p>
<h2 id="jsr-292">JSR 292</h2>
<p>자바는 동적 언어를 지원하기 위해 JSR 292 를 내놓았다.</p>
<p>컴파일 타임에는 기존 invoke~ 명령어들 처럼 매서드 수신 객체의 타입이 아닌 <code>메서드 이름, 매개 변수 타입, 리턴 타입</code>에 대한 <code>심벌 참조만</code> 남겨 놓고 (상수풀의 CONSTANT_invokedynamic_info) <code>런타임에 실제 실행할 메서드를 결정</code>하는 것이다.</p>
<blockquote>
<p>메서드 해석 책임을 가상 머신으로부터 특정한 사용자 코드로 옮기는 역할</p>
</blockquote>
<p>또한 이 때 JIT 컴파일러의 인라인을 포함한 최적화가 가능한 구조로 만들어졌다.</p>
<p>이 제안이 JSR 292 이며 invokedynamic 이다.
구체적으로는 두가지가 제안되었다.</p>
<ol>
<li>invokedynamic 바이트코드 명령어</li>
<li>java.lang.invoke 패키지</li>
</ol>
<h2 id="invokedynamic-구현">invokedynamic 구현</h2>
<p>invokedynamic 을 이용한 메서드의 동적 선택은 다음 네가지 요소로 이뤄진다.</p>
<ol>
<li>CONSTANT_invokedynamic_info</li>
<li>BootStrap</li>
<li>Callsite</li>
<li>MethodHandle</li>
</ol>
<h3 id="methodhandle">MethodHandle</h3>
<p>java.lang.invoke 에 포함된 클래스.
만들어진 MethodHandle 객체는 하나의 메서드를 가리키게 된다.
메서드를 특정하기 위해 <code>클래스, 메서드 이름, 리턴 타입, 매개변수 타입</code> 를 사용한다.
Method 타입의 리플렉션과 비슷하다.</p>
<pre><code class="language-java">public class MethodHandleTest {

    public String test(int a, int b) {
        return  String.valueOf(a) + b;
    }

    public static void main(String[] args) throws Throwable {
        MethodType mt = MethodType.methodType(String.class, int.class, int.class);
        MethodHandle mh = MethodHandles.lookup()
                .findVirtual(MethodHandleTest.class, &quot;test&quot;, mt) // (1) invokevirtual 메서드 찾기
                .bindTo(new MethodHandleTest()); // (2) 리시버(수신 객체) 지정

        Object res = mh.invoke(33, 2);
        System.out.println(res);
    }
}</code></pre>
<p>다만 메서드 리플랙션은 메서드에 대한 포괄적인 정보를 가지고 있고 무겁다.
MethodHandle 은 메서드를 실행하는데 필요한 정보만 가지고 있어 가볍기 때문에, 리플렉션의 일부를 대체할 수 있다.</p>
<p>중요한건 (2) 주석 부분이다.
테스트 코드에서는 <code>test</code> 라는 이름을 가진 메서드를 찾고 있고 해당 메서드를 가진 객체를 수신 객체로 지정했다.
수신 객체를 다음과 같이 아무 메서드가 없는 클래스의 객체로 바꿔보자.</p>
<pre><code class="language-java">// public class Test {}
.bindTo(new Test());</code></pre>
<p><strong>컴파일 타임에는 아무 에러도 나지 않는다.</strong>(런타임에 에러가 난다)
동적 타입 언어와 같이 컴파일 타임에는 수신 객체의 타입을 검사하지 않는다.</p>
<p>자바의 해결책은 클래스 파일의 심벌 참조만으로 런타임에 동적 타입 언어의 기능을 지원하는것이라 했다.
위 코드에서 MethodHandle, MethodType 에 들어가는 매개변수들을 보면 심벌 참조(CONST_invokedynamic_info)에 있는 정보임을 알 수 있다. 심벌 참조만 있으면 MethodHandle 을 이용하여 동적으로 메서드를 찾을 수 있다는 말이다.</p>
<h3 id="callsite">CallSite</h3>
<p>마찬가지로 java.lang.invoke 에 포함된 클래스.
MethodHandle 하나를 필드로 가지는 래퍼다.</p>
<p>MethodHandle 은 수신 객체까지 구체적으로 지정된 메서드 표현이다.
딱 한 객체의 한 메서드를 가리킨다.</p>
<p>동적 타입 언어라면 컴파일 타임의 심벌 참조가 런타임에 여러 메서드(MethodHandle)로 동적 <code>변경</code> 이 가능해야 한다.
이 역할을 해주는 래퍼가 CallSite 이다.</p>
<p>CallSite 에 대해 정리하자면 다음과 같다.</p>
<ol>
<li>CallSite 는 MethodHandle 을 포워딩해주는 객체다. invokedynamic 은 CallSite 객체를 본다.</li>
<li>CallSite 는 setType() 메서드를 통해 자신이 가진 MethodHandle 을 변경할 수 있다.</li>
<li>invokedynamic 은 첫 호출시 이후 설명할 BootStrapMethod 를 실행하여 CallSite 를 만든다. 두번째 호출부터는 CallSite 를 통해 MethodHandle 로 빠르게 바로 접근할 수 있다.</li>
<li>MethodHandle 이 변경될 수 있다면 Jit 컴파일러가 인라인 최적화를 무조건적으로 할 수 없다. CallSite 는 이를 대응하는 구조를 가진다.<ul>
<li>Jit Compiler 는 일단 MethodHandle 이 바뀌지 않을 것이라 가정하고 낙관적으로 인라인 최적화를 진행한다. 한번 정해진 MethodHandle 은 한동안 바뀌지 않을 것이라는 가정이다. 내부적으로 <a href="https://github.com/openjdk/jdk/blob/ca95e5f3ddd5961dd43f825ed6c47054284c6798/src/hotspot/share/code/dependencies.hpp#L156">바뀜을 감지하는 필드</a>를 가지고 있다가 MethodHandle 이 바뀌면 그 때 인라인을 다시 한다.</li>
<li>CallSite 는 abstract 클래스다. setType() 이 가능한 MutableCallSite 과 setType() 호출시 예외로 막혀있는 ConstantCallSite 하위 클래스가 있다. ConstantCallSite 는 MethodHandle 이 바뀌지 않을 때 사용한다. 이 때에는 위의 MethodHandle 변경 감지를 스킵할 수 있다.</li>
</ul>
</li>
</ol>
<h3 id="bootstrapmethod">BootStrapMethod</h3>
<p>BootStrapMethod 는 클래스 파일의 심볼 참조를 받아서 CallSite 를 반환하는 메서드다.
invokedynamic 은 이 메서드를 호출하여 CallSite 를 받은 뒤 이후 호출시 CallSite 를 통해 메서드를 실행한다.</p>
<p>java 언어 중 invokedynamic 을 사용하는 string concat 구문을 예시로 보자.</p>
<pre><code class="language-java">public class StringConcator {

    public String concat(String a, String b) {
        return a + b;
    }
}</code></pre>
<p>아래는 위 코드 클래스 파일의 일부이다.</p>
<pre><code>    Code:
        ...
         2: invokedynamic #7,  0     // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;


BootstrapMethods:
  0: #19 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #25 \u0001\u0001
InnerClasses:
  public static final #32= #28 of #30;    // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles</code></pre><p>클래스 파일에는 invokedynamic 이 실행되었을때 어떤 BootStrapMethod 가 실행되어야하는지 명시된다.
<code>a + b</code> 처럼 StringBuilder 구문을 사용하면 <code>StringConcatFactory.makeConcatWithConstants()</code> 메서드가 BootStrapMethod 로 쓰인다고 명시되어 있다. 이는 Javac 컴파일러가 만들어낸 것이다.</p>
<pre><code class="language-java">    public static CallSite makeConcatWithConstants(MethodHandles.Lookup lookup,
                                                   String name,
                                                   MethodType concatType,
                                                   String recipe,
                                                   Object... constants)
        throws StringConcatException</code></pre>
<p>위는 makeConcatWithConstants 의 시그니처다. 메서드 이름과 매개 변수 타입, 리턴 타입 등을 받아서 CallSite 를 반환한다.</p>
<p>이제 동작이 어떻게 이뤄지는지 알 수 있다.</p>
<ol>
<li>invokedynamic 이 실행되면 호출할 BootStrapMethod(위 예시에서 StringConcatFactory.makeConcatWithConstants(). CallSite 를 반환하는 함수) 를 원하는 동적 메서드 동작에 맞게 자바로 구현한다. 동적 언어 구현인 CallSite, MethodHandle 도 구현되어야 할 것이다.</li>
<li>JRuby 같은 동적 타입 언어의 컴파일러 구현을 한다. 원하는 언어 구문을 컴파일하면 클래스 파일 BootStrapMethods 속성에 1번에서 구현한 BootStrapMethod 를 사용하도록 하면 된다.</li>
</ol>
<blockquote>
<p>이와 같이 언어 컴파일러와 자바 코드를 작성 함으로서 개발자가 디스패치 로직을 결정할 수 있다.</p>
</blockquote>
<h2 id="자바-언어에서의-이득">자바 언어에서의 이득</h2>
<p>원래 invokedynamic 은 동적 타입 언어를 위해 만들어졌으나, 자바 언어에서도 유용하게 써먹고 있다.</p>
<ol>
<li>람다</li>
<li>StringBuilder 의 대체. StringConcat</li>
<li>Record</li>
<li>리플렉션의 대체</li>
</ol>
<h2 id="참고">참고</h2>
<p>리플랙션 대안으로서의 MethodHandle
<a href="https://www.itworld.co.kr/article/3568004/%EB%A6%AC%ED%94%8C%EB%A0%89%EC%85%98%EB%B3%B4%EB%8B%A4-%EB%82%AB%EB%8B%A4-%EC%9E%90%EB%B0%94%EC%9D%98-%EB%A9%94%EC%86%8C%EB%93%9C-%ED%95%B8%EB%93%A4%EA%B3%BC-%EB%B3%80%EC%88%98-%ED%95%B8%EB%93%A4.html">https://www.itworld.co.kr/article/3568004/%EB%A6%AC%ED%94%8C%EB%A0%89%EC%85%98%EB%B3%B4%EB%8B%A4-%EB%82%AB%EB%8B%A4-%EC%9E%90%EB%B0%94%EC%9D%98-%EB%A9%94%EC%86%8C%EB%93%9C-%ED%95%B8%EB%93%A4%EA%B3%BC-%EB%B3%80%EC%88%98-%ED%95%B8%EB%93%A4.html</a></p>
<p>invokedynamic jdk 발표 ppt
<a href="https://cr.openjdk.org/~vlivanov/talks/2015-Indy_Deep_Dive.pdf">https://cr.openjdk.org/~vlivanov/talks/2015-Indy_Deep_Dive.pdf</a></p>
<p>Deep Static Modeling of invokedynamic
<a href="https://yanniss.github.io/invokedynamic-ecoop19.pdf">https://yanniss.github.io/invokedynamic-ecoop19.pdf</a></p>
<p>baeldung
<a href="https://www.baeldung.com/java-invoke-dynamic">https://www.baeldung.com/java-invoke-dynamic</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[바이트 코드 실행 엔진 - 메서드 호출]]></title>
            <link>https://velog.io/@jimmy_neutron/%EB%B0%94%EC%9D%B4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%8B%A4%ED%96%89-%EC%97%94%EC%A7%84-%EB%A9%94%EC%84%9C%EB%93%9C-%ED%98%B8%EC%B6%9C</link>
            <guid>https://velog.io/@jimmy_neutron/%EB%B0%94%EC%9D%B4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%8B%A4%ED%96%89-%EC%97%94%EC%A7%84-%EB%A9%94%EC%84%9C%EB%93%9C-%ED%98%B8%EC%B6%9C</guid>
            <pubDate>Fri, 30 Jan 2026 14:26:40 GMT</pubDate>
            <description><![CDATA[<p>이번 글의 내용은 메서드 내용의 실행이 아닌 메서드의 호출이다.</p>
<p>컴파일된 클래스 파일에는 호출할 메서드의 이름(FQCN)이 적혀있다.</p>
<p>JVM 런타임에는 해당 이름의 메서드는 여러개가 존재할 수 있다. (오버라이딩, 오버로딩)
클래스 파일의 단순 이름인 심벌 참조를 런타임에 존재하는 같은 이름을 가진 1개 이상의 <code>버전</code>의 메서드 중 어떤 것을 실행할지 고르는 작업은 해석과 디스패치가 있다.</p>
<h1 id="호출할-메서드-선택---정적-해석">호출할 메서드 선택 - 정적 해석</h1>
<p>클래스 파일을 만드는 컴파일 타임에 어떤 버전의 메서드를 선택할지 정해지는 경우가 있다.
이를 <code>정적 해석</code>이라 한다.</p>
<ul>
<li>invokestatic : static 메서드</li>
<li>invokespecial : private 메서드, 인스턴스 생성자, 부모 클래스의 메서드</li>
<li>invokevirtual : final 한정자가 붙은 메서드 only</li>
</ul>
<p>위 세가지 메서드 실행 바이트코드 명령어의 다섯가지 케이스는 컴파일 타임에 메서드의 버전이 정해지는 <code>정적 해석</code>이다.</p>
<p>다르게 말하면, 이 메서드들은 오버라이딩이 불가하므로 컴파일 타임에 어떤 메서드를 사용할지 정해진다.</p>
<p>이러한 메서드들을 비가상 메서드라 한다.
이후 나올 나머지 메서드들은 모두 가상 메서드이다.</p>
<blockquote>
<p>static 메서드 같은 경우 오버로딩이 가능하긴 하나 정적 디스패치인 오버로딩 또한 컴파일 타임에 메서드가 정해지므로 정적 해석이라 봐도 될 것이다.</p>
</blockquote>
<h1 id="호출할-메서드-선택---디스패치">호출할 메서드 선택 - 디스패치</h1>
<p>자바는 오버로딩과 오버라이딩을 지원한다.
클래스 파일에 명시된 메서드 이름을 가진 런타임의 여러 메서드 버전 중 하나를 골라 호출하는 것을 <code>디스패치</code> 라 한다.</p>
<p>위의 정적 해석과는 다른 수준의 메서드 선택이다.</p>
<p>디스패치는 정적/동적, 단일/더블로 나뉜다.</p>
<ul>
<li>정적 단일 디스패치</li>
<li>정적 더블 디스패치</li>
<li>동적 단일 디스패치</li>
<li>동적 더블 디스패치</li>
</ul>
<p>자바에서 위 네개를 다 지원하는게 아니라 언어 무관하게 개념적으로 그렇다는 것이다.</p>
<blockquote>
<p>정적 디스패치는 컴파일 타임에 이뤄진다. 엄연하게는 정적 해석이다. 그러나 많은 곳에서 오버라이딩, 오버로딩과 연관지어 설명하기 위해 동적이라는 의미를 가진 디스패치와 묶어서 설명된다.</p>
</blockquote>
<h2 id="정적-타입과-실제-타입">정적 타입과 실제 타입</h2>
<p>디스패치를 이해하기 위해선 정적 타입과 실제 타입을 다시 이해하고 가면 좋다</p>
<pre><code class="language-java">abstract class Human {}
class Man extends Human {}
class Woman extends Human {}</code></pre>
<pre><code class="language-java">Human h = new Man();</code></pre>
<p>위 h 객체의 <code>정적 타입은 Human</code>, <code>실제 타입은 Man</code> 이다.</p>
<p><strong>실제 타입은 런타임에 변할 수 있다.</strong>
<strong>정적 타입은 컴파일 타임부터 정해지는 값이다.</strong>
디스패치를 이해하기 위해선 이 개념이 중요하다.
아래 코드를 보자.</p>
<pre><code class="language-java">Human h = (new Random()).nextBoolean() ? new Man() : new Woman();</code></pre>
<p>런타임의 random 실행값에 따라 실제 타입은 변할 수 있지만, 정적 타입은 Human 으로 고정된다.</p>
<h2 id="정적-디스패치">정적 디스패치</h2>
<p><code>컴파일 타임</code>에 <code>정적 타입</code> 을 보고 메서드 버전을 결정하는 것을 <code>정적 디스패치</code>라 한다.
자바에서 정적 디스패치의 가장 일반적인 사용처는 <code>오버로딩</code>이다.</p>
<pre><code class="language-java">class StaticDispatch {
    // 오버로딩 1
    public void hello(Human human) {
        System.out.println(&quot;Hello, human!&quot;);
    }

    // 오버로딩 2
    public void hello(Man man) {
        System.out.println(&quot;Hello, man!&quot;);
    }

    // 오버로딩 3
    public void hello(Woman woman) {
        System.out.println(&quot;Hello, woman!&quot;);
    }
}</code></pre>
<pre><code class="language-java">Human m = new Man();
Human w = new Woman();
StaticDispatch sd = new StaticDispatch();
sd.hello(m);
sd.hello(w);

// 결과 : 둘 다 오버로딩 1 실행
// Hello, human!
// Hello, human!</code></pre>
<p>정적 타입은 Human 이지만 실제 타입이 Man, Woman 인 두 객체를 생성하였다.
오버로딩 된 메서드를 실행한 결과를 보면 둘 다 인자를 Human(정적 타입) 인 메서드가 실행되었다.</p>
<p>이유는,
컴파일러는 <strong>오버로딩된 메서드 선택시 인자로 전달된 객체의 정적 타입을 보고 판단</strong>하기 때문이다.</p>
<h2 id="동적-디스패치">동적 디스패치</h2>
<p><code>런타임</code>에 수신 객체의 <code>실제 타입</code>을 알아내어 메서드 버전을 결정하는 것을 <code>동적 디스패치</code>라 한다.
자바에서 동적 디스패치의 가장 일반적인 사용처는 <code>오버라이딩</code>이다.</p>
<pre><code class="language-java">abstract class Human {
    abstract void hello();
}

class Man extends Human {
    @Override
    void hello() {
        System.out.println(&quot;hello, man!&quot;);
    }
}

class Woman extends Human {
    @Override
    void hello() {
        System.out.println(&quot;hello, woman!&quot;);
    }
}</code></pre>
<pre><code class="language-java">Human m = new Man();
Human w = new Woman();

m.hello();
w.hello();

m = new Woman();
m.hello();

// 결과
// hello, man!
// hello, woman!
// hello, woman!</code></pre>
<p>여러 하위 타입의 오버라이딩 된 메서드 중 하나를 선택하기 위해서는 정적 타입만으로는 불가능하다.
수신 객체의 실제 타입을 먼저 판단해야 한다.
수신 객체의 실제 타입을 판단함으로서 오버라이딩 된 메서드 중 하나를 고르는 과정에 동적 디스패치가 동작한다.</p>
<pre><code>// main 클래스 javac, javap -verbose 중 일부

상수풀
#13 = Methodref          #14.#15        // dispatch/Human.hello:()V

Code Attribute
 17: invokevirtual #13                 // Method dispatch/Human.hello:()V
 21: invokevirtual #13                 // Method dispatch/Human.hello:()</code></pre><p>위는 main 클래스 바이트 코드 일부이다.
보다시피 <code>invokevirtual</code> 명령어는 인자로 메서드의 심볼 참조를 갖는다.
Code Attribute 의 17 인덱스는 <code>m.hello()</code> 코드와 대응되고, 21 인덱스는 <code>w.hello()</code> 코드와 대응되는데, 컴파일 타임에는 이 둘의 차이가 없다. (상수풀의 같은 메서드 심볼 참조를 인자로 갖는다)</p>
<p>컴파일 타임에는 아무런 메서드 선택이 일어나지 않는다는 이야기다.</p>
<p>반면 런타임에는 invokevirtual 바이트코드 명령어가 실행되며 실제 메서드 선택이 일어난다.
이는 동적 디스패치는 invokevirtual 의 동작에서 기인한다.</p>
<p><strong>invokevirtual 동작</strong></p>
<ol>
<li>피연산자 스택 상단 첫번째 요소가 가리키는 객체의 <strong>실제 타입</strong> 을 찾는다.</li>
<li>실제 타입의 클래스 파일에서 상수(#13) 의 서술자와 이름이 일치하는 메서드를 찾는다.</li>
<li>메서드가 존재하고, 접근 권한이 있다면 해당 메서드의 직접 참조를 반환한다.</li>
<li>메서드가 존재하지 않는다면 상속 계층을 따라 상위 클래스로 가서 찾는다.</li>
<li>최상위 클래스까지도 서술자와 이름이 일치하는 메서드를 찾지 못하면 AbstractMethodError 를 던진다.</li>
</ol>
<blockquote>
<p>invokevirtual 은 메서드 호출 명령어다. 필드는 아니다.
동적 디스패치의 근본은 Invokevirtual 동작이기 때문에, 필드에 대해선 오버라이딩이 없는 것.</p>
</blockquote>
<h2 id="단일-디스패치와-더블-디스패치">단일 디스패치와 더블 디스패치</h2>
<p>단일/더블 디스패치를 나누는 기준은 <code>수신 객체</code> 와 <code>매개 변수</code> 중 몇가지를 디스패치의 메서드 결정에 사용하느냐다.</p>
<pre><code class="language-java">public class Dispatch {
    static class QQ {}
    static class _360 {}

    public static class Father {
        public void hardChoice(QQ arg) {
            System.out.println(&quot;Father chose a qq&quot;);
        }

        public void hardChoice(_360 arg) {
            System.out.println(&quot;Father chose a 360&quot;);
        }
    }

    public static class Son extends Father {
        public void hardChoice(QQ arg) {
            System.out.println(&quot;Son chose a qq&quot;);
        }

        public void hardChoice(_360 arg) {
            System.out.println(&quot;Son chose a 360&quot;);
        }
    }
}</code></pre>
<pre><code class="language-java">Father father = new Father();
Father son = new Son();
Son son2 = new Son();

father.hardChoice(new _360());
son.hardChoice(new QQ());
son2.hardChoice(new QQ());

// 결과
// Father chose a 360
// Son chose a qq
// Son chose a qq</code></pre>
<h3 id="자바의-정적-디스패치는-더블-디스패치다">자바의 정적 디스패치는 더블 디스패치다</h3>
<p>위 코드의 클래스 파일의 일부를 보자.</p>
<pre><code>32: invokevirtual #16    // Method Dispatch$Father.hardChoice:(LDispatch$_360;)V
43: invokevirtual #23    // Method Dispatch$Father.hardChoice:(LDispatch$QQ;)V
54: invokevirtual #26    // Method Dispatch$Son.hardChoice:(LDispatch$QQ;)V</code></pre><p>위 세줄은 정적 디스패치에 의해 정해진 메서드 버전을 보여준다.
변수의 정적 타입에 따라 <code>Father.</code> 또는 <code>Son.</code> 이 정해진다.
또한 매개 변수의 타입에 따라 인지의 <code>QQ</code> 또는 <code>360</code> 이 정해진다.</p>
<p>즉 수신 객체와 매개 변수 둘 다 보고 메서드를 결정하므로 더블 디스패치이다.</p>
<h3 id="자바의-동적-디스패치는-단일-디스패치다">자바의 동적 디스패치는 단일 디스패치다</h3>
<p>컴파일 타임에 정적 디스패치에 의해 매개 변수의 타입은 정해졌다.
런타임에는 오버라이딩 된 메서드 중 어떤 클래스의 메서드를 사용하면 되느냐만 결정하면 된다.</p>
<p>이는 동적 디스패치에서 설명했듯, <code>수신 객체의 실제 타입</code> 을 보고 결정한다.</p>
<p>따라서 자바의 동적 디스패치는 수신 객체 하나만 보면 되므로 단일 디스패치이다.</p>
<h2 id="자바-동적-디스패치-최적화---vtable">자바 동적 디스패치 최적화 - vtable</h2>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/5f9fc372-64e9-4c70-aa9c-d1db64ebd895/image.png" alt=""></p>
<blockquote>
<p>(Child Class vtable 의 offset 0 의 Address 가 0x1000 이 되어야 하지 않나 싶다.) 
<a href="https://medium.com/@devjohnpark/java-method-call-process-and-dispatch-cded3f2c3103">https://medium.com/@devjohnpark/java-method-call-process-and-dispatch-cded3f2c3103</a></p>
</blockquote>
<p>vtable 은 C++, 자바등 동적 디스패치를 최적화하는 기법 중 하나다.</p>
<p>동적 디스패치는 런타임에 실제 타입이 어떤것일지 모르므로 컴파일 타임에는 심벌 참조만 남기고 런타임에 직접 참조로 바꾼다고 했다.</p>
<p>더욱이 객체의 실제 타입은 런타임에 언제든 바뀔 수 있으므로, 메서드 호출마다 항상 invokevirtual 을 실행해야 한다.
Method Area 의 클래스 메타 데이터에서 호출하고자 하는 메서드의 시그니처에 적합한 메서드를 찾는 동작을 invokevirtual 마다 해야한다는 이야기다.</p>
<p>클래스 안에 메서드가 n 개 있다면 한번 찾는 동작은 O(n) 의 시간 복잡도를 가진다. invokevirtual 이 실행될 때 마다 O(n) 의 탐색이 이뤄질 것이다.
이는 메서드 수가 많아질수록 느린 동작이 된다.</p>
<h3 id="vtable">Vtable</h3>
<p>이 문제를 해결하기 위해 각 <a href="https://github.com/openjdk/jdk/blob/0a3809d380bcae8cb24d50886057d8586fa77f7c/src/hotspot/share/oops/klass.hpp#L558C15-L558C21">클래스 메타데이터는 vtable 이라는 테이블 구조체</a>를 가진다.</p>
<p>vtable <a href="https://github.com/openjdk/jdk/blob/0a3809d380bcae8cb24d50886057d8586fa77f7c/src/hotspot/share/oops/klassVtable.hpp#L250">메서드의 address</a> 를 가지고 있는 캐시다.</p>
<p>vtable 은 [index : method_address] 데이터를 담고 있으며 메서드 호출시 또는 클래스 로딩시 (jvm 구현에 따라 다름) 채우게 된다.</p>
<p>invokevirtual 이 호출되어 심벌 참조를 보고 Method Area 에서 직접 참조를 읽어오면, 그 안에는 vtable 의 index 가 있다. (<a href="https://github.com/openjdk/jdk/blob/0a3809d380bcae8cb24d50886057d8586fa77f7c/src/hotspot/share/oops/method.hpp#L79">Method</a>)
vtable[인덱스] 를 하면 O(1) 의 속도로 빠르게 메서드의 실제 주소(직접 참조)를 얻을 수 있다.</p>
<h3 id="vtable-과-상속">Vtable 과 상속</h3>
<p>클래스를 상속한 뒤, 오버라이딩 하지 않은 메서드를 호출하면 부모 클래스의 메서드가 호출된다.</p>
<p>Vtable 을 만들때는 부모 클래스의 Vtable 을 복사하여 만든다.
오버라이딩하지 않는다면 하위 클래스 vtable 주소 항목은 부모 클래스 vtable 주소 항목과 같다.
하위 클래스에서 오버라이딩을 했을 때 하위 클래스의 vtable 의 주소값이 오버라이딩 메서드 시작점으로 바뀐다.</p>
<p>반대로 말하면 vtable 에 부모 메서드의 주소가 있다면 오버라이딩되지 않았다고 볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/c9648d0e-d10c-49b5-8db2-66a7b4a0b295/image.png" alt="">
<a href="https://blog.csdn.net/weixin_36274355/article/details/114353090">https://blog.csdn.net/weixin_36274355/article/details/114353090</a></p>
<p>Object 는 모든 클래스의 부모다.
Father 과 Son 은 Object 의 메서드를 오버라이딩 하지 않았기 때문에 Object 의 메서드를 가리키게 되고, <code>hardChoice</code> 는 Father 과 Son 둘 다 오버라이딩 했기 때문에 각자의 메서드 주소를 가리키는 그림이다.</p>
<h1 id="출처">출처</h1>
<p>예제 코드 출처 <a href="https://github.com/WegraLee/JVM">https://github.com/WegraLee/JVM</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[바이트 코드 실행 엔진 - 런타임 스택 프레임]]></title>
            <link>https://velog.io/@jimmy_neutron/%EB%B0%94%EC%9D%B4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%8B%A4%ED%96%89-%EC%97%94%EC%A7%84-%EB%9F%B0%ED%83%80%EC%9E%84-%EC%8A%A4%ED%83%9D-%ED%94%84%EB%A0%88%EC%9E%84</link>
            <guid>https://velog.io/@jimmy_neutron/%EB%B0%94%EC%9D%B4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%8B%A4%ED%96%89-%EC%97%94%EC%A7%84-%EB%9F%B0%ED%83%80%EC%9E%84-%EC%8A%A4%ED%83%9D-%ED%94%84%EB%A0%88%EC%9E%84</guid>
            <pubDate>Fri, 30 Jan 2026 14:26:35 GMT</pubDate>
            <description><![CDATA[<h1 id="스택-프레임">스택 프레임</h1>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/92e449a0-378a-45c0-a46b-7e5cf0f6feba/image.png" alt=""></p>
<p>메서드는 JVM 의 기본 실행 단위이다.
하나의 메서드가 실행될 때 JVM 은 내부적으로 <code>스택 프레임</code> 이라는 데이터 구조를 사용한다.
하나의 <code>스택 프레임</code> 내부에는 하나의 메서드를 실행시키기 위한 데이터들이 저장된다.</p>
<p>컴파일 시점에 지역 변수 테이블과 피연산자 스택이 필요로하는 <code>슬롯 개수</code> 가 정해지고,
JVM 스택 메모리 레이아웃에 따라 <code>슬롯 하나의 비트수</code> 가 정해진다.
런타임에는 메서드가 필요로 하는 메모리 크기가 변하지 않는다는 말이다.</p>
<h1 id="지역-변수-테이블">지역 변수 테이블</h1>
<p>메서드 실행시 메서드 매개변수와 지역변수를 저장한다.
메서드의 행동을 정의하는 바이트코드 명령어는 이 곳의 값을 load 하여 피연산자 스택으로 옮겨가 사용한다.</p>
<p>크기는 클래스 파일 메서드 테이블에 Code 속성의 max_locals 에 슬롯 수가 적혀있다.
슬롯의 크기는 32bit 시스템에서는 32bit, 64bit 시스템에서는 64bit 이다.</p>
<p>32bit 데이터 boolean, byte, char, short, int, float, 참조 타입(compressed oops) 의 경우 슬롯 하나에 저장된다.
long, double 의 경우 32bit 시스템에서는 두개의 슬롯에 나눠 저장한다. 이 두 슬롯은 무조건 같이 읽히며, 하나의 슬롯만 접근하는 명령어는 클래스 로딩 검증 시점에 차단된다.</p>
<p>this 는 암묵적인 매개변수이다. 따라서 지역 변수 테이블에 저장되는 데이터는 다음과 같다.</p>
<ol>
<li>this</li>
<li>매개 변수들</li>
<li>지역 변수들</li>
</ol>
<h2 id="지역-변수-슬롯-재사용-및-코딩-팁">지역 변수 슬롯 재사용 및 코딩 팁</h2>
<p>지역 변수는 메서드 내에서도 유효한 범위가 정해져 있다.
JVM 은 공간을 절약하기 위해 지역변수 슬롯을 재사용한다. 유효범위가 끝난 지역변수 자리에 새로운 지역 변수를 저장한다.</p>
<p>하지만 이런 지역 변수 슬롯 재사용은 잘 알고 사용해야 한다.</p>
<pre><code class="language-java">//-verbose:gc
public class SlotReuse {
    public static void main(String[] args) {
        {
            byte[] placeHolder = new byte[64 * 1024 * 1024];
        }
        // int a = 0;
        System.gc();
    }
}</code></pre>
<p>위 코드에서 placeHolder 지역 변수의 유효 범위는 {} 블록이 끝나면 끝난다. 만약 JVM 이 유효 범위가 끝난 지역 변수의 슬롯을 재사용하기 위해 비운다면, gc() 를 호출했을때 가비지 컬렉팅이 되어야 할 것 같다. 하지만 로그를 보면 여전히 메모리를 차지한다.</p>
<p>이는 변수 슬롯에 여전히 바이트 배열의 참조가 있고, 유효한 GC Root 라는 뜻이다.</p>
<p>placeHolder 가 gc 되기 위해서는 주석친 코드를 해제하면 된다.
이로 재사용의 메커니즘을 할 수 있다. 유효 범위가 끝난 placeHolder 가 있던 슬롯에 a 변수값이 들어가게 되고 더이상 스택 프레임에는 placeHolder 가 없으므로 GC Root 가 아니게 되어 GC 되는 것이다.</p>
<p>과거에는 이 슬롯 재사용을 활용하기 위해 위 <code>int a = 0</code> 같은 의미없는 코드를 권유하기도 했지만, </p>
<p><strong>그 이전에 지역 변수 범위에 맞게 메서드 자체를 나누는게 더 우아한 해결책이다.</strong></p>
<h1 id="피연산자-스택">피연산자 스택</h1>
<p>피연산자 스택(operand stack) 은 바이트 코드 명령어가 연산을 실행할 때 사용하는 공간이다.</p>
<p>예를 들어 iadd 는 피연산자 스택의 가장 위 두 슬롯의 숫자를 pop 하여 더한 후 다시 피연산자 스택에 push 한다.</p>
<p>피연산자 스택의 크기 역시 지역 변수 테이블 처럼 컴파일 시점에 계산되어 정해진다. 클래스 파일에 Code 속성의 <code>max_stacks</code> 값으로 명시되어 있다.</p>
<h2 id="hotspot-에서-피연산자-스택-최적화">HotSpot 에서 피연산자 스택 최적화</h2>
<pre><code class="language-java">class A {
    public int sum(int a, int b){
        return a+b;
    }
}

class B {
    public int calculate(){
        A a = new A();

        int sum = a.sum(10, 20); // (1)
        return sum * 3;
    }
}

public static void main(String[] args) {
    B b = new B();
    int result = b.calculate();
    System.out.println(result);
}</code></pre>
<p>B 메서드에서 매개 변수가 있는 A 의 메서드를 호출하는 순간(1)에는 피연산자 스택 최상위에는 다음과 같은 데이터가 들어있다.</p>
<ol>
<li>Callee(B) 의 객체<ul>
<li>매서드 인자에 암묵적으로 들어가는 메서드의 객체다. 메서드 내에서 this 로 쓰인다.</li>
<li>위 예시 코드에서는 A 객체</li>
<li>바이트 코드상 aload_n 으로 표시</li>
</ul>
</li>
<li>Callee 인자 1<ul>
<li>위 예시 코드에서는 10</li>
</ul>
</li>
<li>Callee 인자 2<ul>
<li>위 예시 코드에서는 20</li>
</ul>
</li>
</ol>
<p>이 피연산자 스택 최상위 데이터는 B 메서드로 호출이 넘어갔을때에는 매개 변수로서 지역 변수 테이블에 저장된다.</p>
<p>피연산자 스택이든 지역 변수 테이블이든 메모리상의 데이터이므로 이 두 구조를 복사해서 만들기보단 그대로 재사용한다면 좋을 것이다.</p>
<p>실제로 HotSpot 을 포함한 여러 JVM 에서는 이를 재사용하는 최적화가 존재한다.</p>
<h1 id="동적-링크">동적 링크</h1>
<p>이는 상수 풀에 대한 참조이다.
다른 글에서 자세히 서술할 예정</p>
<h1 id="반환-주소">반환 주소</h1>
<p>invoke~ 바이트 명령어가 실행되면 새로운 스택 프레임을 만들면서 메서드가 호출된다. 이 때 새로운 스택 프레임에는 <code>반환 주소</code> 가 저장된다.</p>
<p>정확하게는 다음 2가지 정보가 저장된다.</p>
<ol>
<li>돌아와야할 스택 프레임의 위치</li>
<li>돌아와야할 PC Counter(명령어) 주소</li>
</ol>
<p>만약 호출이 끝나고 <a href="https://docs.oracle.com/javase/specs/jvms/se25/html/jvms-7.html">return, ireturn, areturn</a> 등 반환 명령어를 만난다면, 다음 세가지를 진행함으로서 호출했던 메서드로 돌아간다.</p>
<ol>
<li><code>반환 주소</code> 를 보고 이전의 스택 프레임으로 귀환</li>
<li>기존의 피연산자 스택과 지역 변수 테이블 복원</li>
<li>반환값을 피연산자 스택에 쌓음</li>
</ol>
<p><a href="https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-2.html">https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-2.html</a></p>
<p>반면, 위의 경우는 정상적으로 메서드 호출이 끝났을 때이다. 메서드가 종료되는 경우는 예외가 터졌을때도 있다.</p>
<p>이 때에도 마찬가지로 현재 활성화된 스택 프레임을 종료하고 이전 스택 프레임으로 이동하는건 같다.
하지만 PC Counter 로 이동하는것이 아닌 예외 테이블(Exception Table) 을 기준으로 동작한다. 적절한 예외 처리 handler 가 나올 때 까지 스택 프레임을 pop 하면서 찾는다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[클래스 로더]]></title>
            <link>https://velog.io/@jimmy_neutron/%ED%81%B4%EB%9E%98%EC%8A%A4-%EB%A1%9C%EB%8D%94</link>
            <guid>https://velog.io/@jimmy_neutron/%ED%81%B4%EB%9E%98%EC%8A%A4-%EB%A1%9C%EB%8D%94</guid>
            <pubDate>Thu, 29 Jan 2026 11:06:19 GMT</pubDate>
            <description><![CDATA[<h1 id="클래스-로더">클래스 로더</h1>
<p>클래스 로딩 단계 중 첫번째 로딩 단계 중 <code>완전한 이름을 보고 해당 클래스를 정의하는 바이너리 바이트 스트림 가져오기</code> 는 ClassLoader 라는 JVM 외부 컴포넌트에서 이뤄진다.</p>
<p>이는 JVM 설계 의도이다. JVM 설계자들은 이 행동이 외부에서 이뤄져서 JVM 과 독립적으로 다양하게 사용자가 사용할 수 있도록 의도되었다.</p>
<h1 id="클래스-로더의-독립성">클래스 로더의 독립성</h1>
<p>클래스 로더가 로딩한 클래스는 클래스 로더 별로 독립적인 네임 스페이스를 가진다.
따라서 같은 클래스라 하더라도 다른 클래스 로더가 로딩하면 동등성 비교가 false 이다.</p>
<p>따라서 동등성을 사용하는 <code>equals()</code>, <code>isAssignableFrom()</code>, <code>instanceOf</code> 등의 구문들에 영향을 끼칠 수 있다.</p>
<h1 id="클래스-로더의-부모-위임-모델">클래스 로더의 부모 위임 모델</h1>
<p>JDK 8 까지 클래스 로더는 <code>3계층 부모 위임 클래스 로딩 아키텍처</code> 를 사용했다.
JDK 9 이후에는 모듈 추가에 의해 조금 변형되었지만 큰 틀은 비슷하다.</p>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/1a11cc29-f9a6-4b30-b97b-3eb7ab04e159/image.png" alt=""></p>
<h3 id="클래스-로더-종류">클래스 로더 종류</h3>
<ol>
<li>부트스트랩 클래스 로더<ul>
<li>로드하는 것<ul>
<li>rt.jar, tools.jar 같은 JVM 이 클래스 라이브러리로 인식하는 파일</li>
<li>JAVA_HOME/lib 경로의 파일</li>
<li>-xbootclasspath 설정위치 파일</li>
</ul>
</li>
<li>가장 기본적인 JDK 클래스들을 로드하는게 이 부트스트랩 클래스 로더이다.</li>
<li>부트스트랩 클래스 로더는 자바에서 직접 참조할 수 없다. C++ 로 구현되어 있다. 만약 커스텀 클래스 로더에서 부트스트랩 클래스 로더로 작업을 위임하고 싶다면, 클래스 로더 참조 자리에 null 을 넣어야 한다.</li>
</ul>
</li>
</ol>
<blockquote>
<p>rt.jar 에는 java.lang, java.io 등이 들어있다.</p>
</blockquote>
<ol start="2">
<li>확장 클래스 로더<ul>
<li>sun.misc.Launcher$ExtClassLoader</li>
<li>로드하는 것<ul>
<li>JAVA_HOME/lib/ext 경로의 파일</li>
<li>java.ext.dirs 시스템 변수값 위치의 파일</li>
</ul>
</li>
<li>사용자가 위의 위치에 범용 라이브러리 클래스 파일을 넣어 SE 기능을 확장하여 사용</li>
</ul>
</li>
</ol>
<ol start="3">
<li>애플리케이션 클래스 로더<ul>
<li>sun.misc.Launcher$AppClassLoader</li>
<li>로드하는 것<ul>
<li>클래스 패스상의 클래스 라이브러리들</li>
</ul>
</li>
<li>ClassLoader.getSystemClassLoader(); 의 결과가 이 클래스 로더</li>
<li>따로 클래스 로더를 만들지 않으면, 애플리케이션에서 실행에 기본적으로 사용되는 클래스 로더</li>
</ul>
</li>
</ol>
<blockquote>
<p>클래스 패스 : 애플리케이션 클래스 로더가 로딩할 클래스 파일들의 위치</p>
</blockquote>
<h3 id="위임-모델">위임 모델</h3>
<p>클래스 로더가 로딩 요청을 받으면, 먼저 상위 클래스 로더에 위임하여 상위 클래스 로더에서 로딩을 시도한다.</p>
<p>만약 상위 클래스 로더의 로딩 대상이 아니라면 그제서야 하위 클래스로더로 넘어간다.</p>
<p>따라서 어느 클래스 로더를 사용하든 가장 먼저 부트스트랩 클래스 로더로 로딩을 시도한다.</p>
<p>이 구조는 공통적인 클래스는 같은 클래스 로더가 생성함을 보장한다.
예를 들어 공통적으로 사용하는 java.lang.Object 의 경우 어떤 커스텀 클래스 로더에 의해 로드하더라도 하나의 Object 클래스가 생성되는게 맞다. 계층구조 위임 모델은 이를 보장한다.</p>
<blockquote>
<p>계층 구조 위임 모델의 구현은 상속보다는 컴포지션을 사용한다. 커스텀 클래스 로더를 만들 일이 있으면 참고하자.</p>
</blockquote>
<h1 id="커스텀-클래스-로더">커스텀 클래스 로더</h1>
<p>추상 클래스인 <code>java.lang.ClassLoader</code> 를 상속하여 구현하면 된다.</p>
<p>커스텀 클래스 로더를 만들때 핵심적인 두 메서드가 있다.</p>
<ul>
<li>loadClass()</li>
<li>findClass()</li>
</ul>
<p>커스텀 클래스 로더를 만들 때에는 findClass() 만 오버라이딩하면 된다.</p>
<p><code>java.lang.ClassLoader</code> 은 이미 컴포지션, 위임 개념이 들어간 loadClass() 를 구현하고 있다. loadClass() 안에서 실질적으로 클래스 파일을 얻는 부분은 findClass() 이다.</p>
<p>부모 위임 모델과 컴포지션을 그대로 사용하면서 커스텀 클래스 로더를 만드려면 findClass() 만 오버라이딩하면 된다.</p>
<h1 id="thread-context-클래스-로더">Thread Context 클래스 로더</h1>
<p>자바의 모든 스레드는 기본적으로 TCCL 이라 부르는 클래스 로더를 가지고 있다.
<code>Thread.currentThread().getContextClassLoader();</code> 를 실행해 보면 해당 클래스 로더를 볼 수 있다.
기본적으로 AppClassLoader 가 들어간다.</p>
<p>이런 설계의 원인은 다음과 같다.</p>
<p>JNDI 는 rt.jar 의 일부로서 부트스트랩 클래스 로더가 로드한다. 하지만 JNDI 는 사용자가 설정한 설정 클래스를 읽어야 한다. 부모 위임 모델에서는 상위 클래스 로더인 부트스트랩 클래스 로더가 애플리케이션 클래스를 볼 수 없다.</p>
<p>따라서 스레드는 주머니에 하위 클래스 로더를 따로 하나씩 들고 다니다가 필요시에 사용하도록 했다.
이것이 TCCL(Thread Context Class Loader) 이다.</p>
<p>JDBC, JCE, JAXB 등 SPI 서비스 관련 로딩은 모두 이 방식을 사용한다.</p>
<blockquote>
<p>들어간 원인은 이 때문인데, spring 이든 여기저기서 스레드에서 클래스 로더 꺼내서 잘 활용하는듯 하다. 막 어디서 찾을 필요 없이 스레드에서 바로 꺼낼 수 있으니..</p>
</blockquote>
<h1 id="jdk-9-모듈-이후-클래스-로더">JDK 9 (모듈) 이후 클래스 로더</h1>
<p><a href="https://docs.oracle.com/en/java/javase/21/migrate/migrating-jdk-8-later-jdk-releases.html#GUID-A868D0B9-026F-4D46-B979-901834343F9E">https://docs.oracle.com/en/java/javase/21/migrate/migrating-jdk-8-later-jdk-releases.html#GUID-A868D0B9-026F-4D46-B979-901834343F9E</a></p>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/81f27eb0-8961-4f1c-8f86-095ab0c70273/image.png" alt=""></p>
<p>JDK 9 에는 jdk 의 왠만한 클래스를 모두 포함하는 rt.jar 가 사라지고, 내부에 있던 많은 패키지들은 각각 모듈로 바뀌었다.</p>
<p>이에 따라 ClassLoader 계층구조에도 몇가지 변화점이 생겼다.</p>
<ul>
<li>계층구조 자체는 유지.</li>
<li>ExtClassLoader -&gt; PlatformClassLoader 로 변경</li>
<li>부트스트랩, 플랫폼, 애플리케이션 클래스 로더가 jdk.internal.loader.BuiltinClassLoader 로부터 파생되도록 변경(기존에는 URLClassLoader)</li>
<li>클래스 로더가 부모에게 위임하기 전, 로딩할 클래스가 특정 모듈에 속하는지 확인하고 그렇다면 해당 모듈의 클래스 로더로 위임하는 과정 추가.</li>
</ul>
<blockquote>
<p>[rt.jar 가 모듈로 쪼개짐]
JAVA_HOME/jmods 에는 컴파일용 jmod 파일들이,
JAVA_HOME/lib/modules 파일에는 런타임용 모듈들이 정의되어 있음. 내용물은 같음.
<code>jimage extract $JAVA_HOME/lib/modules</code> 명령어로 extract 가능</p>
</blockquote>
<blockquote>
<p>[ExtClassLoader 의 삭제]
모듈화가 되면서, 기존에 모든 커스텀 클래스 로더를 확장할 용도로 쓰이던 ExtClassLoader 가 필요 없어졌다.</p>
<p>확장할 기능을 모듈로 만든 뒤 의존성을 정해주는 방식으로 확장하게 권장한다.
다만 gradle/maven 의 의존성 관리가 좀 더 많이 쓰이는듯..</p>
</blockquote>
<blockquote>
<p>[PlatFormClassLoader 가 굳이 필요한가? BootstrapClassLoader 로 통합했으면 안되었나?]
platform 클래스 로더는 말 그대로 플랫폼 클래스들을 로딩하기 위해 사용한다. 
실제로는 java.lang 은 BootstrapClassLoader, java.sql 은 PlatformClassLoader 처럼 클래스 로더가 로딩하는 모듈이 나뉘어 있다.</p>
</blockquote>
<p>하지만 위임 모델로 PlatformClassLoader 를 통해 BootStrapClassLoader 가 담당하는 클래스도 모두 로딩할 수 있으며 사용자 입장에서 Bootstrap/Platform 각각이 어떤 클래스를 로딩하는지의 구분을 알 필요는 없다.</p>
<blockquote>
</blockquote>
<p>다만 BootStrapClassLoader 는 C++ 로 짜여져서 코드상 직접 사용할 수 없다. 그렇다고 AppClassLoader 는 애플리케이션용이라 이 클래스로 플랫폼 클래스들을 로딩하는건 부자연스럽다.
따라서 java.lang, java.sql 등 플랫폼 클래스 로딩을 제어를 자바 코드에서 하기 위해 만든 클래스 로더 계층이라 할 수 있다.</p>
<pre><code class="language-java">public static void main(String[] args) throws ClassNotFoundException {
    ClassLoader cl = ClassLoader.getPlatformClassLoader();

    Class&lt;?&gt; c1 = cl.loadClass(&quot;java.lang.String&quot;); // BootStrapClassLoader 로 위임
    Class&lt;?&gt; c2 = cl.loadClass(&quot;java.sql.Date&quot;); // PlatformClassLoader

    System.out.println(c1.getName());
    System.out.println(c2.getName());
}</code></pre>
<h1 id="spring-boot-tomcat-의-클래스-로더">Spring Boot, Tomcat 의 클래스 로더</h1>
<h2 id="tomcat-class-loader">Tomcat Class Loader</h2>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/e822eb11-dfba-4ad9-8ab0-585b0292b1ae/image.png" alt=""></p>
<p>WebappClassLoader 는 톰켓에 올라갈 수 있는 여러개의 애플리케이션 각각에서 사용되는 클래스로더다. 예를 들어 war 3개를 올려놨다면 3개의 WebappClassLoader 가 사용되는 것이다.</p>
<p>WebappClassloader 는 <code>/WEB-INF</code> 경로의 애플리케이션 파일과 의존 라이브러리 jar 를 로드하는 역할이다. (war 파일 형식의 /WEB-INF 를 보자)</p>
<p>WebappClassLoader 의 상위 클래스로더인 Common 클래스 로더가 있다. 이는 lib 디렉토리(Tomcat 6 부터)를 클래스패스로 하는 클래스로더이며, 여러 애플리케이션이 공통으로 사용한다.</p>
<h2 id="spring-boot-class-loader">Spring Boot Class Loader</h2>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/7350b684-51f3-477b-bd2d-dd8afac9f028/image.png" alt=""></p>
<p>spring boot 는 임베디드 톰켓을 사용한다.
spring boot 의 bootjar 는 독립적으로 실행 가능한 jar 로서, 내부에 애플리케이션 클래스 파일과 의존 라이브러리 jar 를 가지고 있다.</p>
<p>즉, bootjar 는 nested jar 이다.</p>
<h3 id="문제">문제</h3>
<p>Tomcat 의 기본적인 WebappClassLoader 는 클래스 로딩시 SystemClassLoader(보통 AppClassLoader) 를 사용할 수 있다. (WebappClassLoader 의 생성자를 보면 parent = null 시 SystemClassLoader 사용)</p>
<p>하지만 SystemClassLoader 는 의존 라이브러리들인 중첩된 jar 를 읽을 수 없다. 따라서 따로 중첩 jar 를 읽을 수 있는 클래스로더 메커니즘을 제공해야한다.</p>
<h3 id="해결">해결</h3>
<p>spring boot 는 <a href="https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-loader">spring-boot-loader</a> 에 정의된 LaunchedClassLoader 를 사용한다.</p>
<p>이 클래스 로더는 JarURLClassLoader 로서, 중첩된 Jar 를 해석할 수 있게 구현되어 있다.</p>
<p>그렇다면 WebappClassLoader 부모로서 LaunchedClassLoader 를 가지게 하면 될까?
아니다. WebappClassLoader 는 때때로 SystemClassLoader 를 사용할 수 있다 했다. spring boot 에서는 <code>SystemClassLoader 가 아닌 LaunchedClassLoader 를 사용해야한다</code>. 이 로직이 들어간 것이 <code>TomcatEmbeddedWebappClassLoader</code> 이다.</p>
<p>즉 <code>TomcatEmbeddedWebappClassLoader</code> 는 spring boot 에서 온전히(SystemClassLoader 관여 없이) 부모 클래스 로더인 LaunchedClassLoader 만 사용되도록 하는 구현을 가지고 있다.</p>
<h3 id="결론">결론</h3>
<p>스프링 부트에서는 TCCL(Thread Context Class Loader) 로 <code>TomcatEmbeddedWebappClassLoader</code> 를 가지고 다니면서 Nested Jar 구조에서 정상적으로 클래스 로딩이 가능하다.</p>
<blockquote>
<p>intellij 같은 IDE 에서는 외부 의존성 jar 에 대한 class path 를 IDE 가 자동으로 지정해주므로 System ClassLoader(AppClassLoader) 로 의존성 라이브러리 클래스를 로드할 수 있다.
IDE 와 BootJar 에서 스프링 부트를 실행시키는 방법이 다름에서 오는 차이다.</p>
</blockquote>
<h3 id="참고">참고</h3>
<p>이 내용은 <a href="https://docs.spring.io/spring-boot/specification/executable-jar/restrictions.html?utm_source=chatgpt.com">spring boot docs</a> 에서도 엿볼 수 있다.</p>
<p>Thread.getContextClassLoader() 로 얻은 클래스 로더를 사용하라는 것이다.
ClassLoader.getSystemClassLoader() 로 얻은 클래스 로더를 사용하면 실패한다.</p>
<p>다음 코드를 spring boot 에서 bootJar 로 구동하여 실행해보자.
logback 라이브러리 클래스를 로드하는 코드인데, ClassLoader.getSystemClassLoader() 클래스 로더로 실행시 실패한다.</p>
<pre><code class="language-java">val threadClassLoader = Thread.currentThread().contextClassLoader
println(threadClassLoader.loadClass(&quot;ch.qos.logback.core.hook.DefaultShutdownHook&quot;)) // 이건 성공
println(&quot;thread = $threadClassLoader&quot;)

val systemClassLoader = ClassLoader.getSystemClassLoader()
println(systemClassLoader.loadClass(&quot;ch.qos.logback.core.hook.DefaultShutdownHook&quot;)) // 여기서 실패
println(&quot;system = $systemClassLoader&quot;)</code></pre>
<p>반면 IDE 에서 실행할 경우에는 둘 다 성공한다.</p>
<p>다음은 Thread.getContextClassLoader() 클래스 로더의 toString() 값이다.
부모 클래스 로더로 <code>LaunchedClassLoader</code> 를 가지고 있음을 알 수 있다.</p>
<pre><code>thread = TomcatEmbeddedWebappClassLoader
  context: ROOT
  delegate: true
----------&gt; Parent Classloader:
org.springframework.boot.loader.launch.LaunchedClassLoader@28a418fc</code></pre><h3 id="참고-2---javautillogging">참고 2 - java.util.logging</h3>
<p>JDK 에는 로깅관련 라이브러리가 기본적으로 있다.
하지만 spring boot 를 사용할때면 logback, log4j2 등 로깅 라이브러리를 따로 사용한다.</p>
<p>이는 클래스 로더 문제와 관련있다.</p>
<p>java.util.logging 이 가진 문제가 System ClassLoader 가 아니면 클래스 로딩이 재대로 안된다.
따라서 nested jar 구조에서는 사용할 수 없다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[클래스 로딩]]></title>
            <link>https://velog.io/@jimmy_neutron/%ED%81%B4%EB%9E%98%EC%8A%A4-%EB%A1%9C%EB%94%A9</link>
            <guid>https://velog.io/@jimmy_neutron/%ED%81%B4%EB%9E%98%EC%8A%A4-%EB%A1%9C%EB%94%A9</guid>
            <pubDate>Wed, 28 Jan 2026 11:38:43 GMT</pubDate>
            <description><![CDATA[<p>C언어는 컴파일 시점에 심벌 참조(논리적인 참조)를 직접 참조(실제 메모리 주소)로 변환하는 링킹 과정을 실행한다. 컴파일 단계에서 실제 호출 대상이나 코드 실행 전략 등이 고정된다.</p>
<p>반면 자바는 컴파일시 심벌 참조만 가진 클래스 파일을 만든다. JVM 런타임에 이 심벌 참조는 직접 참조로 바뀌어 실제 메모리 주소값을 가진 호출을 가지게 된다.</p>
<p>이 구조는 인터페이스와 구현체들을 만들어 놓고 실제 사용할 구현체를 런타임에 결정하거나, 클래스로더를 사용하여 런타임에 실행할 바이너리 스트림을 동적으로 외부에서 읽어오는 등 정적인 언어가 동적인 언어로 확장될 수 있는 기반이 된다.</p>
<p>반면 런타임에 하는 일이 많아지므로 살짝의 느림과 AOT 에 제약이 걸리긴 한다.</p>
<h1 id="클래스-로딩">클래스 로딩</h1>
<p>[_JVM 밑바닥까지 파헤치기_]
<img src="https://velog.velcdn.com/images/jimmy_neutron/post/ab6cfd0f-a14e-43cc-a857-a41b6b4169c6/image.png" alt=""></p>
<p>JVM 에서 클래스 로딩은 위와 같은 단계를 거친다.</p>
<p>단계들의 실행시간이 겹쳐 병렬로 실행되는것 처럼 보일 수 있지만, 시작 시간은 반드시 위와 같은 순서로 시작되어야 한다.</p>
<h1 id="클래스-로딩-시작">클래스 로딩 시작</h1>
<p>자바 가상 머신 명세에는 가장 첫 단계인 <code>로딩</code> 이 언제 시작해야하는지는 명시가 없다.
다만, <code>초기화</code> 단계는 언제 시작되어야 하는지 명시가 되어 있다. 아래의 6가지 경우 <code>초기화</code> 가 진행되어야 하며 이것이 <code>로딩</code> 부터 시작하여 클래스 로딩이 시작되는 트리거다.</p>
<ol>
<li>다음 바이트 코드 명령어 실행시 대상의 타입이 초기화 되어있지 않을 때.<ul>
<li><code>new</code> : 새 객체 인스턴스 생성</li>
<li><code>getstatic</code>, <code>putstatic</code> : 정적 필드를 읽거나 설정(final 지정된 정적 필드는 컴파일 타임에 상수 풀에 채워지므로 제외)</li>
<li><code>invokestatic</code> : 정적 메서드 호출</li>
</ul>
</li>
<li>Class 클래스, 리플랙션 패키지 등 표준 클래스 라이브러리에서 제공하는 리플렉션 메서드를 사용할 때 해당 타입이 아직 초기화 되어 있지 않은 경우</li>
<li>클래스를 초기화할 때 상위 클래스가 초기화 되어 있지 않은 경우</li>
<li>가상 머신은 구동 직후 사용자가 정한 main() 을 찾아 실행함. 이 때 main 타입의 초기화 먼저 시작</li>
<li><code>REF_getStatic</code>, <code>REF_putStatic</code>, <code>REF_invokeStatic</code>, <code>REF_newInvokeSpecial</code> 타입 메서드 핸들을 해석해 얻은 MethodHandle 인스턴스를 호출할 때 해당하는 클래스가 초기화되어 있지 않은 경우<ul>
<li>MethodHandle 은 동적 언어 지원 메서드.</li>
<li>쉽게 말하면 <code>MethodHandle 의 invokeExact 메서드</code> 실행 시 초기화 된다는 것.</li>
</ul>
</li>
<li>인터페이스에 디폴트 메서드가 있다면, 해당 인터페이스의 구현한 클래스가 초기화될 때 인터페이스도 초기화</li>
</ol>
<p>위의 6가지 경우는 모든 가상 머신에서 클래스 로딩 및 초기화가 시작된다.
나머지 경우는 타입에 대한 수동 참조로서 클래스 로딩이 진행될지는 JVM 구현에 따라 다르다.</p>
<h2 id="클래스-로딩이-될-것-같은데-안되는-예시">클래스 로딩이 될 것 같은데 안되는 예시</h2>
<p>상위 클래스의 상수를 하위 클래스를 통해 실행하면, 하위 클래스의 초기화는 안된다.
아래의 결과는 <code>상위 클래스 초기화!</code> 다.
하위 클래스를 통해 호출했지만 하위 클래스의 초기화는 되지 않는다.</p>
<p>이 때 로딩과 링킹은 진행할지 말지 자유인데, 핫스팟 VM 의 경우 <code>-Xlog:class+load=info</code> ㄸ또는<code>-XX:+TraceClassLoading</code> 으로 보면 하위 클래스(SubClass) 의 로딩까진 진행하는 것을 볼 수 있다.</p>
<pre><code class="language-java">class SuperClass {
    static {
        System.out.println(&quot;상위 클래스 초기화!&quot;);
    }

    public static int value = 123;
}

class SubClass extends SuperClass {
    static {
        System.out.println(&quot;하위 클래스 초기화!&quot;);
    }
}

public class NotInitialization_1 {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}</code></pre>
<p>아래의 경우 SuperClass, SubClass 모두 초기화되지 않는다.
다만 <code>[L</code> 이 붙은 컴파일러가 만든 클래스가 초기화가 된다. (<code>anewarray</code> 바이트코드 명령에 의해)
이는 배열에 대한 length, clone() 속성 및 메서드를 구현하는 클래스이다.</p>
<pre><code>public class NotInitialization_1 {
    public static void main(String[] args) {
        SuperClass[] sca = new SuperClass[10];
    }
}</code></pre><p>아래 역시 <code>ConstantClass 초기화!</code> 가 불리지 않는다.
컴파일 타임에 ConstClass.HELLO_WORLD 가 최적화 되어 NotInitialization_3 의 상수 풀에 들어간다. 이는 javap 를 사용하면 볼 수 있다.</p>
<pre><code>class ConstClass {
    static {
        System.out.println(&quot;ConstClass 초기화!&quot;);
    }

    public static final String HELLO_WORLD = &quot;hello world&quot;;
}

public class NotInitialization_3 {
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLO_WORLD);
    }
}</code></pre><h1 id="클래스-로딩-과정">클래스 로딩 과정</h1>
<p>로딩 -&gt; 검증 -&gt; 준비 -&gt; 해석 -&gt; 초기화</p>
<h2 id="1-로딩">1. 로딩</h2>
<ul>
<li>완전한 이름을 보고 해당 클래스를 정의하는 바이너리 바이트 스트림 가져오기</li>
<li>바이스 스트림(정적인 저장 구조)을 메서드 영역에서 사용하는 런타임 데이터 구조로 변환</li>
<li>java.lang.Class 객체를 힙에 생성. 사용자는 이를 통해 메서드 영역 데이터 활용.</li>
</ul>
<p>ClassLoader 의 findClass(), loadClass() 메서드가 해당 단계이다. 이 메서드를 조절하면 로딩 단계를 커스텀할 수 있다.</p>
<p>로딩 단계에서 검증 단계의 일부(파일 형식 검증) 이 실행될 수 있다.</p>
<h2 id="2-검증-링킹---1">2. 검증 (링킹 - 1)</h2>
<p>바이트 스트림은 자바 코드를 컴파일한 클래스 파일 외에도 여러 경로로 만들어질 수 있다.
따라서 자바 컴파일러 외에 JVM 에서 공통적으로 검증 과정이 있어야 한다.</p>
<p>목적은 다음과 같다.</p>
<ul>
<li>자바 가상 머신 명세의 제약들을 다 지켰는지</li>
<li>실행했을때 보안상의 위협이 없는지</li>
</ul>
<p>검증은 4가지 항목에 대해 이뤄진다.</p>
<ol>
<li>파일 형식 검증</li>
<li>메타데이터 검증</li>
<li>바이트 코드 검증</li>
<li>심벌 참조 검증</li>
</ol>
<p>검증 단계는 필수는 아니다. 따라서 오래 걸리는 검증 단계를 생략함으로 코드 실행 시간을 단축 할 수 있다. (<code>-Xverify:none</code>) 만.. 그럴 수 있을까.</p>
<h3 id="파일-형식-검증">파일 형식 검증</h3>
<p>파일 형식 검증은 로딩 단계에 같이 진행된다. 파일 형식 검증은 바이트 스트림을 대상으로 진행한다.</p>
<p>로딩과 파일 형식 검증이 끝나면 메서드 영역에 저장이 끝난다. 따라서 이후 메타데이터, 바이트 코드, 심벌 참조 검증은 메서드 영역에 대해 진행된다.</p>
<ul>
<li>매직 넘버 <code>0xCAFEBABE</code> 인지</li>
<li>메이저, 마이너 버전이 자바 허용 범위인지</li>
<li>상수 풀에 지원하지 않는 타입이 있는지</li>
<li>상수 풀 인덱스 중 유효하지 않은게 있는지</li>
<li>CONSTANT_Utf8_info 타입에 UTF-8 인코딩 아닌게 있는지</li>
</ul>
<p>등등..</p>
<h3 id="메타데이터-검증">메타데이터 검증</h3>
<p>자바 가상 머신 명세의 요구 사항 검사.</p>
<ul>
<li>부모 클래스가 있는가? (모든 클래스의 상위는 Object)</li>
<li>부모 클래스에 final 이 없는가?</li>
<li>추상 클래스를 구현한 경우 모든 추상 메서드를 구현했는가?</li>
</ul>
<p>등등..</p>
<h3 id="바이트-코드-검증">바이트 코드 검증</h3>
<p>데이터 흐름과 제어 흐름을 분석하여 프로그램의 의미가 적법하고 논리적인지 확인.
메서드 본문(클래스 파일의 Code 속성) 을 대상으로 검증한다.</p>
<ul>
<li>피연산자 스택 데이터가 int 라면 명령어에 의해 지역 변수 테이블에 저장되는 데이터도 int 여야 함.</li>
<li>점프 명령어가 이상한 곳 가면 안됨</li>
<li>안전하지 않은 형 변환이 있는지 확인</li>
</ul>
<p>등등..</p>
<blockquote>
<p>이 단계는 검증할게 너무 많다. 느림의 주된 원인이 될 수 있다.
클래스 파일의 stackMapTable 속성은 일부 바이트 코드 검증 단계에서 할 작업을 컴파일 단계로 넘김으로 느린 문제를 해결한다.</p>
<p>분기, 점프, 예외 핸들러는 여러 케이스가 한 지점으로 모이는 구문들이다. 이 때의 피연산자 스택과 지역 변수 테이블에 저장된 데이터의 타입을 컴파일 시점에 명시적으로 저장해두고, 바이트 코드 검증 시점에는 해당 타입이 유효한지만 확인하면 된다.</p>
<p>이게 있기 전에는 바이트 코드 검증 단계에서 피연산자 스택과 지역 변수 테이블에 저장된 데이터의 타입을 추론해야 했기에 느렸다.</p>
</blockquote>
<h3 id="심벌-참조-검증">심벌 참조 검증</h3>
<p>링킹의 세번째인 해석 단계에서 심벌 참조가 직접 참조로 변환될 때 실행하는 검증.
현재 클래스가 참조하는 외부 클래스, 메서드, 필드 등에 접근할 권한이 있는지 확인.</p>
<ul>
<li>심벌 참조에서 리터럴로 기술된 클래스를 실제로 찾을 수 있는가?</li>
<li>서술자에 적힌 메서드나 필드가 실제로 클래스에 존재하는가?</li>
<li>접근 지정자가 접근을 허용하는가?</li>
</ul>
<p>여기서 NoSuchFieldError, NoSuchMethodError 등 IncompatibleClassChangeError 하위를 던진다.</p>
<h2 id="3-준비-링킹---2">3. 준비 (링킹 - 2)</h2>
<p>클래스 변수(정적 변수)를 메모리에 할당 및 초깃값 설정</p>
<p>또한 코드에서 지정한 초기값이 아닌 0 등 기본값으로 초기화된다.</p>
<blockquote>
<p>숫자형은 0, boolean 은 false, 객체는 null 로 초기화.</p>
</blockquote>
<p>기본값으로 채워지지 않는 예외의 경우가 있는데,
ConstantValue(클래스 파일 중 필드 테이블의 속성) 이 만들어지는 final static String 과 primitive type 의 경우에는 이 단계에서 ConstantValue 값이 초기화된다.</p>
<h2 id="4-해석-링킹---3">4. 해석 (링킹 - 3)</h2>
<p>심벌 참조를 직접 참조로 변환한다.</p>
<p>심벌 참조를 다루는 바이트 코드 명령어 (<code>anewarray</code>, <code>checkcast</code>, <code>getfield</code>, <code>getstatic</code>, <code>instanceof</code>, <code>invokedynamic</code>, <code>invokeinterface</code>, <code>invokespecial</code>, <code>invokestatic</code>, <code>invokevirtual</code>, <code>ldc</code>, <code>ldc_w</code>, <code>ldc2_w</code>, <code>multianewarray</code>, <code>new</code>, <code>putfield</code>, <code>putstatic</code>) 가 사용하는 심벌 참조를 나타내는 상수 풀의 <code>CONSTANT_Class_info</code>, <code>CONSTANT_Fieldref_info</code>, <code>CONSTANT_Methodref_info</code>, <code>CONSTANT_InterfaceMethodref_info</code>, <code>CONSTANT_MethodType_info</code>, <code>CONSTANT_MethodHandle_info</code>, <code>CONSTANT_Dynamic_info</code>, <code>CONSTANT_InvokeDynamic_info</code> 를 직접 참조로 변환한다.</p>
<p>동적으로 결과가 바뀌는 invokedynamic 을 제외한 명령어들은 여러번 해석해도 항상 같은 결과가 나와야 한다. 따라서 한번 해석한 심벌 참조의 경우 상수 풀에 해석되었다고 표시하는 캐싱이 이뤄진다.</p>
<p>해석을 클래스 로딩 시점에 할 것인지, 실제 사용될 때 할 것인지는 JVM 구현에 달려있다.</p>
<p>이 단계는 리터럴 심벌 참조를 실제 주소인 직접 참조로 바꾸는 과정이기 때문에, 다른 클래스의 연쇄적인 클래스 로딩을 유발할 수 있다.</p>
<h2 id="5-초기화">5. 초기화</h2>
<p>클래스 생성자 <code>&lt;clinit&gt;()</code> 을 실행한다.
<code>&lt;clinit&gt;()</code> 는 컴파일러가 자동 생성한다.
클래스 변수를 프로그램에 작성한 대로 초기화하고, 정적 블록(static {})을 실행한다.</p>
<p><code>&lt;clinit&gt;()</code> 은 부모 클래스가 있다면 먼저 실행한다. 따라서 자바에서 가장 먼저 실행되는 클래스 생성자는 Object 의 <code>&lt;clinit&gt;()</code> 이다.</p>
<p>반면 인터페이스(정적 변수 가능)는 실제 사용될 때 <code>&lt;clinit&gt;()</code> 가 실행된다. 즉, 구현 클래스의 <code>&lt;clinit&gt;()</code> 가 불리더라도 인터페이스의 <code>&lt;clinit&gt;()</code> 는 불릴 필요 없다.</p>
<p>정적 블록이나 정적 변수가 없다면 <code>&lt;clinit&gt;()</code> 가 존재하지 않을 수도 있다.</p>
<p>정적 블록은 멀티 스레드 문제에 민감하다. n개 스레드가 한 클래스를 동시에 로딩할 수 있다. 만약 정적 블록에 오래 걸리는 일이 있다면, n-1 개의 스레드는 블로킹 대기하게 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[바이트 코드 명령어]]></title>
            <link>https://velog.io/@jimmy_neutron/%EB%B0%94%EC%9D%B4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EB%AA%85%EB%A0%B9%EC%96%B4</link>
            <guid>https://velog.io/@jimmy_neutron/%EB%B0%94%EC%9D%B4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EB%AA%85%EB%A0%B9%EC%96%B4</guid>
            <pubDate>Wed, 28 Jan 2026 08:20:42 GMT</pubDate>
            <description><![CDATA[<p>클래스 파일의 Code Attribute 에 순서대로 나열되는 바이트 코드 명령어는 u1 타입이기에 총 256 개의 명령어를 나타낼 수 있다.
현재는 200개 넘는 수의 바이트 코드 명령어가 있다.</p>
<h1 id="바이트-코드-명령어">바이트 코드 명령어</h1>
<ul>
<li>클래스 파일에 <code>[바이트 코드 명령어] + [필요시 피연산자]</code> 형태로 작성된다.</li>
<li>최대 256 개 표현 가능. u1 타입으로 정의되어 있기 때문.</li>
<li>피연산자 길이 정렬(length alignment) 를 지원하지 않는다. CPU 는 무조건 1byte 단위로만 읽는다는 이야기다.<ul>
<li>JVM 의 철학(어느 환경이든 일관된 실행) - 1 byte 로만 해석</li>
<li>예를 들어 16비트 double 은 메모리상 2 bytes 로 나눠 저장되고, jvm 이 (byte1 &lt;&lt; 8) | byte2 와 같은 연산으로 계산해서 사용</li>
<li>단점 : 이는 바이트 코드 해석 및 실행 속도를 약간 늦춘다.</li>
<li>장점 : 패딩(공백)을 없앨 수 있고, 일관된 1byte 라 짧고 간결함. 최소 데이터로 전송 효율을 높이기 위한 자바 초기의 스마트 가전 시장에 적합했음</li>
</ul>
</li>
</ul>
<p>바이트 코드 명령어의 실행을 간단하게 표현하면 아래 4가지의 반복이다.</p>
<ol>
<li>PC 레지스터의 값을 계산</li>
<li>바이트 코드 스트림에서 PC 레지스터가 가리키는 위치의 바이트 코드 명령어 가져옴</li>
<li>피연산자가 필요하다면 가져옴</li>
<li>명령어 실행</li>
</ol>
<h1 id="특징">특징</h1>
<ul>
<li><p>대다수 명령어는 앞에 타입을 나타내는 문자가 적혀있다. ex) iload(int load), fstore(float store), laload(long array load)</p>
<ul>
<li>타입을 구분하기 쉽게 하기 위한 설계</li>
</ul>
</li>
<li><p>일부 명령어는 머신 내에서 타입 변환하여 쓰인다</p>
<ul>
<li>ex) cload(char load) 같은건 없다. char 타입에 대한 load 는 iload 명령어로 동작한다.</li>
<li>byte, char, short 는 int 로 변환 가능하기에 이 타입 문자가 앞에 붙은 명령어는 거의 없다.</li>
<li>boolean 타입 문자가 앞에 붙은 명령어는 하나도 없다.</li>
<li>256 개 안으로 명령어 수를 맞추기 위함이다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/b374faff-9a15-4735-bb86-75ee5da2457c/image.png" alt=""></p>
<p><a href="https://docs.oracle.com/javase/specs/jvms/se25/html/jvms-7.html">https://docs.oracle.com/javase/specs/jvms/se25/html/jvms-7.html</a></p>
</li>
</ul>
<h1 id="명령어">명령어</h1>
<h2 id="로드와-스토어">로드와 스토어</h2>
<ul>
<li>지역 변수를 피연산자 스택으로 읽기 : <code>load</code><ul>
<li><code>iload</code>, <code>~iload_&lt;n&gt;</code>, <code>lload</code>, <code>~lload_&lt;n&gt;</code>, <code>fload</code>, <code>~fload_&lt;n&gt;</code>, <code>dload</code>, <code>~dload_&lt;n&gt;</code>, <code>aload</code>, <code>~aload_&lt;n&gt;</code></li>
</ul>
</li>
<li>피연산자 스택의 값을 지역변수 테이블에 저장하기 : <code>store</code><ul>
<li><code>istore</code>, <code>istore_&lt;n&gt;</code>, <code>lstore</code>, <code>lstore_&lt;n&gt;</code>, <code>fstore</code>, <code>fstore_&lt;n&gt;</code>, <code>dstore</code>, <code>dstore_&lt;n&gt;</code>, <code>astore</code>, <code>astore_&lt;n&gt;</code></li>
</ul>
</li>
<li>상수를 피연산자 스택으로 읽어오기<ul>
<li><code>bipush</code>, <code>sipush</code>, <code>ldc</code>, <code>ldc_w</code>, <code>ldc2_w</code>, <code>aconst_null</code>, <code>icont_m1</code>, <code>icont_m1</code>, <code>lcont_&lt;l&gt;</code>, <code>fcont_&lt;f&gt;</code>, <code>dcont_&lt;d&gt;</code></li>
</ul>
</li>
<li>더 넓은 인덱스를 사용하여 더 많은 지역 변수에 접근하거나 더 큰 피연산자에 접근하기<ul>
<li><code>wide</code></li>
</ul>
</li>
</ul>
<p><code>_&lt;n&gt;</code> 과 같이 써 있는 것들은 피연산자를 포함하고 있는 명령어들로, 자주 쓰이는 피연산자를 명령어에 합쳤다.
예를 들어 <code>iload_0</code> 은 <code>iload 0</code> 과 같다.</p>
<h2 id="산술-명령어">산술 명령어</h2>
<p>피연산자 두개의 값으로 특정 산술 연산 후 결괏값을 다시 피연산자 스택의 맨 위에 저장한다.
byte, short, char, boolean 은 직접 지원하는 산술 명령어가 없으며, int 명령어로 사용한다.</p>
<p>정수 연산의 오버플로는 명시가 없다
정수 나누기와 나머지 연산시 나누는 값이 0 이면 ArithmeticException 은 규정에 있다
이 외의 다른 정수 산술 시나리오에서는 런타임 예외를 던지지 않아야 한다.</p>
<p>IEEE754 - 부동 소수점
비정규화된 부동 소수점 수, 점진적 언더플로 지원해야함
모든 ㅇ녀산 결과를 적절한 정밀도로 반올림해야함
정확하지 않은 결과는 표현 가능한 가장 가까운 값으로 반올림
표현 가능한 두 값이 수학적으로 정확한 값과 차이가 똑같다면 최하위 비트가 0인 값을 우선시 -&gt; 가까운 값으로 반올림
부동 소수점 수를 정수로 반환할 때는 0에 가까운 값으로 반올림(양수면 내림, 음수면 올림) -&gt; 소수부의 모든 중요 바이트를 버림 - 대상 숫자 타입이 표현할 수 있는 숫자 중 정확한 값과 가장 가까우면서 더 크지 않은 값 선택해줌</p>
<p>부동 소수점 연산을 수행할 때 런타임 예외를 던지지 않는다.
연산이 오버플로 되면 부호있는 무한대로 표현
연산 결과에 대한 명확한 수학적 정의가 없다면 NaN</p>
<p>long 타입 비교시엔 부호 있는 비교 사용
jcmpg, dcmpl, fcmpg, fcmpl 등이 IEEE754 가 정의한 예외(시그널) 을 보내지 않는 비교</p>
<h2 id="형-변환-명령어">형 변환 명령어</h2>
<p>숫자 타입 -&gt; 다른 숫자 타입
코드에 명시한 형변환, char 등 전용 연산 코드가 없을때의 자동 현변환</p>
<p>데이터 타입의 표현 범위가 넓어지는 경우는 형 변환 명령어가 없어도 자동 처리됨
표현 범위가 줄어드는 경우 형 변환 명령어가 반드시 필요(i2b, i2c, i2s...)</p>
<h3 id="큰-범위의-정수intlong---작은-단위의-정수shortintlong">큰 범위의 정수(int/long) -&gt; 작은 단위의 정수(short/int/long)</h3>
<ul>
<li>초과하는 윗 부분의 비트가 버려진다.</li>
<li>이로 인해 부호가 바뀌거나, 값이 바뀔 수 있다.
ex)(short) 65537 = 0, (short) 32768 = -1</li>
</ul>
<h3 id="부동-소수점---정수-intlong">부동 소수점 -&gt; 정수 int/long</h3>
<ul>
<li>부동 소수점 값이 NaN 이면 -&gt; 0 으로 변경</li>
<li>무한대가 아닌 수는 0과 가까운 값으로 반올림. 표현할 수 없다면 int/long 의 MIN/MAX</li>
</ul>
<h3 id="double---float">double -&gt; float</h3>
<ul>
<li>기본적으로 가까운 값으로 반올림.</li>
<li>범위를 벗어나면 0/MIN/MAX<ul>
<li>절대값이 너무 작은 경우 음수0 또는 양수0 이 될 수 있다.</li>
</ul>
</li>
</ul>
<h2 id="객체-생성과-접근-명령어">객체 생성과 접근 명령어</h2>
<ol>
<li>클래스 인스턴스 생성 :new</li>
<li>배열 생성 : newarray, anewarray, multianewarray</li>
<li>인스턴스 필드 접근 : getfield, putfield</li>
<li>클래스 필드 접근 : getstatic, putstatic</li>
<li>배열의 원소를 피연산자 스택으로 읽어 오기 : baload, caload, saload, iaload, laload...</li>
<li>피연산자 스택의 값을 배열의 원소에 저장 : bastore, castore ...</li>
<li>배열의 길이 얻기 : arraylength</li>
<li>클래스 인스턴스의 타입 확인 : instanceof</li>
<li>배열의 타입 확인 : checkcast</li>
</ol>
<h2 id="피연산자-스택-관리">피연산자 스택 관리</h2>
<ol>
<li>피연산자 스택에서 최상위 원소 한개/두개 꺼내기 : pop, pop2</li>
<li>스택의 최상위 값 한개/두개 복제 후 복제된 값을 스택 최상위에 다시 넣기 : dup, dup2, dup_x1, dup2_x1, dup_x2, dup2_x2</li>
<li>스택 최상위 값 두개 치환 : swap</li>
</ol>
<h2 id="제어-전이-명령어">제어 전이 명령어</h2>
<p>PC 레지스터 값을 조건적으로 또는 무조건 적으로 다른 명령어로 변환하는 명령어다.
변경 대상 명령어는 제어 전이 명령어가 아니어야 한다.</p>
<ol>
<li>조건 분기 : ifeq, iflt, ifle, ifgt, ifnull, if_icmpeq, if_icmplt, if_acmpeq, if_acmpne...<ul>
<li>스택의 두 값을 대상으로 비교 연산을 실행 후 결과에 따라 특정 라인으로 이동</li>
<li>비교 연산은 산술 연산과 일관되게 byte, short, char, boolean 은 int 형 명령어로 사용</li>
<li>if_acmpeq(a 가 붙은 명령어) 는 참조타입용</li>
</ul>
</li>
<li>복합 조건 분기 : tableswitch, lookupswitch</li>
<li>무조건 분기 : goto, goto_w, jsr, jsr_w, ret</li>
</ol>
<blockquote>
<p>jsr_w : jump to subroutine wide
ret : return from subroutine</p>
</blockquote>
<h2 id="메서드-호출과-반환-명령어">메서드 호출과 반환 명령어</h2>
<ol>
<li><code>invokevirtual</code> : 인스턴스 메서드 호출. 객체의 실제 타입에 따라 디스패치.</li>
<li><code>invokeinterface</code> : 인터페이스 메서드 호출. 런타임에 인터페이스 구현 객체의 메서드를 찾아 호출.</li>
<li><code>invokespecial</code> : 인스턴스 초기화 메서드, private 메서드, 부모 클래스의 메서드 등</li>
<li><code>invokestatic</code> : static 메서드</li>
<li><code>invokedynamic</code> : 런타임에 callsite 한정자가 참조하는 메서드를 동적으로 찾아 호출. 앞의 네 명령어의 디스패치 로직은 사용자가 변경할 수 없지만, invokedynamic 의 디스패치 로직은 jvm 실행 시 사용자가 설정할 수 있다.</li>
</ol>
<h3 id="invokedynamic">invokedynamic</h3>
<p>호출 대상 결정을 런타임으로 미룬다.
대표적인 예로 람다가 있다. 람다는 컴파일시 invokedynamic 명령어 실행으로 나온다. 호출 대상 클래스가 정해져 있지 않다.
람다의 첫 호출시 컴파일때 정해진 방법으로 BootStrapMethod 를 호출한다. 이 때 어떤 메서드를 실행할지 결정하고 만든다. 따라서 첫 호출시에는 느릴 수 있다.
이후 호출시에는 callsite 라 불리는 미리 기억된 경로로 BSM 에서 생성된 메서드를 빠르게 실행한다.</p>
<blockquote>
<ol>
<li>바이트코드에서 invokedynamic 만남</li>
<li>Bootstrap Method(BSM) 호출 (딱 한 번)</li>
<li>BSM이 CallSite 반환</li>
<li>CallSite 안의 MethodHandle 이 실제 호출 대상</li>
<li>이후 호출은 직접 jump (거의 invokestatic 급)</li>
</ol>
</blockquote>
<p>dynamicinvoke 
동적 언어를 정적 언어 처럼 최적화 가능</p>
<pre><code class="language-java">public class InvokedynamicTest {

    static Runnable makeStatelessLambda() {
        return () -&gt; System.out.println(&quot;no capture&quot;);
    }

    static Runnable makeStatusLambda(int x) {
        return () -&gt; System.out.println(&quot;capture x = &quot; + x);
    }

    public static void main(String[] args) {
        Runnable r1 = makeStatelessLambda();
        Runnable r2 = makeStatelessLambda();

        System.out.println(r1 == r2);
        System.out.println(System.identityHashCode(r1));
        System.out.println(System.identityHashCode(r2));

        Runnable r3 = makeStatusLambda(10);
        Runnable r4 = makeStatusLambda(10);

        System.out.println(r3 == r4);
        System.out.println(System.identityHashCode(r3));
        System.out.println(System.identityHashCode(r4));
    }
}</code></pre>
<h2 id="예외-처리-명령어">예외 처리 명령어</h2>
<ul>
<li>자바의 <code>throw</code> 구문으로 명시적 예외 발생시 <code>athrow</code> 바이트 코드 명령어가 사용됨</li>
</ul>
<h2 id="동기화-명령어">동기화 명령어</h2>
<ul>
<li>synchronized 와 관련된 명령어</li>
<li>synchronized 함수인지는 메서드 테이블의 ACC_SYNCHRONIZED 플래그를 보고 확인한다.
ACC_SYNCHRONIZED 플래그가 켜진 함수에 진입시 바이트 코드 명령어 <code>monitorenter</code> 를 사용하여 락(모니터 라고도 함) 을 획득한다.</li>
<li><code>monitorexit</code> 은 락은 해제하는 명령어이다.</li>
<li>jvm 은 자동으로 예외 처리 테이블을 만들어 줌으로서 반드시 <code>monitorenter</code> 에 대응되는 <code>monitorexit</code> 가 실행되도록 보장한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[클래스 파일]]></title>
            <link>https://velog.io/@jimmy_neutron/%ED%81%B4%EB%9E%98%EC%8A%A4-%ED%8C%8C%EC%9D%BC</link>
            <guid>https://velog.io/@jimmy_neutron/%ED%81%B4%EB%9E%98%EC%8A%A4-%ED%8C%8C%EC%9D%BC</guid>
            <pubDate>Fri, 23 Jan 2026 07:32:48 GMT</pubDate>
            <description><![CDATA[<p>JVM 이 플랫폼 독립이 될 수 있었던 이유는 클래스 파일 덕분이다.
JVM 은 클래스 파일을 해석하여 실행하는 머신이고, 자바는 클래스 파일을 만들 수 있는 언어 중 하나다.</p>
<p>과거에는 리눅스, 윈도우 등 플랫폼에 독립적으로 아무 곳에서 작성해도 실행할 수 있는 것 이었지만, 이제는 언어 독립성도 생긴다. 코틀린, 자바, 스칼라 등 언어가 달라도 바이트 코드를 만들 수 있는 각각의 컴파일러가 있어서 JVM 에서 실행할 수 있는 코드를 언어 독립적으로 만들 수 있다.</p>
<p>이러한 것이 클래스 파일이라는게 있기 때문이며, 클래스 파일의 형태 및 규칙은 JDK1 부터 거의 바뀌지 않고 있다.</p>
<h1 id="클래스-파일-구조">클래스 파일 구조</h1>
<ul>
<li>구분자가 없는 바이트 스트림. 따로 구분자가 없기에 데이터가 작성되는 순서, 의기, 길이 등이 엄격하게 지켜져야 한다.</li>
<li>1byte 단위로 작성</li>
<li>클래스 파일이 저장하는 데이터(순서대로)<ul>
<li>매직 넘버와 클래스 파일의 버전</li>
<li>상수 풀</li>
<li>접근 플래그</li>
<li>클래스 인덱스, 부모 클래스 인덱스, 인터페이스 인덱스</li>
<li>필드 테이블</li>
<li>메서드 테이블</li>
<li>속성 테이블</li>
</ul>
</li>
<li>두가지 타입 존재<ul>
<li>부호 없는 숫자 : u1, u2, u4, u8 / 각각 1bytes 2bytes 4bytes 8bytes</li>
<li>테이블 : 여러개의 부호 없는 숫자 또는 다른 테이블로 구성된 복합 데이터 타입. 관례적으로 
테이블 이름에  &#39;_info&#39; 접미어가 붙는다.</li>
</ul>
</li>
<li>같은 타입의 데이터 여러개를 표한할 때 수가 정해져 있지 않다면 개수를 알려주는 부호 없는 숫자 타입이 바로 앞에 등장한다. 이러한 데이터에는 &#39;_count&#39; 접미어가 붙는다.</li>
<li>클래스파일은 꼭 파일 형태로 있지만은 않다. 런타임에 동적으로 생성되어 클래스 로더에 제공되는 이진 스트림 집합체도 JVM 에 올라가는 클래스 파일일 수 있다.</li>
</ul>
<blockquote>
</blockquote>
<p>다음은 클래스 파일을 테이블 형태로 나타낸 것. 클래스 파일도 일종의 테이블이다.</p>
<pre><code class="language-kotlin">ClassFile {
  u4                magic;
  u2                minor_version;
  u2                major_version;
  u2                constant_pool_count;
  cp_info           constant_pool[constant_pool_count-1];
  u2                access_flags;
  u2                this_class;
  u2                super_class;
  u2                interfaces_count;
  u2                interfaces[interfaces_count];
  u2                fields_count;
  field_info        fields[fields_count];
  u2                methods_count;
  method_info       methods[methods_count];
  u2                attributes_count;
  attribute_info    attributes[attributes_count];
}</code></pre>
<h2 id="테스트-코드">테스트 코드</h2>
<p>이번 포스팅은 </p>
<pre><code class="language-java">package classfiletest;

public class TestClass {
    private int x;
    private int y;

    public int sum() {
        return x + y;
    }

    public int sub(int z) {
        return x + y - z;
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/1a8c8865-a1cd-467f-b47b-7afe7b7d7a88/image.png" alt=""></p>
<p>코드(위)를 javac 로 컴파일 한 뒤 <a href="https://hexfiend.com/">hex 편집기</a> 로 해석한 16진수 값(아래) 와 비교하면서 공부 할 예정이다.</p>
<p>16진수 값은 1Byte 단위로 그룹핑되어 있는 모습이다.</p>
<p><code>javap -v 클래스파일</code> 내용도 참고했다.</p>
<h2 id="매직-넘버와-클래스-파일의-버전">매직 넘버와 클래스 파일의 버전</h2>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/04de39fb-7d96-4b10-8963-fa48911dcbd9/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/5f2bdf21-1fea-4166-a027-624ede46d7b1/image.png" alt=""></p>
<p><strong>매직 넘버</strong>
이 파일이 JVM 에서 사용하는 클래스 파일임을 나타내는 4Bytes(u4) 데이터. 이 파일이 클래스 파일인지 체크하는 용도. <code>CAFEBABE</code> 로 고정.</p>
<p><strong>버전 번호</strong>
앞의 두 바이트(u2) = minor version
뒤의 두 바이트(u2) = major version</p>
<p>JDK1 은 Major Version 이 45 이다.
테스트 코드는 3D = 61 즉, JDK 17(61-45+1) 에서 컴파일 되었음을 알 수 있다.
마이너 버전은 대부분 0 으로 고정이다. JDK 1 의 일부와 12 에서 일부 기능의 베타 출시때 잠시 사용되었다.</p>
<h2 id="상수-풀">상수 풀</h2>
<p>클래스 파일에서 상수 풀
<img src="https://velog.velcdn.com/images/jimmy_neutron/post/191b8186-67e6-42a0-be35-815ee9b24471/image.png" alt="">
테스트 코드에서의 상수 풀 내용(length 표현 1개, 상수 23개)
<img src="https://velog.velcdn.com/images/jimmy_neutron/post/23cc5741-6506-477a-9acf-4c01879a7739/image.png" alt="">
상수 풀에서 쓰이는 타입의 17가지 테이블
<img src="https://velog.velcdn.com/images/jimmy_neutron/post/83fbec83-24f5-4681-a0f2-c55da9050095/image.png" alt=""></p>
<blockquote>
<p><strong>필드 서술자, 메서드 서술자(descriptor)</strong></p>
</blockquote>
<ul>
<li><p>필드 서술자 : 데이터 타입</p>
</li>
<li><p>메서드 서술자 : 매개 변수 목록(개수, 타입, 순서 포함), 반환 타입</p>
<blockquote>
</blockquote>
<p>상수 풀에서 서술자 상수는 다음 표와 같이 표시된다. 객체 타입의 경우 FQCN 앞에 L 을 붙인다.
<img src="https://velog.velcdn.com/images/jimmy_neutron/post/e516bce4-558b-42b1-85e8-1239abf009ad/image.png" alt=""></p>
<blockquote>
</blockquote>
<p>배열 서술자는 차원 수 만큼 앞에 <code>[</code> 가 붙는다. ex) <code>int[]</code> -&gt; <code>[I</code></p>
<blockquote>
</blockquote>
<p>필드 서술자의 경우 위 식별 문자 하나만 표시되나,
메서드 서술자는 N 개의 매개 변수 타입과 반환 타입 모두 나타내므로 규칙을 알아야 한다.
예시를 몇가지 적어보자면</p>
</li>
<li><p>void inc() -&gt; <code>()V</code></p>
</li>
<li><p>java.lang.String.toString() -&gt; <code>()Ljava/lang/String</code></p>
</li>
<li><p>int indexOf(Char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex) -&gt; <code>([CII[CIII)I</code></p>
</li>
<li><p>클래스 내부에서 쓰이는 상수를 저장하는 테이블 구조 데이터</p>
</li>
<li><p>상수 풀에 담기는 상수는 두가지 종류가 있다 : 리터럴 / 심벌 참조</p>
<ul>
<li>리터럴 : 자바에서로 치면 final로 선언된 문자열이나 상수</li>
<li>심벌 참조<ul>
<li>모듈에서 임포트/익스포트 하는 패키지</li>
<li>클래스와 인터페이스의 FQCN</li>
<li>필드 이름과 서술자</li>
<li>메서드 이름과 서술자</li>
<li>메서드 핸들과 메서드 타입</li>
<li>동적으로 계산되는 call site와 동적으로 계산되는 상수</li>
</ul>
</li>
</ul>
</li>
<li><p>심벌 참조는 클래스 파일을 클래스 로더가 로드하고 클래스를 해석할 때 실제 메모리 주소로 변환한다.</p>
</li>
<li><p>모든 데이터 타입은 u1 의 tag 값을 가진다. 자신 데이터 타입을 나타내는 id 라고 볼 수 있으며, 이걸 봐야 뒤에 나오는 몇개의 바이트 스트림이 어떤 의미인지 해석할 수 있다.</p>
</li>
<li><p>bytes 값들은 값 자체를 나타낸다.</p>
</li>
<li><p>index 값은 상수 풀의 상수를 1부터 순서대로 순번을 매겼을 때 해당 순서의 데이터의 포인터다. 이건 실제 예시를 보면 더 쉽다.</p>
</li>
<li><p>가장 앞의 2bytes 는 상수 풀에 들어있는 상수의 개수이다. 예시에서는 0x18(=24) 이며 23개의 상수를 가지고 있다는 뜻</p>
<ul>
<li>이 개수 데이터는 특별히 개수를 1부터 세기에 실제 개수는 -1 해야 한다.</li>
<li>0은 상수 풀 항목을 참조하지 않음을 표현해야하는 특수한 경우에 쓰임</li>
</ul>
</li>
</ul>
<h3 id="실제-예시-해석">실제 예시 해석</h3>
<ul>
<li>위에서 상수를 모두 네모쳐놨다. 개수 데이터를 빼면 총 23개임을 볼 수 있다.</li>
<li>첫번째 데이터는 <code>0A 00 02 00 03</code>. tag=0x0A 면 <code>CONSTANT_Methodref_info</code> 이다. 데이터 구조를 보면 <code>CONSTANT_Class_info</code> 와 <code>CONSTANT_NameAndType_info</code> 의 인덱스를 각각 u2 데이터로 가지고 있다. 즉 02 인덱스 상수가 <code>CONSTANT_Class_info</code>, 03 인덱스의 상수가 <code>CONSTANT_NameAndType_info</code> 이다.<ul>
<li>생성자 메서드다.</li>
</ul>
</li>
<li>두번째 상수가 <code>CONSTANT_Class_info</code> 인지 확인해보자. tag = 07 인 것을 확인할 수 있다. 실제 데이터가 <code>07 00 04</code> 이다. 04 인덱스에는 Utf8 상수 값이 들어있을 것이다.<ul>
<li>위에서 <code>CONSTANT_Methodref_info</code> -&gt; <code>CONSTANT_Class_info</code> 로 왔으므로 인덱스 4에 있는 상수는 메서드 서술자의 완전한 이름 정보가 있을 것이다.</li>
</ul>
</li>
<li>네번째 상수는 tag = 01 인 <code>CONSTANT_Utf8_info</code> 이다. 실제 바이트 스트림은 <code>01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74</code> 이며 실제 데이터 영역만 디코딩해보면 <code>java/lang/Object</code> 이다.<ul>
<li>이런식으로 해석하며, 이 메서드 서술자가 어떤 메서드를 나타내는지 알기 위해선 첫번째 인덱스에 있는 데이터에서 <code>CONSTANT_NameAndType_info</code> 까지 따라가야 한다.</li>
</ul>
</li>
</ul>
<p>바이트 코드 상수값을 항상 이렇게 해석할순 없다.
<strong><code>javap -verbose TestClass</code></strong>
javap 는 클래스 파일 디스어셈블 명령이고, -verbose 옵션은 더 자세히 나타낸다.</p>
<p>이 명령을 실행하면 아래와 같이 상수풀을 쉽게 볼 수 있다.
<img src="https://velog.velcdn.com/images/jimmy_neutron/post/40f944ca-fd3c-4368-9c49-0557d79bd689/image.png" alt=""></p>
<blockquote>
<p>CONSTANT_Utf8_info 의 length 가 u2 타입이다.
이는 2바이트이며 나타낼 수 있는 최대값은 65535 이다.
따라서 자바 프로그램에서 변수나 메서드의 이름은 64KB 를 넘으면 컴파일 되지 않는다.</p>
</blockquote>
<h2 id="접근-플래그">접근 플래그</h2>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/171d74fa-cc08-442b-957f-fedb4df5f68a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/aa66da3a-0f0a-4a9c-a2ec-958153cdbe79/image.png" alt=""></p>
<p>대상 클래스(인터페이스)의 접근제한자, final 등의 정보이다.
u2, 2Bytes 정보이며 비트로 정보를 나타낸다. 따라서 총 16개의 정보를 true/false 로 나타내며, 현재는 다음과 같이 9개 비트만 쓰인다.</p>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/28d44578-9581-4ac1-b503-6703c1f8b699/image.png" alt=""></p>
<p>ACC_SUPER 에 대해 보충설명 하자면, invokespeical 바이트코드 명령어는 JDK 1.0.2 버전 이전과 이후 명령어 의미가 다르다. JDK 1.0.2 이상이라면 항상 이 플래그는 true 다.</p>
<p>테스트 코드의 경우 public 클래스이기 때문에, <code>0x0001 | 0x0020</code> = <code>0x0021</code> 임을 볼 수 있다.</p>
<h2 id="클래스-인덱스-부모-클래스-인덱스-인터페이스-인덱스">클래스 인덱스, 부모 클래스 인덱스, 인터페이스 인덱스</h2>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/c3dce990-b0ed-4432-964b-5aa9539fd69c/image.png" alt="">
<img src="https://velog.velcdn.com/images/jimmy_neutron/post/efaf8685-a4c5-4ef6-9e8a-14c0374fd8ac/image.png" alt=""></p>
<ul>
<li>클래스 인덱스 : 상수 풀에서 클래스 정보를 가진 상수의 인덱스</li>
<li>부모 클래스 인덱스 : 상수 풀에서 부모 클래스 정보를 가진 상수의 인덱스(바로 윗 부모만)<ul>
<li>다중 상속 안되므로 컬렉션이 아닌 하나</li>
</ul>
</li>
<li>인터페이스 인덱스 : 상수 풀에서 인터페이스 정보를 가진 상수의 인덱스 <code>들</code><ul>
<li>인터페이스는 여러개 구현할 수 있으므로, <code>_count</code> 를 가진 컬렉션으로 표현</li>
</ul>
</li>
</ul>
<p>예시에서는 <code>00 08 00 02 00 00</code> 이다.
<code>00 08</code> 상수풀을 따라가면 <code>classfiletest/TestClass</code> 라는 클래스 이름이 나오고,
<code>00 02</code> 상수풀을 따라가면 <code>java/lang/Object</code> 라는 상위 클래스가 나온다. (상속을 선언하지 않았으므로 자바 기본 상위 클래스인 Object)
인터페이스 구현은 하지 않았으므로 <code>00 00</code> 으로 작성되었음을 알 수 있다.</p>
<h2 id="필드-테이블">필드 테이블</h2>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/b9769a71-f7e1-4ae0-96dc-5aa046df691a/image.png" alt="">
<img src="https://velog.velcdn.com/images/jimmy_neutron/post/0fe86d72-c7e7-4d4b-9da9-48a2cd67637c/image.png" alt=""></p>
<pre><code class="language-java">// 필드 테이블 구조
field_info {
  u2                access_flags;
  u2                name_index; // 필드 단순 이름의 상수풀 인덱스
  u2                descriptor_index; // 필드 서술자의 상수풀 인덱스
  u2                attributes_count;
  attribute_info    attributes[attributes_count];
}</code></pre>
<ul>
<li>필드 테이블은 클래스 변수와 인스턴스 변수를 포함한다. 메서드 변수는 필드가 아니므로 표현되지 않는다.</li>
<li>필드 테이블 부분은 필드 개수를 나타내는 fields_count(u2) 와 그 개수만큼의 field_info 테이블이 나열된다. 테스트의 경우 <code>00 02</code> 로 두개의 필드가 있다.</li>
<li>상속받은 필드는 나열하지 않는다.</li>
<li>중첩 클래스의 암묵적 this 처럼 코드에 나타나지 않고, 컴파일러가 자동으로 추가한 필드가 있을 수 있다.</li>
<li>자바에서는 필드 이름이 같은 오버로딩을 할 수 없지만, 클래스 파일에서는 이름이 같고 서술자가 다른 두 필드는 다른 필드로 취급한다. 이를 이용하여 필드 오버로딩이 가능한 언어를 만들 수 도 있겠다.</li>
</ul>
<h3 id="access_flag">access_flag</h3>
<p>field_info 의 첫번째 항목이다. 접근제한자 외 필드에 붙을 수 있는 것들을 표시한다.</p>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/d52abda9-3f25-4fc7-8a9e-f1393992aeb1/image.png" alt=""></p>
<p>테스트에서 선언한 두 필드 모두 private 외에는 딱히 없으므로, 두 필드 모두 <code>00 02</code> 이다.</p>
<h3 id="name_index-descriptor_index">name_index, descriptor_index</h3>
<p>다음은 필드 단순 이름과 필드 서술자의 상수 풀 인덱스이다.</p>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/acbdf8d7-e481-4fb7-92bc-b135a72d76f1/image.png" alt="">
<code>00 0B 00 0C</code> -&gt; x:I
<code>00 0F 00 0C</code> -&gt; y:I
위 상수풀과 같이 해석하면 필드의 FQCN 이 아닌 단순 이름 + 타입(I. Integer) 를 나타내는 상수 풀의 인덱스를 가진다.</p>
<h3 id="attributes">attributes</h3>
<p>만약 값이 초기화가 되어 있고 한다면 이 attribute 테이블에 명시된다.
이는 이후 속성 테이블에서 더 설명할 예정.</p>
<h2 id="메서드-테이블">메서드 테이블</h2>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/c8a98c70-7d26-498d-91d9-c42e33bd8c8d/image.png" alt="">
<img src="https://velog.velcdn.com/images/jimmy_neutron/post/cca70a26-b98e-443d-adbe-cf668a5719a9/image.png" alt=""></p>
<pre><code>method_info {
  u2                access_flags;
  u2                name_index; // 메서드 단순 이름의 상수풀 인덱스
  u2                descriptor_index; // 메서드 서술자의 상수풀 인덱스
  u2                attributes_count;
  attribute_info    attributes[attributes_count];
}</code></pre><blockquote>
<p>좀 길지만, 메서드 세개(<code>00 03</code>) 과 이후 세개의 메서드를 빨간색 네모로 구분했다. 생성자 1개와 선언된 메서드 2개가 그 내용이다.</p>
</blockquote>
<ul>
<li>메서드 테이블은 필드 테이블과 형태가 같다.</li>
<li>자동으로 만들어지는 메서드가 있을 수 있다. <code>&lt;init&gt;()</code>, <code>&lt;cinit&gt;()</code> 이 그 예다.</li>
<li>자바에서 메서드 오버로딩 판단에는 반환 타입이 들어가지 않는다. 즉, 자바에서는 메서드 이름과 매개변수가 같은데 반환 타입을 다르게 하여 오버로딩 할 수 없다. 반면 클래스 파일에서는 반환 타입 서술자만 다르게 하는것이 문제되지 않는다.</li>
</ul>
<h3 id="access-flags">access flags</h3>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/f1d8e3d9-5ecd-49fc-a45d-9b9f8f206760/image.png" alt=""></p>
<h2 id="클래스-파일-레벨-attributes">클래스 파일 레벨 Attributes</h2>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/af0bd52b-8d2a-48d2-a62d-0f34331d5c11/image.png" alt="">
<img src="https://velog.velcdn.com/images/jimmy_neutron/post/87b3d489-8d67-4a84-b9e2-984f592d2688/image.png" alt=""></p>
<p>attribute 는 뒤에서 자세히 설명하겠다. 여기서는 테스트 클래스의 내용만 설명해보자.</p>
<ul>
<li><code>00 01</code> : 클래스 파일 레벨의 attributes count. 1개</li>
<li><code>00 16</code> : attribute_name_index. 상수 풀에서 0x16(22) 를 조회하면 <code>SourceFile</code> 이 있다.</li>
<li><code>00 00 00 02</code> : 속성 테이블 길이</li>
<li><code>00 17</code> : 상수 풀의 0x17(23) 인덱스에는 <code>TestClass.java</code> 가 있다. 이 소스 파일의 이름이다.</li>
</ul>
<h2 id="attributes-1">Attributes</h2>
<p>위에 나오지 않은 메서드의 연산 로직이라던지, 애너테이션, 런타임 디버깅에 사용할 줄번호 등은 어디에 있을까.
또 비교적 최근에 나온 sealed class, record class, module 임을 나타내는 정보는 어디 있을까.</p>
<p>이런 나머지 정보들이 들어가는 곳이 Attributes 다.</p>
<p>Attributes 는 아래 명시한 총 5가지의 사용처에서 각각 사용처를 설명하기 위한 값들이 들어간다. 예를들어 클래스 파일 attribute 로 Record Attributes 가 포함된다면 해당 클래스는 레코드 클래스임을 클래스파일에서 명시한 것이다.</p>
<h3 id="attributes-종류">Attributes 종류</h3>
<p>JDK25 기준 클래스파일 구조에서 <a href="https://docs.oracle.com/javase/specs/jvms/se25/html/jvms-4.html#jvms-4.7">Attributes 는 총 30가지</a>이다.</p>
<p>각각의 Attribute 는 사용처가 다르다. 예를 들어 Code Attribute 는 메서드 테이블에서만 쓰인다. 반면 애너테이션을 표현하는 RuntimeVisibleAnnotation Attribute 는 클래스 파일, 메서드 테이블, 필드 테이블, Record Attribute 의 record_component_info 에서 공통적으로 쓰일 수 있는 속성이다.</p>
<h3 id="attributes-사용처">Attributes 사용처</h3>
<p>JDK25 기준 클래스파일 구조에서 Attributes 타입을 쓰는 곳은 다섯곳이다.</p>
<ul>
<li><a href="https://docs.oracle.com/javase/specs/jvms/se25/html/jvms-4.html#jvms-4.1">클래스파일</a></li>
<li><a href="https://docs.oracle.com/javase/specs/jvms/se25/html/jvms-4.html#jvms-4.5">필드테이블</a></li>
<li><a href="https://docs.oracle.com/javase/specs/jvms/se25/html/jvms-4.html#jvms-4.6">메서드테이블</a></li>
<li><a href="https://docs.oracle.com/javase/specs/jvms/se25/html/jvms-4.html#jvms-4.7.3">Code Attribute</a></li>
<li><a href="https://docs.oracle.com/javase/specs/jvms/se25/html/jvms-4.html#jvms-4.7.30">Record Attribute 의 record_component_info</a></li>
</ul>
<h3 id="attributes-공통-구조">Attributes 공통 구조</h3>
<pre><code>attribute_info {
  u2 attribute_name_index;
  u4 attribute_length;
  u1 info[attribute_length];
}</code></pre><p>Attribute 는 위의 공통 구조 하에서 각각 다른 형태를 가진다.
각 Attribute 가 다른 구조이지만 위 구조는 공통적으로 포함한다.</p>
<h4 id="code-속성">Code 속성</h4>
<pre><code>Code_attribute {
    u2 attribute_name_index; // Code 상수를 가지고 있는 상수 풀의 인덱스
    u4 attribute_length; // 이후 나올 모든 Code Attribute 의 길이
    u2 max_stack;
    u2 max_locals;
    u4 code_length;
    u1 code[code_length];
    u2 exception_table_length;
    {   u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}</code></pre><p>코드 속성은 어쩌면 가장 중요한 속성이다. 메서드 테이블의 Attribute 로 존재하며, 메서드의 바이트 코드 명령어를 담고 있다.</p>
<ul>
<li>max_stack : 피연산자 스택의 최대 깊이. 메서드 실행 중 이 스택의 깊이를 절대 넘을 수 없으며, 가상 머신은 깊이가 이 값만큼인 피연산자 스택을 스택 프레임에 할당</li>
<li>max_local : 지역 변수 테이블에 필요한 저장소 공간. 단위는 변수 슬롯(32비트. long, double 은 슬롯 두개 차지). 지역 변수의 개수가 아니라 슬롯 개수임을 유의. 지역 변수 테이블에는 매개 변수(this 포함), try-catch 매개 변수 예외, 지역 변수 모두 들어간다. 또한 이 값은 단순 지역 변수 테이블에 들어갈 요소 개수가 아니다. jvm 은 메모리를 절약하기 위해 슬롯을 최소한으로 만들어 재사용한다. 따라서 동시에 존재해야 하는 최대값을 max_local 로 정한다.</li>
<li>code_length : 코드 길이. u4 이지만 jvm 명세에 65535 라 정해져서 이를 넘을 수 없다. 사실상 u2 타입이다.</li>
<li>code : 실제 바이트 코드 명령어 스트림이다. 바이트 코드 명령어는 u1 이며, 실제로 바이트코드는 1바이트가 표현한 256개 이하이다. 각 <a href="https://docs.oracle.com/javase/specs/jvms/se25/html/jvms-7.html">바이트 코드 명령어는 1바이트 OpCode 와 맵핑</a>되어 있다.
이 부분을 해석할 때에는 명령어의 바이트도 필요하지만 명령어가 매개 변수를 필요로 하는가도 알아야 함. 예를 들어 invokespecial 같은 경우 뒤에 2바이트의 매개변수가 따라온다. 매개변수는 상수 풀의 인덱스 값이 적힌다.<ul>
<li>명령어가 실행되어야 하는 순서대로 클래스 파일에 작성된다.</li>
</ul>
</li>
</ul>
<blockquote>
<p>javap -verbose 를 통해 code 를 보면 args_size 도 나온다.
메서드 매개 변수의 개수이며, 메서드 서술자로 계산된 값이다.
this 도 암묵적으로 들어가기 때문에 인스턴스 메서드의 경우 기본적으로 매개 변수가 없다면 1이다.
Static 메서드의 경우 this 가 없기에 매개 변수가 없다면 0이다.</p>
</blockquote>
<ul>
<li>exception_table : 예외 처리 구문이 있는 경우 이 테이블이 생성된다. 예외 처리 대상 바이트코드 명령어 인덱스 범위(try 블록)인 start_pc 와 end_pc, 예외 처리 코드 (catch 블록) 인 handler_pc, 그리고 예외 타입인 catch_type 네가지 필드를 가진 테이블이 try-catch-finally 구문 개수만큼 클래스 파일에 만들어진다. (아래 예시와 같이)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/aab91309-17e3-4288-9aa2-1081309d1a55/image.png" alt=""></p>
<h4 id="그외">그외</h4>
<ul>
<li><code>Exceptions</code> : 메서드 레벨에 throws 로 명시된 예외 정보</li>
<li><code>LineNumberTable</code> : 코드 라인넘버와 바이트 코드 오프셋의 매핑이다. 이 기능 덕분에 예외시 스택 트레이스에 라인 넘버가 나오며, 디버깅시 중단점을 적용할 수 있다. <ul>
<li>javap 에 LineNumberTable 과 Code Attribute 를 같이 보면 어떤 라인에서 어떤 바이트 코드 명령어가 쓰인건지 알 수 있다.</li>
</ul>
</li>
<li><code>LocalVariableTable</code> : Code 의 속성으로, 지역 변수 테이블 안의 변수와 소스 코드의 변수 사이의 관계를 나타낸다. 지역 변수 유효 범위 바이트 코드 인덱스와 변수 이름, 서술자가 있고 지역 변수 테이블에서의 슬롯 인덱스가 있다.<ul>
<li>이 정보를 통해 외부에서 매서드 사용시 매개 변수의 이름을 알 수 있다. 이게 없으면 arg0 이런식으로 보임</li>
<li>책에는 기본 활성화 된다 되어있는데... 나는 <code>javac -g</code> 옵션을 줘서 컴파일해야 보인다.</li>
<li>Code 의 속성이므로 Code 속성이 없는 abstract 클래스의 메서드나 인터페이스 메서드의 매개 변수를 포함할 수 없다. 이를 해결하려면 MethodParameters 속성을 사용한다.</li>
</ul>
</li>
<li><code>LocalVariableTypeTable</code> : LocalVariableTable 와 같은 내용이나 제네릭 매개 변수용이다. 제네릭이라 Code 속성이 아닌 클래스 파일 속성이다.(제네릭은 클래스 객체 단위로 정해지니) 타입 소거 때문에 서술자는 매개 변수화 된 타입 정보를 담을 수 없으므로 따로 만들었다.</li>
<li><code>SourceFile</code> : 파일 이름이다. 이게 없으면 예외 발생시 어느 파일에서 발생했는지 보이지 않는다.</li>
<li><code>ConstantValue</code> : 필드 테이블의 속성이다. 정적 변수(클래스 변수, static) 에 본인이 가진 상수 풀 인덱스의 값을 초기화 하라고 알리는 역할<ul>
<li>클래스 변수는 클래스 생성자인 <code>&lt;clint&gt;()</code> 또는 <code>ConstantValue</code> 속성 중 하나를 사용해서 초기화할 수 있다. 오라클 javac 는 final static 중 기본타입과 String 을 ConstantValue 를 만들어 준다.</li>
<li>final static 변수에 ConstantValue 를 만들긴 하지만 해당 필드에 ACC_FINAL 플래그가 켜지진 않는다. 자바 가상 머신 명세에는 ACC_STATIC 만 활성화 하도록 명시하였고, 오라클 javac 가 final static 에 대해 만든다는건 컴파일러에서 구현된 제약이다.</li>
<li>ConstantValue 의 속성은 상수 풀의 인덱스를 가진다. 따라서 클래스 변수 중 객체 타입은 ConstantValue 를 통해 초기화할 수 없기에 오라클이 그렇게 만든 것.</li>
</ul>
</li>
<li><code>InnerClasses</code> : 내부 클래스들과 호스트 클래스의 관계</li>
<li><code>Deprecated</code> : 내용이 boolean 인 속성. @Deprecated 에 생성됨</li>
<li><code>Synthetic</code> : 내용이 boolean 인 속성. 컴파일러가 자동으로 추가한 필드나 메서드를 표현.<ul>
<li>ex) enum 의 원소 배열, 중첩 클래스의 브릿지 메서드</li>
<li><code>&lt;clinit&gt;()</code>, <code>&lt;init&gt;()</code> 는 자동으로 만들어지긴 하지만 예외</li>
</ul>
</li>
<li><code>StackMapTable</code> : Code 의 속성. 클래스 로딩시 파일의 타입 검증기에서 타입 검사와 적법성 검사에 사용. JDK5 까지의 타입 추론 검사기를 대체하여 성능이 좋아진 역사적인 그런..</li>
<li><code>Signature</code> : 클래스 파일, 필드 테이블, 메서드 테이블 다 가능. 제네릭 시그니처 정보를 담음. 이거 덕분에 리플렉션으로 제네릭 타입 정보를 얻을 수 있음.</li>
<li><code>BootstrapMethods</code> : 클래스 파일 속성. invokedymanic 명령어가 참조하는 부트스트랩 메서드 한정자가 담긴다.</li>
<li><code>MethodParameters</code> : 메서드 테이블의 속성이다. 메서드 파라미터의 이름이 담긴다. LocalVariableTable 이 Code 속성에 있어서 추상 메서드나 인터페이스 메서드의 매개 변수를 표현할 수 없음을 보완하기 위해 만들어졌다.<ul>
<li>컴파일시 <code>-parameters</code> 옵션을 명시적으로 줘야 한다. (기본적으로 생성되지 않는다)</li>
<li>이를 통해 리플렉션으로 메서드 이름을 가져올 수 있다.<ul>
<li>아래 예시 코드 [1] 의 코드를 <code>-parameters</code> 컴파일 옵션을 주고 안주고 차이를 보자. 출력에 이름이 args0 이렇게 보이다가 실제 매개 변수 이름이 보일 것.</li>
</ul>
</li>
<li>jar 파일 만들때 이 옵션 없이 만들면 사용자가 불편할 것. spring 을 gradle 로 jar 파일 빌드하면 이 옵션이 자동으로 들어간다. (빌드 파일을 javap 로 보면 이 속성이 있음)</li>
</ul>
</li>
<li><code>RuntimeVisibleAnnotations</code>, <code>RuntimeInvisibleAnnotations</code>, <code>RuntimeVisibleParameterAnnotations</code>, <code>RuntimeInvisibleParameterAnnotations</code> : 애너테이션 정보를 담는다. 리플렉션으로 애너테이션 정보를 가져올 때 사용된다.</li>
<li><code>Record</code> : 클래스 파일 속성이며, 해당 클래스가 Record Class 임을 나타낸다.</li>
<li><code>PermittedSubclasses</code> : 클래스 파일 속성이며, 해당 클래스가 Sealed Class 임을 나타낸다.<ul>
<li>허용하는 클래스의 상수 풀 인덱스 목록을 가진다.</li>
<li>Sealed Class 는 자신 내부에서만 하위 클래스를 가질 수 있는 상위 클래스다. 따라서 하위 클래스가 딱 정해져서 코틀린에서는 switch 에서 else 를 쓰지 않아도 되는 그런 부가적인 이점이 있다.</li>
</ul>
</li>
</ul>
<h1 id="예시-코드">예시 코드</h1>
<p>[1]</p>
<pre><code class="language-java">import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class TestInterface {
    public static void main(String[] args) throws Exception {
        Method m = TestInterface.class.getDeclaredMethod(
                &quot;transfer&quot;,
                String.class, String.class, long.class
        );

        System.out.println(&quot;=== parameter info ===&quot;);
        for (Parameter p : m.getParameters()) {
            System.out.println(
                    &quot;name=&quot; + p.getName()
                            + &quot;, isNamePresent=&quot; + p.isNamePresent()
            );
        }
    }

    static void transfer(String fromUserId,
                         String toUserId,
                         long amount) {
        // no-op
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[저지연 컬랙터 - 셰넌도어, ZGC]]></title>
            <link>https://velog.io/@jimmy_neutron/%EC%A0%80%EC%A7%80%EC%97%B0-%EC%BB%AC%EB%9E%99%ED%84%B0-%EC%85%B0%EB%84%8C%EB%8F%84%EC%96%B4-ZGC</link>
            <guid>https://velog.io/@jimmy_neutron/%EC%A0%80%EC%A7%80%EC%97%B0-%EC%BB%AC%EB%9E%99%ED%84%B0-%EC%85%B0%EB%84%8C%EB%8F%84%EC%96%B4-ZGC</guid>
            <pubDate>Mon, 19 Jan 2026 15:53:45 GMT</pubDate>
            <description><![CDATA[<p>GC 는 처리량, 메모리 사용량, 지연 시간 세가지 지표가 있다. 처리량과 메모리 사용량은 하드웨어가 발전하며 좋은 지표를 내기 쉽지만, 지연 시간은 그렇지 않다. 더 많은 용량의 RAM 이 개발될 수록 GC 를 위한 시간은 점점 더 멀어진다.</p>
<p>따라서 GC 는 지연 시간을 줄이는 방향으로 발전했다.</p>
<p>G1 GC 은 Young GC 및 Mixed GC 에서 생존한 객체를 다른 Region 으로 옮기는 소위 <code>Compact</code> 라 불리는 작업은 <code>STW</code> 로 동작한다.
저지연 컬렉터는 Compact 를 유저 스레드와 동시에 진행하여 아주 낮은 일시 정지로 낮은 지연 시간을 달성하는 컬렉터이다.</p>
<p>처리량 영향을 최소로 하면서 10ms 이하의 일시 정지 시간이 목표다.</p>
<h1 id="shenandoah">Shenandoah</h1>
<h2 id="개요">개요</h2>
<p>섀넌도어는 레드햇에서 만든 저지연 컬렉터이다. 포워딩 포인터 기법을 이용하여 Compact 를 동시에 처리한다.</p>
<p>Oracle JDK 에서는 제공하지 않으므로, Oracle JDK 를 유료로 지원받을 계획이 있다면 사용하지 않는게 좋다.</p>
<p>세대구분 셰넌도어도 존재하나, 처음에는 세대구분이 없었다. 세대 구분 없는 셰넌도어 기준으로 설명한다.</p>
<h2 id="gc-cycle">GC Cycle</h2>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/9f5b1003-cbaf-40dd-a732-7f6e8bb22cf5/image.png" alt="">
<a href="https://wiki.openjdk.org/spaces/shenandoah/pages/25002018/Main?utm_source=chatgpt.com">https://wiki.openjdk.org/spaces/shenandoah/pages/25002018/Main?utm_source=chatgpt.com</a></p>
<p>OpenJDK 문서에 따르면 셰넌도어는 9가지 단계로 이뤄진다.</p>
<ol>
<li><p>최초 표시 : GC Root 를 스캔 마킹</p>
</li>
<li><p>동시 표시 : 마킹된 GC Root 로 부터 동시 마킹, 변경이 발생하면 SATB 큐에 기존 값 저장</p>
</li>
<li><p>최종 표시 : 동시 마킹시 발생한 참조 변경(SATB 큐)에 의해 발생한 마킹을 시작 스냅샷 기준으로 수정하기 위한 최종 작업과 회수 집합을 생성</p>
</li>
<li><p>동시 청소 : 빈 리전 회수</p>
</li>
<li><p>동시 이주 : 마킹시 생존한 객체를 유저 스레드와 동시에 다른 리전으로 복사하며 Compact</p>
</li>
<li><p>최초 참조 갱신 : 동시 참조 갱신을 위한 준비 - 동시 이주 작업이 모두 끝나길 기다리는 벽 역할을 하는 STW</p>
</li>
<li><p>동시 참조 갱신 : 생존한 객체를 참조하는 곳을 변경.</p>
<ul>
<li>기존 마킹을 진행한 객체 참조에서 복사/이주 한 객체 참조로 변경.</li>
<li>GC Root 에서 객체 참조를 따라가는 방식이 아닌, 힙 메모리를 물리적인 순서대로 선형 검색하며 변경.</li>
</ul>
<ol start="8">
<li>최종 참조 갱신 : GC Root 의 참조도 복사/이주 한 객체 참조로 변경. GC Root 이기에 STW.</li>
</ol>
</li>
<li><p>동시 청소 : 4번과 같은 역할. 참조 갱신까지 완료된 후 빈 영역 회수</p>
</li>
</ol>
<h2 id="포워딩-포인터와-barrier">포워딩 포인터와 barrier</h2>
<p>메모리 파편화를 피하기 위해서는 Compact 과정이 불가피하다. 즉, 객체가 다른 위치로 이동해야 한다는 것이다.</p>
<p>이 때 고려해야할 것은 두가지다.
첫번째로, 객체를 이동할 때 복사를 하고 기존 객체를 나중에 정리할 것이냐 or 새 위치에 새로운 객체를 생성하면서 기존 객체를 삭제 할 것이냐다.
언뜻 보면 객체를 두벌 관리하여 공간을 차지하는 복사보단 삭제후 생성이 좋아 보인다. 하지만 삭제 - 생성하게 되면 기존 객체가 사라지니 반드시 기존 객체에 대한 참조를 새로운 객체 참조로 변경하는 것도 같이 발생해야 한다. 하지만 특정한 하나의 객체를 새로 생성할 때 해당 객체를 사용하는 곳을 모두 찾아 참조를 변경하는 것은 STW 에서 일어나야 하며, 이는 시간이 많이 걸린다. 따라서 두벌을 유지하는 복사 이주 방식을 택한다.</p>
<p>두번째로, 그럼 복사 이동을 택했을 때 객체를 참조하는 곳의 참조값을 이전 참조값에서 새 참조값으로 변경하는 방법이다. 이는 아래에서 자세히 설명하겠다.</p>
<h3 id="동시-복사이주의-문제">동시 복사/이주의 문제</h3>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/2dec60de-a4db-440c-8292-4dded9b4fd74/image.png" alt="">
<a href="https://shipilev.net/talks/javazone-Sep2018-shenandoah.pdf">https://shipilev.net/talks/javazone-Sep2018-shenandoah.pdf</a></p>
<p>위 그림은 7번 <code>동시 참조 갱신</code> 중의 살아남은 객체의 상태다.
객체는 동시 이주 단계를 거쳐 두벌로 복사된 상태이다.
검정 네모는 GC Root 이며, 객체를 참조하는 GC Root 는 총 4개가 있다.</p>
<p>동시 참조 갱신 단계 중 2개의 GC Root 참조는 이주/복사된 새 객체를 참조하도록 갱신되었으나, 2개의 GC Root 참조는 아직 이주/복사 이전 객체를 참조한다. 이러한 타이밍은 반드시 있다.</p>
<p>이 때 만약 예를들어 참조를 옮기지 못한 왼쪽의 GC Root 를 통해 값의 수정이 이뤄지고(y=2 -&gt; 4), 참조 갱신이 끝난 후 값을 조회한다면 수정되지 않은(y=2) 의 값을 읽어버릴 수 있다.</p>
<h3 id="포워딩-포인터와-barrier-1">포워딩 포인터와 barrier</h3>
<p>이를 위해 컬러 포인터 개념이 있다.</p>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/42a5eddd-5c6c-432e-9e50-69bea7152094/image.png" alt="">
포워딩 포인터는 위와 같이 기존 객체 layout 앞에 추가된 포인터이다.
객체 레퍼런스를 가지고 있으며, 시스템에 따라 32/64 bits 이다.</p>
<p>이 포인터는 처음에는 위 그림처럼 자기 자신 객체 참조 주소를 가진다.</p>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/368422f9-3a47-41be-b05b-8da3f0207d25/image.png" alt="">
복사시점의 상태다. from-space(기존 객체) 와 to-space(복사/이동 된 객체) 의 포워딩 포인터는 모두 자기 자신을 가리킨다.</p>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/6f556384-6cac-470f-9606-cc6528464990/image.png" alt="">
복사가 완료되면 from-space 포워딩 포인터를 to-space 를 가리키도록 변경한다.
이 동작에는 동시성 문제가 있는데,</p>
<ol>
<li>복사</li>
<li>유저 스레드 의 기존 참조 수정</li>
<li>from-space 포워딩 포인터 변경</li>
</ol>
<p>위와 같이 복사 후 포워딩 포인터를 옮기기 전 mutator 스레드의 객체 참조가 발생하면 from-space 객체를 볼 수 있다.
따라서 쓰는 동작에서 CAS 를 이용한 write barrier 가 존재하여 to-space 객체에 쓸 수 있도록 동작한다.</p>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/325212e6-91fb-4af0-a707-81af3ddef154/image.png" alt="">
이 그림은 참조 갱신 중에도 정상적으로 포워딩 포인터를 통해 to-space 의 객체를 수정하는 예시다.
<img src="https://velog.velcdn.com/images/jimmy_neutron/post/ab31b449-49c8-4630-b9f0-b03ee493637f/image.png" alt="">
모든 참조 갱신이 완료되면 기존  from-space 를 정리할 수 있다.</p>
<h2 id="loadreferencebarrier">LoadReferenceBarrier</h2>
<p>셰넌도어 1.0 에서는 두벌로 존재하는 객체에서 힙 일관성을 지키기 위해 모든 참조에 대해 Write/Read 배리어를 건다.</p>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/6c4977b0-466c-4a04-8a8c-69d5b251f395/image.png" alt="">
<a href="https://developers.redhat.com/blog/2019/06/27/shenandoah-gc-in-jdk-13-part-1-load-reference-barriers?p=602377">https://developers.redhat.com/blog/2019/06/27/shenandoah-gc-in-jdk-13-part-1-load-reference-barriers?p=602377</a></p>
<p>위의 코드에서 value 객체가 primitive type 이라도 barrier 를 건다.
barrier 를 쓰는 이유는 참조 타입 객체를 읽거나 쓸 때 해당 참조 타입 객체가 이동되는걸 방지하기 위한 것이라서, primitive type 은 사실 barrier 를 걸지 않아도 된다.</p>
<p>LoadReferenceBarrier 의 탄생으로 객체를 읽을때 to-space 에서 읽는것을 보장함으로서 barrier 의 수를 많이 줄일 수 있었다.</p>
<h2 id="포워딩-포인터-객체-헤더-통합">포워딩 포인터 객체 헤더 통합</h2>
<p>포워딩 포인터는 태초에 위처럼 마크 워드 앞에 자리를 차지했다.
마크 워드는 최하위 2비트의 플래그 비트로 여러 데이터를 표현할 수 있었는데, 그 자리에 뒀으면 안됐을까?</p>
<p>기존의 섀넌도어에서는 모든 객체 접근에서 to-space 를 보기 위해 마크워드 디코딩 “매번” 실행하여 검사하는 hotpath가 되어버려서 비용이 너무 컸다.</p>
<p>Load Reference Barrier 의 등장으로 포워딩 포인터는 mark-word 에 병합되었다.</p>
<ol>
<li>Barrier 가 모든 객체, 특히 primitive 접근에 필요하지 않다.<ul>
<li>복사가 되었는지 확인하기 위해 포워딩 포인터를 확인하는 횟수가 많이 줄어 디코딩 비용이 줄었다.</li>
</ul>
</li>
<li>from-space 객체에 더이상 접근하지 않는다.<ul>
<li>from-space 의 내용을 모두 지우고, 포워딩 포인터 데이터만 남긴다.</li>
<li>복사되어 포워딩해야 하는가, 아직 복사 안되어 포워딩 안해도 되는가의 2가지만 디코딩 하면 되어 디코딩 비용이 줄었다.</li>
</ul>
</li>
</ol>
<p>이로 인해 메모리 사용량이 많이 줄게 되었다.</p>
<p><a href="https://developers.redhat.com/blog/2019/06/28/shenandoah-gc-in-jdk-13-part-2-eliminating-the-forward-pointer-word?utm_source=chatgpt.com">https://developers.redhat.com/blog/2019/06/28/shenandoah-gc-in-jdk-13-part-2-eliminating-the-forward-pointer-word?utm_source=chatgpt.com</a></p>
<h1 id="zgc">ZGC</h1>
<h2 id="개요-1">개요</h2>
<p>ZGC 는 오라클에서 주도로 만든 저지연 컬렉터이다. 마찬가지로 Compact 를 유저 스레드와 동시에 진행하여 저지연을 달성한다.</p>
<p>Oracle 문서를 보면 다음과 같이 표현되어 있다.
<code>Scalable Low-Latency Garbage Collector</code>
Scalable 이란, 힙의 크기나 객체의 양이 많아져도 여전히 낮은 일시 정지 시간과 높은 처리량을 낼 수 있다는 말이다.</p>
<p>세대구분 ZGC도 존재하나, 처음에는 세대구분이 없었다. 세대 구분 없는 ZGC 기준으로 설명한다.</p>
<h2 id="page">Page</h2>
<p>ZGC 는 메모리를 Page 로 관리한다. (G1 의 리전과 비슷)
G1 과 다르게 ZGC 의 Page 는 소/중/대 세가지가 존재한다.</p>
<ul>
<li>소 : 2MB 고정 크기. 256KB 미만의 작은 객체 저장용</li>
<li>중 : 32MB 고정 크기. 256KB 이상 4MB 이하 객체 저장용</li>
<li>대 : 크기가 동적으로 변할 수 있음. 4MB 이상의 큰 객체 저장용.<ul>
<li>대는 큰 객체 하나만 저장. 따라서 32MB 를 담을 수 있는 중리전보다 가지고 있는 실제 객체크기가 적을 수 있음.</li>
<li>큰 객체의 복사 비용이 높기에, 재배치의 대상이 되지 않음.</li>
</ul>
</li>
</ul>
<p>Page 는 동적으로 생성/파괴되며, 동적으로 바뀔 수 있다.</p>
<h2 id="gc-cycle-1">GC Cycle</h2>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/9a5f5449-9791-4337-b938-5f4e64338513/image.png" alt=""></p>
<h4 id="1-stw1--start-of-the-zgc">1. STW1 : Start Of The ZGC</h4>
<p>이 단계에서는 크게 세가지 일을 한다.</p>
<ol>
<li>Good Color 를 001(marked0) 또는 010(marked1) 중 선택한다. 전 사이클에 사용되지 않은 것이 선택된다.</li>
<li>동시 마킹 중 새로 생성된 객체를 할당할 Page 를 생성한다. 이 Page 에 있는 객체는 모두 이번 사이클에서 Marked 로 간주하여 Mark Stack 에 적재한다.</li>
<li>GC Root 를 마킹 : GC Root 에 Mark Barrier 가 적용된다.</li>
</ol>
<h4 id="2-concurrent-markingremapping">2. Concurrent Marking/Remapping</h4>
<blockquote>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/f02b71a3-f543-4bb9-8ab6-aef86c72e372/image.png" alt="">
위는 마킹 루프 스켈레톤 코드다. 마크 스택의 객체를 pop 하여 mark_obj(bitmap 에 마킹)하고, MB(Marking Barrier) 로직을 실행한다.
아래는 마킹 배리어(MB) 는 마킹 단계에서 컬러포인터를 확인하는 과정, 로드 베리어(LB) mutate thread 의 객체 참조 과정에서 컬러포인터를 확인하는 과정을 보여준다.</p>
</blockquote>
<p>이 단계에서는 객체 마킹과 이전 사이클 Relocation 객체의 Remapping 을 동시에 진행한다.</p>
<p>위 marking loop 스켈레톤 코드가 이 단계에서 진행하는 내용을 간략화 한 것이다. mark_obj() 함수에서는 bitmap 에 살아있음을 마킹하고, 이후에 해당 객체의 필드들을 순회하며 Mark Barrier 를 적용한다. </p>
<p>MB 에서는 컬러포인터를 확인하고 EC 에 포함되는 객체라면 Remapping, 아니면 컬러포인터 수정을 한 뒤 Marking Stack 에 쌓는다.</p>
<blockquote>
<p>이 단계에서 사용되는 EC 는 이전 Cycle 에서 만들어진 EC</p>
</blockquote>
<p>이 동작의 반복으로 살아있는 객체 그래프를 순회하며 Marking 과 Remapping 을 동시에 처리하게 된다. 그리고 그 첫 시작은 STW1 에서 Stack 에 넣은 GC Root 일 것이다.</p>
<p>따라서, <code>이 단계가 진행된 후에는 Dangling(Relocation 이전 주소를 가진 참조) 가 없음</code>이 보장된다.</p>
<p>Mutator Thread 의 객체 참조시 사용되는 LB 와 다른점은 두가지다.</p>
<ol>
<li>마킹에 사용되므로 반환값이 void 인 것</li>
<li>모든 객체 그래프를 순회하면서 마킹해야하므로 컬러포인터가 정상이더라도 fast path 없이 모두 slow path 적용하여 marking stack 에 집어넣는것</li>
</ol>
<p>2번 때문에 같은 객체가 중복으로 Marking Stack 에 들어가는 부작용이 있지만, Bitmap Marking 동작은 CAS 로 Thread-Safe 하므로 문제는 없으며, Mutator Thread 의 Stack 에는 할일이 많지만 GC Thread 가 할일이 없어 Marking/Remapping Phase 가 끝남을 방지하기 위함이다. (G1 과 같이 이것도 Mutator Thread 에 로컬 스택이 있고, 글로벌 스택이 있는듯)</p>
<p>또한, ZGC 는 <code>incremet-update</code> 방식(Not SATB) 을 사용한다. Mutator Thread 에 의해 접근된 객체는 LB 로 Marking Stack 에 적재되며 동작한다.</p>
<blockquote>
<p>마킹에 Mark0, Mark1 비트 두개가 사용되는 이유는 전 사이클의 Remapping 과 현 사이클의 Marking 이 동시에 진행되기 때문이다.
예를들어 전 사이클에서 마킹되었지만 Relocation 은 되지 않은 객체가 있다고 가정하자. 현 Marking/Remapping 에서 Çolor Pointer 는 0001. 현 사이클에서도 살아있는 객체라 새로 0010 으로 마킹 될 예정이다. 하지만 마킹이 진행되기 전에 Mutator Thread 로 부터 참조 조회가 있었고, LB 가 진행되는 상황이라 치자. 이 때 만약 Marking 을 나타내는 필드가 하나라면, <code>이전 사이클에는 마킹했으나 현 사이클에는 아직 마킹을 하지 않아서 이번 사이클의 good color 로 색칠해야함</code> 을 표현할 수 없다. 비트가 하나라면 그냥 1 일 것이고 이번 사이클에서 마킹한 것인지 전 사이클에서 마킹한 것인지 구분할 수 없다는 것이다.
이는 결국 Marking 과 Remapping 을 동시에 진행함으로서 발생된 것이며, Marking 을 표현하는데 두 비트를 사용하는 것은 결국 성능상의 트레이드 오프라 할 수 있다.</p>
</blockquote>
<h4 id="3-stw2--end-of-the-marking-phase">3. STW2 : End of the Marking Phase</h4>
<p>Mark Stack 이 비어 모든 객체가 마킹 되길 기다리는 STW</p>
<h4 id="4-reference-processing">4. Reference Processing</h4>
<p>이 단계는 Soft, Weak, Phantom Reference 를 처리하는 단계인데, 복잡하여 일단 생략</p>
<h4 id="5-selection-of-evacuation-candidate">5. Selection Of Evacuation Candidate</h4>
<p>Evacuation Candidate (또는 Relocation Set) 은 살아남은 객체가 많지 않아 살아남은 객체를 다른 곳으로 이주시키고 해당 Page 를 회수할 Page 의 집합이다.</p>
<p>이 단계에서는 몇가지 정보를 삭제한다.</p>
<ol>
<li>이전 Cycle 의 EC 집합을 삭제</li>
<li>이전 Cycle EC Page 의 Fowarding 테이블 삭제 : Marking 단계가 지나며 Dangling Reference 가 없음이 보장되었음이 근거</li>
<li>살아있는 객체가 하나도 없는 Page 회수</li>
</ol>
<p>그리고 EC 를 만든다. 먼저 이번 GC Cycle 에서 새로 생성된 객체를 넣기 위해 STW1 에서 생성한 Page 를 제외한 모든 Page 중, 살아있는 객체가 적은 순으로 정렬(살아있는 객체 집계값은 marking 에서 생성)한 후, 파편화 임계값에 따라 Relocation 대상 Page 를 선정하여 EC 를 완성한다.</p>
<blockquote>
<p>큰 Page 는 Relocation 대상이 아니다. EC 에는 소, 중 Page 만 포함</p>
</blockquote>
<h4 id="stw3--transitioning-to-relocation">STW3 : Transitioning to Relocation</h4>
<ol>
<li>Good Color 를 0100(Remapped) 로 변경한다.</li>
<li>GC Root 가 EC 를 바라본다면, 해당 EC 를 relocation 하고 그렇지 않다면 단순 color 를 0100 으로 수정한다.<ul>
<li>GC Root 작업을 하기에 STW</li>
</ul>
</li>
</ol>
<blockquote>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/32134ee1-b3f1-4418-a85d-158ee106eb25/image.png" alt="">
Mark0/1 과 Remapping 의 Load Barrier 차이
4번줄에 EC 로 변수는 같으나 Mark 단계 LB 의 EC 는 이전 Cycle EC 이고, Remapped 단계 EC 는 Selection of Evacuation Candidate 에서 선정한 EC 인 점이 다르다.
또한, EC 에 포함되었을때 왼쪽은 단순 remapping 이지만 오른쪽은 relocation 을 한다.</p>
</blockquote>
<h4 id="relocation">Relocation</h4>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/974676f5-921c-44ce-b14c-118cfc7fe79c/image.png" alt=""></p>
<p>이 단계의 스켈레톤 코드는 위와 같다.
EC Page 의 객체들을 Relocation 하면서 Fowarding 테이블에 저장한다. Fowrading 테이블을 이후 LB, MB 에서 사용된다.</p>
<p>Relocation 이 끝난 Page 는 메모리에서 해제한다.</p>
<p>이 단계가 끝나서 객체가 모두 이주하더라도 여전히 이전 객체 참조값을 가진 곳이 있을 수 있다. 컬러포인터 + Barrier + Fowarding Table 덕에 이는 급하게 처리할 일은 아니며, 다음 사이클의 Marking/Remapping 단계에서 빠짐없이 처리될 것이다.</p>
<h2 id="컬러포인터">컬러포인터</h2>
<p>ZGC 는 동시 이주를 실현하기 위한 기술</p>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/932984f5-f1a9-4e38-a22d-363effc20156/image.png" alt=""></p>
<p>컬러포인터는 <code>객체 참조 주소값 자체에 포함</code>되는 <code>4비트</code>의 메타데이터로서, GC 와 동시에 실행되는 유저의 객체 참조시 참조가 이동했더라도 정상적으로 이동한 참조를 찾아갈 수 있게 하는 메커니즘이다.</p>
<p>각 객체의 컬러포인터는 GC 사이클에서 Good 또는 Bad 상태를 나타낸다. Good 상태는 해당 참조값을 바로 사용할 수 있음이며, Bad 는 객체가 이동했을 수 있으니 체크해야한다(potentially invalid) 는 뜻이다.</p>
<p>참조값의 Load 시 미리 정의된 Load Barrier 트랩이 발동하여 Good 이라면 참조값을 반환하고, Bad 라면 </p>
<h3 id="컬러-포인터-비트들">컬러 포인터 비트들</h3>
<ul>
<li>marked0, marked1 : 이번 마킹 사이클에서 이미 live로 ‘표시(mark)’되었다고 알려져 있는가?<ul>
<li>왜 2비트인지는 위 Cycle 설명 참조</li>
</ul>
</li>
<li>remapped :  리로케이션 셋(relocation set) 안(옛 위치)을 가리키지 않는다고 알려져 있는가<ul>
<li>1 : 재배치 집합을 가리키지 않는다는게 확실하다 = 재배치된 객체 주소</li>
<li>0 : 재배치 집합을 가리킬 수 있다 = 재배치 필요 확인 후 재배치 또는 참조 갱신</li>
</ul>
</li>
</ul>
<h3 id="good-color-bad-color-판단">Good Color, Bad Color 판단</h3>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/42088b1e-9a71-49ca-9dce-79e3bbe1592e/image.png" alt=""></p>
<table>
<thead>
<tr>
<th align="left"></th>
<th>goodMask</th>
<th align="center">badMask</th>
</tr>
</thead>
<tbody><tr>
<td align="left">marked0</td>
<td>001</td>
<td align="center">110</td>
</tr>
<tr>
<td align="left">marked1</td>
<td>010</td>
<td align="center">101</td>
</tr>
<tr>
<td align="left">remapped</td>
<td>100</td>
<td align="center">011</td>
</tr>
</tbody></table>
<p>GC Cycle 에서는 위 그림과 같이 현재 시점의 Good Color 가 정해진다. Mutator Thread 의 참조 또는 GC Thread 의 Marking 이 실행되면, 참조값의 Color pointer 와 현재 시점의 Good Color 를 비교하여 다르다면, Bad 상태로 간주하고 처리를 한다. 이 동작은 Barrier(LB/MB) 로서 동작한다.</p>
<p>Color Pointer 를 확인하고 참조를 최신화하는 이 동작은 3군데서 발생한다.</p>
<ol>
<li>Marking/Remapping Phase 에서 Marking 시 Marking Barrier</li>
<li>Marking/Remapping Phase 에서 Mutator 에 의한 조회 Load Barrier</li>
<li>Relocation Phase 에서 Mutator 에 의한 조회 Load Barrier</li>
</ol>
<p>각 단계에서 실제로 어떤 단계로 어떻게 처리하는지는 위의 Cycle 설명에서의 스켈레톤 코드를 참조하자.</p>
<h3 id="self-healing">self healing</h3>
<p>위 Cycle 에서 설명했듯, ZGC 는 Relocation 직후에 이동한 참조값에 대해 STW 를 하여 갱신하지 않는다.</p>
<p>유저 스레드 동작과 함께 천천히 진행한다.</p>
<p>이는 객체 조회시 Relocation 이전 참조를 가지고 있다 하더라도, Color Pointer 를 통해 이전 참조임을 판단하고 Fowarding Table 을 통해 새 참조값을 가져오는 동작을 객체를 참조하는 Mutator Thread 가 직접 할 수 있기 때문이다.</p>
<p>이를 Self-Healing 이라 한다.</p>
<h3 id="multi-mapping">Multi Mapping</h3>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/e92c4432-7a0d-48af-9363-3a827422b605/image.png" alt="">
<a href="https://dev.java/learn/jvm/tool/garbage-collection/zgc-deepdive/">https://dev.java/learn/jvm/tool/garbage-collection/zgc-deepdive/</a></p>
<blockquote>
<p>위 그림은 아마 객체 주소에 42bits 를 사용하던 시절의 그림이라 4, 8, 10 이 된것 같다. 44bits 를 사용한다면 1, 2, 4 가 되어야 할 듯.</p>
</blockquote>
<p>Color Pointer 는 객체 참조 주소값 자체에 저장된다. 주소를 나타내는 값을 임의로 변경했다. ZGC 가 관리하는 메모리 공간에서는 이를 알고 있으나, OS 는 다르다. Color Pointer 가 다르면 다른 주소로 인식 할 뿐이다.</p>
<p>하지만 컬러포인터가 다르더라도 이들은 모두 같은 객체다. 따라서 자바의 가상 주소 공간에서 Color Pointer 로 다른 주소 공간을 나타내더라도 OS 메모리 공간에는 하나의 메모리 주소를 표현하도록 Multi-Mapping 을 사용한다.</p>
<h3 id="컬러-포인터의-장점">컬러 포인터의 장점</h3>
<ul>
<li><p>한 리전안의 객체들이 이동하면 그 즉시 해당 리전을 재사용할 수 있다. Self Healing 덕분이다. 섀넌도어처럼 이전 객체를 유지 할 필요가 없다.</p>
<ul>
<li>Cycle 에서 STW3 에 빈 Page(마킹이 하나도 없는 Page) 를 최대한 빨리 회수하고, ReLocation 과정에서도 Page 단위로 Relocation 한 뒤 바로바로(최대한 빨리) Page 를 회수한다.</li>
</ul>
</li>
<li><p>Read Barrier(LB/MB) 만 가지고도 처리할 수 있으므로 장벽의 수가 많이 줄어든다.</p>
</li>
<li><p>아직 자바 객체 주소에는 사용하지 않는 Bit 가 많이 남아서, 확장 가능성이 열려 있다.</p>
</li>
</ul>
<p><a href="https://openjdk.org/jeps/333">https://openjdk.org/jeps/333</a></p>
<h3 id="컬러-포인터의-제한">컬러 포인터의 제한</h3>
<ul>
<li>32bit 시스템은 ZGC 를 못쓴다.</li>
<li>Compressed OOPS 를 사용하지 못한다.</li>
<li>ZGC 주소공간 메모리가 16TB(2^44) 로 제한된다.</li>
</ul>
<h1 id="세대-구분-저지연-컬렉터">세대 구분 저지연 컬렉터</h1>
<p>세대 구분 이론은 세가지 가설로 탄생했다. 그 중 대다수 객체는 일찍 죽는다는 가설로 Young GC 를 자주 실행시킨다. 이는 곧 죽을 확률이 높은 객체만을 GC 판별한다는 것이다. 이는 GC 의 CPU 사용 효율을 높일 수 있다.</p>
<p>ZGC 나 섀넌도어는 처음에는 세대 구분 없이 이를 출시했다. 세번째 가설에 의한 세대간 참조를 해결하는 기억 집합이 필요 없었기에, 메모리 사용량은 약간 줄 수 있었다.</p>
<p>하지만 결국 매 GC 마다 (세대 구분 GC 였다면 Old 였을) 오래 살아남는 객체를 매번 판별해야했다. 이것이 결국 ZGC, 섀넌도어도 세대 구분으로 발전하게된 이유다.</p>
<p>세대 구분 ZGC 는 높은 CPU 효율로 단일 세대 ZGC 보다 높은 처리량을 달성할 수 있다.
또한, 일시 정지 시간이 줄어들기에 P99 Latency 를 완화할 수 있다.</p>
<p>단일 세대 GC 의 가장 큰 문제는 할당 지연(allocation stall) 이다. 이는 GC 속도가 객체 할당속도보다 느릴 때 발생한다. 세대 구분 ZGC 는 같은 일시 정지 시간에 더 많은 객체를 정리 할 수 있으므로 이 또한 많이 완화되었다.</p>
<p><a href="https://inside.java/2023/11/28/gen-zgc-explainer/">https://inside.java/2023/11/28/gen-zgc-explainer/</a></p>
<h1 id="저지연-가비지-컬렉터의-선택">저지연 가비지 컬렉터의 선택</h1>
<p>저지연 가비지 컬렉터는 회수조차 유저 스레드와 동시에 실행하기 때문에 극단적인 일시정지 시간 감소를 가져올 수 있었다.</p>
<p>일시 정지 시간이 길면 어떤게 나쁠까? P99 의 유저 경험이 나빠질 수 있다는 것이다.
한번씩 발생하는 긴 일시정지는 유저에게 한번씩 큰 지연 시간을 겪게 할 것이다.
반면 유저 스레드와 동시에 실행하는 것들은 많아지기 때문에 전체적인 유저의 지연 시간은 좀 더 늘어날 수 있다. CPU 자원을 나눠 쓰며 컨텍스트 스위칭 등이 일어날 수 있기 때문이다.</p>
<p>정리하자면 이렇다.
긴 정지 시간을 줄일 수록 P99 의 Latency 는 완화할 수 있으나 전체적인 Latency 는 조금 늘어날 수 있으나.
긴 정지 시간을 허용하면 그 시간동안 오롯이 GC 를 실행하여 전체적인 처리량 및 Latency 는 줄어든다. 하지만 긴 정지 시간에 요청을 보낸 유저는 긴 시간 후 응답을 받을 것이다.</p>
<p>따라서 유저 대상 서비스와 연결된 자바 애플리케이션이라면 정지 시간을 줄이는 저지연 컬렉터가 좋다.
반대로 유저를 대상으로 하지 않고 뒷단에서만 동작하는 배치, 데이터 정제 등의 작업 들은 처리량 컬렉터가 좋을 것이다.</p>
<p>이론상은 위와 같은데, 현재 많은 연구결과들은 처리량이든 지연시간이든 ZGC 가 좋다~ 라는 것 같다. 이는 참고만 하고 각자 환경에서 테스트하는게 제일 중요할 것.</p>
<blockquote>
<p>ZGC 기대 가능한 장점</p>
<ul>
<li>P99 응답 지연의 감소</li>
</ul>
<p>기대 가능한 단점</p>
<ul>
<li>barrier 의 증가</li>
<li>threadlocal handshake</li>
<li>gc 와 application 의 리소스 경쟁으로 인한 처리량 감소</li>
<li>zgc 의 경우 compressed oops 의 사용 불가
<a href="https://netflixtechblog.com/bending-pause-times-to-your-will-with-generational-zgc-256629c9386b">https://netflixtechblog.com/bending-pause-times-to-your-will-with-generational-zgc-256629c9386b</a></li>
</ul>
</blockquote>
<h1 id="참고">참고</h1>
<h3 id="섀넌도어">섀넌도어</h3>
<p>[논문] Shenandoah - Christine H. Flood...
<a href="https://tech-notes.accel.dk/programming/java/java12/docs/PPPJ2016.pdf?utm_source=chatgpt.com">https://tech-notes.accel.dk/programming/java/java12/docs/PPPJ2016.pdf?utm_source=chatgpt.com</a></p>
<p>shipliev 이사람 자료들이 좀 자세하고 좋음..
<a href="https://shipilev.net/talks/javazone-Sep2018-shenandoah.pdf">https://shipilev.net/talks/javazone-Sep2018-shenandoah.pdf</a></p>
<p>openjdk
<a href="https://wiki.openjdk.org/spaces/shenandoah/pages/25002018/Main?utm_source=chatgpt.com">https://wiki.openjdk.org/spaces/shenandoah/pages/25002018/Main?utm_source=chatgpt.com</a></p>
<p>섀넌도어 제작한 redhat 자료.
<a href="https://developers.redhat.com/blog/2019/06/27/shenandoah-gc-in-jdk-13-part-1-load-reference-barriers?p=602377">https://developers.redhat.com/blog/2019/06/27/shenandoah-gc-in-jdk-13-part-1-load-reference-barriers?p=602377</a>
<a href="https://developers.redhat.com/blog/2019/06/28/shenandoah-gc-in-jdk-13-part-2-eliminating-the-forward-pointer-word?utm_source=chatgpt.com">https://developers.redhat.com/blog/2019/06/28/shenandoah-gc-in-jdk-13-part-2-eliminating-the-forward-pointer-word?utm_source=chatgpt.com</a>
<a href="https://developers.redhat.com/blog/2019/07/01/shenandoah-gc-in-jdk-13-part-3-architectures-and-operating-systems?p=606497#">https://developers.redhat.com/blog/2019/07/01/shenandoah-gc-in-jdk-13-part-3-architectures-and-operating-systems?p=606497#</a>
<a href="https://developers.redhat.com/articles/2021/05/20/shenandoah-garbage-collection-openjdk-16-concurrent-reference-processing?utm_source=chatgpt.com#traditional_reference_processing">https://developers.redhat.com/articles/2021/05/20/shenandoah-garbage-collection-openjdk-16-concurrent-reference-processing?utm_source=chatgpt.com#traditional_reference_processing</a>
<a href="https://developers.redhat.com/articles/2024/05/28/beginners-guide-shenandoah-garbage-collector?utm_source=chatgpt.com#">https://developers.redhat.com/articles/2024/05/28/beginners-guide-shenandoah-garbage-collector?utm_source=chatgpt.com#</a></p>
<p>jep
404 - generational Shenandoah</p>
<h3 id="zgc-1">ZGC</h3>
<p>[논문] Deep Dive into ZGC: A Modern Garbage Collector in OpenJDK - Albert Mingkun Yang..
기본 개념 설명 여기서 보는게 제일 좋음
<a href="https://dl.acm.org/doi/full/10.1145/3538532">https://dl.acm.org/doi/full/10.1145/3538532</a></p>
<p>Naver D2 개념 설명. 2등. 같이 보기.
<a href="https://d2.naver.com/helloworld/0128759#ch1-3">https://d2.naver.com/helloworld/0128759#ch1-3</a></p>
<p><a href="https://www.blog-dreamus.com/post/zgc%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C">https://www.blog-dreamus.com/post/zgc%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C</a></p>
<p><a href="https://dev.java/learn/jvm/tool/garbage-collection/zgc-deepdive/">https://dev.java/learn/jvm/tool/garbage-collection/zgc-deepdive/</a></p>
<p>openjdk
<a href="https://wiki.openjdk.org/spaces/zgc/pages/34668579/Main">https://wiki.openjdk.org/spaces/zgc/pages/34668579/Main</a></p>
<p>ppt 그림자료 좋음
<a href="https://www.jfokus.se/jfokus18/preso/ZGC--Low-Latency-GC-for-OpenJDK.pdf">https://www.jfokus.se/jfokus18/preso/ZGC--Low-Latency-GC-for-OpenJDK.pdf</a>
<a href="https://cr.openjdk.org/~pliden/slides/ZGC-PLMeetup-2019.pdf">https://cr.openjdk.org/~pliden/slides/ZGC-PLMeetup-2019.pdf</a></p>
<p>jep
333 - A Scalable Low-Latency Garbage Colloector
439 - Generational ZGC
490 - Remove the Non-Generational Mode</p>
<p>세대구분 ZGC
<a href="https://inside.java/2023/11/28/gen-zgc-explainer/">https://inside.java/2023/11/28/gen-zgc-explainer/</a></p>
<p>ZGC Lower Tail Latencies(P99)
<a href="https://www.morling.dev/blog/lower-java-tail-latencies-with-zgc/">https://www.morling.dev/blog/lower-java-tail-latencies-with-zgc/</a></p>
<p>Netflix 세대구분 ZGC 전환
<a href="https://netflixtechblog.com/bending-pause-times-to-your-will-with-generational-zgc-256629c9386b">https://netflixtechblog.com/bending-pause-times-to-your-will-with-generational-zgc-256629c9386b</a></p>
<p>AWS EMR HBase 에서 G1 vs ZGC
<a href="https://aws.amazon.com/ko/blogs/big-data/improve-amazon-emr-hbase-availability-and-tail-latency-using-generational-zgc/">https://aws.amazon.com/ko/blogs/big-data/improve-amazon-emr-hbase-availability-and-tail-latency-using-generational-zgc/</a></p>
<p>parallel vs g1 vs zgc : throughput, latency..
<a href="https://kstefanj.github.io/2021/11/24/gc-progress-8-17.html">https://kstefanj.github.io/2021/11/24/gc-progress-8-17.html</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[가비지 컬렉터 (3) - G1 GC]]></title>
            <link>https://velog.io/@jimmy_neutron/%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%ED%84%B0-3-G1-GC</link>
            <guid>https://velog.io/@jimmy_neutron/%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%ED%84%B0-3-G1-GC</guid>
            <pubDate>Thu, 15 Jan 2026 10:16:58 GMT</pubDate>
            <description><![CDATA[<h1 id="g1-gc">G1 GC</h1>
<blockquote>
<p>대용량 메모리를 갖춘 멀티프로세서 시스템을 대상으로 합니다. G1은 최소한의 설정으로 높은 처리량을 달성하면서 가비지 컬렉션 일시 중지 시간 목표를 높은 확률로 충족하도록 설계되었습니다. G1은 다음과 같은 특징을 가진 현재의 대상 애플리케이션 및 환경을 사용하여 지연 시간과 처리량 간의 최적의 균형을 제공하는 것을 목표로 합니다.
<a href="https://docs.oracle.com/en/java/javase/11/gctuning/garbage-first-g1-garbage-collector1.html">https://docs.oracle.com/en/java/javase/11/gctuning/garbage-first-g1-garbage-collector1.html</a></p>
</blockquote>
<ul>
<li>목표한 짧은 정지 시간을 유지하며 최대한의 처리량을 끌어내는 두가지 목표의 균형<ul>
<li>목표 정지 시간을 지정할 수 있고, G1 GC 는 그 시간을 지키려 <strong>노력</strong></li>
<li>옵션 조정을 통해 더 높은 처리량 or 짧은 정지 시간 택1의 조정도 가능</li>
</ul>
</li>
<li>리전간 복사는 <code>mark-copy</code>, 정리떄에 한 리전으로 모인다는 개념은 <code>mark-compact</code></li>
<li>JDK 9부터 기본 컬렉터</li>
<li>CMS 를 <strong>완전히 대체</strong></li>
<li>Young/Gen 영역용 GC 로 구분되지 않는 GC 이다. G1 GC 는 Young/Old 모두를 처리한다.<ul>
<li><code>-XX:+UseG1GC</code> 로 명시적으로 활성화 할 수 있다.</li>
</ul>
</li>
</ul>
<h1 id="주요-개념">주요 개념</h1>
<ol>
<li><code>일시 정지 시간 조절</code> : Young GC 기록을 통해 Young Gen 의 크기를 수정함. GC 에 걸리는 시간이 조절됨.</li>
<li><code>일시 정지 시간 조절</code> : Marking 은 Concurrent(SATB)</li>
<li><code>일시 정지 시간 조절</code> : CMS 에 비해 Compact 가 있으므로 메모리 단편화 X. Full GC 의 빈도가 낮음.</li>
<li><code>높은 처리량</code> : 살아남은 객체를 Copy, Compact 하는 과정(오래 걸리는 작업)은 STW 상태에서 병렬로 실행됨</li>
<li><code>둘 다</code><ul>
<li>Concurrent Marking 단계에서 공간이 큰 Old 영역에 대한 CSet 의 선정을 위한 작업을 동시에 실행하여 일시 정지 시간을 줄임</li>
<li>Mixed GC 시 CSet 만 Scan 하여 높은 처리량(짧은 GC 시간)</li>
<li>Reclamation Efficiency 기반 GC 로 짧은 시간에 많은 양의 메모리를 정리</li>
</ul>
</li>
</ol>
<blockquote>
</blockquote>
<p>CMS 와 다르게 회수 단계에 Mark-Compact 를 사용한다. 객체를 이동시키는 과정에서는 유저 스레드와 동시에 실행하지 않으므로 이론상 CMS 보다는 <strong>정지 시간이 길 수 있다.</strong>
하지만 최대한 짧은 시간이 목표인 CMS 와 최대의 처리량과 목표한 정지 시간이라는 G1 의 정지 시간 차이는 납득할 만 하며, 실제로 거의 나지 않는다.</p>
<h1 id="region">Region</h1>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/b5d45dee-d4ca-402b-8866-7714ed46075f/image.png" alt=""></p>
<ul>
<li>G1 에서 힙을 나누는 단위<ul>
<li>기존 Serial 같은 연속된 공간의 세대 구분 레이아웃과 다름</li>
</ul>
</li>
<li>크기는 <code>-XX:G1HeapRegionSize</code> 로 설정<ul>
<li>설정하지 않는다면 initial 과 max 힙 크기에 따라 달라짐. 힙 내에 2048 개 정도의 region 이 만들어질 수 있는 크기가 셋팅 (크기는 항상 2의 제곱수)</li>
</ul>
</li>
<li>Region 단위로 메모리를 관리함으로써, 높은 처리량 달성이 가능<ul>
<li>Region 단위의 메모리 회수 비용 예측(후술)</li>
</ul>
</li>
<li>모든 Region은 Eden, Survivor, Old 가 될 수 있으며 동적으로 바뀜</li>
</ul>
<h1 id="garbage-collection-cycle">Garbage Collection Cycle</h1>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/bd56adcd-5676-4b23-91df-7bf006368832/image.png" alt=""></p>
<p>Young GC 와 Concurrent marking 을 진행하는<code>Young-Only Phase</code> 와 Mixed GC 진행하는 <code>Space Reclamation Phase</code> 의 반복</p>
<ul>
<li>그림에서 모든 원은 STW</li>
<li>파란색 작은 원 - Young GC</li>
<li>빨간색 작은 원 - Mixed GC</li>
</ul>
<h2 id="concurrent-marking">Concurrent Marking</h2>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/e5c75357-8819-4de4-b214-b1d015e1e907/image.png" alt="">
<a href="https://tschatzl.github.io/2022/08/04/concurrent-marking.html">https://tschatzl.github.io/2022/08/04/concurrent-marking.html</a></p>
<h3 id="gc-root-를-마킹하는-단계">GC Root 를 마킹하는 단계</h3>
<p>SATB 를 만들고, Marking 을 시작하기 위해 Root 를 마킹하는 단계이다.
Concurrent Marking 의 시작이며, IHOP 임계점을 넘으면 시작된다.</p>
<ol>
<li>Concurrent Marking Start<ul>
<li>STW 단계지만, Young GC 의 STW 와 같이 실행하여 STW 최소화.<ul>
<li>Young GC 에 piggyback 한다. 이 때 승격된 Region 들을 기록한다. 이는 다음 Concurrent Root Region Scan 에서 쓰인다.</li>
</ul>
</li>
<li>GC Root 로 부터 직접적인 참조 객체 마킹</li>
<li>SATB 생성(TAMS 포인터 셋팅)</li>
</ul>
</li>
<li>Concurrent Root Region Scan<ul>
<li>Start 단계에서 Marking 한 Region 의 객체들을 root 로 취급하여 마킹한다.</li>
<li>SATB 스냅샷에 의해 고정되어 있으므로 Region 만 STW 인 첫번째 단계에서 정해주면 이 단계는 Concurrent 하게 처리할 수 있다.</li>
</ul>
</li>
</ol>
<blockquote>
<p>동시 마킹은 Mixed GC 에서 Old Generation 에서 살아있는 객체를 Marking 하는 것이다.
Old 영역에서 살아남아야 할 객체는 다음 두가지로, 둘 다 Root 로서 마킹하고 객체 그래프를 순회해야한다.</p>
<ol>
<li>스택 등 에서 참조중인 객체</li>
<li>Young Gen 에서 참조중인 객체</li>
</ol>
<p>1번은 별 문제가 없는데, 2번은 문제가 있다. Young GC 에서 세대간 참조인 Young Gen &lt;- Old Gen 참조는 RSet 을 통해 쉽게 해결이 가능하지만, Old GC 에서 세대간 참조인 이 경우는 RSet 으로 해결이 불가능하다. 따라서 Young Gen 을 모두 스캔하여 Old Gen 으로의 참조가 있는지 체크해야 한다. 이를 위해 <code>Concurrent Marking Start</code> 는 Young GC 에 얹혀 실행되며 Young GC 의 스캔에 껴서 Old Gen 으로의 참조가 있는 Young Region 을 골라둔다.</p>
</blockquote>
<blockquote>
<p>Young -&gt; Old 로의 세대간 참조가 RSet 만으로 해결되지 않는다는건.. <a href="https://www.alibabacloud.com/blog/a-detailed-explanation-of-jvm-garbage-collector-g1%26zgc_601536">이 글</a>에서 참고했는데, 근거는 명확하진 않다.
내 생각으로는 <a href="https://docs.oracle.com/en/java/javase/21/gctuning/garbage-first-g1-garbage-collector1.html">oracle 문서</a> 에서 Young Gen 의 RSet 은 항상 유지하지만, Old Gen 의 RSet 은 lazily 하게 완성되기 때문에 완벽하지 않기 때문이지 않나 싶다. Old Gen 의 RSet 을 lazily 하게 세팅되는 이유는 Old Gen 은 리전이 엄청 큰데 Rset 을 유지하면 메모리가 많이 들어서 그렇다. (<a href="https://bugs.openjdk.org/browse/JDK-8180415?utm_source=chatgpt.com">이슈</a>)</p>
</blockquote>
<h3 id="동시-마킹">동시 마킹</h3>
<ol start="3">
<li>Concurrent Marking<ul>
<li>이전 단계를 통해 Root 객체들은 모두 마킹되어 있음.</li>
<li>비트맵 보고 마킹되어 있으면 거기부터 탐색 시작하여 살아 있는 객체를 마킹.</li>
<li>이 단계 중 TAMS 이하의 객체에 대해 변경 작업이 있으면, SATB Pre-Write-Barrier 로 원래 참조값을 SATB Buffer 에 ADD. 이는 SATB 로 첫 스냅샷 기준으로 Marking 을 진행하기 위해 버퍼에 원래 값을 집어넣어놓고 사용하는 과정이다. 버퍼의 값(원래값)을 꺼내서 마킹하는 식으로 초기 스냅샷 기준 마킹을 진행.</li>
</ul>
</li>
<li>Remark<ul>
<li>STW</li>
<li>SATB Buffer 를 drain 하여 marking 마무리.<ul>
<li>SATB Buffer 에는 변경 전 oops 가 들어있으며, 해당 oops marking 후 객체 그래프 탐색. 이 작업 반복.</li>
</ul>
</li>
<li>클래스 언로딩, 비어있는 Region 회수</li>
<li>Garbage 양에 따른 CSet Candidate 선정</li>
<li>TARS(Top-At-Rebuild-Start) 포인터를 Top 에 추가</li>
</ul>
</li>
<li>Concurrent Rebuild RSet, Scrub Region<ul>
<li>CSet Candidate 에 속한 Old Gen Region 의 Region Bottom ~ TARS 사이 객체에 대해 cross-region 참조를 찾고 RSet 을 갱신<ul>
<li>이는 mixed gc 시 RSet 을 참고하여 GC 를 하기 위해 RSet 을 정비하는 작업</li>
</ul>
</li>
</ul>
</li>
<li>Cleanup<ul>
<li>STW</li>
<li>CSet Candidate 을 효율성 및 연결성 순으로 정렬하여 최종 확정</li>
<li>CSet 의 상태를 보고 Reclaimation Phase 를 아예 실행하지 않기로 결정할 수도 있음</li>
</ul>
</li>
</ol>
<h2 id="가비지-컬렉팅">가비지 컬렉팅</h2>
<p>Young GC 와 Mixed GC 둘 다 크게 네가지 단계로 이뤄진다.
다른건 CSet 이 Young Gen Region 이냐, Young Gen Region + Old Gen Region 이냐다.</p>
<ol>
<li>Pre Evacuate Collection Set<ul>
<li>TLAB 를 유저 스레드에서 해제</li>
<li>CSet candidate 에서 CSet 선택</li>
</ul>
</li>
<li>During Merge Heap Roots<ul>
<li>CSet 의 RSet 을 병합. 여러 Region 을 처리할 것이기 때문에 중복을 제거해두기 위해.</li>
</ul>
</li>
<li>Evacuate Collection Set<ul>
<li>루트 참조로부터 객체 그래프 끝까지 스캔 및 복사/이동<ul>
<li>상위에서 참조된 객체는 살아있다는 것이고, 해당 객체를 객체를 복사/이동</li>
<li>이후 해당 객체를 CSet Root 로 삼아서 하위 그래프 스캔</li>
<li>더이상 하위 객체가 없을 때 까지 반복</li>
</ul>
</li>
</ul>
</li>
<li>Post Evacuate Collection Set <ul>
<li>Free CSet 등 정리 작업</li>
</ul>
</li>
</ol>
<h3 id="young-gc">Young GC</h3>
<ul>
<li>대상은 Eden, Survivor 전체</li>
<li>Eden 의 객체를 Survivor 로 복사</li>
<li>Survivor 의 객체를 Old 로 복사</li>
<li>죽은 객체는 CSet 에 추가되어 다음 Young GC 또는 Mixed GC 에서 정리</li>
</ul>
<h4 id="young-gc-에서-일시-정지-시간-목표-달성">Young GC 에서 일시 정지 시간 목표 달성</h4>
<p>최대 일시 정지 시간 설정인 <code>-XX:MaxGCPauseTimeMillis</code> 와 GC 간격인 <code>-XX:GCPauseIntervalMillis</code> 는 일시 정지 시간에 영향을 미칠 수 있다.</p>
<p>이 옵션으로 셋팅된 정지 시간 목표를 달성하기 위해 긴 시간 관찰된 비슷한 크기의 young gen 이 GC 되는데 걸린 시간, GC 과정에서 복사했던 객체의 수, 등등의 정보를 근거로 Young GC 가 끝날 때 마다 Young 영역의 크기를 다시 결정한다.</p>
<p><code>-XX:G1NewSizePercent=5</code>, <code>-XX:G1MaxNewSizePercent=60</code>
이 두 사이의 값에서 G1 가 동적으로 Young Gen 의 크기를 결정한다. 값은 전체 힙 크기 중 Young Gen 의 비율이다.</p>
<p>이렇게 Young GC 는 Young Gen 의 크기를 매번 조정해가면서 목표한 일시 정지 시간을 맞추는데 노력한다.</p>
<h3 id="mixed-gc">Mixed GC</h3>
<ul>
<li>Space-Reclamation 의 목표는 mixed gc 를 통해 Old 영역의 크기를 줄이는 것</li>
<li>Mixed GC 는 Reclaimation Phase 에서 N 번으로 나뉘어 실행<ul>
<li><code>XX:G1MixedGCCountTarget = 8</code> : 몇번 실행할지 결정</li>
<li>CSet Candidate 개수 / XX:G1MixedGCCountTarget = Mixed GC 에서 대략적으로 정리되는 Region 수</li>
</ul>
</li>
<li>Young/Old 두 영역 모두의 객체를 대상으로 GC 한다고 해서 <code>Mixed</code></li>
<li>Space-Reclamation 가 시작되면 Young Gen 의 크기는 <code>-XX:G1NewSizePercent</code> 값으로 고정.</li>
<li>목표 정지 시간을 넘어서지 않는 선에서 CSet 순서대로 회수 Region 선택(마킹 단계에서 회수효율 순으로 정렬됨)</li>
<li>CSet Candidate 에 포함된 Region 은 Conc Marking 단계에서 <code>-XX:G1MixedGCLiveThresholdPercent=85</code> 옵션으로 필터링 되어 있는  상태. 즉, 15% 이상이 죽은 객체인 리전만 들어 있고, 그게 GC 대상 Region.</li>
<li><code>XX:G1HeapWastePercent=5</code> : space-reclamation 를 종료하는 트리거. 전체 힙 크기 대비 CSet 에 남은 미회수 공간 크기의 합의 비율이 이 값보다 낮다면 space-reclamation 를 중지. 쉽게 말하면, CSet 에 정리할 공간이 적게 남았다면 끝낸다.</li>
</ul>
<h1 id="tams">Tams</h1>
<ul>
<li>Bottom ~ prev Tams : 이전 Marking 에서 확정된 Bitmap</li>
<li>prev Tams ~ next Tams : 이전 Marking 시작단계 ~ 현재 Marking 시작단계 까지 새로 생긴 객체들. <ul>
<li>bottom ~ prev Tams 구간과 함깨 현재 marking 단계의 마킹 대상이 된다. (현재 마킹 이후는 Bottom ~ next Tams 까지의 비트맵이 생긴다)</li>
</ul>
</li>
<li>next Tams ~ end : 이번 marking 단계 중 새로 생긴 객체들. alive 상태로 간주.
마킹 단계에서는 현재 리전에 대한 마킹과 이전 리전 마킹 기록 efficency 계산이 동시에 이뤄진다. 따라서 두개의 포인터를 두고, 이전 마킹 단계에서 비트맵이 확정 된 곳이 어디까지인가의 저장과, 현재 마킹 단계 시작 이후 생긴 객체들은 어디부터인가를 같이 저장해야한다.
단 이건 jdk20 에서 방식이 바뀌었다.
비트맵에는 마킹 여부를 표시. 이후 객체 생사를 판단하고, 살아있는 객체가 얼마나인가(회수 가능한 객체가 얼마나인가) 판단에 쓰임.</li>
</ul>
<h1 id="write-barrier">write barrier</h1>
<ol>
<li>RSet Card Marking Write Barrier(post write barrier) : 객체 참조 변경시 Rset 의 변경을 큐에 넣는 베리어. 한꺼번에 처리하기 위해 큐에 넣음.</li>
<li>Concurrent Marking SATB Write Barrier(pre write barrier) : SATB 동작에서 객체 참조 변경(삭제)시 old ref(이전 값) 을 보존하기 위해 SATB Buffer 에 저장하는 베리어</li>
</ol>
<h1 id="기억집합rset">기억집합(RSet)</h1>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/94c97870-1bae-4f55-b498-15e5c42eef64/image.png" alt="">
<a href="https://www.alibabacloud.com/blog/a-detailed-explanation-of-jvm-garbage-collector-g1%26zgc_601536">https://www.alibabacloud.com/blog/a-detailed-explanation-of-jvm-garbage-collector-g1%26zgc_601536</a></p>
<p>기존의 Serial GC 에서의 Card Table 은 Old Gen 의 크기를 일정 크기 Page 로 나눈 뒤 해당 영역에 대한 Dirty Check 만 하는 Byte Array 이다. 이는 Young Gen 하나 Old Gen 하나의 구조이기 때문에 가능했다. Young Gen 에서 Dirty 표시를 하면 참조하고 있는 대상인 비회수영역은 Old Gen 하나밖에 없다는 말이다.</p>
<p>하지만 Region 여러개로 나뉜 G1 GC 에서는, 회수 대상 Region 의 객체를 참조하는 비회수 대상 Region 이 여러개일 수 있다. 따라서 G1 에서는 리전간 참조를 기억하기 위해 Remember Set(RSet) 이라는 새로운 구조를 만들어 사용한다.
G1 은 기존과 같이 (기본적으로 힙을 512 byte 단위로 나눠서) Card Table 을 관리한다. 다만 Region 마다 존재한다. RSet 은 카드 테이블에 dirty 된 영역이 어떤 외부 Region 으로부터의 참조 때문인지를 인덱스로 관리하는 것이다.</p>
<p>G1 은 Young-Only Phase 에서 Young Gen 에 있는 객체만 정리한다. 이 때 Old Gen Region 에서 참조하고 있는 객체는 정리하면 안된다. 또한 살아남은 객체를 복사 및 이동시에 Old Gen 의 참조를 변경시켜줘야 한다. 이럴 때를 위해 기억집합이 존재한다.</p>
<p>Mutate Thread(유저 스레드) 는 객체의 포인터를 변경(살아남은 객체 이동 등)시에 Post Write Barrier 로 Card Table 에 Dirty 표시를 하고, 해당 사실을 Log Buffer 에 저장한다. Concurrent Refinement Threads 는 버퍼에서 Log 를 꺼내 RSet 을 업데이트한다.
이런 지연된 구조는 한꺼번에 RSet 을 처리하여 효율을 높이기 위해서다.</p>
<p>Concurrent Marking 단계에서 RSet 을 최신화 시키는 단계가 있다.
가비지 컬렉션의 Merge Heap Roots 단계에서는 중복을 제거하기 위해 리전당 RSet 으로 부터 Global RSet 를 만드는 단계가 있다.
RSet 은 리전마다 저장하므로 메모리 소비가 G1 내부에서 큰 편이다. 특히 Young Gen 의 크기를 작게 설정할 수록 그만큼 Old Gen 으로 승격되는 객체가 많아져 RSet 의 크기가 커질 수 있음을 유의하자.</p>
<h1 id="cset">CSet</h1>
<p>CSet 은 정리 및 복사(Evacuate) 할 대상 Region 의 집합.
CSet 은 GC 의 Pre Evacuate Collection 단계에서 정해진다.</p>
<p>Young GC : Young Gen 의 전체가 CSet 이 된다.
Mixed GC : Young Gen 전체 + Concurrent Marking 의 결과로 Mixed GC 의 pre-evacuation. 에서 만든 것.</p>
<p>Young GC 는 전체 Young Gen 이 CSet 이기 때문에 고르고 뭐고 하는 과정은 없다.</p>
<p>Mixed GC 에서 사용할 CSet 은 다음과 같이 정해진다. <a href="https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1.html#GUID-3F6AE9BA-32AF-4C36-B8C0-721EE45D17A7">[참고]</a> <a href="https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1.html#GUID-3A99AE6C-F80A-4565-A27C-B4AEDF5CDF71">[참고]</a></p>
<ol>
<li>Concurrent Marking의 Remark 에서 점유율이 낮은 영역, 즉 여유 공간이 많은 영역을 선택하여 CSet Candidate 을 선정.</li>
<li>Concurrent Marking 의 Clean Up 에서 CSet Region 의 점유율과 연결성을 고려하여 GC 효율 순으로 정렬하여 최종 CSet Candidate 를 만듦. (Refine)</li>
<li>Mixed GC 에서는 이 정렬된 CSet Candidate 중 N 개의 CSet 을 선택하여 정리. Mixed GC 는 Space Reclaimation 단계에서 여러번 실행될 수 있으며, 각각의 GC 에서 CSet Candidate 중 CSet 를 선택. </li>
</ol>
<h1 id="ihop">IHOP</h1>
<p>Recaimation Phase 로 넘어가기 위해 Young-Only Phase 에서 Concurrent Marking 을 시작할 임계점.
<code>-XX:InitiatingHeapOccupancyPercent=45</code> 로 셋팅할 수 있다.
G1 은 Adaptive IHOP 기능을 지원한다. 처음 두세번의 Concurrent Marking 시작 임계점은 <code>-XX:InitiatingHeapOccupancyPercent=45</code> 으로 하되, 몇번의 GC Cycle 로 정보가 쌓인 후에는 처리량과 정지 시간에 알맞은 IHOP 를 자동으로 정해준다. <code>-XX:+G1UseAdaptiveIHOP</code> 로 켤 수 있다.</p>
<blockquote>
<p>이렇게 G1 GC 가 자동으로 현재 상태에 맞게 자원이나 옵션을 자동으로 변경하는 것을 Ergonomic 이라 한다.</p>
</blockquote>
<h1 id="주기적-가비지-컬렉팅">주기적 가비지 컬렉팅</h1>
<p>운영체제에 힙을 할당/반환하는 과정은 동시 마킹의 Remark 또는 Full GC 의 STW 상태에서만 진행한다.</p>
<p>하지만 G1 은 Full GC 를 최대한 피하는 방식으로 설계되었고, 애플리케이션이 유휴상태라 새로운 객체가 생성되지 않아 동시 마킹이 시작되는 임계값(IHOP) 을 넘지 못한다면 운영체제에 반환할 수 있는 Heap 메모리가 있다 하더라도 반환하지 못하게 될 수 있다. 이런 현상은 On-Demand Cloud 환경에서 비용적으로 불리할 수 있다.</p>
<p><code>-XX:G1PeriodicGCInterval</code>
<code>-XX:G1PeriodicGCSystemLoadThreshold</code></p>
<h1 id="거대-객체">거대 객체</h1>
<p>리전의 크기의 반 이상 크기인 객체를 거대 객체(Humongous Objects
)라 한다.
거대 객체는 Old Gen 의 연속된 리전인 거대 리전에 저장된다. 만약 1.5개 리전의 크기의 객체라면 2개의 리전을 차지하며 나머지 0.5 Region 은 쓰이지 않는다.</p>
<p>이러한 거대 객체는 Concurrent Marking 의 CleanUp 또는 Full GC 에서만 Reclaim 될 수 있다. 다만 기본타입 거대 객체의 경우 GC Pause 도중 회수할 수 있다.</p>
<p>거대 리전은 연속된 거대한 Region 을 필요로 하기 때문에 Full GC 를 발생시킬 확률이 있다.
또한 거대 객체는 Full GC 중에도 Compaction 을 위해 이동되지 않는다. 따라서 거대 객체가 많아지면 파편화가 심해져 메모리 부족 오류를 만날 수 있다.
더욱이, 메모리 공간이 충분하더라도 연속된 공간을 찾지 못해 객체 할당 실패 오류를 겪을 가능성도 있다. 이럴때는 Region Size 를 늘려 Region 이 수용할 수 있는 크기를 늘리거나, 힙 자체를 늘리는 방법 밖에 없다.</p>
<p><code>gc+heap=info</code> 옵션에서 거대 객체의 개수를 알 수 있다.</p>
<h1 id="satb">SATB</h1>
<p>SATB 는 marking 을 위한 객체 그래프 스캐닝 중 유저 스레드에 의해 참조 삭제가 일어날 경우 그 사실을 기록한 후 나중에 적용한다. 즉, 마킹 시작시에 살아있던 객체는 끝까지 살아있는 것으로 간주한다.</p>
<p>mutator thread 가 만약 A.a = b 인 상태에서 A.a = c 로 참조 변경을 했다고 치자. 이 때 A.a -&gt; b 로의 참조는 사라지게 되는 것이다.
이 때 Pre Write Barrier 가 동작하여 기존 A.a 에 들어있던 b 참조를 SATB Buffer 에 저장한다.</p>
<p>이후 SATB Buffer 에 있던 참조(b) 는 marking 단계에서 GC Root 로 등록된다.</p>
<p>A.a -&gt; b 의 참조가 사라진 것인데, SATB Buffer 에는 b 값만 저장해도 될까? 어디서 온 참조인지는 저장하지 않아도 될까?
SATB 의 목적은 &#39;시작단계에 살아있던건 살아있다 간주&#39; 이기 때문에, 삭제 대상이던 b 가 살아있도록만 만들어주면 된다. b 를 GC Root 로 등록하면 b 부터 시작하여 하위의 참조 객체들이 다 살아남을 수 있다. 그렇기 때문에 SATB Buffer 에는 b 만 넣으면 된다.</p>
<p>SATB 는 같은 문제를 해결하는 incremental-update 방식에 비해 일시 정지 시간을 줄여줄 수 있다. Marking 과 동시에 수정되는 객체 참조의 유형은 대부분이 새로운 객체의 생성이기 때문이다.
삭제된 객체만 관심을 가지는 SATB 방식이 새로운 참조에 관심을 가지는 incremental-update 보다 할일이 적다. [참고 : Compile-Time Concurrent Marking Write Barrier Removal]</p>
<p>SATB 의 단점도 있다.
Concurrent Marking 의 초기 단계에서 SATB 를 확정하면 이후 Marking 이나 Mixed GC 는 모두 시작단계 스냅샷 기준으로 동작한다. 즉, SATB 직후에 죽은 객체가 있더라도 다음 사이클이 도래하여 SATB 를 다시 찍기 전에는 죽은걸로 인식되지 않는다.
이런 객체를 부유 쓰레기(floating garbage. 실제로 죽었는데 살아있는것 처럼 보여 회수되지 못함) 라 하며, 메모리 사용량을 높게 한다.</p>
<h1 id="full-gc">Full GC</h1>
<p>Old Gen 의 힙 점유율이 높아 객체를 할당하지 못하면 Full GC 가 발생한다. 특히 거대 객체를 할당할 때 발생할 수 있다.</p>
<ul>
<li>애플리케이션이 회수할 수 없을 정도로 너무 많은 객체를 할당할 경우<ul>
<li>Concurrent Marking 이 진행중일 때 계속해서 객체가 쌓이면 Mixed GC 에서 Old Gen 의 객체를 정리하기 전에 Full GC 가 발생해버릴 수 있음.</li>
</ul>
</li>
<li>거대 객체를 할당하는데 연속된 메모리 공간을 찾지 못할 경우</li>
</ul>
<p>이러한 Full GC 는 긴 STW 를 발생시켜서, 최대한 줄이는 노력을 해야한다.
GC Log 에 <code>Pause Full (Allocation Failure)</code> 로 표시된다.</p>
<p>FUll GC 를 줄이기 위한 방법은 다음과 같다.</p>
<ol>
<li>거대 객체를 확인하고 리전 사이즈 늘리기</li>
<li>힙 사이즈 늘리기</li>
<li><code>-XX:ConcGCThreads</code> 로 동시 처리 스레드 늘리기</li>
<li>IHOP 수정으로 동시 마킹 더 빠르게 하기</li>
</ol>
<h1 id="adaptive-option">Adaptive Option</h1>
<p>G1 에는 <code>Ergonomic</code> 이라는 애플리케이션이 실행되며 자동으로 수정되는 값들이 있다. 이를 알아두면 좋을 것. </p>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/79b81d5e-ca86-4096-b07d-5d34229e5f21/image.png" alt=""></p>
<p><a href="https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1%60Ergonomic%60">https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1`Ergonomic`</a> 이라는 .html#GUID-082C967F-2DAC-4B59-8A81-0CEC6EEB9016 </p>
<h1 id="실제로-보기-옵션">실제로 보기 옵션</h1>
<p><code>-XX:+UseG1GC -XX:G1HeapRegionSize=1m -XX:InitiatingHeapOccupancyPercent=20 -XX:MaxGCPauseMillis=50 -Xlog:gc*=debug</code>
Mixed GC 가 일어날 수 있게 Concurrent Marking 이 발생하는 임계값을 좀 낮춰준다.
-Xlog:gc 에는 ergo, heap, age... 많은 태그를 같이 쓸 수 있는데, 모두 볼 수 있게 * 와 레벨을 debug 로 해줄 수 있다.</p>
<hr>
<h1 id="참고">참고</h1>
<p>마킹 전체 내용 - <a href="https://tschatzl.github.io/2022/08/04/concurrent-marking.html">https://tschatzl.github.io/2022/08/04/concurrent-marking.html</a>
전체 내용 잘 설명 - <a href="https://www.alibabacloud.com/blog/a-detailed-explanation-of-jvm-garbage-collector-g1%26zgc_601536">https://www.alibabacloud.com/blog/a-detailed-explanation-of-jvm-garbage-collector-g1%26zgc_601536</a>
oracle 문서, 버전마다 조금씩 다르다 - <a href="https://docs.oracle.com/en/java/javase/21/gctuning/garbage-first-g1-garbage-collector1.html#GUID-572C9203-AB27-46F1-9D33-42BA4F3C6BF3">https://docs.oracle.com/en/java/javase/21/gctuning/garbage-first-g1-garbage-collector1.html#GUID-572C9203-AB27-46F1-9D33-42BA4F3C6BF3</a>
redhat. GC 과정에 전체 리전이 어떻게 변화하는지 그림이 잘되어 있음 - <a href="https://www.redhat.com/en/blog/part-1-introduction-g1-garbage-collector">https://www.redhat.com/en/blog/part-1-introduction-g1-garbage-collector</a>
redhat. jdk8 기준으로 로그 보면서 설명. - <a href="https://www.redhat.com/en/blog/collecting-and-reading-g1-garbage-collector-logs-part-2">https://www.redhat.com/en/blog/collecting-and-reading-g1-garbage-collector-logs-part-2</a></p>
<p>G1 논문. 기본적인 아이디어가 거의 다 설명되어 있음 - Garbage-First Garbage Collection. David Detlefs..</p>
<p>jdk20 bitmap 관련 변경. 이런 식으로 메모리 사용량이 줄고 있음. - <a href="https://github.com/openjdk/jdk/pull/8957">https://github.com/openjdk/jdk/pull/8957</a></p>
<p>활동의 거의 없는 애플리케이션의 주기적 GC JEP - <a href="https://openjdk.org/jeps/346">https://openjdk.org/jeps/346</a></p>
<p>RSet, Write Barrier 등 - <a href="https://www.jfokus.se/jfokus17/preso/Write-Barriers-in-Garbage-First-Garbage-Collector.pdf">https://www.jfokus.se/jfokus17/preso/Write-Barriers-in-Garbage-First-Garbage-Collector.pdf</a></p>
<p>그 외</p>
<ol>
<li><a href="https://www.oracle.com/technical-resources/articles/java/g1gc.html#Taming">https://www.oracle.com/technical-resources/articles/java/g1gc.html#Taming</a></li>
<li><a href="https://tschatzl.github.io/2025/02/21/new-write-barriers.html">https://tschatzl.github.io/2025/02/21/new-write-barriers.html</a></li>
<li><a href="https://tschatzl.github.io/2021/02/26/early-prune.html">https://tschatzl.github.io/2021/02/26/early-prune.html</a></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[가비지 컬렉터 (2) - CMS]]></title>
            <link>https://velog.io/@jimmy_neutron/%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%ED%84%B0-2-CMS</link>
            <guid>https://velog.io/@jimmy_neutron/%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%ED%84%B0-2-CMS</guid>
            <pubDate>Wed, 07 Jan 2026 16:03:50 GMT</pubDate>
            <description><![CDATA[<h1 id="cms">CMS</h1>
<blockquote>
</blockquote>
<p>CMS 는 가비지 컬렉션 일시 중지 시간을 최소화하고, 애플리케이션 실행 중에 가비지 컬렉터와 프로세서 리소스를 공유할 수 있는 애플리케이션을 위해 설계되었습니다. 일반적으로 장기간 보존되는 데이터(tenured 크기가 큰 경우)가 비교적 많고 프로세서가 두 개 이상인 시스템에서 실행되는 애플리케이션은 이 컬렉터를 사용하면 이점을 얻을 수 있습니다. 하지만 일시 중지 시간이 짧은 모든 애플리케이션에 이 컬렉터를 고려해 볼 수 있습니다.
<a href="https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html">https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html</a></p>
<p>CMS 는 STW 에 의한 정지 시간을 최소화시키는 컬렉터다. Concurrent-Mark-Sweep 의 약자다.
STW 정지 시간을 최소화하는 방법이 뭘까. 이름에서 힌트를 얻자면 &#39;동시에&#39; 한다는 것이다.</p>
<p>Garbage Collection 단계에서 STW 를 일으키는 단계는 크게 두가지다.</p>
<ol>
<li>Marking : 살아있는 객체를 마킹하는 과정에서 객체 그래프가 변경되면 안된다.</li>
<li>Compact : 살아남은 객체의 메모리 파편화를 막기 위해 한 곳으로 모으는 과정이다. 객체의 위치를 옮겨야 하므로 애플리케이션을 멈추고 진행한다.</li>
</ol>
<p>CMS 는 꼭 필요한 부분을 남기고 많은 부분을 <code>STW 없이 유저 스레드와 동시에</code> 진행하는 방법으로 일시 정지 시간을 줄였다.</p>
<h1 id="cms-특징">CMS 특징</h1>
<ul>
<li>Old Gen 용 GC.<ul>
<li><code>-XX:+UseConcMarkSweepGC</code> 로 활성화. Young Gen GC 는 ParNew 가 사용된.</li>
</ul>
</li>
<li>Mark-Sweep 알고리즘 기반</li>
<li>JDK 14 부터는 없어져 사용할 수 없음<ul>
<li>대체제는 G1 GC</li>
</ul>
</li>
<li>지연 시간 최소화 컬렉터라 서버용 빠른 응답을 하기에 적절하다.</li>
</ul>
<h1 id="없어진걸-왜-알아야-하나">없어진걸 왜 알아야 하나..</h1>
<p>서버용 JVM 은 지연 시간을 줄이는 목표로 발전했다. 최근의 그러한 Garbage Collector 가 ZGC 이다.</p>
<p>지연 시간을 줄이는데 가장 크게 발목 잡는 것이 STW 이고, STW 를 줄이는 방법은 객체의 생성/삭제/이동을 유저 스레드의 동작과 안전하게 같이 진행하는 것이다.</p>
<p>CMS 이전의 GC(Serial, ParNew, PS..) 는 GC 모든 과정에서 STW 를 하였다. CMS 는 유저 스레드와 GC 의 일부를 동시에 실행시킨 첫 GC 로서 의미가 있으며, 어떻게 그렇게 했는지를 이해하면면 CMS 이후에 나온 GC 를 이해하기 더 쉽다.</p>
<h1 id="garbage-collection-중-동시에-실행하는-것">Garbage Collection 중 동시에 실행하는 것</h1>
<ol>
<li>GC Root 로 부터 참조 체인을 따라가면서 Marking 하는 작업</li>
<li>Sweep</li>
</ol>
<p>Marking 단계중 CMS 가 유저 스레드와 동시에 실행하는 작업은 GC Root 로 부터 참조 체인을 따라가며 살아있는 객체에 마킹하는 작업이다. 루트 노드를 열거하는 작업은 여전히 STW 로 실행된다.</p>
<p>그러나 루트 노드 열거보다 참조 체인을 따라가는 작업이 훨씬 오래 걸리기에, 의미있는 발전이다. </p>
<p>또한 Compact 가 아닌 Sweep 이라서, 죽은 객체를 회수(Sweep)하는 작업을 유저 스레드와 동시에 진행할 수 있었다.</p>
<h1 id="동작-방식">동작 방식</h1>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/9891ba3e-7d34-4aef-97ef-ec8fbc1f274c/image.png" alt=""></p>
<h3 id="1-최초-marking">1. 최초 Marking</h3>
<p><code>STW</code> 루트 노드와 직접 연결된 객체만 mark 한다. 빠르게 끝남.</p>
<h3 id="2-동시-marking">2. 동시 Marking</h3>
<p><code>NO-STW</code> 루트 노드와 직접 연결된 객체로 부터 객체 그래프를 탐색하며 mark 한다. 새로 생긴 객체는 <code>증분 업데이트</code> 방식에 따라 따로 기록해둔다.</p>
<h3 id="3-re-marking">3. Re-Marking</h3>
<p><code>STW</code>. <code>증분 업데이트</code> 방식에 의해 따로 기록한 객체를 반영한 mark 를 진행한다. 최초 표시보다 정지가 조금 더 길지만, 여전히 짧은 시간이다.</p>
<h3 id="4-동시-sweep">4. 동시 Sweep</h3>
<p><code>NO STW</code> 유저 스레드가 실행되는 것과 동시에 죽은 객체를 회수한다.</p>
<h1 id="cms-의-단점">CMS 의 단점</h1>
<h3 id="1-프로세서-자원에-민감">1. 프로세서 자원에 민감</h3>
<p>유저 스레드와 동시에 GC 를 실행한다는 것은 하나의 시간에 CPU 처리량을 GC 가 뺏어간다는 이야기다. 
CMS 는 <code>(프로세서 코어 수 + 3)/4 개의 스레드</code> 로 GC 를 진행한다. 프로세서 코어 수가 적으면 적을수록 GC 가 사용하는 자원의 비용이 커진다.</p>
<p>따라서 <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html">Oracle 문서</a> 에서도 프로세서가 2개 이상인 환경에서 적합하다고 명시되어 있다.</p>
<h3 id="2-동시-모드-실패">2. 동시 모드 실패</h3>
<p>CMS 는 유저 스레드와 동시에 실행된다.
따라서 Old Gen 의 공간이 100% 찼을때 GC 를 하면 안된다. 동시에 실행되는 스레드에 의해 만들어질 객체가 할당될 공간이 필요하기 때문이다.</p>
<p>특히 동시에 실행되는 유저 코드에 의해 한 객체가 죽었다 하더라도, 그 시점이 CMS 의 Marking 단계가 끝난 직후라면 다음 GC 까지 기다려야 한다. 이런 객체를 부유 쓰레기라 한다.</p>
<p><code>-XX:CMSInitiatingOccupancyFraction</code> 옵션은 Old Gen 이 몇 % 차야 CMS GC 가 실행될지 설정할 수 있다. JDK6 부터 기본값은 92% 이다.
Old Gen 의 여유 공간이 8% 남게 되면 CMS GC 가 시작된다.</p>
<p>만약 8% 남은 상태에서 더 큰 공간의 할당 요청이 오면?
이 상황을 <strong>동시 모드 실패</strong> 라고 하며, CMS 는 애플리케이션을 멈추고 Serial Old GC 로 Major GC 를 실행한다.
이 동작은 매우 느리기 때문에 <code>-XX:CMSInitiatingOccupancyFraction</code> 옵션을 잘 조정하는게 중요하다.</p>
<h3 id="3-메모리-파편화">3. 메모리 파편화</h3>
<p>mark-sweep 알고리즘의 한계이다. CMS 역시 직면해야하는 문제다.
메모리 파편화가 심해지면 전체 GC 를 실행해야 한다.</p>
<p>JDK 8 까지는 Foreground Collection 이라고 해서 <code>-XX:+UseCMSCompactAtFullCollection</code>, <code>-XX:+UseCMSCompactAtFullCollection</code> 옵션을 활용하여 Full GC 전 Compact 를 하여 파편화로 인한 메모리 부족을 해결할 수 있었다.</p>
<p>하지만 <a href="https://bugs.openjdk.org/browse/JDK-8010202">JDK-8010202</a>에서 Foreground Collection 은 전체 GC 보다 더 오래 걸리고 문제가 많아서 <a href="https://openjdk.org/jeps/214">JEP-214</a> 에 의해 JDK9 부터는 사라졌다.</p>
<p>그래서 메모리 파편화는 결국 CMS 에서 근본적 문제로 남게 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[가비지 컬렉터 (1) - Classic]]></title>
            <link>https://velog.io/@jimmy_neutron/%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%ED%84%B0-%EC%A2%85%EB%A5%98-1</link>
            <guid>https://velog.io/@jimmy_neutron/%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%ED%84%B0-%EC%A2%85%EB%A5%98-1</guid>
            <pubDate>Wed, 07 Jan 2026 12:37:03 GMT</pubDate>
            <description><![CDATA[<h1 id="garbage-collector-를-볼-때">Garbage Collector 를 볼 때</h1>
<p>Garbage Collector 의 성능은 <code>처리량(Throughput)</code>, <code>지연 시간(Latency)</code>, <code>메모리 사용량(footprint)</code> 세가지 요소로 측정된다. 이 세가지는 모두 만족시키기 어렵다.</p>
<p>Garbage Collector 를 알아볼때에는 각 컬렉터들이 어떤 성능을 중요시하는지 알고 보는게 좋다.</p>
<h2 id="지연-시간">지연 시간</h2>
<p>지연 시간은 여러 요소에 의해 바뀌겠지만, Garbage Collector 관점에서는 일시 정지 시간과 관련이 많다. Stop-The-World 라고 하는 일시 정지 시간은 죽은 객체를 판별(mark)하거나, 살아남은 객체를 모으는(compact) 과정에서 필요하다.</p>
<p>이 STW 는 애플리케이션 전체를 멈추게 하기 때문에 P99, P999 같이 튀는 latency 를 유발할 수 있다. 멈춰있는 시간이 길수록, 그 사이에 해야 할 일이 많이 쌓일수록 요청중 튀는 latency 가 발생할 확률이 높다.</p>
<p>따라서 CMS, 셰넌도어, ZGC 같은 경우는 STW 를 최소한으로 줄이기 위한 방법들로 발전해 왔다. 큰 틀에서는 mark, compact 과정을 유저 스레드와 <code>동시에</code> 실행하는 방법을 각 garbage collector 에서 다르게 구현한다.</p>
<p>지연 시간이 짧은 garbage collector 는 유저 대상으로 하는 서버 애플리케이션에서 적절하다. java 언어가 서버 애플리케이션 개발에서 가장 많이 쓰이기 때문에, 최신의 garbage collector 들은 지연 시간을 최소화 하는 방향으로 진화하고 있다.</p>
<h2 id="처리량">처리량</h2>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/91d23c41-6411-4032-ac5c-61c77405d466/image.png" alt=""></p>
<p>처리량은 위와 같다. 사용자 코드 실행 시간이 99초, GC 시간이 1초라면 처리량은 99% 이다.
한정된 CPU 자원에서 GC 를 위해 스레드가 사용되는 비율이 얼마나 되는가를 수치화 한 것이다.
즉 <code>짧은 GC 시간에 많은 양의 객체를 정리할 수 있는</code> garbage collector 가 처리량이 좋다고 할 수 있다.</p>
<p>보통 짧은 지연 시간을 추구하면 처리량은 낮아진다.</p>
<ol>
<li>처리해야 하는 객체는 변하지 않는데 지연 시간을 짧게 목표한다면 한번의 STW 를 짧게 유지해야한다. 그렇게 되면 결국 GC 를 짧게 여러번 호출해야하기 때문에 그만큼 처리량은 낮아진다.</li>
<li>지연 시간을 짧게 하기 위해 유저 스레드와 동시에 GC 를 실행한다고 했다. 그 만큼 유저 스레드가 CPU 자원을 사용한다는 것이므로 처리량이 낮아질 수 있다.</li>
</ol>
<p>저지연 컬렉터인 ZGC, 셰넌도어의 목표는 지연 시간을 최소로 함과 동시에 G1 에 비해 처리량의 감소가 10% 미만으로 되게끔 하는 것이었다.</p>
<p>처리량이 높은 GC 는 분석 애플리케이션 등 유저와 상호작용이 적은 애플리케이션에 적합하다.</p>
<h2 id="메모리-사용량">메모리 사용량</h2>
<p>세대 구분 이론에 따른 메모리 layout 을 처리하는 Garbage Collector 들은 세대간 참조를 해결하기 위해 카드 테이블을 관리한다.
이 카드 테이블도 Gacbage Collector 마다 재각각이며, 메모리 사용량이 다르다.</p>
<p>또한, 메모리 사용량은 아니지만 Garbage Collector 들은 카드 테이블 관리에 사후 write barrier 를 사용하거나, ZGC 에서 컬러 포인터 구현에 load barrier 를 쓰는 등 여러가지 barrier 를 사용한다. 이는 애플리케이션에 가하는 실행 오버헤드이며 주시해야 하는 footprint 이다.</p>
<h2 id="세대-구분-이론에-따른-알고리즘-선택">세대 구분 이론에 따른 알고리즘 선택</h2>
<p><img src="https://velog.velcdn.com/images/jimmy_neutron/post/cfb02acb-f03c-4af9-8a84-adc0fdd6f713/image.png" alt="">
<a href="https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html">https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html</a>
G1 GC 이전의 GC(Serial, Parallel, CMS) 들은 모두 위와 같은 메모리 레이아웃에서 동작했다.</p>
<p>weak generational hypothesis 에 따르면 새로 생성되는 객체중 대다수는 빠르게 죽기 때문에 Young Generation 의 GC 는 메모리 파편화를 방지하며 객체가 적을 때 효율적인 Mark-Copy 알고리즘을 사용한다.</p>
<p>반대로 Old Generation 의 GC 는 큰 영역을 청소하기 때문에 오래걸린다. 최대한 많은 양을 저장하여 GC 횟수를 줄이기 위해 크기의 반만 사용할 수 있는 Mark-Copy 대신 Mark-Sweep 또는 Mark-Compact 를 사용하는게 좋다.</p>
<p>초창기의 JDK 에서는 Garbage Collector 들이 Young/Old Gen 각각용으로 만들어졌고 조합을 하여 사용 가능했다. 시간이 지남에 따라 Garbage Collector 를 Young/Old 각각 설정할 수 있는 것이 아닌 하나만 설정하면 최적의 조합으로 셋팅되는 방식으로 하나로 합쳐졌다.</p>
<p>예를 들어 CMS 는 Old Gen 용 Collector 이다. 하지만 JDK9 부터는 CMS 를 사용하면 Young Gen 용 Collector 로서 Parnew 가 자동으로 선택된다.</p>
<p>이런 역사를 알아놓으면 이해하기 한층 편할 것 같다.</p>
<h1 id="gc-뭐쓰는지-알기">GC 뭐쓰는지 알기</h1>
<ol>
<li>ManagementFactory.getGarbageCollectorMXBeans()</li>
<li>-XX:+PrintGCDetails / -Xlog:gc* (버전에 따라 둘 중 하나)</li>
<li>jhsdb jmap --heap --pid [pid]<ul>
<li>jps 로 pid 얻을 수 있음.</li>
</ul>
</li>
</ol>
<h1 id="기본적-컬렉터들">기본적 컬렉터들</h1>
<h2 id="young-generation-garbage-collector">Young Generation Garbage Collector</h2>
<h3 id="serial--xxuseserialgc">Serial [-XX:+UseSerialGC]</h3>
<ul>
<li>mark-copy</li>
<li>메모리 사용량이 가장 적다.</li>
<li>단일코어, 또는 코어가 적은 환경에서 스레드 상호작용이 없으므로 오버헤드가 없다.</li>
<li>DefNew GC 라고도 한다.</li>
</ul>
<h3 id="파뉴parnew--xxuseparnewgc">파뉴(ParNew) [-XX:+UseParNewGC]</h3>
<ul>
<li>mark-copy</li>
<li>Serial 과 다른점은 GC 를 병렬로 처리한다는 것이다. 하지만 일반적으로 Parallel Collector 라고 부르는 컬렉터는 파뉴가 아니다. 처리량을 목표로 두고 처리량을 옵션으로 조절할 수 있는 PS 컬렉터가 Parallel 컬렉터다.</li>
<li>JDK8 까지는 default GC 가 Parallel 이며 CMS 가 권장되었다. Old 영역의 CMS 는 Young 영역에 PS 를 조합하지 못하였으므로 파뉴가 대신 많이 쓰였다.<ul>
<li><code>-XX:+UseConcMarkSweepGC</code> 로 cms 를 사용하면 기본적으로 <code>-XX:+UseParNewGC</code> 가 켜진다. <code>-XX:-UseParNewGC</code> 옵션을 주면 Serial Collector(DefNew young collector) 가 쓰인다.</li>
</ul>
</li>
<li>JDK9 부터는 G1 GC 가 default GC 이며 권장된다. <code>-XX:+UseParNewGC</code> 옵션은 사라졌으며, CMS 선택시 파뉴가 Young 영역 컬렉터로 강제된다.</li>
<li>싱글스레드 환경에서는 시리얼GC 보다 성능이 떨어진다.</li>
<li><code>-XX:ParallelGCThreads</code>  로 GC 스레드 수 제한 가능. CMS 쓸때 가능한 옵션.</li>
</ul>
<h3 id="페러렐-스캐빈지ps--xxuseparallelgc">페러렐 스캐빈지(PS) [-XX:+UseParallelGC]</h3>
<ul>
<li>mark-copy</li>
<li>처리량을 제어하는게 목표인 컬렉터.</li>
<li><code>-XX:MaxGCPauseMillis</code> : 정지 시간이 이 시간이 넘지 않도록 노력한다. 이 시간이 너무 적으면 Young 영역의 크기가 적게 할당되어 오히려 처리량이 떨어질 수 있다.</li>
<li><code>-XX:GCTimeRatio</code> : GC 시간이 1% 이상이 되지 않도록 보장. 기본값은 99.</li>
<li><code>-XX:+UseAdaptiveSizePolicy</code> : 가상머신이 성능 모니터링 정보를 수집하여 최적의 정지 시간과 처리량을 보장하기 위해 -Xmn, -XX:SurvivorRatio 같은 값들을 자동 조정해준다.<ul>
<li>-XX:MaxGCPauseMillis, -XX:GCTimeRatio 만 설정해주고 JVM 이 설정해주는 값을 모니터링하는것도 좋은 방법.</li>
</ul>
</li>
</ul>
<h2 id="old-generation-garbage-collector">Old Generation Garbage Collector</h2>
<h3 id="시리얼-올드--xxuseserialgc">시리얼 올드 [-XX:+UseSerialGC]</h3>
<ul>
<li>mark-compact</li>
<li>코어가 적은 환경에서 주로 쓰인다.</li>
<li>따로 Old Gen 만 이 컬렉터를 지정하는 옵션은 없다. <code>-XX:+UseSerialGC</code> 로 Young/Gen 모두 시리얼로 동작할 때 쓰이는 <code>MarkSweepCompact GC</code>(이름임) 가 이것이다.</li>
</ul>
<blockquote>
<ul>
<li><code>-XX:+UseParallelGC -XX:-UseParallelOldGC</code> 이렇게 옵션을 주면 Old 영역에 PS MarkSweep 가비지 컬렉터가 쓰인다.<ul>
<li>PS Mark Sweep GC 는 아래 페러렐 올드에서 쓰이는 병렬 버전과, 지금 설명에서의 조합에서 쓰이는 Serial 버전이 있다. </li>
<li>-XX:+PrintGCDetails 로 Old Gen 이름을 보면 -XX:+UseParallelOldGC 는 ParOldGen, -XX:-UseParallelOldGC 는 <a href="https://github.com/openjdk/jdk/blob/662cdf07462ccc6330bdb3d8f56694eafc49d71f/hotspot/src/share/vm/gc_implementation/parallelScavenge/psOldGen.cpp#L37">PSOldGen</a> 으로 표시되는걸 볼 수 있다.</li>
<li><a href="https://openjdk.org/jeps/366">JEP-366</a> 에서 JDK 14 에 이 조합은 폐기대상이 되었고, 15 부터는 UseParallelOldGC 옵션 자체가 없어졌다.</li>
</ul>
</li>
</ul>
</blockquote>
<ul>
<li>cms 의 동시 모드 실패시 백업용 컬렉터로도 Serial Old 가 쓰일 수 있다.</li>
</ul>
<h3 id="페러렐-올드--xxuseparalleloldgc">페러렐 올드 [-XX:+UseParallelOldGC]</h3>
<ul>
<li>mark-compact</li>
<li>멀티스레드로 병렬 회수한다.</li>
<li>Young Gen PS 의 Old Gen 버전이다. 처리량이 중요하다면 PS + 페러렐 올드 조합을 사용하면 된다.</li>
<li>Old Gen 을 <a href="https://github.com/openjdk/jdk/blob/662cdf07462ccc6330bdb3d8f56694eafc49d71f/hotspot/src/share/vm/gc_implementation/parallelScavenge/psOldGen.cpp#L37">ParOldGen</a> 으로 관리하는 PS MarkSweep 컬렉터를 사용한다.</li>
<li>JDK8 의 기본 컬렉터 옵션인 -XX:+UseParallelGC 를 사용하면 Old Gen 에는 이 컬렉터가 자동 선택된다.</li>
<li>JDK9 이후 기본 컬렉터는 G1 으로 바뀌었다. 이때부턴 <code>-XX:+UseParallelGC</code> 옵션으로 Young Gen 에는 PS, Old Gen 에는 페러렐 올드를 사용한다.</li>
<li>JDK15 부터는 -XX:+UseParallelOldGC 옵션이 없어졌다.(<a href="https://openjdk.org/jeps/366">JEP-366</a>)</li>
</ul>
<hr>
<p>참고
<a href="https://openjdk.org/jeps/366">https://openjdk.org/jeps/366</a>
<a href="https://www.fasterj.com/articles/oraclecollectors1.shtml">https://www.fasterj.com/articles/oraclecollectors1.shtml</a>
<a href="https://stackoverflow.com/questions/16442934/serial-mark-sweep-compact-psoldgen-ps-stands-for?utm_source=chatgpt.com">https://stackoverflow.com/questions/16442934/serial-mark-sweep-compact-psoldgen-ps-stands-for?utm_source=chatgpt.com</a>
<a href="https://blog.ragozin.info/2011/12/garbage-collection-in-hotspot-jvm.html">https://blog.ragozin.info/2011/12/garbage-collection-in-hotspot-jvm.html</a>
<a href="https://stackoverflow.com/questions/39929758/ps-marksweep-is-which-garbage-collector">https://stackoverflow.com/questions/39929758/ps-marksweep-is-which-garbage-collector</a></p>
]]></description>
        </item>
    </channel>
</rss>