자바 메모리 구조는 1.8 이후로 일부분 바뀌었다.

이 부분에 대한 정리를 다시 하고 싶었고 GC 알고리즘에 대한 종류와 상세 내용을 정리하고 싶었다. 그럼 이 두 가지 사항에 대해 가볍게 정리해보자.

Java 메모리 구조

Method Area

프로그램이 실행되는 도중에 아직 사용되지 않은 클래스들의 코드는 new를 통해 클래스의 인스턴스가 생성되면 JVM Method Area에 인스턴스 변수, 메스드 코드, 클래스 변수등을 저장한다. 해당영역은 모든 쓰레드 사이에서 공유되고 static 키워드로 생성된 변수 또한 저장을 Runtime Constant Pool 영역에 저장한다. 실 데이터를 저장하는 것이 아니라 레퍼런스만 저장하며 실제 데이터는 Heap 영역에 저장한다.

 

JVM Language stack

각 스레드들은 생성과 동시에 각자의 stack을 생성하게 되는데 이 영역에 메소드가 실행될 때 사용된 메스드의 데이터들을 저장한다. 그리고 메서드 실행이 끝나면 해당 stack영역은 사라진다.

 

PC Registers

각 스레드별로 PC Registers가 존재하며 JVM 머신이 가장 최근에 실행한 명령어의 주소를 저장한다.

 

Native Method Area

Native Library에 의존하는 native 코드들을 저장하는 곳이다. (JNI)

 

Permenent (Java8 metaspace 대체)

permenent 영역에 로드된 클래스의 메타 정보와 static한 변수들 정보들이 담겨져 있는데 그 중 static 영역과 상수 영역은 heap으로 옮겨졌다.

-XX:MetaspaceSize : JVM이 사용하는 네이티브 메모리 
-XX:MaxMetaspaceSize : metaspace의 최대 메모리 

 

Heap 영역

GC가 발생되는 대표적인 영역이며 new를 통해 인스턴스가 동적으로 생성된 데이터와 배열정보를 저장하는 공간으로 xms, xmx등의 옵션으로 기본 힙사이즈를 설정할 수 있다. 해당 힙사이즈도 모든 쓰레드 사이에서 공유된다.

-Xms : JVM 시작 시 힙 영역 크기
-Xmx : 최대 힙 영역 크기

힙 영역은 GC가 발생되는 방법에 따라 Young, Old영역으로 나뉘게 된다.

1. Young 영역 

Eden

새롭게 할당된 데이터가 쌓이는 곳으로 일정주기 동안 참조가 유지되면 Survivor로 옮겨진다. Survivor로 옮겨지지 못한 데이터는 GC에 의해 청소된다.

Survivor

Eden영역에서 넘어온 데이터가 1 또는 2영역으로 나눠서 저장된다. 참조가 살아있는 경우 주기에 맞춰서 다른 Survivor영역으로 이동하고 그렇지 못한 데이터들은 GC에 의해서 처리된다. 위와 같은 경우를 Minor GC라고 한다. 

-XX: NewRatio      : New영역과 Old 영역의 비율
-XX: NewSize       : New 영역의 크기
-XX: SurvivorRatio : Eden 영역과 Survivor 영역의 비율

 

2. Old 영역

Survivor 영역에서 오래 살아남은 데이터의 경우 Old 영역으로 넘어가게 된다. Old 영역은 이렇게 넘어온 데이터가 많기 때문에 Young크기 보다 더 크게 설계되며 이부분에서 발생된 GC를 Major GC라고 한다.

 

GC 알고리즘

YOUNG, OLD GC가 발생되는 알고리즘 종류에 대해 정리해보자. 우선 GC의 경우 mark > sweep > compaction 작업이 순서대로 동작한다. 우선 GC 대상을 고르는 mark 작업이 선행되고 실제 제거를 수행하는 sweep가 동작한다. 그리고 메모리의 파편화가 된 부분을 채워 나가는 Compaction 작업으로 마무리한다.

Serial GC

위에서 언급한 3가지 작업이 진행되는 간단한 GC 알고리즘이다. 이 GC의 경우 처리하는 쓰레드가 단 하나이기 때문에 처리하는 과정 동안에 발생하는 STW (Stop the world) pause 시간이 길다.

 

Parallel GC

Serial GC에서 동작하는 스레드가 하나였기 때문에 문제가 자주 발생하였는데, 여기에 작업을 진행하는 스레드를 추가하여 병렬로 작업을 진행한 알고리즘 이다.

왼쪽부터 Serial GC, Parallel SerialGC (https://www.oracle.com/technetwork/java/index.html)

CMS GC

기존에 사용되던 GC 알고리즘 보다 STW pause 시간을 줄이기 위해 고안된 방법으로 없애야 하는 데이터를 정확하게 선별하는 작업이 추가로 진행된다. 그만큼 연산작업이 추가되어 CPU같은 리소스 자원 사용이 증가하였다. 최초 GC 판단하는 initial Mark, initial mark때 선정된 객체를 참조 하는 객체의 GC 대상인지 판단하는  Concurrent Mark, 마지막 검증 작업을 하는 Remark작업을 통해 대상을 선정한 후 Concurrent Sweep 작업을 통해 데이터를 지운다. 그리고 기존 Serial GC와의 차이점은 데이터를 sweep한 후 compaction 작업을 자주 진행하지 않는다. 파편화된 메모리를 자주 매꾸면 그만큼 오버헤드가 많이 발생할 수 있기 때문에 심각한 파편화가 발생했을 때만 매꾼다.

 

G1GC

기존에 GC 알고리즘과는 다른 알고리즘이 나온게 G1GC이다. 기존에는 Eden, Survivor, Old, Permanent영역으로 정확하게 나누어져 있었다. 하지만 G1GC 사용할 경우 heap 영역을 2048개 region 영역으로 쪼개고 이 지역의 크기는 G1HeapRegionSize를 통해 32mb 까지 지정이 가능하다.

또한 새로운 형태의 상태값이 생겼는데 Humongous와 Available/Unused이다. Humongous는 region크기의 50%를 초과하는 큰 데이터를 저장하기 위한 곳이고 Available/Unused는 아직 사용하지 않는 region을 뜻 한다.

각 Region은 Eden, Survivor, Old, Permanent, Humongous, Available/Unused 상태로 지정이 가능하고 데이터가 가장 많이 찬 Region에서 GC가 발생된다. 

G1GC 사용 시 heap 영역 (https://c-guntur.github.io/java-gc/#/6)

 

 

출처

https://www.guru99.com/java-virtual-machine-jvm.html
https://d2.naver.com/helloworld/1329

  1. goodGid 2019.09.24 10:28

    JVM Language stack
    에서 오타가 있네요 ㅎㅎ
    실행될 때 사용된 메스드의 데이터들을 저장한다.
    --> 실행될 때 사용된 메소드의 데이터들을 저장한다.

자바 유료화 관련해서 궁금해서 알아보던중 좋은 글을 작성해주신 분들이 있어서 공유합니다.

결론적으로 개인 개발환경에서 Oracle JDK를 사용하는건 상관없고 솔루션같이 상업적으로 사용 되는 부분에서는 OpenJDK를 사용하면 된다는 건가...

아직도 잘 모르겠다. 시간이 지나면 답이 나오겠지.



Java8의 Stream에 map 기능을 사용하다가 이런문제를 겪었다.



Iterable과 Iterator 정확한 정리를 하지 않고 무턱대고 사용하다보니 발생한 문제였다.

정확하게 집고 넘어가기 위해 정리해보자.

Iterator

Iterator는 자바 1.2에 발표된 인터페이스이다.  hasNext, next 등을 통해 현재 위치를 알 수 있고 다음 element가 있는지를 판단하는 기능등에 대한 명세를 제공한다. 이를 사용하기 위해서는 Iterator 인터페이스의 내용을 직접 구현해야 한다. 

대게 Collection 인터페이스를 사용하는 클래스의 경우 별도의 Iterator를 구현하여 사용하고 있다. 밑에 Iterable을 설명하면서 정리해보자.

1
2
3
4
5
6
7
8
9
10
11
12
public interface Iterator<E> {
    boolean hasNext();
    E next();
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
    default void forEachRemaining(Consumer<super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}
cs


Iterable

Java 1.5부터 나온 인터페이스로 Iterator보다 더 늦게 나온 인터페이스로 Iterator를 제공하는 메서드를 보유하고 있는 인터페이스이다.

이 인터페이스는 실질적으로 for-each를 사용할 수 있는 클래스라는것을 명세해주는 기능을 제공하고, Iterable을 상속받은 클래스는 Iterator를 사용하여 for- each 기능을 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface Iterable<T> {
   Iterator<T> iterator();
    
   default void forEach(Consumer<super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
 
   default Spliterator<T> spliterator() {
       return Spliterators.spliteratorUnknownSize(iterator(), 0);
   }
}
cs

조금 더 이해가 가능하도록 ArrayList를 예로 들어보자.

ArrayList는 List를 구현하고 있고, List는 Collection을 상속받고 있으며 Collection은 Iterable을 상속받고 있다.

1
2
3
4
5
6
7
// ArrayList
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
// List
public interface List<E> extends Collection<E> {
// Collection
public interface Collection<E> extends Iterable<E> {
cs


그래서 ArrayList에 보면 Iterator<E> iterator()가 구현되어 있다.

그리고 Iterator()에서 반환하는 Iterator인터페이스를 구현한 Itr 클래스도 제공하고 있으며 이를 사용하여 반복 동작을 가능하도록 사용한다.

※Java 1.8 부터는 Iterable에 forEach default method를 제공하고 있어서 바로 사용할 수 있다.


결론은
for-each 기능을 제공하는 Collection의 경우 Iterable을 구현하고 있으며, 그 Iterable 인터페이스에 있는 Iterator<E> iterator() 메소드를 구현하여 Collection의 요소들은 Iterate 하는데 사용된다.


static 메소드를 자기고 있는 클래스를 상속받은 자식 클래스에서 그 static 메소드를 override 할 수 있을까?

안될거 알지만 한번 확인해보고 싶었다.

먼저 static method를 가지고 있는 Parent을 만들었다.


1
2
3
4
5
6
7
8
9
10
11
12
/**
 * 부모 클래스
 */
static class Parent {
    public static void getData() {
        System.out.println("부모 getData");
    }
 
    public void method() {
        System.out.println("부모 method");
    }
}
cs


그리고 이를 상속하는 Child 클래스를 만들어보자.

1
2
3
4
5
6
7
8
9
10
11
12
/**
 * 자식 클래스
 */
static class Child extends Parent {
    public static  void getData() {
        System.out.println("자식 getData");
    }
 
    public void method() {
        System.out.println("자식 method");
    }
}
cs


그리고 실행시켜보자.

1
2
3
4
5
6
7
8
9
// 부모클래스
Parent c1 = new Parent();
c1.getData();
c1.method();
 
// 자식 클래스
Parent c2 = new Child();
c2.getData();
c2.method();
cs


만약 정상적으로 상속이 되었을 때 우리가 원하는 결과는 이렇다.

예상 결과

부모 getData
부모 method
자식 getData
자식 method

하지만 실제 결과는 다음과 같다.

실제결과
부모 getData
부모 method
부모 getData
자식 method

왜냐하면 static method는 상속이 되지 않기 때문이다. 왜냐면 static 메서드는 클래스가 컴파일 되는 시점에 결정이 되지만 Override에 경우에는 런타임 시점에 사용될 메서드가 결정이 된다. 그래서 애초에 성립하기 어렵다.


그리고 애초에 static에 경우 클래스단위로 만들어지기 때문에 객체 단위로 형성되는 Override 성립될 수 없다.
이런 문제를 방지하기 위해서는 재정의 하기 위해서는 무조건 @Override를 붙혀 주자. 

@Override만 붙여주어도 이렇게 바로 문제가 된다는 것을 확인 할 수 있다.


참고사항

https://docs.oracle.com/javase/tutorial/java/IandI/override.html

https://blog.naver.com/gngh0101/221206214829


+ Recent posts