2020-11-15 자바스터디 1주차. JVM이란

백기선님 스터디 1주차 과제

JVM이란

  • Java Virtual Machine의 약자로 .java 파일을 JVM에서 컴파일하여 .class파일로 변경한다. JVM이 설치된 곳이라면 어느곳에서든지 자바파일을 실행할 수 있다.
  • JVM 때문에 자바가 플랫폼 독립적이라고 할 수 있다.

출처 : https://d2.naver.com/content/images/2015/06/helloworld-1230-1.png

바이트코드란 무엇인가

  • 바이트코드란 .java 파일을 JVM에서 컴파일하여 .class 파일로 만든 파일을 의미한다.
  • src 디렉토리에 있는 .java파일을 컴파일하면 out 혹은 target 디렉토리에 .class파일 생성된다.
// jimmy.java
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter @Setter
@ToString
public class Jimmy {
    String name;
    int age;
}

// jimmy.class
package me.jimmy;

public class Jimmy {
    String name;
    int age;

    public Jimmy() {
    }

    public String getName() {
        return this.name;
    }

    public int getAge() {
        return this.age;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String toString() {
        String var10000 = this.getName();
        return "Jimmy(name=" + var10000 + ", age=" + this.getAge() + ")";
    }
}
  • 일단 .java 파일을 실행시키면 .class 파일과 큰 차이가 없다. 하지만 lombok을 적용된 코드를 보면 바이트코드에서 getter, setter, toString의 코드가 구현된것을 알 수 있다.
  • 롬복은 어노테이션 프로세싱을 이용한 방식인데, 아직 공부가 부족해서 잘 모르겠다. 롬복에 관한 내용은 인프런 백기선님 더자바 바이트코드 조작을 공부하는걸 추천한다.
  • 강의링크

JVM의 구성요소

  • JVM은 클래스를 로드하는 Classloader 영역, 올라간 클래스를 실행하는 실행엔진 그리고 runtime data access로 구성되어있다. 흔히 JVM 메모리 구조를 이야기하라고 하면 나오는 부분이 바로 Runtime Data Access이다.
  • Runtime Data Access은 아래 영역으로 구성되어있다.
    • Heap영역 : 객체가 생성되는 영역이다. GC가 이루어지는 부분이고, 세부 영역은 Young 영역, Old영역과 Young영역은 Eden과 Survivor 영역으로 구분된다. 우리가 흔히 알고있는 GC는 MajorGC이고 Old영역에서 이루어진다. 하지만 JDK11부터는 default GC가 JDK1.8부터 지원되는 G1GC로 변경되었다. G1GC는 Young, Old영역처럼 나누는 것이 아니라 논리적으로 메모리를 나누기 때문에 Heap메모리에서 객체가 생성될때 바둑판식으로 생성된다. 자세한 내용은 추후 공부를 해야한다.
    • Method영역
    • Stack영역 :
    • PC Register : 스레드가 시작될때 현재 수행중인 JVM 명령어 주소를 가리킨다.
    • Native Method Stack : 자바 외 언어로 작성된 네이티브 코드를 위한 스택이다.

JDK와 JRE의 차이

  • JRE : Java Runtime Environment의 약자로 JVM과 Java API로 구성된다. 자바의 바이트코드는 JRE 위에서 동작한다.
  • JDK : Java Development Kit의 약자

GC

  • c++에서는 new, delete를 통해 메모리를 명시적으로 해지. 자바에서는 gc가 자동으로 이 역할을 한다. 더 이상 사용하지 않는 객체를 찾아서 지우는것이 gc의 역할이다.
  • gc가 실행되면 jvm이 일시적으로 애플리케이션을 멈추는데 이를 stop-the-world라한다. 이때 gc를 실행하는 스레드를 제외한 나머지 스레드는 모두 작업을 멈춘다. gc가 완료된 이후에 다시 작업을 실행.
  • 어떤 gc알고리즘을 사용하더라도 stop-the-world는 발생. 대개 gc튜닝이란 stop-the-world타임을 줄이는거다.

gc의 두가지 가설

  • 대부분의 객체는 금방 접근 불가능 상태(unreachable)가 된다.
  • 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재.
  • 이러한 가설을 weak generational hypothesis라 한다.

  • 위 가설을 최대한 검증하기 위해 hotspot에서 vm을 물리적으로 Young영역과 Old영역으로 나누었다.

출처 : https://tramyu.github.io/images/etc/image-20180602160808763.png

  • permanent generation : Method영역이라 한다. 객체나 억류된 문자열을 저장. => Java8에서 삭제되고 Metaspace로 대체되었다.

출처 : https://d2.naver.com/content/images/2015/06/helloworld-1329-2.png

  • Old영역에는 512바이트의 덩어리(chunk)로 되있는 카드테이블이 존재
  • Old영역에 있는 객체가 Young영역의 객체를 참조할 때마다 정보가 표시된다. Young영역의 GC를 실행할때, Old영역의 모든 객체의 참조를 확인하지 않고, 카드테이블을 뒤져서 GC대상인지 식별한다.
  • 카드테이블은 write barrier를 사용하여 관리한다. write barrier는 minor gc를 빠르게 할 수 있도록 하는 장치인데, 오버헤드가 발생하지만 전반적인 GC시간은 줄어든다.

Young 영역

  • Young영역은 Eden영역과 2개의 Survivor영역으로 총 3가지 영역으로 구성된다.

출처 : https://i.imgur.com/mBzfrWM.png

출처 : https://d2.naver.com/content/images/2015/06/helloworld-1329-3.png

  • 새로 생성된 대부분의 객체는 Eden영역에 존재. => 간혹 객체 크기가 커서 바로 Old 영역으로 넘어가기도 한다.
  • Survivor영역중 한쪽은 반드시 비어있어야한다. 양쪽에 데이터가 모두 존재하거나 두 영역 모두다 비어있으면 시스템이 비정상이다.
  • HotSpotVM에서는 빠른 메모리할당을 위해 bump-the-pointer, TLABs(Thread Allocation Buffers)라는 기술을 사용한다.
  • young영역 : 새로 생성된 데이터 대부분 여기에 속한다. 객체가 할당해제가 되면 Minor GC가 발생되었다고 한다. Eden에서 살아남으면 survivor로 이동. survivor이 꽉차면 다른 survivor로 간다. 이때 survivor영역은 2개이다.

bump-the-pointer

  • Eden영역에 할당된 마지막 객체를 추적. 마지막 객체는 Eden영역 맨위에 있다.
  • 다음 객체가 생성될때 크기가 Eden영역에 넣기 적당한지 확인하고, 크기가 적당하면 Eden영역에 넣는다. 객체가 생성할때 마지막 추가된 객체만 점검하면 되므로 빠르게 메모리할당이 된다.
  • 멀티쓰레드 환경은 다르다. Thread-safe하기 위해 여러 스레드에서 사용하는 객체를 Eden에 저장하기 위해 락이 발생한다. lock-contention때문에 성능이 매우떨어지고 HotSpot-VM에서 해결하기 위한 방법이 TLABs이다.

TLABs

  • 각각의 스레드가 몫에 해당하는 Eden영역의 작은 덩어리를 가질 수 있도록 하는것.
  • 각 쓰레드는 자기가 갖고 있는 TLAB만 접근할 수 있어서 bump-the-pointer라는 기술을 사용하더라도 락없이 메모리할당이 가능

Old 영역

  • young영역에서 살아남은 객체가 여기로 복사. young영역보다는 gc가 적게발생. 여기서 할당해제가 되는 경우 major gc라고 한다. old영역은 데이터가 가득차면 Major GC가 발생. 이때 GC를 실행하는 스레드를 제외한 모든 스레드가 작업을 멈춘다

  • Old영역에 있는 모든 객체들을 검사하여 참조되지 않은 객체들을 한꺼번에 삭제.
  • 시간이 오래걸리고 실행 중 프로세스가 정지된다. stop-the-world라고 한다. Major GC가 발생하면 GC를 실행하는 스레드를 제외한 나머지 스레드는 모두 작업을 멈춘다.
  • 종류는 SerialGC, ParallelGC, Parallel Old GC, Concurrent Mark & Sweep(CMS) gc, G1 GC 5가지 종류가 있다.
    • 운영서버에서 SerialGC는 쓰면 안된다. SerialGC는 코어가 1개있을때 사용하기 위한 방법이다.

Major GC알고리즘 종류

소멸시키는 원리

  • GC는 Heap내의 가비지를 찾아내고 가비지를 처링해서 힙의 메모리를 회수한다.
  • 참조되지 않은 객체를 가비지라고 한다. 가비지인지 아닌지 판별하기 위해 reachability라는 개념을 사용.
  • 참조된 객체가 유효한 참조가 있으면 reachability 없다면 unreachability로 판단.
  • 가비지 컬렉션 대상이 되었다고 해서 바로 소멸되는 것은 아니다. 빈번한 가비지 컬렉션 실행은 시스템 부담이된다. 실행 타이밍은 별도의 알고리즘 기반으로 계산.

Serial GC

  • Old영역의 GC는 mark-sweep-compact알고리즘을 사용한다.
    1. Old영역에 살아있는 객체를 식별
    2. Heap앞부분부터 확인해서 살아있는 것만 남긴다(sweep)
    3. 힙의 가장 앞부분부터 객체가 존재하는 부분과 없는 부분으로 나눈다(compaction)

Parallel GC

  • serialGC와 기본알고리즘은 같다. serialGC는 싱글스레드. parallelgc는 멀티쓰레드이다.

Parallel Old GC

  • Parallel Old GC는 JDK5 update 6부터 제공한 방식이다. ParallelGC와 알고리즘이 다르다. Mark-Summary-Compation방식을 쓴다.
  • Summary단계는 앞서 GC를 수행한 영역에 대해 별도로 살아있는 객체를 식별하는점에서 다르다.

CMS GC

출처 : https://d2.naver.com/content/images/2015/06/helloworld-1329-5.png

  • 초기 Initial Mark단계에서 클래스로더에서 가장 가까운 객체중 살아있는 객체만 찾고 끝난다. 멈추는 시간이 매우 짧다.
  • Concurrent Mark 단계는 방금 살아있다고 확인한 객체에서 참조하고 있는 객체들을 따라가면서 확인한다. 다른 스레드가 실행중인 상태에서 동시에 진행
  • Remark단게에서 Concurrent Mark단계에서 새로 추가되거나 참조가 끊긴 객체를 확인한다.
  • Concurrent Sweep 단계에서 쓰레기를 정리하는 작업실행
  • 단계별로 진행되기 때문에 stop-the-world시간이 매우 짧다. 애플리케이션 응답속도가 중요할때 많이 쓴다. Low Latency GC라고도 한다.
  • 단점
    • 다른 GC들에 비해 CPU, 메모리를 더 많이 사용
    • Compaction단계가 기본적으로 제공 안된다.
    • 조각난 메모리가 많아 Compaction작업을 실행하면 stop-the-world시간이 매우 길다.

G1 GC

  • Garbage First GC.
  • Java9이후로 default GC

출처 : https://d2.naver.com/content/images/2015/06/helloworld-1329-6.png

  • Young, Old영역은 잊는게 좋다. G1 GC는 바둑판의 각 영역에 객체를 할당하고 GC를 실행한다.
  • 해당 영역이 꽉차면 다른 영역에 객체할당하고 GC를 실행.
  • Young의 세가지 영역에서 데이터가 Old영역으로 이동하는 단계가 사라진 GC방식. CMS GC를 대체하기 위해 만들어졌다.

GC 모니터링

  • JVM이 어떻게 GC를 수행하는지 알아내는 과정. Young -> Old로 언제 이동했는지, stop-the-world가 언제 일어나고 얼마나 일어났는지 알 수 있다.
  • CUI : jstat와 JVM -verbosegc옵션사용하기
  • GUI : jconsole, jvisualvm, visual gc
VisualVM + VisualGC
  • VisualVM은 오라클이 제공하는 툴이다.

GC튜닝

  • -Xms, -Xmx옵션으로 메모리 크기 지정. -server옵션 포함. 시스템에 타임로그와 같은 로그가 남지않는다 => GC튜닝을 안해도된다.
  • 메모리 크기도 지정안하고, timeout 로그가 계속 출력되면 GC튜닝을 해야한다.
  • GC튜닝은 가장 마지막에 하는 작업이다.

Old 영역으로 넘어가는 객체의 수 최소화

  • G1 GC를 제외한 Oracl JVM에서 제공하는 모든 GC는 generational GC이다. => Eden영역에서 객체가 처음 만들어지고. Survivor영역을 오가다가 끝까지 살아남는객체가 Old영역으로 이동한다.
  • Old영역의 GC는 오래걸리기때문에 Old영역으로 이동하는 객체수를 줄이면 majorGC의 빈도가 줄어든다.

Full GC시간 줄이기

  • Full GC는 Minor GC에 비해 길다. Full GC실행에 시간이 오래걸리면(1초이상) 연계된 여러부분에서 타임아웃이 발생한다.
  • Full GC시간을 줄이기 위해 Old영역의 크기를 줄이면 OutOfMemoryError가 발생하거나 Full GC횟수가 늘어난다.
  • Old영역의 크기를 늘리면 Full GC횟수는 줄지만 실행시간이 늘어난다.
GC에서는 메모리관련 옵션이 성능에 많은 영향을 준다.
구분 옵션 설명
힙영역크기 -Xms JVM시작시 힙 영역크기
  -Xmx 최대힙영역크기
New영역크기 -XX:NewRatio New영역과 Old영역의 비율
  –XX:NewSize New영역의 크기
  –XX:SurvivorRatio Eden영역과 Survivor영역의 비율
  • -Xms, -Xmx옵션은 필수로 써라. –XX:NewRatio 설정에 따라 GC성능이 많이 차이난다.

튜닝절차

  1. GC 상황 모니터링
  2. 결과 분석후 튜닝여부 결정 : GC시간이 0.1~0.3초이면 굳이 튜닝안해도 된다. 1~3초면 튜닝을 해야한닫.
  3. GC방식/ 메모리 크기 지정
  4. 결과분석(WAS에서 jstat)

자바 메모리 누수

  • JDK8의 기본 GC는 PARALLEL GC이다.
  • 많은 트래픽이나 버그로 인해 Heap의 사용량이 순간 증가할 수 있다. GC가 과도하게 일어나면 성능이 저하되고, 심하면 OOM(Out Of Memory)가 발생하여 애플리케이션이 죽는다.
  • Heap의 높은 사용량을 알기 위해 Heap dump를 쓴다. Heap상태를 기록하여 어떤 Java 객체들이 많이 만들어졌는지 봐야한다.
  • 모니터링 도구는 VisualVM이 있다. Dump파일 분석으로는 MAT를 쓸 수 있다.

$jps // 프로세스 PID를 알아낸다

$jmap -dump:format=b, file=[FILE_NAME][PID] // 이 명령어로 fileName이라는 이름으로 덤프파일이 생성된다.

중간마무리..

  • 원래는 더자바, 코드를 조작하는 다양한 방법 1강에 나온 내용을 올렸었습니다. 블로그 내용이 강의를 복붙한 내용이라 라이브 방송을 보면서 급히 작성했습니다. 주중에 보완해서 올려보겠습니다.
Written on November 15, 2020