싱글톤(Singleton)

•  인스턴스를 오직 한 개만 생성할 수 있는 클래스를 싱글톤 이라고 한다.

•  객체 생성 요청이 여러번 발생하더라도 새롭게 메모리를 할당하여 인스턴스를 만드는 것이 아닌,
    기존에 생성되어 있던 인스턴스를 참조한다.

•  사용 예시 : Scheduling 처리 객체 등

•  장점 : 불필요한 메모리 사용을 줄일 수 있다

•  단점 : 이를 사용하는 클라이언트를 테스트하기 어려워 질 수 있다.

    ◦  싱글톤 인스턴스를 가짜(mock) 구현으로 대체할 수 없기 때문.


싱글톤을 만드는 두가지 방법

•  공통점 : 생성자는 private으로, 인스턴스에 접근할 수 있는 유일한 수단 = public static 멤버

1) public static 멤버를 final 필드로 선언

public class Elvis {
		**public** static **final** Elvis INSTANCE = new Elvis();
		private Elvis() { ... }
}

•  private Elvis 생성자는 Elvis.INSTANCE 를 생성할 때(new Elvis) 딱 한 번 호출됨

•  다른 클래스에서 참조할 수 없는 생성자(private) 이므로 Elvis 클래스가 가지는 인스턴스는 Elvis.INSTANCE 단 한 개 뿐임을 보장할 수 있다.

•  예외) 권한이 있는 클라이언트가 AccessibleObject.setAccessible(리플렉션 API)을 사용해 private 생성자를 호출하는 경우

   ◦  리플렉션 : 개발자가 클래스의 구조를 확인하거나, 값/메소드를 호출해야 할 때 사용됨 (런타임 동작 검사 및 수정에 주로 이용된다)

   ◦  setAccessible : 필드나 메서드의 접근제어 지시자에 의한 제어를 변경하는 메서드

Elvis.setAccessible(true); // 이제 외부에서 private Elvis() 접근가능!

•  장점) 해당 클래스가 싱글톤임이 명백하게 드러나며 간결하다.

   ◦  public static 필드가 final 이므로 절대 다른 객체를 받을 수 없기 때문

 

2) 정적 팩토리 메서드를 public static 멤버로 제공

public class Elvis {
		**private** static **final** Elvis INSTANCE = new Elvis();
		private Elvis() { ... }
		**public static Elvis getInstance() { return INSTANCE; }**
}

•  Elvis.getInstance : 항상 같은 객체의 참조를 반환하므로 제2의 Elvis 인스턴스는 생성될 수 없음.

•  장점 1) API를 바꾸지 않고도 싱글톤이 아니게 변경 가능 (스레드별로 다른 인스턴스를 넘겨주는 등)

•  장점 2) 정적 팩토리 → 제네릭 싱글톤 팩토리 로 변경 가능

•  장점 3) 정적 팩토리의 메서드 참조를 공급자(supplier)로 사용 가능

   ◦  Elvis::getInstance 를 Supplier<Elvis>로 사용하는 식


싱글톤 클래스의 직렬화

•  직렬화란? : 자바 시스템 내부에서 사용되는 객체 또는 데이터들을 외부의 자바 시스템에서도 사용할 수 있도록
                       바이트(byte) 형태로 데이터 변환하는 기술

•  역직렬화 : 바이트로 변환된 데이터를 다시 객체로 변환하는 기술 (JVM)

•  모든 객체 필드를 일시적(transient)이라고 선언하고 readResolve 메서드를 제공해야 함

   ◦  안그러면 직렬화된 인스턴스를 역직렬화할 때 마다 새로운 인스턴스가 만들어진다.

       -  예 ) 새로운 인스턴스를 생성하는 것 = 가짜 Elvis를 생성하는 것

   ◦  역직렬화 과정에서 만들어진 인스턴스 대신 기존에 생성된 싱글톤 인스턴스를 반환해주는 역할

public class Elvis implements Serializable {
	private static final Elvis INSTANCE = new Elvis();
	private Elvis() { ...	}
	public static Elvis getInstance() { return INSTANCE; }

	**private Object readResolve() { return INSTANCE;	}**
}

싱글톤을 만드는 세번째 방법 : 원소가 한개인 Enum 클래스 사용하기

public enum Elvis { INSTANCE; }

•  코드가 매우 간결하고! 또 더 쉽게 직렬화가 가능하다.

•  복잡한 직렬화 상황 혹은 Reflection에 의해 제 2의 인스턴스가 생기는 일을 방지한다.

   ◦  enum은 기본적으로 serializable 하기 때문에 serializable interface 를 따로 구현할 필요가 없다. 따라서 역직렬화 시에 runtime 내부에 존재하는 enum class, 즉 동일한 값을 참조한다.

   ◦  enum class는 외부에서 액세스 할 수 있는 생성자 자체가 없으므로 reflection에 면역! (enum class 생성자 = Sole Constructor / 컴파일러에서 사용하는 것으로 사용자가 직접 호출 불가)

   ◦  따라서 대부분의 상황에서 Enum 선언이 싱글톤을 만드는 가장 좋은 방법이다.

       -  단, 만들고자 하는 싱글톤이 Enum 외의 클래스를 상속해야 한다면 이 방법은 사용할 수 없다.


참고자료

 

싱글톤(Singleton) 패턴이란?

이번 글에서는 디자인 패턴의 종류 중 하나인 싱글톤 패턴에 대해 알아보자. 싱글톤 패턴이 무엇인지, 패턴 구현 시 주의할 점은 무엇인지에 대해 알아보는 것만으로도 많은 도움이 될 것이라

tecoble.techcourse.co.kr

 

[Java] Enum의 사용법

안녕하세요. 지난 시간엔 EnumClass가 무엇이고 어떤 장점들이 있는지 알아보았습니다. 2017/06/27 - [Java] - [Java] enum 이란? (enum 개념익히기) 이번 시간은 Enum 사용방법을 더 알아보면서 Enum에 대한 이.

limkydev.tistory.com

 

[Java & Kotlin] enum class가 완벽한 싱글톤이라 불리는 이유

싱글톤 패턴(Singleton Pattern)이란? 싱글톤은 애플리케이션 상 특정 클래스가 최초 한 번만 메모리를 할당하고 그 메모리에 인스턴스를 만들어 사용하는 디자인 패턴을 의미한다. 객체 생성 요청이

dataportal.kr

 

[java] enum 의 serialize 에 대한 이야기

- enum 은 기본적으로 serializable 하다. - enum 의 serialization 은 다른 serialization 과 조금 다르다. enum 의 serialize 결과는 constant 의 이름뿐이다. enum serialize 시 ObjectOutputStream 에서는 'n..

aroundck.tistory.com

 

GC(Garbage Collector) 란 무엇인가?

•  자바는 객체를 생성하는 new 연산자는 있지만 객체를 소멸시키는 연산자는 없다.

   ◦  개발자가 직접 객체를 소멸시킬 수 없다.

•  객체를 소멸시킨다는 것의 의미는, new에 의해 생성된 객체가 차지하고 있던 공간을 JVM에게 돌려주어
     다시 사용할 수 있는 메모리 영역으로 만들어 준다는 것을 의미한다.

•  new로 할당받은 이후 사용하지 않게 된 객체 혹은 배열 메모리를 Garbage 라고 부른다.

•  이러한 Garbage를 정리하는것이 Garbage Collector의 역할이며, 적절한 시점에 자동으로 Garbage를 수집하여
     가용 메모리에 반환한다.
   ◦ 
GC의 스케줄링은 JVM에서 관리한다.

•  객체는 Heap 영역에 저장되고, Stack 영역에 이를 가리키는 주소값이 저장된다.

•  Heap 영역에는 존재하지만 Stack영역에서 해당 객체를 가리키는 주소값이 없는 객체를 ‘참조되지 않는
     (자신을 가리키는 포인터가 없는, unreachable)객체’ 라고 이야기한다.

•  GC는 해당 객체를 더 이상 사용되지 않는 객체라고 간주하여 이를 메모리에서 제거한다.

 

 

GC 의 필요성

•  GC가 없다면?

   ◦  C/C++ 언어처럼 일일이 메모리 해제를 해줘야 한다.

 

•  메모리 해제를 해주지 않는다면?

   ◦  코드가 돌아갈수록 메모리 가용 공간을 하나씩 차지함에 따라, 속도 등의 프로그램 성능이 매우 악화된다.

 

•  개발자가 직접 메모리 해제가 가능한 C++ 언어에서도 GC 기능을 제공한다.

   ◦  메모리 관리에 있어서 프로그래머의 부담을 덜어주기 때문.

         -  메모리 관리를 프로그래머가 직접 할 수 있지만, 부담이 크고 생산성이 떨어진다.

   ◦  new와 delete 만으로는 이룰 수 없는 무언가가 있다 → 메모리 파편화 현상

         -  new와 delete를 반복하면서 heap메모리에 사용하는 메모리와 사용하지 않는 메모리가 듬성듬성 위치하는 현상으로,
            메모리 사용 효율 저하의 원인이 된다.

         -  GC는 힙을 직접 관리하기 때문에 메모리 파편화 현상을 일정부분 해결할 수 있다.

         -  GC가 동작하는 동안 사용하지 않는 메모리를 정리하면서 힙 메모리의 빈부분을 최적화 할 수 있음

 

 

GC의 종류

Serial GC

•  GC를 처리하는 스레드가 1개

•  CPU 코어가 1개만 있을 때 사용하는 방식이다.

•  Mark-Compact collection 알고리즘을 사용해 GC를 실행한다.

 

Parallel GC

•  GC를 처리하는 스레드가 여러개

•  Serial GC보다 빠르게 객체 처리가 가능하다.

•  메모리가 충분하고 코어의 개수가 많을 때 사용하면 좋다.

 

Concurrent Mark Sweep GC(CMS GC)

•  Stop-The-World

   ◦  GC를 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 것

   ◦  stop-the-world가 발생하면 GC를 실행하는 스레드를 제외한 나머지 스레드는 모두 작업을 멈춤

   ◦  GC 작업을 완료한 이후에 중단한 작업을 다시 시작

•  CMS GC는 stop-the-world 시간이 짧다.

• 애플리케이션의 응답 시간이 빨라야 할 때 유용하다.

• 다른 GC 방식보다 메모리와 CPU를 더 많이 사용한다.

• Compaction 단계가 제공되지 않는다.

 

G1 GC

• 각 영역을 Region 영역으로 나눈다.

• GC가 일어날 때, 전체 영역(Eden, Survivor, Old generation)을 탐색하지 않는다.

• 바둑판 각 영역에 객체를 할당하고 GC를 실행한다.

• 해당 영역이 꽉 차면 다른 빈 영역에 객체를 할당하고 GC 실행한다.

• stop-the-world 시간이 짧다.

• Compaction 단계를 제공한다.

 

Z GC

• 확장 가능하고 낮은 지연율(low latency)을 가진 GC

   ◦  정지 시간이 최대 10ms를 초과하지 않는다.

• Heap의 크기가 증가하더라도 정지 시간이 증가하지 않는다.

   ◦  8MB~16TB에 이르는 다양한 범위의 Heap 처리가 가능하다.

• Stop-The-World 정지 시간을 줄이거나 없앰으로써 애플리케이션의 성능 향상에 기여한다.

   ◦  ZGC의 주요 원리는 Load barrierColored Pointer를 함께 사용하는 것

   ◦  이를 통해 Thread가 동작하는 중간에도 객체 재배치 같은 작업을 수행할 수 있다.

• Z GC는 메모리를 Z Pages라고 불리는 영역으로 나눈다.

   ◦  ZPages는 동적 사이즈(G1 GC와 다름)로 2MB의 배수가 동적으로 생성 및 삭제될 수 있다.

 

 

GC의 장단점

• 장점 : 편리성

   ◦  개발자가 동적으로 할당된 메모리 전체를 관리할 필요가 없다.

   ◦  유효하지 않은 포인터에 접근하거나 이미 한번 해제한 메모리를 또 해제하는 등의 버그를 피할 수 있다.

 

• 단점 : 예측 불가능성

   ◦  가비지 컬렉션이 수행되는 정확한 시점을 알 수가 없다.

         -  프로그램이 예측 불가능하게 일시 정지 될 수 있기 때문에, 실시간 시스템에는 적합하지 않다.

   ◦  가비지 컬렉션이 실행될 때는 반드시 애플리케이션을 중지시키는 Stop The World 가 수행되는데,
        이는 성능 저하의 원인이 될 수 있는 Overhead 를 일으킨다.

 

 

GC의 메모리 수거 방식

• 앞에서 정리했듯이, GC는 동적으로 할당한 메모리 영역 중 사용하지 않는 영역을 탐지하여 해제한다.

   ◦  Stack : 정적으로 할당한 메모리 영역

         -  원시 타입의 데이터 값과 함께 할당, Heap영역에 생성된 Object 타입의 데이터 참조 값 할당

   ◦  Heap : 동적으로 할당한 메모리 영역

         -  모든 Object 타입의 데이터가 할당, Heap영역의 Object를 가리키는 참조 변수가 Stack에 할당

 

• Heap에 저장된 객체를 Stack에서 참조하고 있는지 어떻게 확인할까? : Mark and Sweep

   ◦  가비지 컬렉터가 Stack의 모든 변수를 스캔하면서, 각각 객체를 참조하고 있는지 확인 후 마킹한다 (Mark)

   ◦  Reachable Object가 참조하고 있는 객체도 찾아서 마킹한다 (Mark)

   ◦  마킹되지 않은 객체를 Heap에서 제거한다 (Sweep)

→ JVM의 Heap 영역은 GC관점에서 다시 위의 3가지(4가지) 영역으로 구분될 수 있다.

 

Eden

• 새로운 객체가 할당되는 영역

• Eden 영역의 메모리가 전부 사용중인 경우 GC 발생 (Minor GC)

 → Eden 영역에서 Mark and Sweep

 

S0(Survivor 0)

• Eden 영역의 Reachable 객체가 S0 영역으로 옮겨짐

• Eden 영역의 Unreachable 객체는 메모리에서 해제

• S0 영역의 메모리가 전부 사용중인 경우 GC 발생 (Minor GC)

 → S0 영역에서 Mark and Sweep

 

S1(Survivor 1)

• S0 영역에 있던 객체는 S1 영역으로 이동

• 이동한 객체는 Age 값 증가

• Eden 영역의 Reachable 객체가 S1 영역으로 옮겨짐

• Eden 영역의 Unreachable 객체는 메모리에서 해제

• S1 영역의 메모리가 전부 사용중인 경우 GC 발생 (Minor GC)

 → S1 영역에서 Mark and Sweep

 

S0(Survivor 0)

• S1 영역에 있던 객체는 S0 영역으로 이동

• 이동한 객체는 Age 값 증가

• Eden 영역의 Reachable 객체가 S0 영역으로 옮겨짐

• Eden 영역의 Unreachable 객체는 메모리에서 해제

• S0 영역의 메모리가 전부 사용중인 경우 GC 발생 (Minor GC)

 → S0 영역에서 Mark and Sweep

 

위의 과정을 계속 반복한다

 

Old Generation

• Age 값이 특정 값 이상이 되면 Old Generation 영역으로 이동

   ◦  이 과정을 'Promotion' 이라고 한다.

   ◦  Old Generation 영역이 모두 사용중인 경우 GC 발생 (Major GC)

→ Old Generation 영역에서 Mark and Sweep

 

위의 과정 또한 계속 반복된다

 

• 만약 Young(New) Generation 영역과 Old Generation 영역이 모두 사용중인 경우 GC 발생 (Full GC)

→ 전체 영역에서 Mark and Sweep

 

S0 영역과 S1 영역 중 한쪽은 반드시 비어있다.

 

Permanent Generation

• 해당 영역은 GC의 관리영역이 아니다.

• JVM이 애플리케이션에서 사용하는 클래스 및 메소드를 설명하는 데 필요한 메타데이터를 포함하는 영역

• 애플리케이션에서 사용 중인 클래스에 기반하여, 런타임에 JVM에 의해 채워진다.

• Java SE 라이브러리 클래스 및 방법을 여기에 저장할 수 있다.

 

 

참고자료

 

[JAVA] 가비지 컬렉터(Garbage Collector)

객체의 소멸과 가비지 자바는 객체를 생성하는 new 연산자는 있지만 객체를 소멸시키는 연산자는 없음 => 개발자가 마음대로 객체를 소멸시킬 수 없음 객체 소멸이란 new에 의해 생성된 객체 공간

transferhwang.tistory.com

 

들어가기에 앞서

•  왜 JVM을 공부해야 할까?

  ◦  메모리 구조를 알아야 메모리 관리가 잘 되는 코드를 작성할 수 있기 때문

 

•  왜 메모리 관리를 해야 하는가?

  ◦  메모리 관리에 따라 프로그램의 성능은 월등하게 차이날 수 있다.

  ◦  메모리 부족으로 인한 성능저하 현상은 대부분 메모리에 대한 이해없이 코드를 설계한 경우 발생한다.

 

 

JVM의 개념과 특징

•  Java는 특정 운영체제에 종속되지 않도록 JVM이라는 가상머신 위에서 실행되게끔 만들어진 언어

  ◦  운영체제가 바뀌면? Java 코드는 그대로, JVM 만 다른것을 사용하면 OK

  ◦  리눅스 환경에서 만든 자바 파일을 윈도우에서 실행하고 싶다면 윈도우용 JVM을 설치하면 된다.

 

•  JVM(Java Virtual Machine) 자바와 운영체제 사이에서 중재자 역할을 수행하는 가상 머신

  ◦  자바가 운영체제에 구애 받지 않고 프로그램을 실행할 수 있도록 도와준다.

  ◦  자동으로 메모리 관리를 해준다. (GC)

  ◦  다른 하드웨어와 달리 레지스터 기반이 아닌 스택 기반으로 동작한다는 점이 특징

 

 

.java  프로그램이 실행되는 과정

•  자바 컴파일러에 의해 자바 소스 파일이 바이트 코드 형태의 클래스 파일로 변환된다.

•  클래스 로더가 해당 클래스 파일을 읽어들여 JVM 내에 객체를 로드하고 배치한다.

 

 

JVM 구성 요소

•  Class Loader

  ◦  컴파일 결과로 만들어진 .class 바이트코드 파일을 읽어들여 메모리에 배치

  ◦  로딩, 링크, 초기화 세 가지 과정을 거친다.

      -  런타임 시에 동적으로 클래스를 로드한다.

 

•  Execution Engine

  ◦  클래스 로더를 통해 Runtime Data Area에 배치된 바이트 코드들을 명령어 단위로 읽어서 실행한다.

  ◦  초기 JVM은 인터프리터 방식이었기 때문에 속도가 느리다는 단점이 있었지만,
      바이트 코드를 어셈블러 같은 네이티브 코드로 바꿔주는 JIT 컴파일러 방식을 통해 이를 보완했다.

      -  그러나 JIT를 통해 코드를 변환하는데에도 비용이 발생함

      -  때문에 JVM은 모든 코드를 JIT 컴파일러 방식으로 실행하지 않고,
         인터프리터 방식을 사용하다가 일정 기준이 넘어가면 JIT 컴파일러 방식으로 실행한다.

 

•  Garbage Collector

  ◦  힙 메모리 영역에 저장된 객체들 중 더이상 참조되지 않는 객체들을 탐색 및 제거하는 역할을 수행

  ◦  GC 동작하는 정확한 시간은 예측할 수 없다.

 

•  Runtime Data Area

  ◦  JVM 메모리

  ◦  Java 어플리케이션이 실행되는 와중에 할당받은 메모리영역이다.

 

  ◦  Runtime Data Area 의 주요 영역

       Method area (Meta Space)

      •  모든 쓰레드가 공유하는 메모리 영역

      •  클래스, 인터페이스, 메소드, 필드, Static 변수 등의 바이트 코드를 보관

 

       Heap area

      •  모든 쓰레드가 공유하는 메모리 영역

      •  new 키워드로 생성된 객체와 배열이 생성되는 영역

      •  Method area에 로드된 클래스만 생성 가능

      •  Garbage Collector가 작동하는 영역

 

       Stack area (Thread Stack)

      •  Method를 호출할 때 마다 각각의 스택 프레임(그 메서드만을 위한 공간)이 생성된다.

      •  Method의 parameter, 반환값, 지역변수등을 저장하는 용도로 사용된다.

      •  Method의 동작이 끝나면 해당 스택 프레임은 삭제된다.

 

       Code Cache

      •  JIT 컴파일러가 데이터를 저장하는 영역

      •  자주 접근하는 '컴파일된 코드 블록'이 저장된다.

      •  일반적으로 JVM은 바이트 코드를 기계어로 변환하는 작업을 수행하는데,
          이곳에 저장된 코드는 기계어로 이미 변환된 채 캐시되어 있으므로 빠르게 실행할 수 있다.

 

       Shared Library

      •  애플리케이션에서 사용할 공유 라이브러리가 기계어로 변환되어 저장되는 영역

      •  OS에서 프로세스당 한 번씩 로드된다.

 

       PC Register

      •  쓰레드가 시작될 때 생성되는 메모리로, 쓰레드마다 PC Register가 하나씩 존재한다.

      •  쓰레드가 어떤 부분을 무슨 명령으로 실행해야할 지에 대한 정보를 기록하는 부분

      •  현재 수행중인 JVM 명령의 주소를 가진다.

 

       Native method stack

      •  Java 외의 언어(C/C++)로 작성된 네이티브 코드를 위한 메모리 영역

        ◦  Java Native Interface를 이용하여 네이티브 코드 작성 가능

        ◦  네이티브 코드를 사용하는 이유

            -  하나의 Java 파일은 해당 운영체제 환경에 상관없이 똑같은 결과를 반환해야 한다.
               (운영체제에 따라 서로 다른 JVM이 존재하기 때문에 이것이 가능)
            -  단점 : 운영체제의 모든 기능을 JVM이 담지 못한다

                →  리눅스에서만 돌아가는 코드를 허용하면 윈도우즈에서는 안돌아가는 상황 발생

            -  따라서 운영체제의 고유한 일부 기능은 Java 언어만으로 구현이 불가능하다.

 

 

Class Loader의 개념과 동작 원리

•  자바는 동적 로드, 즉 컴파일 타임이 아니라 런타임(바이트 코드를 실행할 때)에 클래스 로드하고 링크하는 특징이 있다.

•  이 동적 로드를 담당하는 부분이 JVM의 클래스 로더

•  로딩, 링크, 초기화 단계로 나누어진다.

  ◦  로딩

      -  자바 바이트 코드(.class)를 메소드 영역에 저장한다.

      -  각 자바 바이트 코드(.class)는 JVM에 의해 메소드 영역에 다음 정보들을 저장한다.

          1) 로드된 클래스를 비롯한 그의 부모 클래스의 정보

          2) 클래스 파일과 Class, Interface, Enum의 관련 여부

          3) 변수나 메소드 등의 정보

      -  로딩이 끝나면, 해당 클래스 타입의 class 객체를 생성해 Heap 영역에 저장한다.

          →  객체이름.class 또는 인스턴스의 getClass() 형태로 호출했을 때 리턴되는 값을 말함

 

  ◦  링크

      -  Verfiy : 읽어 들인 클래스가 자바 언어 명세 및 JVM 명세에 명시된 대로 잘 구성되어 있는지 검사

      -  Prepare : 클래스가 필요로 하는 메모리를 할당하고, 클래스에서 정의된 필드, 메소드, 인터페이스를 나타내는 데이터 구조를 준비

      -  Resolve : Symbolic Memory Reference 를 Method Area에 있는 실제 Reference로 교체

          →  Book book = new Book(); 이라는 코드가 있다고 할 때, book이라는 참조변수가 Heap에 저장된 실제 Book 클래스를
               가리킬 수 있도록 연결하는 과정이 바로 Resolve 과정이다.

 

  ◦  초기화

      -  static 필드들을 설정된 값으로 초기화된다.

      -  static으로 선언된 변수와 메소드에 메모리를 할당하고 초기값을 채우는 과정.

static final String name = "staticName";

 

 

클래스 로더의 동작

•  클래스 로더는 새로운 클래스를 로드해야 할 때, 위와 같은 방식으로 로드를 수행한다

 

부트스트랩 클래스 로더 (Bootstrap Class Loader)

•  JVM 시작 시 가장 최초로 실행되는 클래스 로더

•  자바 자체의 클래스 로더와 최소한의 자바 클래스(java.lang.Object, Class, ClassLoader)만을 로드한다.

확장 클래스 로더 (Extension Class Loader)

•  부트스트랩 클래스 로더를 부모로 갖는 클래스 로더

•  확장 자바 클래스들을 로드한다. java.ext.dirs 환경 변수에 설정된 디렉토리의 클래스 파일을 로드하고,
     이 값이 설정되어 있지 않은 경우 ${JAVA_HOME}/jre/lib/ext 에 있는 클래스 파일을 로드한다.

시스템 클래스 로더 (System Class Loader)

•  자바 프로그램 실행 시 지정한 Classpath에 있는 클래스 파일 혹은 jar 에 속한 클래스들을 로드

•  작성자가 직접 만든 .class 확장자 파일을 로드한다.

 

 

클래스 로더의 동작 과정

•  JVM의 메소드 영역에 클래스가 로드되어 있는지 확인한다. 만일 로드되어 있는 경우 해당 클래스를 사용

•  메소드 영역에 클래스가 로드되어 있지 않을 경우, 시스템 클래스 로더에 클래스 로드를 요청

•  시스템 클래스 로더는 확장 클래스 로더에 요청을 위임

•  확장 클래스 로더는 부트스트랩 클래스 로더에 요청을 위임

•  부트스트랩 클래스 로더는 부트스트랩 Classpath(JDK/JRE/LIB)에 해당 클래스가 있는지 확인.
     클래스가 존재하지 않는 경우 확장 클래스 로더에게 요청을 넘긴다.

•  확장 클래스 로더는 확장 Classpath(JDK/JRE/LIB/EXT)에 해당 클래스가 있는지 확인한다.
     클래스가 존재하지 않을 경우 시스템 클래스 로더에게 요청을 넘긴다.

•  시스템 클래스 로더는 시스템 Classpath에 해당 클래스가 있는지 확인한다.
     클래스가 존재하지 않는 경우 ClassNotFoundException을 발생시킨다.

 

 

동적 클래스 로딩

•  자바의 클래스 로딩은 클래스 참조 시점에 JVM에 코드가 링크되고, 실제 런타임 시점에 로딩되는 동적 로딩을 거친다.

•  런타임에 동적으로 클래스를 로딩한다는 것은, JVM이 미리 모든 클래스에 대한 정보를 메소드 영역에 로딩하지 않는다는 것을 의미

 

•  로드 타임 동적 로딩 (Load-time Dynamic Loading)

public class HelloWorld { 
        public static void main(String[] args) { 
                System.out.println("안녕하세요!"); 
        } 
}

  ◦  JVM이 시작되고 부트스트랩 클래스 로더가 생성된 후에 모든 클래스가 상속받고 있는 Object 클래스를 읽어온다.

  ◦  클래스 로더는 명령 행에서 지정한 HelloWorld 클래스를 로딩하기 위해, HelloWorld.class 파일을 읽는다.

  ◦  HelloWorld 클래스를 로딩하는 과정에서 필요한 클래스인 java.lang.String과 java.lang.System을 로딩한다.

  ◦  이처럼 하나의 클래스를 로딩하는 과정에서 동적으로 다른 클래스를 로딩하는 것 : 로드 타임 동적 로딩

 

•  런타임 동적 로딩 (Run-time Dynamic Loading)

public class RuntimeLoading { 
        public static void main(String[] args) { 
                try { 
                        Class cls = Class.forName(args[0]); 
                        Object obj = cls.newInstance(); 
                        Runnable r = (Runnable) obj; 
                        r.run(); 
                } catch (Exception e) {
                        e.printStackTrace();
                }
        }
}

  ◦  Class.forName() 메소드가 실행되기 전까지는 RuntimeLoading 클래스에서 어떤 클래스를 참조하는지 알 수 없다.

  ◦  따라서 RuntimeLoading 클래스를 로딩할 때는 어떤 클래스도 읽어오지 않고, RuntimeLoading 클래스의 main() 메소드가
       실행되고 Class.forName(args[0]) 를 호출하는 순간에 비로소 args[0] 에 해당하는 클래스를 로딩한다.

  ◦  클래스를 로딩할 때가 아닌, 코드를 실행하는 순간에 클래스를 로딩하는 것 : 런타임 동적 로딩

 

 

클래스가 로딩되지 않는 경우

•  클래스에 접근하지 않을 때

•  클래스의 정적 변수 사용 (final 키워드)

 

 

Java 메모리 할당 방식

public static void main(String[] args) {
	int number = 12;
	Mydata mydata = new Mydata(15);
}

•  JVM은 기본적으로 Stack과 Heap 두가지 저장 공간에 메모리를 할당한다.

•  간단한 변수인 Primitive Type의 변수들은 Stack에 저장이 가능하지만, 복잡한 변수는 Heap 공간에 저장되고
     Stack에는 해당 Heap 공간을 가리키는 변수가 저장된다.

  ◦  JVM은 코드 한 줄 씩 읽으며 객체를 메모리에 할당한다.

  ◦  읽어들인 줄에 저장해야 할 객체가 있을때, 객체가 Primitive Type이면 값 자체가 Stack에 저장된다.

  ◦  그 외는 Stack에는 객체의 주소값만 저장되고, 실제 값은 Heap 영역에 저장된다.

 

 

call by value 와 메모리 할당

•  Java는 call by reference가 존재하지 않는다.

•  call by reference 같은 call by value 만 존재할 뿐

 

•  call by reference 같은 call by value = 주소값을 복사하여 가져가는 call by value

  ◦  실제로 해당 주소를 가리키고 있는 것이 아니다!

  ◦  단지 주소값을 복사해서 가지고 있을 뿐이다.

  ◦  하나의 공간을 여러 객체가 동시에 가리키고 있을 수 없다!

  ◦  단지 한 공간의 주소를 여러 객체가 동시에 가지고 있을 수 있을 뿐이다.

 

•  참조하고 있는 값을 단순히 바꿀수도 있고, 실제로 가리키고 있는 주소의 값을 바꿀수도 있다.

  ◦  개발자가 선택적으로 수행할 수 있음 (신기하네)

•  arg2 = arg1 을 수행해도 이는 run 메서드 내에 존재하는 arg2가 arg1이 가진 주소값을 복사하여 저장하는 것일 뿐,
     원본 a2와는 독립된 변수이기 때문에 원본 a2는 변경되지 않는다.

public static void main(String args[]) {

	class A {
		public int value;
		A(int value) { this.value = value; }

		void run(int x, int y) {
			x.value = 111; // 주소에 저장된 값이 달라짐
			x = y;         // 주소에 저장된 값에는 영향 X
		}
	}

	A a1 = new A(1);
	A a2 = new A(2);
	run(a1,a2);        // a1 = 111, a2 = 2

}
#include<iostream>
using namespace std;

class A {
	public:
		int value;
		A(int value) { this->value = value; }
};

void run(A* arg1, A* arg2) {
	arg1->value = 111;
	*arg2 = *arg1;
};


int main() {
	A* a1 = &A(1);
	A* a2 = &A(2);

	run(a1, a2);

	cout << a1->value;   // 111
	cout << a2->value;   // 111

	system("pause");
	return 0;
}

 

 

참고자료

 

[Java] JVM의 클래스 로더란?

java-study에서 스터디를 진행하고 있습니다. 클래스 로더란? 자바는 동적 로드, 즉 컴파일 타임이 아니라 런타임(바이트 코드를 실행할 때)에 클래스 로드하고 링크하는 특징이 있다. 이 동적 로드

steady-coding.tistory.com

 

[Java] Java는 Call by reference가 없다

Call by Value와 Call By Reference가 뭘까? 프로그래밍을 하다 보면 꼭 알고 넘어가야 하는 개념이 있습니다. 바로 Call By Value, Reference입니다. 어떤 언어를 공부하든 나오는 개념이기도 합니다. Call by va..

deveric.tistory.com

 

OF를 사용하는 자유프로젝트 주제로 파이프 게임을 떠올린건 수업시간에 Waterfall 프로젝트를 진행하면서였다.

대충 물줄기가 선반을 타고 흐르는 것을 표현하는 프로젝트였는데

 

구멍에서 물이 수직으로 떨어지고, 선분을 만나면 선분을 따라 흘러내린다

그냥 물이 흐르니... 수도꼭지를 생각하게 되었고... 자연스럽게 파이프 게임이 떠오른...

암튼 어릴때 많이 했던 파이프 게임도 이 원리를 이용하면 쉽게 구현할 수 있겠다 싶어서

자유 프로젝트 주제로 파이프 게임 구현을 선택하게 되었다.

수업시간에 배운 내용을 바탕으로 주제 구상하라고 했으니 안성맞춤.

전체적인 설계는 아래와 같다.

[ 게임을 구성하는 요소는 무엇인가? ]

1. 좌표 평면을 일정한 개수의 그리드로 나눈다. 이 때 가로 세로 그리드의 개수는 각각 3의 배수가 되어야 한다.

 

3*3의 파이프 블럭들을 빈틈없이 배치할 수 있도록 구성된 필드

2. 3*3의 그리드 안에 파이프를 배치한다. 정사각형 그리드 안에 배치하는 이유는 파이프를 원활하게 회전시키기 위함.

 

파이프를 4 방향으로 회전하기 위해서는 3*3 만큼의 자리가 필요하다

3. 파이프의 종류는 총 3가지이다 : 1자 파이프 1개와 ㄱ자 파이프 2개.

ㄱ자 파이프가 2개 필요한 이유는 물이 흐르는 방향에 따라 같은 모양의 ㄱ자 파이프를 2가지로 나누었기 때문이다.

 

총 세 종류의 파이프가 존재한다.

4. 물은 언제나 배치된 파이프의 중앙부로 흐르며, 빨간색 바가 있는 쪽으로만 흐를 수 있다.

 

즉 두번째 줄은 파이프를 잘못 배치한 경우에 해당한다.

5. 플레이어는 물줄기가 출발점에서 도착점으로 흐를수 있게 파이프를 배치해야 한다.

 

6. 스페이스바를 눌러 파이프를 배치할 수 있으며, 방향키로 파이프를 회전하거나 파이프의 종류를 바꿀 수 있다.

[ 물이 어떻게 흐르는가? ]

1. OF는 입력받은 정보를 주기적으로 화면에 반복하여 그린다. 이 사실을 이용하면 시간이 지남에 따라 물줄기가 점점 흐르는 것을 표현할 수 있다.

2. 이를 위해 연결리스트를 사용해 아래의 정보들을 저장한다.

- 파이프가 놓여진 위치 좌표 ( = 앞으로 물이 흘러야 할 위치 좌표)

- 이미 물이 흘러간 위치 좌표 ( = 물을 화면에 표시해야할 위치 좌표)

3. 시간이 지남에 따라 프로그램이 파이프가 놓여진 위치 좌표, 즉 앞으로 물이 흘러야 할 위치 좌표를 하나씩 읽는다.

4. 그 좌표를 다시 물이 흘러간 위치 좌표를 저장하는 연결리스트에 그 저장한다.

5. 화면에는 매번 물이 흘러간 위치 좌표에 파란색 흔적을 출력할 것이다. 물이 흘러간 위치 좌표가 점점 늘어날수록 출력되는 파란색 흔적, 즉 물이 흘러간 자국은 점점 길어질 것이다.

 

물줄기는 배치된 파이프를 따라 점점 흘러간다.

[ 언제 게임오버가 되는가? ]

게임오버가 되는 상황은 세가지 이다.

1. 파이프가 이미 놓여진 곳에 또 파이프를 배치해야 하는 경우

>> 파이프를 배치할 때 마다 현재 파이프를 배치하고자 하는 필드의 색상을 check 한다.

>> 색상이 흰색(게임 기본 배경색)이면 그 자리에 파이프가 없는 것으로 간주, 색상이 흰색이 아니면 파이프가 이미 놓여진 것으로 간주한다. 이미 놓여진 경우 그 자리에 파이프를 배치하면 게임오버가 된다.

 

2. 물이 흘러야 하는데 더 이상 놓여진 파이프가 없는 경우(or 경로가 끊어진 경우)

>> 프로그램은 시간이 지남에 따라 물이 흘러야 하는 경로를 저장하는 연결리스트를 처음부터 차례대로 읽는다.

>> 아직 도착점 좌표에 도달하지 않았는데 연결리스트가 갖고 있는 좌표점의 개수가 모두 떨어졌다면? 물이 흘러야 하는데 더이상 놓여진 파이프가 없는 것 : 게임오버 처리된다.

>> 즉 플레이어는 물이 흐르는 속도보다 빠르게 파이프를 배치해야 한다.

 

3. 파이프가 화면의 경계를 넘어가는 경우

>> 파이프를 배치한 다음 파이프를 배치할 수 있는 위치는 해당 파이프의 빨간색 바를 기준으로 선택된다.

>> 즉 그리드를 오른쪽으로 세칸 이동할 것인지, 왼쪽/위/아래로 세칸 이동할 것인지는 빨간색 바를 기준으로 결정된다.

>> 예를들어 게임을 표시하는 화면의 크기가 10*10이라고 했을 때, 이동한 그리드의 좌표가(10,10)을 넘는다면 화면의 경계를 넘어간 것 : 게임오버 처리된다.

 

[ 점수는 어떻게 측정되는가? ]

1. 점수는 플레이 시간에 비례한다. 즉 파이프의 경로를 구불구불하고 복잡하게 만들어 물줄기가 오래 흐르게 할수록 높은 점수를 얻을 수 있다.

2. 점수는 우측 상단에 계속 표시되며, 물줄기가 도착점에 도착하면 점수 계산이 멈추고 최종 점수를 확인할 수 있다.


이제 위 설계 내용을 바탕으로 코드를 작성해보자.

해당 부분은 다음 글에서 자세히 설명하겠다.

 

 

프로젝트 과제로 제출했던 파이프 게임의 개발 과정을 소개한다.

OpenFrameworks(OF)는 C++을 기반으로 한 오픈 소스 라이브러리이다.

이미지, 그래픽, 사운드 등의 요소를 C++로 편하게 다룰 수 있도록 설계된 라이브러리 정도로 생각할 수 있겠다.

일반적인 C언어나 C++ 라이브러리와 달리 Import로 단순히 가져와서 쓰는게 아니라

C++ 프로젝트를 생성해주는 프로그램 형식으로 사용할 수 있어 간편하다.

 

단 생성된 프로젝트 파일을 사용하기 위해서 IDE(integrated Development Environment)가 필요하다.

여기서 IDE는 우리가 흔히 사용하는 Xcode또는 Visual Studio인데,

즉 그냥 터미널에서 쌩으로 돌리는건 불가능하고 저 둘 중 하나를 설치해줘야 한다.

 

Xcode는 맥북에서만 사용 가능하고, VS는 Windows 맥 모두 가능한데 난 맥용은 잘 안돌가더라.

나는 맥북이지만 윈도우즈 가상환경에서 VS쓰는 것에 익숙해져 있기 때문에 VS로 작업했다.

 

아래 사이트에서 OpenFreamworks에서 제공하는 프레임워크를 다운받을 수 있다.

프레임워크란 위에서 말한 프로젝트를 생성하는 프로그램 같은거라고 생각하면 된다.

 

download | openFrameworks

linux arm openFrameworks for arm boards running linux like Raspberry Pi, Beaglebone (black), Pandaboard, BeagleBoard and others.We have setup guides for some of the most common boards but it should work on any armv6 and armv7 board.

openframeworks.cc

여기서 VS 나 Xcode 중에 골라서 다운받으면 된다.

다운받는데 시간이 제법 걸리니 자기 전에 다운버튼 누르고 자는걸 추천

 

다운받으면 프로그램 같은게 다운로드 되는게 아니라, 웬 파일 뭉텅이가 다운받아져 있을 것이다.

거기서 projectGenerator 폴더의 projectGenerator.exe 를 눌러 C++ 프로젝트를 생성할 수 있다.

 

projectGenerator.exe를 실행시키면 아래와 같은 화면이 나오는데

Project name에 원하는 프로젝트 이름 입력하면 되고 그 외의 것들은 따로 건드릴 필요 없다.

 

Update를 눌러 프로젝트를 생성한 다음, 다시 상위 폴더로 돌아가 apps/myApps에 들어가면

생성한 프로젝트 폴더가 있을 것이다.

 

프로젝트 폴더 내부의 프로젝트이름.sln을 클릭해 VS를 실행시킬 수 있다.

 

그 다음은 일반적으로 VS에서 솔루션 만들고 프로젝트를 작성하는 방법과 동일하다.

 

여기까지 왔으면 OF를 이용해 코드를 작성하기 위한 기본적인 준비는 끝이다.

 

추가적으로 알아두어야 할 점은 OF는 이미지를 다루는 창을 제공하는데,

그 창 위의 일정한 위치에 그림을 그릴 때 자연스럽게 좌표점을 사용하게 된다.

 

이 때 우리가 일반적으로 아는 좌표평면과 다르게 y축의 방향이 거꾸로 되어 있다.

즉 화면의 아래로 갈수록 y좌표값이 증가하고, 위로 갈수록 감소한다.

이 점을 유의해서 좌표값을 설정하도록 하자.

 

일반적인 좌표 평면 / OF 에서의 좌표평면

다음 글에서는 파이프 게임이 어떤 원리로 작동하는지, 그 세부적인 설계를 살펴보자.