JVM 이란 

  • java 바이트코드를 실행하기 위한 가상머신이다.
  • 컴파일러: 자바 소스 코드는 컴파일러가 바이트 코드로 변환한다.

JVM의 구성

  • 클래스 로더, 실행 엔진, 런타임 데이터 영역, 네이티브 인터페이스 로 구성한다.
  • 클래스 로더: 클래스 파일을 메모리에 가져온다
  • 실행 엔진: 바이트 코드를 실제로 실행한다.
  • 런타임 데이터 영역: 런타임에 필요한 메모리를 관리한다
  • 네이티브 인터페이스: c/c++ 등 자바 외부 코드와 연결할 때 사용한다.

JVM 메모리 구조 

  • 메소드 영역: 지역변수나 호출 정보를 저장한다.
  • 힙: 동적으로 할당하는 객체가 저장되는 공간
  • 스택: 스레드별로 생성된다.
  • PC레지스터: 현재 실행중인 jvm 명령의 주소를 저장한다.
  • 네이티브 메서드 스택: jni 를 통해 호출되는 네이티브 코드의 스택이다.

 

GC (Garbage Collection)

  • 더이상 참조되지 않는 객체를 메모리에서 제거하는 기능이다. 
  • 메모리 관리를 자동화 해주니까 개발자의 부담이 줄어든다는 장점이 있다. 
  • 단점
    • 객체를 메모리에서 제거하려면 일시적으로 애플리케이션이 멈춰야 한다. 이 타이밍을 개발자가 예측하기는 어렵다 
    • 따라서 실시간성이 중요한 시스템에서는 부적합할 수 있다 

 

GC 를 모니터링 해야 하는 이유

  • GC 가 자주 발생하면 애플리케이션 성능이 저하될 수 있다. 따라서 얼마나 자주, 얼마나 오래 멈추는지 확인해야 한다. 
  • 이를 통해 메모리 사용량이 적절한지, 어느 객체가 오래 살아남는지 등을 알 수 있다. 이를 기반으로 튜닝 포인트를 찾는다. 

 

메모리 누수를 어떻게 확인할 수 있을까 

  • 메모리 누수는 객체가 참조된 상태로 계속 남아 GC 가 제거하지 못할 때 발생한다. 
  • JVisualVM, Eclipse MAT 같은 도구로 힙 덤프를 분석할 수 있다. 
    • 어떤 클래스가 얼마나 많은 객체를 가지고 있는지, 참조 체인이 어떻게 되는지 시각적으로 보여준다.

 

GC 에서 사용하는 알고리즘 

 

1. Mark and Sweep

2. 카피 알고리즘 

3. Generation GC 

    객체가 얼마나 오래 생존하는지에 따라 관리 

4. Parallel GC (-> 자바 8 이 기본으로 채택한 방식) 

 

 

Java 8 기준으로 GC 는 어떤 방식으로 수행될까 

  • 힙 메모리를 Young Generation과 Old Generation으로 나눠 관리한다
  • Young 영역에서 발생하는 GC를 Minor GC, Old에서 발생하는 GC를 Major GC 또는 Full GC라고 한다
  • Young 영역은 Eden과 Survivor 영역 두 개로 나뉘고, 새로 생성된 객체는 Eden에 저장한다.
    • Eden이 가득 차면, GC가 실행되고 살아남은 객체는 Survivor 영역으로 이동한다 
    • 몇 번을 살아남으면 Old 영역으로 이동한다.
  • Old 영역은 주로 Mark-Sweep-Compact 알고리즘으로 관리되고, 기본적으로는 Parallel GC가 적용된다.

 

 

 

728x90

'프로그래밍 > JAVA' 카테고리의 다른 글

synchronized  (0) 2025.04.03
volatile  (0) 2025.04.03
스레드 생명 주기  (0) 2025.04.03
스레드 생성과 실행  (0) 2025.04.03
List  (0) 2025.03.28

 

멀티스레드 사용 시 가장 주의할 점은

여러 스레드가 같은 자원에 동시에 접근할 때 발생하는 동시성 문제다.

대표적인 공유 자원은 인스턴스의 필드(멤버 변수) 다.

 

 

synchronized 키워드를 붙이면, 한 번에 하나의 스레드만 실행할 수 있는 코드 구간을 만들 수 있다.

 

public class BankAccountV2 implements BankAccount {

  private int balance; // 잔액

  public BankAccountV2(int balance) {
    this.balance = balance;
  }

  @Override
  public synchronized boolean withdraw(int amount) {
    log("거래 시작: " + getClass().getSimpleName());
    log("[검증 시작] 출금액: " + amount + ", 잔액:" + balance);

    if (balance < amount) {
        log("[검증 실패] 출금액: " + amount + ", 잔액:" + balance);
        return false;
    }
    log("[검증 완료] 출금액: " + amount + ", 잔액:" + balance);
    sleep(1000); // 출금 소요 시간 1초
    balance -= amount;

    log("[출금 완료] 출금액: " + amount + ", 잔액:" + balance);
    log("거래 종료");
    return true;
  }

  @Override
  public synchronized int getBalance() {
      return balance;
  }
}

 

 

모든 인스턴스는 자신만의 Lock 을 가지고 있다.

  • 이것을 모니터 락(monitor lock) 으로도 불린다.
  • 객체 내부에 있는데 개발자가 확인하기는 어렵다.
  • t1 스레드 작업이 종료되면, synchronized 블럭을 나갈 때, BankAccount 인스턴스의 락을 반납한다.
  • BLOCKED 상태로 락 획득을 대기하는 t2 스레드는 자동으로 락을 획득한다.
    • t2 스레드 상태변경: BLOCKED → RUNNABLE
    • [참고] BLOCKED 상태인 스레드가 여러개일 때, 락을 획득하는 순서는 보장되지 않는다.
  • t2 스레드는 검증 로직을 통과하지 못한다. 락을 반납하면서 return 한다.
  • volatile 를 사용하지 않아도, synchronized 안에서 접근하는 변수의 메모리 가시성 문제는 해결된다.

 

BLOCKED 상태 스레드는

  • 락이 풀릴 때 까지 무한 대기한다
  • 중간에 인터럽트를 걸 수 없다
  • BLOCKED 상태인 스레드가 여러개일 때, 락을 획득하는 순서는 보장되지 않는다
  • → 순서를 모르니까 특정 스레드가 엄청 오래 기다리게 될 수도 있다.

 

synchronized 코드 블럭

synchronized 는 전체적으로 보면 성능이 떨어질 수 있다.

따라서 동시에 실행할 수 없는 코드 구간은 꼭! 필요한 곳으로 한정해서 설정해야 한다.

 

 

특정 코드 블럭만 synchronized 를 붙이자

  • 메서드 단위가 아니라 특정 블럭에 최적화하여 적용할 수 있다.
public boolean withdraw(int amount) {
    log("거래 시작: " + getClass().getSimpleName());

    synchronized (this) {
        log("[검증 시작] 출금액: " + amount + ", 잔액:" + balance);
        if (balance < amount) {
            log("[검증 실패] 출금액: " + amount + ", 잔액:" + balance);
            return false;
        }
        log("[검증 완료] 출금액: " + amount + ", 잔액:" + balance);
        sleep(1000); // 출금 소요 시간 1초
        balance -= amount;
        log("[출금 완료] 출금액: " + amount + ", 잔액:" + balance);
    }
    
    log("거래 종료");
    return true;
}
  • synchronized (this) {} 임계 영역을 코드 블럭으로 지정한다
  • synchronized (this) : 여기서 괄호 안에 들어가는 값은 락을 획득할 인스턴스의 참조다.
    • 여기서는 BankAccountV3(x001) 의 인스턴스의 락을 사용하므로 이 인스턴스의 참조인 this 를 넣는다.
728x90

'프로그래밍 > JAVA' 카테고리의 다른 글

JVM, GC  (0) 2025.04.10
volatile  (0) 2025.04.03
스레드 생명 주기  (0) 2025.04.03
스레드 생성과 실행  (0) 2025.04.03
List  (0) 2025.03.28

 

메모리 가시성 문제

  • 멀티스레드에서 어려운 문제 중 하나가 메모리 가시성 문제다.
  • 한 스레드가 변경한 값이 다른 스레드에서는 언제 보이는지에 대한 문제다
  • 무슨 문제이고 왜 발생하고 어떻게 해결할 지 살펴보자.

일반적으로 생각하는 메모리 접근 방식

실제 메모리 접근 방식

  • main 스레드가 runFlag = false 실행하면,
  • main 스레드를 실행하는 CPU 코어의 캐시 메모리의 runFlag = false 부터 반영한다.
  • 그래서 메인 메모리의 runFlag 값이 즉시 반영되지 않는다!

CPU 캐시 메모리

  • 캐시 메모리를 사용하면 CPU 처리 성능을 개선할 수 있다.
  • 코어1 캐시 메모리에 있는 runFlag 의 값이 언제 메인 메모리에 반영될까?
  • 메인 메모리에 변경된 runFlag 값이 언제 CPU 코어2의 캐시 메모리에 반영될까?

→ 둘 다 “ 알 수 없다.” CPU의 설계 방식에 따라 다르다.

  • 주로 컨텍스트 스위칭이 될 때, 캐시 메모리가 갱신된다.
  • 하지만, 이것이 갱신을 보장하는 것은 아니다.

 

 

값을 읽을 때, 값을 쓸 때 모두 메인 메모리에 직접 접근하자!

자바에서는 volatile 키워드로 이런 기능을 제공한다.

 

volatile 키워드 적용하기

  • 캐시 메모리 접근하지 말고, 메인 메모리 접근해!
public class VolatileFlagMain {

  public static void main(String[] args) {
      MyTask task = new MyTask();
      Thread t = new Thread(task, "work");
      log("runFlag = " + task.runFlag);
      t.start();

      sleep(1000);
      log("runFlag 변경 시도");
      task.runFlag = false;
      log("runFlag = " + task.runFlag);
      log("main 종료");
  }

  static class MyTask implements Runnable {
  
      volatile boolean runFlag = true;

      @Override
      public void run() {
          log("task 시작!");
          while(runFlag) {
              // runFlag 가 false 되면 탈출하자
          }
          log("task 종료");
      }
  }
}
  • runFlag 변수를 false 바꾸는 순간 즉시 메인 메모리에 반영한다.
  • 여러 스레드에서 같은 값을 읽고 써야 한다면, volatile 키워드를 붙이자.
  • 단, 캐시 메모리를 쓸 때보다는 성능이 느려지는 단점이 있기 때문에 꼭 필요한 곳에서만 사용해야 한다.
728x90

'프로그래밍 > JAVA' 카테고리의 다른 글

JVM, GC  (0) 2025.04.10
synchronized  (0) 2025.04.03
스레드 생명 주기  (0) 2025.04.03
스레드 생성과 실행  (0) 2025.04.03
List  (0) 2025.03.28

 

스레드는 생성 → 시작 → 종료되는 생명주기를 가진다.

스레드 생명 주기

 

  • 일단 실행 가능한 상태가 되면, 일시 중지 상태로 바뀔 수 있다
    • 참고: 자바에서 스레드의 ‘일시 중지 상태’ 라는 것은 없다.
    • 스레드가 기다리는 상태를 묶어서 설명하기 위해 사용한 용어다.

5가지 상태가 있다는 정도만 알아두자. 뒤에서 자세히 다루기 때문이다.

 

1. New (생성만 된 상태)

  • Thread 객체가 생성되지만, start()메서드가 호출되지 않은 상태다.
  • 예: Thread thread = new Thread(runnable)

2. Runnable (실행 가능 상태)

  • 보통 실행 상태라고 부른다
  • start() 메서드가 호출되면, 스레드는 이 상태로 들어간다.
  • 이 상태에서 스레드는 실제로 CPU에서 실행될 수 있다. (이 상태에서만 CPU 스케쥴러에 들어가요!)
  • 운영체제의 스케쥴러가 각 스레드에 CPU 시간을 할당하여 실행하기 때문에, Runnable 상태에 있는 스레드는 스케쥴러의 실행 대기열에 포함되어 있다가 차례로 CPU에서 실행된다.
    • 즉, 운영체제 입장에서 보면, CPU가 실제로 실행하는 스레드와 OS 스케쥴링 큐에 대기한 스레드 둘 다 포함이다.

3. Blocked (차단 상태)

  • 동기화 락을 얻기 위해 기다리는 상태
  • 예를 들어, synchronized 블록에 진입하기 위해 을 얻어야하는 경우 이 상태에 들어간다.
    • 락은 스레드 하나만 가지고 있을 수 있다.

4. Waiting (대기 상태)

  • 스레드가 다른 스레드의 특정 작업이 완료되기를 무기한 기다리는 상태
  • 스레드는 다른 스레드가 notify(), notifyAll() 메서드를 호출하거나, join()이 완료될 때 까지 기다린다.
  • wait(), join() 메서드가 호출되면 이 상태가 된다.
  • 예: object.wait()

5. Timed Waiting (시간 제한 대기 상태)

  • 스레드가 특정 시간 동안만 대기하는 상태
  • 주어진 시간이 경과하거나, 다른 스레드가 해당 스레드를 깨우면 이 상태에서 벗어난다.
  • sleep(long millis), wait(long timeout), join(long millis) 메서드 호출 시 이 상태가 된다.
  • 예. Thread.sleep(1000);

6. Terminated (종료 상태)

  • 스레드 실행이 완료된 상태.
  • 스레드가 정상적으로 종료되거나 예외가 발생한 경우에 Terminated 가 된다.
  • 스레드가 종료되면 다시 시작할 수 없다.
728x90

'프로그래밍 > JAVA' 카테고리의 다른 글

synchronized  (0) 2025.04.03
volatile  (0) 2025.04.03
스레드 생성과 실행  (0) 2025.04.03
List  (0) 2025.03.28
스레드 생성하기  (0) 2025.03.28

 

HelloThread - 스레드 생성

스레드를 생성해보자.

클래스에 Thread 를 상속 받고 run 메서드를 재정의하면 된다!

public class HelloThread extends Thread {
  @Override
  public void run() {
      System.out.println(
      Thread.currentThread().getName() + " : run()");
	}
}

 

HelloThreadMain

  • 해당 코드를 실행하는 스레드 이름을 출력한다 Thread.currentThread().getName()
    • 그래서 메인 메서드를 실행하는 main 이라는 스레드명이 제일 처음에 출력된다.
    • 누가 이 코드를 실행하는지 스레드명을 확인하면서 학습하자.
public static void main(String[] args) {
  /** Thread.currentThread().getName() -> 스레드명이 main 이라고 출력된다 */
  System.out.println(Thread.currentThread().getName() + ": main() start");
  
  HelloThread helloThread = new HelloThread(); // HelloThread 객체를 생성
  System.out.println(Thread.currentThread().getName() + ": start() 호출 전 ");
  
  helloThread.start(); // start() 를 호출한다 ! 
  // 이 시점부터 main 스레드와 Thread-0 이 동시에 실행된다.
  
  System.out.println(Thread.currentThread().getName() + ": start() 호출 후 ");

  System.out.println(Thread.currentThread().getName() + ": main() end");
}
  • HelloThread 인스턴스를 생성하고 HelloThread helloThread = new HelloThread()
    • 이것은 인스턴스 생성만 됬을 뿐, 스택 프레임이 할당된 게 아니다.
  • start() 를 호출한다. helloThread.start()

 

중요 - 어느 시점에 새로운 스택 프레임이 생성될까 

  • helloThread.start() 를 호출하면, HelloThread 의 스택 프레임이 생성된다.
  • (main 스레드와는 별도의 스택 프레임이다)
  • 그리고 HelloThread 가 run() 를 호출한다.
  • 주의할 점은 main 스레드가 아니라, HelloThread 가 run() 을 호출한다는 점이다.
  • main 스레드는 단지 helloThread.start() 호출을 통해서 다른 스레드에게 일을 시작하라고 지시할 뿐이다.

 

스레드 간의 실행 순서와 실행 기간을 둘 다 보장하지 않는다.

  • 스레드는 동시에 실행되기 때문에 실행 순서는 얼마든지 달라질 수 있다.
main: main() start
main: start() 호출 전 
main: start() 호출 후 
Thread-0 : run()
main: main() end
  • 실행 할 때마다 조금 달라진다. Thread-0 이 제일 밑에 출력될 때도 있다
main: main() start
main: start() 호출 전 
main: start() 호출 후 
main: main() end
Thread-0 : run()

정리

  • 스레드 객체를 생성하고, 반드시 start() 를 호출해야 스택 공간을 할당 받고 스레드가 작동한다.
  • 스레드는 순서와 실행 기간을 모두 보장하지 않는다! 이것이 멀티스레드다.
728x90

'프로그래밍 > JAVA' 카테고리의 다른 글

volatile  (0) 2025.04.03
스레드 생명 주기  (0) 2025.04.03
List  (0) 2025.03.28
스레드 생성하기  (0) 2025.03.28
Exception  (0) 2025.03.20

순서가 있고, 중복을 허용하는 자료구조를 List 라 한다.

  • ArrayList 와 LinkedList 는 사용자 입장에서는 기능이 똑같다.
  • 내부 구현만 다르므로 성능이 달라질 수 있다.

 

배열 리스트와 연결리스트 성능 비교

  ArrayList LinkedList
인덱스 조회 O(1) O(n)
데이터로 검색 O(n) O(n)
앞에 추가/삭제 O(n) O(1)
뒤에 추가/삭제 O(1) O(n)
평균 추가/삭제 O(n) O(n)

ArrayList 배열리스트

  • 배열로 구성
  • 인덱스를 통해 추가/삭제할 위치를 O(1) 로 빠르게 찾는다
  • 추가 시, 데이터를 전부 한 칸씩 밀어야 해서 O(n)으로 오래 걸린다
  • 배열이라서 메모리 상에서 연속적으로 위치하여 CPU 캐시 효율이 좋고 메모리 접근 속도가 빠르다.

LinkedList 연결리스트

  • 이중 연결리스트로 구현되어 있다
  • LinkedList 는 개별 노드가 앞/뒤 노드의 참조를 관리하므로 메모리 접근 속도가 배열에 비해서는 떨어진다
    • 인덱스를 제공하지 않으므로 순회해야 위치를 찾을 수 있다. O(n) 소요
    • 추가 시, O(n)으로 오래 걸린다
    • 해당 위치 찾는데 O(n) 소요되고, 참조 변경은 O(1) 소요
  • 만약, 데이터를 앞쪽에 자주 추가/삭제 할 일이 있다면, 연결 리스트를 고려하자
728x90

'프로그래밍 > JAVA' 카테고리의 다른 글

스레드 생명 주기  (0) 2025.04.03
스레드 생성과 실행  (0) 2025.04.03
스레드 생성하기  (0) 2025.03.28
Exception  (0) 2025.03.20
String 클래스  (1) 2025.03.20

HelloThread - 스레드 생성

스레드를 생성해보자.

클래스에 Thread 를 상속 받고 run 메서드를 재정의하면 된다!

public class HelloThread extends Thread {
  @Override
  public void run() {
      System.out.println(
      Thread.currentThread().getName() + " : run()");
	}
}

HelloThreadMain

  • 해당 코드를 실행하는 스레드 이름을 출력한다 Thread.currentThread().getName()
    • 그래서 메인 메서드를 실행하는 main 이라는 스레드명이 제일 처음에 출력된다.
    • 누가 이 코드를 실행하는지 스레드명을 확인하면서 학습하자.
public static void main(String[] args) {
  /** Thread.currentThread().getName() -> 스레드명이 main 이라고 출력된다 */
  System.out.println(Thread.currentThread().getName() + ": main() start");
  
  HelloThread helloThread = new HelloThread(); // HelloThread 객체를 생성
  System.out.println(Thread.currentThread().getName() + ": start() 호출 전 ");
  
  helloThread.start(); // start() 를 호출한다 ! 
  // 이 시점부터 main 스레드와 Thread-0 이 동시에 실행된다.
  
  System.out.println(Thread.currentThread().getName() + ": start() 호출 후 ");

  System.out.println(Thread.currentThread().getName() + ": main() end");
}
  • HelloThread 인스턴스를 생성하고 HelloThread helloThread = new HelloThread()
    • 인스턴스 생성만 됬을 뿐, 스택 프레임이 할당된 게 아니다.
  • start() 를 호출한다.

 

helloThread.start() 를 호출하면, HelloThread 의 스택 프레임이 생성된다.

(main 스레드와는 별도의 스택 프레임이다)

그리고 HelloThread 가 run() 를 호출한다.

주의할 점은 main 스레드가 아니라, HelloThread 가 run() 을 호출한다는 점이다.

main 스레드는 단지 helloThread.start() 호출을 통해서 다른 스레드에게 일을 시작하라고 지시할 뿐이다.

 

  • 스레드는 동시에 실행되기 때문에 실행 순서는 얼마든지 달라질 수 있다.
main: main() start
main: start() 호출 전 
main: start() 호출 후 
Thread-0 : run()
main: main() end
  • 실행 할 때마다 조금 달라진다. Thread-0 이 제일 밑에 출력될 때도 있다

정리 

  • 스레드 객체를 생성하고, 반드시 start() 를 호출해야 스택 공간을 할당 받고 스레드가 작동한다.
  • 스레드는 순서와 실행 기간을 모두 보장하지 않는다!

 

 

Runnable 인터페이스

  • 자바가 제공하는 스레드 실행용 인터페이스다.
public interface Runnable {
		void run();
}

어떻게 사용하는지 살펴보자.

HelloRunnable implements Runnable

public class HelloRunnable implements Runnable {
  @Override
  public void run() { // run() 내에 작업 로직을 구현한다 
      System.out.println(Thread.currentThread().getName() + ": run()");
  }
}

 

HelloRunnableMain

  • 실행 결과는 기존과 같다.
  • 차이가 있다면, Thread 와 해당 스레드가 실행할 작업이 분리되어 있다는 점이다.
  • Thread 객체를 생성할 때, 생성자로 작업 클래스를 전달한다.
public class HelloRunnableMain {
  public static void main(String[] args) {
    System.out.println(Thread.currentThread().getName() + ": main() start");

    HelloRunnable runnable = new HelloRunnable(); // 작업 정의
    Thread thread = new Thread(runnable); // 스레드
    thread.start();

    System.out.println(Thread.currentThread().getName() + ": main() end");
  }
}

 

Runnable 인터페이스 구현하는 방식이 더 나은 이유!

Thread 클래스 상속 방식의 단점

  • 자바는 단일 상속만을 허용하므로, Thread 클래스를 상속받으면, 다른 클래스를 상속받을 수 없다.
  • 인터페이스를 사용하는 방식에 비해 유연성이 떨어진다.

Runnable 인터페이스를 구현하는 방식

  • 상속의 자유로움
  • 코드의 분리: 스레드와 실행할 작업은 분리하여 코드의 가독성을 높일 수 있다.
  • 여러 스레드가 동일한 Runnable 객체를 공유할 수 있어 자원 관리를 효율적으로 할 수 있다!
728x90

'프로그래밍 > JAVA' 카테고리의 다른 글

스레드 생성과 실행  (0) 2025.04.03
List  (0) 2025.03.28
Exception  (0) 2025.03.20
String 클래스  (1) 2025.03.20
자바 객체 지향  (0) 2025.03.13

+ Recent posts