본문 바로가기

프로그래밍/JAVA

JVM

https://huelet.tistory.com/entry/JVM-%EB%A9%94%EB%AA%A8%EB%A6%AC%EA%B5%AC%EC%A1%B0 에서 참고했습니다.



메모리는 프로그램을 실행하기 위한 데이터, 명령어가 저장되는 공간이다. 

한정된 공간이기 때문에 효율적인 관리가 중요하다. 

메모리가 부족하게 되면 성능 문제 뿐만 아니라 프로그램 자체가 뻗어 버릴지도 모른다.




프로그램이 실행되려면 OS가 제어하는 RAM(=memory)이 필요한데 JAVA 이전의 언어들은 OS에 종속적이다.

JAVA에서는 이런 OS 의존성을 없앴는데 그 역할을 해 주는 것이 JVM이다.

JVM이 각 OS로부터 메모리 권한을 할당 받아 프로그램을 실행해 준다.


즉, JAVA는 OS로부터는 독립적이지만 JVM에 종속적이고..

중간에 JVM이 있으니 c 언어로 짜여진 프로그램보단 느리다고 볼 수 있다. 












JVM (Java Virtual Machine)


JVM은 OS로부터 메모리(=RAM)을 할당 받는데, 용도에 따라 여러 영역으로 나눠 관리한다. 



Java source를 JAVA Compiler가 Class files로 만들어주고, 그 classes 파일들이 Class Loader를 통해 로딩 된다.

로딩 된 데이터 들은 Runtime Data Area에서 관리되고, 

Execution Engine을 통해 byte code가 실행되면서 프로그램이 돌아간다. 









Runtime Data Area


- Class 영역 (or method 영역)

-- 클래스 정보, 타입정보, 변수 정보, 메소드 정보, static 변수, constant pool 등이 저장되는 공간





- Stack

-- Last In First Out (최근에 들어온 녀석 먼저 나감)

-- method 호출때마다 각각의  Stack frame이 생성된다. 

-- method 에서 사용되는 매개변수, 지역변수, 리턴 값 및 연산에 사용된다.

-- method 가 끝나면 frame 별로 삭제된다.




- Heap


이녀석은 좀 중요하다. 

JVM에서 중요한 GC가 일어나는 영역이다.


-- new 연산자로 생성된 객체(위의 Class Area에 로드된 객체들만 생성가능) 및 배열이 저장된다.



- GC

JAVA에서 참조되지 않은 객체들을 탐색 후 삭제하는 녀석이다. 

삭제된 객체의 메모리 반환해서 Heap 메모리를 재사용하게 해준다. 



-- Minor GC

--- New 영역에서 일어나는 GC

--- Eden이 꽉차면 GC발생 > Survivor1에 복사 > Survivor1제외 나머지 영역의 객체 삭제 > Eden/Survivor1 메모리 기준치 넘으면 참조되고 있는 객체가 있는지 검사 > 참조중인 객체는 Survivor2에 복사 > Survivor2 제외 다 삭제 > 일정시간이상 참조되는 객체는 Old로 이동 > 반복



-- Major GC(=Full GC)

--- Old영역에 있는 모든 객체를 검사 > 참조 없으면 삭제 

--- Minor GC에 비해 시간이 오래걸리고 실행중 프로세스가 정지됨(stop the world)






JDK 1.8 부터는 메모리 구조의 변화가 있는데

Permanent(=class area)가 두개로 나뉘어 져서 Metaspace/Heap으로 분리 되었다. 

PermGen 은 자바 7까지 클래스의 메타데이터를 저장하던 영역이었고 Heap 의 일부였다. ??



다른점은

- Static Object가 Heap 영역으로 이동했다.

- 상수화된 String Object가 Heap 영역으로 이동 했다. 


즉, Static Object가 GC 대상이 되어 OutOfMemory: PermGen Space error는 더이상 볼 수 없어졌다.

또한 Metaspace는 클래스 메타 데이터를 Native 공간에 저장하고 부족한 경우 자동으로 늘려주어 JVM 옵션을 설정할때의 PermSize, MaxPermSize를 더이상 지정할 필요가 없어졌다. 






위에 기재된 GC는 JDK 1.7까지에서 쓰였다. 

JDK 1.8 부터는 G1 GC라는 새로운 녀석이 등장했다.


Heap 영역이 최소 4GB정도 되는 머신에서 돌리기 적합하다. 

Heap 영역을 여러개의 Region으로 나누고 각 Region들은 Young Generation, Old Generation을 유동적으로 사용한다. 



Stop the world가 꽤 길거나 자주 일어나는 프로그램이라면 G1 GC를 고려해 보는게 좋을 것이다. 

자세히는 모르겠으니 구글링 해봐야 할 듯. 


4G의 Heap 이고, 1024개의 Region으로 쪼개려면 각 Region은 4M.. 아래 값으로 설정하면 된다. 

-XX:G1HeapRegionSize


<https://code-factory.tistory.com/48>

















https://yaboong.github.io/java/2018/06/09/java-garbage-collection/


String url = "https://";

url += "yaboong.github.io";


덧붙이는 것이 아니라, 연산이 수행된 결과가 새롭게 heap 영역에 할당된다. 

기존의 "https://" 라는 문자열을 레퍼런스 하고 있는 변수는 아무것도 없으므로 Unreachable 오브젝트가 된다.

Unreachable Object 란 Stack 에서 도달할 수 없는 Heap 영역의 객체를 말하는데, 지금의 예제에서 "https://" 문자열과 같은 경우가 되겠다. 

Garbage Collection 이 일어나면 Unreachable 오브젝트들은 메모리에서 제거된다.



Garbage Collection 과정은 Mark and Sweep 이라고도 한다.

스택의 모든 변수를 스캔하면서 각각 어떤 오브젝트를 레퍼런스 하고 있는지 찾는과정이 Mark.



첫번째 단계인 marking 작업을 위해 모든 스레드는 중단되는데 이를 stop the world 라고 부르기도 한다.

(System.gc() 를 생각없이 호출하면 안되는 이유이기도 하다)



그리고 나서 mark 되어있지 않은 모든 오브젝트들을 힙에서 제거하는 과정이 Sweep 이다.



(Garbage Collection 이라고 하면 garbage 들을 수집할 것 같지만 실제로는 garbage 가 아닌 것을 따로 mark 하고 그 외의 것은 모두 지우는 것이다. )



System.gc()

System.gc() 를 호출하여 명시적으로 가비지 컬렉션이 일어나도록 코드를 삽입할 수 있지만, 모든 스레드가 중단되기 때문에 코드단에서 호출하는 짓은 하면 안된다.

























https://d2.naver.com/helloworld/37111

Garbage Collection 튜닝

* JDK 7부터 본격적으로 사용할 수 있는 G1 GC를 제외한, Oracle JVM에서 제공하는 모든 GC는 Generational GC이다. 


모든 Java 기반의 서비스에서 GC 튜닝을 진행할 필요는 없다.
Timeout 로그가 수도 없이 출력된다면 여러분의 시스템에서 GC 튜닝을 하는 것이 좋다.
GC 튜닝은 가장 마지막에 하는 작업.



근본적인 원인
생성된 객체가 많으면 많을수록 GC를 수행하는 횟수도 증가.

객체 생성을 줄이는 작업을 먼저 해야 한다.

  • String대신 StringBuilder나 StringBuffer를 사용하는 것을 생활화
  • 로그를 최대한 적게 쌓도록 하는 것이 좋다




어쩔 수 없는 현실

XML과 JSON 파싱은 메모리를 가장 많이 사용한다.

그냥 현실이 그렇다.




GC 튜닝의 목적 두 가지

  • Old 영역으로 넘어가는 객체의 수를 최소화하는 것
  • Full GC의 실행 시간을 줄이는 것


서비스마다 생성되는 객체의 크기도 다르고 살아있는 기간도 다르다.
두 대 이상의 서버에 GC 옵션을 다르게 적용해서 비교해 보고, 옵션을 추가한 서버의 성능이나 GC 시간이 개선된 때에만 옵션을 추가하는 것이 GC 튜닝의 기본 원칙.


구분옵션설명
힙(heap) 영역 크기-XmsJVM 시작 시 힙 영역 크기
-Xmx최대 힙 영역 크기
New 영역의 크기-XX:NewRatioNew영역과 Old 영역의 비율
-XX:NewSizeNew영역의 크기
-XX:SurvivorRatioEden 영역과 Survivor 영역의 비율
GC 튜닝을 할 때 자주 사용하는 옵션은 -Xms 옵션, -Xmx 옵션, -XX:NewRatio 옵션


모니터링도 필요하다. 
$ jstat stat , S1C, S0U S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT 3008.0 3072.0 0.0 1511.1 343360.0 46383.0 699072.0 283690.2 75392.0 41064.3 2540 18.454 4 1.133 19.588 3008.0 3072.0 0.0 1511.1 343360.0 47530.9 699072.0 283690.2 75392.0 41064.3 2540 18.454 4 1.133 19.588 3008.0 3072.0 0.0 1511.1 343360.0 47793.0 699072.0 283690.2 75392.0 41064.3 2540 18.454 4 1.133 19.588

> $JAVA_HOME/bin/jvisualvm
Java 9 부터는 Graal Visual VM 으로 바뀌었다고 한다.









CMS GC

CMS GC 적용을 위한 JVM 옵션 : -XX:+UseConcMarkSweepGC

STW(Stop-The-World) 시간을 최소화 하는데 초점을 맞춘 GC 방식이다.

GC 대상을 최대한 자세히 파악

파악하는 과정이 복잡한 여러단계로 수행되기 때문에 다른 GC 대비 CPU 사용량이 높다





G1 GC (G1: Garbage First)

G1 GC 적용을 위한 JVM 옵션 : -XX:+UseG1GC

하드웨어가 발전되면서 Java 애플리케이션에 사용할 수 있는 메모리의 크기도 점차 켜저갔다. 

큰 힙 메모리에서 짧은 GC 시간을 보장하는데 그 목적을 둔다.

JVM 힙은 2048개의 Region 으로 나뉠 수 있으며, 각 Region의 크기는 1MB ~ 32MB 사이로 지정될 수 있다. (-XX:G1HeapRegionSize 로 설정)



* Default GC:

  • Java 7 - Parallel GC
  • Java 8 - Parallel GC
  • Java 9 - G1 GC
  • Java 10 - G1 GC






WeakReferece


메모리 관리를 위하여 바로 GC가 일어나게 구현하는 방법이 있는가?



https://developer88.tistory.com/115


new() --> StringReference

WeakReferece class를 사용 --> weakReference


GC 되는 기준이 다름.



메모리를 정리할 대상인지를 판단할 때, reachability 기준을 따진다.


new()에 의해 생성된 String Reference는 가장 먼저 참조된 루트 객체로부터 사슬이 계속 연결되어 있는가.

아니라면 unreachable로 GC 대상이 된다.



Weak Reference의 경우, 명확하게 gc의 대상이 되어진다. 


WeakReference<Student> mWeak = new WeakReference<>(new Student("aaa"));

Student mStudent = mWeak.get();






https://www.baeldung.com/java-memory-leaks


GC는 똑똑하지만 완벽하진 않다.

아무리 좋은 개발자가 만든 어플리케이션이더라도 메모리 부족은 여전히 숨어 있을지 모르는 법.

불필요한 Object를 만드는 경우는 얼마든지 존재한다.


메모리 릭의 잠재적인 원인은 무엇이며 런타임시 어떻게 알아차리며 어떻게 처리하는지를 알아야 한다.


메모리 릭?

더이상 사용되지 않는 Heap에 있는 Object들이 GC에 제거 되지 않을 상황이 지속되면 OutOfMemoryError가 발생한다. 



타입

1. static Fields

static variables의 과도한 사용이 원인이 될 수 있다. 

가령 큰 사이즈의 static List


public class StaticTest {
    public static List<Double> list = new ArrayList<>();
 
    public void populateList() {
        for (int i = 0; i < 10000000; i++) {
            list.add(Math.random());
        }
        Log.info("Debug Point 2");
    }
 
    public static void main(String[] args) {
        Log.info("Debug Point 1");
        new StaticTest().populateList();
        Log.info("Debug Point 3");
    }
}


2. Unclosed Resources

스트림을 연다던지, 커넥션을 맺는다던지의 활동을 할 때 조심해야 한다.

자원을 닫지 않으면 GC 대상에서 벗어난다. 


try-with-resources block 의 사용을 습관화 하고 finally다 잘 활용해야 한다.



3. Improper equals() and hashCode() Implementations

새로운 클래스를 만들때 쉽게 관과하는 것이 equals() and hashCode() 를 적절하게 오버라이딩 하지 못하는 것이다.
HashSet과 HashMap이 이 메서드들을 사용하는데, 이 녀석들을 잘 오버라이딩 하지 않으면 키값에 중복이 발생하게 되고 이는 메모리 릭으로 이어질 수 있다.

왜냐? 디폴트 equals는 주소값만 같으면 같은 것으로 보기 때문임.


-->> 아뿔사
public class Person {
    public String name;
     
    public Person(String name) {
        this.name = name;
    }
}


@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
    Map<Person, Integer> map = new HashMap<>();
    for(int i=0; i<100; i++) {
        map.put(new Person("jon"), 1);
    }
    Assert.assertFalse(map.size() == 1);
}


-->> 제대로 된 것

public class Person {
    public String name;
     
    public Person(String name) {
        this.name = name;
    }
     
    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof Person)) {
            return false;
        }
        Person person = (Person) o;
        return person.name.equals(name);
    }
     
    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + name.hashCode();
        return result;
    }
}


4. Inner Classes That Reference Outer Classes

non-static inner class 사용시 주의해야 한다.
초기화를 할 때, inner class 는 감싸고 있는 클래스의 instance를 요구한다. 그래서 scope을 벗어나더라도 GC의 대상이 되질 않는다. 
만약 한 클래스가 대량의 Object를 가지고 있고 inner class도 가지고 있을 때, inner class만 생성했는데도 메모리의 양은 엄청나게 많아진다. 
그러니 inner class를 static 하게 생성해서 감싸고 있는 class의 instance가 생성되지 않도록 하는것이 좋다. 



5. finalize() Methods
finalize 메서드를 오버라이딩 한다고해서 즉시 GC의 대상이 되는게 아니라 GC는 Queue에다가 쌓는다. 
즉, 나중에 메서드가 호출되는 거다.
게다가 finalize안의 내용이 최적화 되어 있지 않으면 더 문제다. 
실행하는데 조금의 시간이 걸린다고 치자. 
finalize를 오버라이딩 한 Object가 대량 생성되었고 이녀석들이 GC처리가 될때 엄청난 부하가 걸리게 될 것이다. 

더 자세히 알고 싶으면 를 참고하자. Guide to the finalize Method in Java그게 아니면 그냥 쓰지말자.



6. Interned Strings

Java7에서 String 관련해서 많은 변화가 있었는데, String pool이 Perm Gen에서 Heap Space로 옮겨간 거다.
버전6이하는 Perm Gen에서 관리하므로 더 주의 깊게 봐야 한다.

(http://www.mimul.com/pebble/default/2008/01/02/1199269440000.html)
Java의 모든 String은 상수 pool에서 관리된다.
String을 new로 생성하지 않고 ""를 통하면 내부적으로 new String() 호출 이후 String.intern()이 호출되어 interned 된다. 
요게 상수 pool에 있는지 없는지를 보고 없으면 등록하는 작업을 한다. 

Perm Gen에 상주하게 되고 어플리케이션이 run 하는 동안은 쭉 있게 되므로 메모리 릭의 잠재 위험이 될 수 있다 

Java 7이상을 쓰던지 Perm Gen 메모리를 늘리던지 해야 한다 .
-XX:MaxPermSize=512m




7. Using ThreadLocals

멀티 스레딩에 매우 유용한 ThreadLocals을 사용할 때도 메모리 릭을 조심해야 한다. 

ThreadLocal의 생성자가 호출되면, 각 스레드는 살아있는 동안 ThreadLocal 변수의 copy의 참조값 및 copy도 들고 있는다.
논쟁거리이며 자세한건 아래 글을 보면 좋겠다. 
Joshua Bloch once commented on thread local usage:

ThreadLocal을 사용하면서 느슨한 Thread pool을 사용하는 것은 의도치 않게 Object가 남아있게 되는 원인이 될 수 있다고 한다. 
스레드가 죽으면 알아서 회수될텐데 뭔소리인가??

옛날엔 thread pool이 없어서 이런얘기를 하는 듯. 
요즘엔 thread pool을 거의 쓰기 때문에, 스레드가 미리 생성되고 재사용된다. 
즉, GC의 대상이 안된다는 거다 .
때문에 명시적으로 ThreadLocal을 GC 대상이되도록 제거해야 한다는 걸 말한다.

try {
    threadLocal.set(System.nanoTime());
    //... further processing
}
finally {
    threadLocal.remove();
}

주의점은 set(null)을 한다고 GC 대상이 되는게 아니니 참고.




다른 대안들

profiler 사용하기



Verbose Garbage Collection

-verbose:gc
GC를 tracking 해서 JVM 설정을 조절해도 좋다.



Use Reference Objects
java.lang.ref package


code review 를 잘하기
















































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

String, StringBuffer, StringBuilder  (0) 2019.03.19
Object의 메서드/equals/hashCode/clone  (0) 2019.03.19
ArrayList + Generic 구현하기  (0) 2019.02.28
Java Collections Framework  (0) 2019.02.28
Exception  (0) 2019.02.28