-
Java - JVM에 대하여Java/java study 2021. 12. 3. 02:33반응형
JVM이란?
C와 C++ 등과 같은 언어는 프로그램이 컴파일이 되면 해당 OS에 종속된 기계어 코드로 변환되어 다른 OS에서 실행이 안되지만, 자바 프로그램은 JVM이 각기 다른 OS에서 자바 프로그램이 실행할 수 있도록 해주는 프로그램이다. 즉, 어느 OS에서 자바 프로그램이 컴파일이 되어도 다른 OS에서 실행될 수 있도록하는 중계자 역활을 한다.
자바 프로그램 실행 과정
- 자바 컴파일러를 통해 Java Source를 컴파일 한다.
- 컴파일된 Class 파일를 Class Loader에게 전달 한다.
- Class Loader는 동적로딩을 통해 ByteCode들을 JVM에 있는 Runtime Date Area에 로딩 시킨다.
- Execution이 Runtime Data Area에 있는 ByteCode들을 명령어 단위로 한 라인씩 해석하고 실행시킨다.
- 컴퓨터가 이해할 수 있도록 Excution은 ByteCode를 기계가 실행할 수 있는 기계어로 변경하는데 이 때 Interpreter와 JIT가 쓰인다.
- 만약 실행 중에 Native(예 : c언어)로 실행되는 것이 있다면 Native Method InterFace 통해 실행 될 수 있다.
JVM의 구성요소
1. Class Loader
ByteCode를 읽어서 링크를 통해 동적으로 Runtime Data Area에 배치한다.
2. Runtime Data Area
- 모든 스레드가 공유하는 영역
Method Area - 클래스 정보, 변수 정보, static으로 선언한 변수가 저장되며 프로그램 시작부터
종료때까지 저장
Heap - 동적으로 생성된 객체가 저장되는 영역, GC의 대상이 되는 공간
더이상 쓰이지 않는 객체는 GC가 실행되기 전까지 저장
- 스레드마다 하나씩 생성되는 영역
Stack - 지역변수나 메서드의 매개변수, 참조변수, 임시적으로 사용되는 변수, 메서드의 정보가 저장
즉, 사용되고 금방 사라지는 변수가 저장
PC Reigister - 스레드가 시작될 때 생성되며 스레드가 어떤 부분을 어떤 명령어로 수행할지를
저장하는 공간(Program Counter)으로 현재 수행 중인 JVM 명령의 주소를 갖는다.
Native Method Stack - Java가 아닌 다른 언어로 작성된 코드를 위한 공간
즉, Java Native Interface를 통해 호출되는 네이티브 메소드를 저장하는 공간
(ex : c언어로 작성된 메소드)
3. Execution
Class Loader에 의해 Runtime Data Area에 배치된 ByteCode들을 하나씩 가져와 실행한다. 이 실행 엔진은
ByteCode를 기계가 실행할 수 있는 기계어로 변경하여 사용되는데 아래 두가지 방법이 있다.
- Interpreter - 우리가 알고 있는대로 방식대로 바이트 코드를 실행 하나의 명령어를 그때그때 해석해서 실행한다.
- JIT(Just - In - Time) - Interpreter의 단점을 보완하기 위한 컴파일러, 자주 사용하는 코드를 캐시에 저장하여 필요 시 캐시에서 가져온다
Garbage Collector - 이미 할당된 메모리에서 더 이상 사용하지 않는 메모리를 해제함
4. Native Method Interface
JNI는 JVM에 의해 실행되는 코드 중 네이티브로 실행하는 것이 있다면 해당 네이티브 코드를 해당 스택에서 호출하거나 호출 될 수 있도록 만든 일종의 프레임워크
5. Native Method Libraries
네이티브 메소드 실행에 필요한 라이브러리
바이트코드란 무엇인가
- 자바 컴파일러에 의해 변환되는 코드의 명령어 크기가 1byte라 바이트코드라 부른다.
- 같은 명령어 집합을 사용하여 JVM에 의해 OS나 개발환경에 관계없이 실행 가능하게 한다.
- 고급언어로 작성된 소스코드를 JVM이 이해할 수 있는 중간 코드로 컴파일한 것을 말한다.
바이트코드 명령어(opcode)
바이트코드의 명령어 크기가 1byte인 것처럼 바이트코드에는 256개 가량의 opcode가 존재한다.
해당 opcode를 자세히 알고 싶으면 아래 링크를 참조하기 바란다.
List of Java bytecode instructions - Wikipedia
바이트코드 공부해야 하는가?
현재 바이트코드를 몰라도 자바 프로그램을 짜면 컴파일러가 바이트코드로 변환해준다. 이렇듯 바이트코드로 변환해주는 컴파일러가 있는데 '굳이 바이트코드로 어떻게 내부적으로 실행되는지 알아야하나..?'라고 지금은 생각한다. 아직은 지금 공부하기엔 이른감이 있어 훗날에 개발자로서 경력이 쌓이면 내부적으로 어떻게 돌아가는지 공부하고 싶은 영역이므로 나중을 기약하겠다.
JIT 컴파일러, 동작 과정
1. JIT 컴파일러
- Interpreter의 단점을 보완하기 위한 컴파일러
- 바이트코드를 CPU가 이해할 수 있는 언어로 바꾸는 컴파일러
- 자주 쓰이는 바이트코드(loop)를 기계어 코드로 변경하여 캐시에 저장하고 다시 쓰일때마다 캐시에서 바로 가져와서 쓴다.
- 프로그램을 오래 실행할수록 프로그램의 실행속도가 빨라진다.
2. 동작과정
자주 쓰이는 바이트코드가 처음 들어왔을때
(1) JIT Compiler에 의해 기계어로 번역
(2) 번역된 기계어를 cashe에 저장
(3) 번역된 기계어를 전달
자주 쓰이는 바이트코드가 다시 들어왔을때
(4) 해당 바이트코드가 들어온 것을 확인
(5) cashe에서 바이트코드에 해당된 기계어를 가져옴
(6) 번역된 기계어 전달
JDK와 JRE의 차이
- JDK와 JRE의 차이 - 간단하게 정리하면 JRE는 읽기만 가능, JDK는 읽기/쓰기 모두 가능한 점이다.
1. JDK(Java Development Kit)
JRE, 인터프리터/로더, 컴파일러(javac), 아카이버(jar), javadoc 및 java 개발에 필요한 기타 도구를 제공한다. 즉 java 언어를 개발하기위해 필수적으로 필요한 것이며, 우리가 쓰는 이클립스에서도 java개발을 위해 jdk를 이용한다.
2. JRE(Java Runtime Environment)
JVM, Java 클래스 라이브러리 등 java 프로그램을 실행할 때 필요한 패키지이다. JDK와 다른점은 java를 실행만 할 수 있지 컴파일러 또는 디버거와 같은 툴이 포함되어있지 않기 때문에 개발은 할 수가 없다.
Garbage Collector와 Paralle GC, CMS GC, G1 GC
1. Garbage Collector
java 개발을 하면 수많은 객체들을 메모리 할당한다. 하지만 메모리를 할당만 할뿐 반환은 시키지 않는다. 그 이유는 JVM에서 자동적으로 GC가 메모리를 관리해주기 때문이다. GC가 어떻게 메모리를 관리해주는지 알아보자.
할당받은 객체나 배열 메모리 중에서 더 이상 사용하지 않게 된 메모리를 garbage라 부른다.
위 그림을 보면 참조변수 b는 처음에 Person("춘향이")를 가리키고 있지만 b=a; 대입 연산에 의해 a가 가리키고 있는 Person(홍길동)을 가리킨다. 이 결과로 Person("춘향이")객체는 어떠한 참조변수도 참조하고 있지 않으므로 가비지 객체가 된다. 이와 같은 가비지 객체를 JVM의 가용 메모리 공간이 일정 크기 이하로 줄어들면 자동으로 가비지 객체를 회수하여 가용 메모리 공간을 늘린다. 이것을 GC라 부른다.
GC는 수행되는 영역에 따라 두가지로 구분된다.
GC 발생 과정
- 객체가 생성되면 Eden 영역에 위치
- Eden영역에 가득차면 Minor GC가 참조가 없는 객체를 삭제, 참조 중인 객체는 Survivor1로 이동
- Survivor1 영역에 가득차면 Minor GC가 참조가 없는 객체를 삭제, 참조 중인 객체는 Survivor2로 이동
- Survivor2 영역에 가득차면 Minor GC가 참조가 없는 객체를 삭제, 참조 중인 객체를 Survivor1로 이동
- 위 2,3 과정을 반복하여 계속 참조 중인 객체는 Old 영역으로 이동
- Eden 영역에서 Minor GC가 발생시, 객체의 메모리가 Survivor에 비어있는 메모리보다 크면 Old 영역으로 이동
- Old 영역에서 메모리가 가득차면 Major GC가 참조가 없는 객체를 삭제
2. Paralle GC, CMS GC, G1 GC
모두 Old 영역에 대한 GC이며 GC 방식에 따라 처리 절차가 달라진다.
Parallel GC
Paralle GC는 Serial GC와 같은 알고리즘이다. 그러나 Serial GC는 CPU 코어가 하나만 있을 때 쓰는 방식으로 스레드가 하나였지만 Parallel GC는 요즘 CPU에 맞게 여러 스레드를 사용하는 방식이다. 즉 GC를 처리하는 쓰레드가 여러개이므로 빠르게 객체를 처리할 수 있다.
Paralle GC는 메모리가 충분할수록, 코어의 갯수가 많을 수록 좋은 성능을 갖는다.
CMS GC
STOP-THE-WORLD 시간이 짧은 장점을 갖고있는 GC으로 응답 속도가 중요할때 CMS GC를 사용한다. CMS GC의 특징으로는 다른 스레드가 실행 중인 상태에서 진행되는 점이다. 그렇기 때문에 STW(STOP-THE-WORLD)의 시간이 줄어 든다는 장점이 있지만 메모리와 CPU를 많이 사용한다는 단점이 있다.
G1 GC
G1 GC는 2차원 형태로 일정 크기의 region으로 구분하여 논리적으로 구분한다. 여태까지 GC과정에서의 Heap 메모리는 Young 영역과 Old 영역으로 명확하게 구분하였지만 G1 GC에서는 2차원 공간에 region으로 구분된 단위로 Eden, Survivor, Old 가 2차원 공간안에 랜덤하게 쪼개져서 들어가 있다. 이 방식은 그 어떤 GC방식보다도 빠르지만 아직 검증된 것이 아니기 때문에 JVM Crash가 발생할수도 있다. 때문에 안정화가 될때까지 기달렸다가 쓰는 것이 좋다.
Java Reflection
Reflection이란 컴파일 시간이 아닌 실행 시간에 동적으로 특정 클래스의 정보를 객체화하여 분석 및 추출할 수있는 프로그래밍 기법이다. 즉, 개발 당시에는 어떤 클래스를 사용할지 모를때, 런타임 시점에서 클래스를 가져와서 실행하는 경우 쓰인다. 대표적으로 Spring 프레임워크의 어노테이션에서 쓰인다.
Reflection이 가져올 수 있는 정보
- Class - 클래스나 인터페이스 정보를 가져온다.
- Constructor - 해당 클래스로부터 생성자를 가져온다
- Method - 해당 클래스로부터 메소드를 가져온다
- Field - 해당 클래스로부터 변수를 가져온다.
예제를 통한 Reflection 사용 이해
package reflection.test; public class Person { public int num1 = 1; private int num2 = 2; public String str1 = "100"; private String str2 = "200"; public Person(){ System.out.println("매개변수 없는 생성자 호출"); } public Person(int num) { } private Person(String str1) { System.out.println("매개변수 있는 생성자 호출"); } public int method1(int num) { System.out.println("public method1호출, "); return num; } private String method2(int num, String str) { System.out.println("private method2호출, "); return num+str; } }
예제를 통해 Reflection 사용법을 알아보자
1. Class 찾기
Class cls = Class.forName("reflection.test.Person"); System.out.println("클래스 정보 : " + cls.getName());
해당 클래스를 import하지 않아도 클래스 정보를 가져올 수 있다.
2. 생성자 찾기
- getDeclaredConstructor : 인자가 없는 생성자를 가져옴
- getDeclaredConstructor(Param) : 매개변수 타입과 일치하는 생성자를 가져옴.
- getConstructors() : Public의 생성자만 가져옴
- getDeclaredConstructors() : 클래스의 모든 생성자를 가져옴
Class<?> cls = Class.forName("reflection.test.Person"); System.out.println("클래스 정보 : " + cls.getName() ); Constructor con1 = cls.getDeclaredConstructor(); Constructor con2 = cls.getDeclaredConstructor(String.class); Constructor [] con3 = cls.getConstructors(); Constructor [] con4 = cls.getDeclaredConstructors(); System.out.println(con1); System.out.println(con2); for(Constructor c : con3) System.out.println(c); for(Constructor c : con4) System.out.println("getDeclaredConstructors() : " + c);
3. Method 찾기
- getDeclaredMethod() : 인자로 메소드와 파라미터를 넘겨주면 해당 메소드를 가져옴
- getDeclaredMethods() : 클래스의 모든 메소드를 가져옴
- getMethods() : public 메소드들을 가져옴, 상속받은 메소드들도 모두 가져옴
Method method1 = cls.getDeclaredMethod("method1", int.class); Method method2 = cls.getDeclaredMethod("method2", int.class, String.class); Method [] method = cls.getDeclaredMethods(); System.out.println("Method1 : " + method1); System.out.println("Method2 : " + method2); for(Method m : method) System.out.println("Method : " + m);
4. Method 호출
- Method.invoke() : 함수 호출
int result1 = (int)method1.invoke(null, 10); System.out.println(result1);
위에는 reflection의 사용법만 적었다. 하지만 어떻게 사용해야 할지 감이 안잡혔는데 좋은 예시를 들며 설명해준 블로그가 있어 링크를 첨부한다.
반응형'Java > java study' 카테고리의 다른 글
Java - LinkedList, Stack, Queue 구현하기 (0) 2022.01.02 JUnit 5 (0) 2022.01.02 Java - 제어문 (0) 2022.01.01 Java - 연산자 (0) 2021.12.19 Java - 프리미티브 타입과 레퍼런스 타입 (0) 2021.12.10