JVM Runtime Data Area는 자바 애플리케이션 실행을 윟나 메모리 영역으로 아래와 같이 나뉜다.

 

  • Register
  • JVM Stack
  • Native Method Stack
  • Method Area
  • Heap

Registert, JVM Stack, Native Method는 각 Thread가 개별적으로 소유한 공간이나, Method Area, Heap은 모든 Thread가 공유하므로 JVM이 기동시 생성된다.

 


Register

CPU 내의 레지스터는 데이터를 신속하게 저장하여 사용하는 공간으로 data, address, status등으로 구성 되어 있다.

JVM 내에도 PC register, optop, frame, vars 등으로  구분되어 있다.

 

PC register는 현재 메서드가 실행할 다음 bytecode의 address를 가지고 있다. 다만 자바 메서드가아닌 Native 메서드를 처리한다면 PC register의 값은 undifined이다.(비 자바 언어를 위한 별도의 Native Method Stack이 존재하기 때문)

 

Stack 영역을 관리하는 3개 Register는 각각 다음 영역에 대한 포인터를 가지고 있다.

  • optop Register -> Operand Stack
  • vars Register -> Local Variable
  • frame Register -> Execution Environment

JVM Stack

각 Thread는 고유의 JVM Stack을 가지고 있다. Stack은 Frame을 여러장 쌓아놓은 구조이며 1개의 Frame은 1개의 Method에 대응한다.

  • JVM Stack의 단위 = Thread 단위
  • Stack Frame = Method의 단위

JVM Stack은 그 이름과 같이 후입선출(LIFO)방식이기 때문에 메서드를 호출하면 Stack의 최상단에 새로운 Frame이 놓인다. 그리고 메소드의 수행이 끝나면 최상단의 Frame은 제거된다. Frame의 내부 구조는 다음과 같다.

 

  • Operand Stack
  • Local Variable
  • Execution Environment

Operand Stack 

연소나 과정의 피연산자가 push, pop되는 공간이다.

int i1 = 1;
int i2 = 2;
int i3 = i1 + i2;

 

  1. int 1을 Operand Stack에 push 한다.
  2. Operand의 int 1을 Local Vairable Slot에 pop 한다.
  3. int 2를 Operand Stack에 Push한다.
  4. Operand의 int 2를 Local Variable Slot에 pop 한다.
  5. Local Variable의 int 1을 Operand에 Push한다.
  6. Local Variable의 int 2을 Operand에 Push한다.
  7. Operand의 두 int를 add 한다.
  8. Operand의 int 3을 Local Vairable Slot에 pop한다.

Operand Stack의 각 slot 크기는 32bit 이므로 long, duble과 같은 타입은 2개의 slot을 사용한다.

 

Local Variavble 내에는 메서드의 Parameter와 Local variable이 존재한다.

boolean, byte, char, long, short 등은 1개의 slot, float, double은 2개의 슬롯을 사용한다. 다만 Object, String은 크기를 알 수 없는 객체의 경우 Heap에 저장되고 Local variable에  reference만 저장되며 1개의 slot을 사용한다. Primitive Type은 Heap 영역을 거치지 않기 때문에 상대적으로 성능이 좋을 수 있다.

 

 JVM Stack 처리중 발생 할 수 있는 오류는 StackOverFlowError와 OutOfMemoryError 등이 있다. 

 

  • StackOverFlowError: 사용 가능한 Stack의 크기보다 많은 메소드 생성 및 호출
  • OutOfMemoryError : 충분하지 않은 메모리;로 Stack의 생성 실패

Native Method Stack

자바 애필리케이션 내 C언어 등 타 언어 코드를 실행 할 수 있다 이 때는 JVM Stack이 아닌 Native Mehtod Stack에서 Native Method를 처리한다.


Method Area

ClassLoader Reference, Runtime Constant Pool, Field Inforamtion, Method Information, Mehtod Code 등을 저장하고 있다. Runtime Constrant Pool은 클래스타 인터페이스의 Constant 뿐만 아니라 Type, Method, Field에 대한 reference 정보를 가지고 있다.

 


Heap

Heap 영역은 Tomcat 등 WAS 운영 관리에 가장 밀접한 영역이다.

 

Heap 영역 구조

New Generation, Old Generation

  • New Generation: 막 생성된 객체, Old Generation으로 이동 하지 않은 비교적 새로운 객체의 공간
    • Eden: New Generation의 객체중에서도 가장 최근에 생성된 객체를 위한 공간이며 Minor GC 발생 시 이 객체들은 Survivor 영역으로 이동한다.
    • Survivor 1,2 : Eden에서 살아남은 객체의 공간으로 MinorGC 발생시마다 1 < -> 2 로 이동한다.
  • Old Generation: 오랜 시간 살아있는 객체의 공간

이러한 메모리 공간의 크기는 JVM의 커맨드 옵션으로 설정할 수 있다.

  • 전체 매모리 설정: -Xms(초기값), -Xmx(최대값)
  • New Generation과 Old Generation의 비율: -XX:NewRatio
  • New Generation 내부의 Eden, Survivor의 비율: -XX:SurvivorRatio

 


Permanent Generation

New Generation과 Old는 객체들을 위한 공간이라면 Permanent Generation은 클래스 정보를 저장하는 공간이다. 간혹 애플리케이션 내부에서 동적으로 많은 클래스를 생성하는 경우가 있는데 이럴 때 Permanent Generation이 부족 해질 수 있다.

 

  •  Permanent Generation 크기설정 :-XX:PermSize(초기값), -XX:MaxPermSize(최대값) 

 Permanent Generation의 확장 시 Full GC가 발생하기 때문에 가급적 두 값을 동일하게 설저 하는 것이 좋다.

 

Metaspace

자바 1.8부터는  Permanent Generation가 사라지고 Metasapce로 변경되었다.

  • Metaspace의 크기 설정: -XX:MetaspaceSize, -XX:MaxMetaspaceSize

Garbvage Collection

GC는 메모리 객체중 Garbage 객체를 식별, 정리하여 여유 메모리를 확보한다.

 

그러나 GC 메커니즘이 문제를 유발하기도 한다. Full GC수행 중 모든 애필리케이션 처리가 중단 되기 때문이다. 이를 Stop-The-World(STW)라고 부른다. STW의 시간은 Heap의 크기에 비례한다. 따라서 Heap을 많이 할당하는 것이 반드시 좋은 것만이 아니다 적잘한 메모리의 할당과 GC 정책이 필요하다.

 

Seral GC

가장 기본적인 GC로 자바 1.4에서는 Serial GC를 기본으로 사용하였다. 반명 1.5이후로는 호스트 파워를 분석하여 어떠한 GC 방식을 사용할지 결정한다 명시적으로 -XX:+UseSerialGC 옵션을 통해 Serial GC를 사용 할 수 있다.

 

Serail GC는 하나의 Thread가 GC 작업을 처리한. 머신의 processor가 복수 개 라고 해도 하나의 Thread가 처리한다. 또한 New Generation은 Generational 알고리즘, Old Generation은 Mark-compact 알고리즘을 처리한다.

 

Parallel Old GC

하나의 Thread가 처리하는 단점을 보안하기 위해 Parallel Old GC가 등장 했으며 Parallel Compacting 알고리즘을 사용한다.

 

Parallel GC의 Thread의 수는 반드시 CPU의 코어 수를 고려하여 설정해야한다. 기본적으로는 1:1이다.

 

G1 GC

메모리 공간을 Region이라는 영역으로 나누어 사용하는데 New Generation, Old 모두 Region을 사용한다.

 


오류의 유형

 

OOM Java heap space: Heap 영역내 객체생성을 위한 충분한메모리가 없을 때

OOM Requested array size exceed VM limit: Heap 현재 메모리 공간에서 처리 할 수 없을 만큼 큰 배열을 생성하는 경우

OOM ParmGen space: 너무 많은 클래스 정보를 로드 할 때 발생

OOM unable to create new native thread: OOM의 원인이 메모리가 아닌 OS 자원에 있을 가능성이 크다

 


OOM의 원인

할당한 Heap메모리가 절대적으로 적기 때문이다. 이때 -XmX옵션을 통해 최대 Heap을 늘려야 한다. 단 이때 GC로그를 보면 Heap의 최적의 값을 찾아야한다.

 

대용량의 데이터 조회/입력으로 인하여 OOM이 발생하는 경우도 많으며 이는 DB 처리 후 수만~ 수십만의 데이터를 패치한 후 Heap영역의 Collection 객체에 로드하게 되는데 주로 조회 리포트, 엑셀 다운로드 작업을하는 관리자 시스템이서 발생한다. 대용량업무를 처리하는 별도의 인스턴스를 분리하는 것이 바람직 하다.

 

메모리 누수 또한 원인이 된다. 주로 Heap 덤프를 분석하거나 APM도구를 통해 찾아 낼 수 있다. 한편 JNI(JAVA Native Interface)를 통해 Native 코드를 호출하는 과정에서 Native 영역에서 누수가 발생할 수 있다.


모니터링 도구 

  • jstat: Heap: DK가 기본으로 제공하는 텍스트 기반 모니터링 도구
  • jmap: Heap을 모니터링 할 수 있으며 Heap영역의 정보를 덤프파일로 생성할 수 있다.
  • jhat: jmap의 dump를 통해 생성한 Heap 덤프 파일을 분석후 html 형태로 제공하는 도구

SkeletalMeshComponent  USkeletalMesh 의 인스턴스를 만드는 데 사용됩니다. 스켈레탈 메시 (외형)에는 복잡한 스켈레톤 (서로 연결된 본)이 안에 있어, 스켈레탈 메시의 버텍스 각각을 현재 재생중인 애니메이션에 일치시키도록 도와줍니다. 그 덕에 SkeletalMeshComponent 는 캐릭터, 동물, 복잡한 기계, 복합적인 동작을 보이거나 변형이 필요한 것에 적합합니다.  - 언리얼 공식문서 -

 

즉 캐릭터, 동물 등 애니메이션이 있는 매쉬를 사용하기 위해 내부에 본이 들어간 매쉬를 위한 컴포넌트이다.


 

1. Skeletal Mesh

까마귀의 스켈레탈 매시

 

우측에 본들을 볼수 있다

 

 

이러한 작업 된 스캘레탈 메쉬는 마야, 블랜더 등에서 작업되어 넘어온다. (본을 삽입하고 움직이는 과정을 블랜더에서는 리깅이라고 한다)


2. Skeletal Mesh Component

위에서 확인한 스켈레탈 매쉬를 사용하기 위해 해당 컴포넌트를 생성하고 스캘레탈 매쉬를 할당해보자

Bird.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "Bird.generated.h"

UCLASS()
class TEST01_API ABird : public APawn
{
	GENERATED_BODY()

public:
	ABird();
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
	virtual void Tick(float DeltaTime) override;


protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

private:
	UPROPERTY(VisibleAnywhere)
	class UCapsuleComponent* Capsule;

	// 여기
	UPROPERTY(VisibleAnywhere)
	class USkeletalMeshComponent* BirdMesh;

};

Bird.cpp

 

// Fill out your copyright notice in the Description page of Project Settings.

#include "Components/CapsuleComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "Bird.h"

ABird::ABird()
{
	PrimaryActorTick.bCanEverTick = true;

	//생성자 위치에서 Capsule의 변수를 설정해줌
	Capsule = CreateDefaultSubobject<UCapsuleComponent>(TEXT("Capsule"));
	Capsule->SetCapsuleHalfHeight(20.f);
	Capsule->SetCapsuleRadius(15.f);
	// 폰의 최상위 컴포넌트를 Capsule로 바꿔준다.
	SetRootComponent(Capsule);

	BirdMesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Bird"));
	BirdMesh->SetupAttachment(GetRootComponent());


}

void ABird::BeginPlay()
{
	Super::BeginPlay();
	
}

void ABird::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

void ABird::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

}

 

 

캡슐에 attech된 것을 볼 수 있다.

아직 뷰포트에는 까마귀의 매쉬가 보이지 않는다.

 

디테일 > 스케레탈 메시 에
스켈레탈 매쉬와 캡슐

좌측 하단에 보면 X, Y, Z가 보인다. X가 정면이므로 지금 까마귀는 측면을 보고 있는 것

까마귀를 회전시켜주자

 

까마귀의 머리가 x축을 바라보고 있다.

그리고 까마귀를 아래로 내려 캡술과 일치시키자

디테일 > 애니메이션

애니메이션을 추가해보자

 

블루 프린트를 드래그하여 화면으로 옮기면 까마귀와 캡슐을 볼 수 있다.

 

 

하늘을 나는 캡슐 까마귀 완성

캡슈컴포넌트는 충돌감지에 사용된다

 

폴리곤에 직접 충돌감지를 할 수 있지만 복잡한 폴리곤의 경우 너무 많은 리소스비용이든다. 

이를 방지하기위해 캡슐컴포넌트를 사용한다.

캡슐 컴포넌트와 충돌(Collision)

 


1. 새로운 pawn의 생성

 

 

 

# Bird.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "Components/CapsuleComponent.h"
#include "Bird.generated.h"

UCLASS()
class TEST01_API ABird : public APawn
{
	GENERATED_BODY()

public:
	ABird();
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
	virtual void Tick(float DeltaTime) override;


protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

private:
	// 캡슐의 변수 선언
	UPROPERTY(VisibleAnywhere)
	UCapsuleComponent* Capsule;

};

 

# Bird.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "Bird.h"

ABird::ABird()
{
	PrimaryActorTick.bCanEverTick = true;

	//생성자 위치에서 Capsule의 변수를 설정해줌
	Capsule = CreateDefaultSubobject<UCapsuleComponent>(TEXT("Capsule"));
        Capsule->SetCapsuleHalfHeight(20.f);
	Capsule->SetCapsuleRadius(15.f);
	// 폰의 최상위 컴포넌트를 Capsule로 바꿔준다.
	SetRootComponent(Capsule);


}

void ABird::BeginPlay()
{
	Super::BeginPlay();
	
}

void ABird::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

void ABird::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

}

 

# 블루프린트

 

캡슐의 생성
우측의 shape에서 높이와 반경을 결정할 수 있다.

 

 


Include Header

위의 예제에서 우리는 UCapsuleComponent를 Bird.h에 include 했다.

이와 같은 방식은 문제가 있다. 

이와 같이 최종적으로 사용하는 파일의 .h 파일은 기하급수적으로 많은 헤더 파일을 include 하게 될 뿐만아니라.

 

각 헤더파일끼리 Cross 참조하게 되는 경우 에러가 발생 한다.

 

이를 해결하기위해 UCapsuleComonent가 Class임을 명시하고 cpp 파일에서 include를 받는것이 바람직하다.

 

 

변경된 Bird.h 파일

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
// UCapsuleComonent의 include가 사라졋다.
#include "Bird.generated.h"

UCLASS()
class TEST01_API ABird : public APawn
{
	GENERATED_BODY()

public:
	ABird();
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
	virtual void Tick(float DeltaTime) override;


protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

private:
	// 캡슐의 변수 선언 해당 변수가 class임을 명시한다.
	UPROPERTY(VisibleAnywhere)
	class UCapsuleComponent* Capsule;

};

 

Bird.cpp

// 컴포넌트 include
#include "Components/CapsuleComponent.h"
#include "Bird.h"

ABird::ABird()
{
	PrimaryActorTick.bCanEverTick = true;

	//생성자 위치에서 Capsule의 변수를 설정해줌
	Capsule = CreateDefaultSubobject<UCapsuleComponent>(TEXT("Capsule"));
	Capsule->SetCapsuleHalfHeight(20.f);
	Capsule->SetCapsuleRadius(15.f);
	// 폰의 최상위 컴포넌트를 Capsule로 바꿔준다.
	SetRootComponent(Capsule);


}
// 이하 생략

 

함수 Exposing

UPROPERTY처럼 함수도 블루프린트로 노출시킬 수 있다. 

Item.h

	UFUNCTION(BluePrintPure)
	float TransformdedSin(float value);

	UFUNCTION(BluePrintCallable)
	float TransformdedSin2(float value);

 

Item.cpp

float AItem::TransformdedSin(float value)
{
	return Amplitude * FMath::Sin(value * TimeConstant);;


}

float AItem::TransformdedSin2(float value)
{
	return Amplitude * FMath::Sin(value * TimeConstant);;
}

 

BluePrintPure를 옵션으로 넣어주면 실행 핀이 없는 함수가 만들어지며 Callable을 옵션으로 준다면 실행핀이 존재하느 함수가 만들어진다 sin함수의 계산은 실행핀이 필요 없기때문에 Pure옵션이 적절해 보인다.

 


탬플릿 함수

탬플릿 함수는 JAVA의 제너릭과 유사해보인다.

 

Item.h

template<typename T>
T Avg(T First, T Second);

 

T 타입을 반환 하고 T 타입의 변수 First, Second를 받는다.

Item.cpp

template<typename T>
T Avg(T First, T Second)
{
	return (First + Second) /2;
}

 

위와 같이 함수를 만들면 해당 함수는 int32 파라미터, float 파라미터, FVector 타입도 받아 처리할 수 있다.

단 FVector의 객체의 경우 내부에 + 연산과 / 연산이 별도로 오버라이드 되어 있다고 한다.

+ Recent posts