백기선님이 과거에 진행했던 Java 스터디 1주차 스터디 입니다. 늦었지만 저는 이제 시작해보겠습니다.
1. JVM이란 무엇인가?
JVM이란 'Java Virtual Machine'의 약어다. 자바를 실행하기 위한 가상 머신(컴퓨터)이라고 이해할 수 있다.
자바로 작성된 모든 프로그램은 JVM 위에서만 실행된다. 따라서 자바 프로그램을 작동시키기 위해서 JVM은 필수적이다.
일반 애플리케이션의 코드는 OS만 거치고 하드웨어로 전달된다. 그러나 Java 애플리케이션은 JVM을 한 번 더 거치기 때문에,
그리고 하드웨어에 맞게 완전히 컴파일된 상태가 아니고 실행시에 해석(Interpret)되기 때문에 속도가 느리다는 단점을 가진다.
그러나 요즘엔 바이트코드(컴파일된 자바 코드)를 하드웨어의 기계어로 바로 변환해주는 JIT 컴파일러와 향상된 최적화 기술이 적용되어서 속도의 격차를 많이 줄였다.
Java는 JVM을 사용하기 때문에 OS 플랫폼에 독립적이다. 즉 윈도우에서 작성한 자바 파일을 리눅스와 MacOs, CentOs 어디서도 실행시킬 수 있다는 것이다. 그러나 JVM OS에 종속적이기 때문에 해당 OS에서 실행 가능한 JVM이 필요하다.
JVM은 자바 바이트 코드(.class 파일)를 OS에 특화된 코드로 변환(인터프리터와 JIT 컴파일러)하여 실행한다.
정리
- 자바 컴파일러는 자바 코드 (.java)를 컴파일해 자바 바이트 코드(.class)로 변환해 클래스 파일을 만든다.
- 자바 바이트 코드(.class)를 실행시키는 주체가 JVM이다.
- JVM은 자바 바이트 코드(.class 파일)를 OS에 특화된 코드로 변환(인터프리터와 JIT 컴파일러)하여 실행한다.
- JVM을 사용해 자바 코드는 특정 하드웨어 OS에 종속적이지 않다.
- 자바는 OS에 종속적이지 않지만 JVM은 OS에 종속적이다.
2. 컴파일 하는 방법
먼저 Hello.java 라는 파일을 만들자.
public class Hello {
public static void main(Strng[] args){
System.out.println("Hello, JVM");
}
}
그러면 이제 아래와 같은 명령어를 입력한다.
javac Hello.java
그리고 Hello.java가 있는 디렉토리에서 ls 명령어를 입력하면 아래와 같이 나온다.
자바 컴파일 명령어를 사용해 클래스 파일이 나오는 것이다.
그러면 이제 아래 명령어를 사용해 .java가 컴파일된 .class 파일 내부를 살펴보자
javap -c Hello
우리는 이제 바이트 코드를 아래 이미지 처럼 확인할 수 있다.
이 바이트코드를 실행하는게 인터프리터와 JIT 컴파일러다. 그러면 OS에 맞춰서 머신코드로 변경되어서 실행될 것이다.
뒷 파트에서 더 설명하겠다.
참고
- javap는 역어셈블러 명령어다. -c 옵션을 이용해 역어셈블된 자바 클래스 파일의 바이트 코드를 확인했다.
- 컴파일(compile)이란 C, Java 등 인간이 이해할 수 있는 고급 언어로 작성된 프로그램 소스 코드를 하드웨어가 이해할 수 있는 기계어 또는 바이트코드로 번역해 주는 것을 말한다. 이 과정에서 목적 파일이 생성되며 이것이 하드웨어가 이해하는 바이트 코드와, 기계어로 이루어진다. 여기서 목적 파일은 .class 파일이겠다.
- 결과적으로 컴파일이라는 개념은 원시코드에서 목적 파일로 바꾸는 것이다.
3. 실행하는 방법
java 명령어를 이용해 컴파일된 클래스 파일을 실행할 수 있다.
Hello.class를 실행시키기 위해 java Hello 명령어를 입력한다. (java Hello.java도 동작한다.)
java <class file name> #java Hello.java도 동작한다.
4. 바이트코드란 무엇인가?
바이트 코드란
바이트코드(Bytecode, portable code, p-code)는 특정 하드웨어가 아닌 가상 컴퓨터에서 돌아가는 실행 프로그램을 위한 이진 표현법이다. 하드웨어가 아닌 소프트웨어에 의해 처리되기 때문에, 보통 기계어보다 더 추상적이다.
자바 바이트 코드란
결론적으로 말하면 자바 바이트코드(Java bytecode)는 JVM이 실행하는 명령어의 형태이다.
이해하기 쉽게 풀어 쓰면 자바 가상 머신이 이해할 수 있는 언어로 변환된 자바 소스 코드를 의미합니다.
소스코드 파일인 .java가 컴파일 되어 이 바이트 코드(.class, 명령어)로 변환된다.
자바 컴파일러에 의해 변환되는 코드의 명령어 크기가 1바이트라서 자바 바이트 코드라고 불리고 있습니다.
.java 파일에서 자바 컴파일러가 .class 파일로 컴파일을 진행한다.
컴파일 과정에서 생성된 목적 파일이 .class다. 자바 바이트 코드의 확장자는 .class입니다.
자바 바이트 코드는 자바 가상 머신만 설치되어 있으면, 어떤 운영체제에서라도 실행될 수 있습니다.
5. JIT 컴파일러란 무엇인며 어떻게 동작하는가
위에서 JIT 컴파일러에 대해 잠깐 언급했다.
'컴파일된 바이트 코드(.class)를 실행하는게 인터프리터와 JIT 컴파일러다. 그러면 OS에 맞춰서 머신코드로 변경되어서 실행될 것이다.'
'바이트 코드를 하드웨어의 기계어로 바로 변환해주는 JIT 컴파일러' 이렇게 두 차례 언급 되었다.
결국 위에서 느낌이 오는데 쉽게 말해 바이트 코드를 실행하는 것이 JIT 컴파일러다. 본격적으로 살펴보자.
JIT 컴파일러
- JIT 컴파일(just-in-time compilation) 또는 동적 번역(dynamic translation)은 프로그램을 실제 실행하는 시점에 기계어로 번역하는 컴파일 기법이다.
- 자바를 실행할 때 인터프리터는 바이트 코드를 한 줄씩 실행한다. 이 인터프리터 효율을 높이기 위해, JIT 컴파일을 사용한다.
- 실행 시점에서 인터프리터 방식으로 네이티브(특정 OS의 기계어) 코드를 생성한다.
- 만약 같은 코드(함수 등)가 발견되면 인터프리터는 JIT 컴파일러를 통해 반복되는 코드를 모두 네이티브 코드로 바꿔둔다.
- 그 이후에는 인터프리터가 반복되는 코드를 발견한다면 JIT 컴파일러를 통해 네이티브 코드로 컴파일된 코드를 바로 사용한다.
- JIT(Just-In-Time) 컴파일러는 런타임에 자바 애플리케이션의 성능을 향상시키는 자바 런타임 환경의 구성 요소다.
자바 컴파일러는 소스 코드를 중간 언어인 바이트코드로 변환한다.
바이트코드는 기계어는 아니지만 JVM 의해 네이티브로 손쉽게 변환할 수 있는 코드이다.
JIT 컴파일러는 바이트코드를 읽어 빠른 속도로 기계어를 생성할 수 있다.
이런 기계어 변환은 코드가 실행되는 과정에 실시간으로 일어나며(그래서 Just-In-Time이다), 전체 코드의 필요한 부분만 변환한다.
기계어로 변환된 코드는 캐시에 저장되기 때문에 같은 코드가 반복되면 재사용시 컴파일을 다시 할 필요가 없다.
그러나 JIt 컴파일러는 시작시에 상당한 프로세서 시간과 메모리 사용량이 필요하다. Jit 컴파일을 진행해 프로그램이 결국 매우 좋은 피크 성능을 달성하더라도, 시작 시간에 꽤나 시간이 걸릴 수 있다. 또한 반복적이지 않거나 가벼운 코드라면 Jit 컴파일러보다는 단순한 인터프리터 방식이 성능적인 측면에서 더 좋다.
JIT 컴파일러는 런타임에 바이트 코드를 네이티브 코드로 바꾸고 캐싱한다. 만약 코드가 반복된다면 네이티브 코드로 컴파일된 코드를 인터프리터가 바로 사용한다. 이런 작업을 통해 성능 향상을 기대할 수 있다.
6. JVM 구성요소
클래스 로더 시스템
클래스 로더 시스템은 .class 에서 바이트코드를 읽고 메모리에 저장한다.
로딩 (클래스를 읽어오는 과정)
- 클래스 로더가 .class 파일을 읽고 그 내용에 따라 적절한 바이너리 데이터를 만들고 "메소드" 영역에 저장한다.
- 이때 메소드 영역에 저장하는 데이터는 아래와 같다.
- FQCN는 Fully Qualified Class Name의 약어다. 패키지, 풀 패키지 경로, 클래스 이름등을 의미한다.
- Class, Interface, Enum 인지
- 메서드와 변수
- 로딩이 끝나면 해당 클래스 타입의 Class 객체를 생성해 힙 영역에 저장한다.
클래스 로더
- 부트 스트랩 클래스 로더 - JAVA_HOME\lib에 있는 코어 자바 API를 제공한다. 최상위 우선순위를 가진 클래스 로더다. JVM을 기동할 때 생성되며, Object 클래스들을 비롯하여 자바 API들을 로드한다. 다른 클래스 로더와 달리 자바가 아니라 네이티브 코드로 구현되어 있다.
- 플랫폼 클래스 로더 - JAVA_HOME\lib\ext 폴더 또는 java.ext.dirs 시스템 변수에 해당하는 위치에 있는 클래스를 읽는다. 기본 자바 API를 제외한 확장 클래스들을 로드한다. 다양한 보안 확장 기능 등을 여기에서 로드하게 된다.
- 애플리케이션 클래스 로더 - 애플리케이션 클래스패스(애플리케이션 실행할 때 주는 -classpath 옵션 또는 java.class.path 환경 변수의 값에 해당하는 위치)에서 클래스를 읽는다.
이렇게 계층형 구조를 이루고 있다.
링크 (레퍼런스를 연결하는 과정)
- Verify : .class 파일이 유효한지 체크한다. 읽어 들인 클래스가 자바 언어 명세(Java Language Specification) 및 JVM 명세에 명시된 대로 잘 구성되어 있는지 검사한다. 클래스 로드의 전 과정 중에서 가장 까다로운 검사를 수행하는 과정으로서 가장 복잡하고 시간이 많이 걸린다.
- Prepare : 메모리를 준비하는 과정이다. : 클래스가 필요로 하는 메모리를 할당, 초기화하고, 클래스에서 정의된 필드, 메서드, 인터페이스들을 나타내는 데이터 구조를 준비한다.
- Resolve : 심볼릭 메모리 레퍼런스를 메소드 영역에 있는 실제 레퍼런스로 교체한다.
초기화 (static 값들 초기화 및 변수에 할당)
- Static 변수의 값을 할당한다. (static 블럭이 있다면 이때 실행된다.)
메모리
메서드 영역
모든 스레드가 공유하는 영역으로 JVM이 시작될 때 생성된다. JVM이 읽어 들인 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메서드 정보, Static 변수, 메서드의 바이트코드 등을 보관한다. 즉 정적 변수(static)과 클래스 수준의 정보를 저장한다.
static 영역, 데이터 영역이라고도 한다.
메서드 영역 내부의 Runtime Constant Pool
클래스 파일 포맷에서 constant_pool 테이블에 해당하는 영역이다. 각 클래스와 인터페이스의 상수뿐만 아니라, 메서드와 필드에 대한 모든 레퍼런스까지 담고 있는 테이블이다. 즉, 어떤 메서드나 필드를 참조할 때 JVM은 런타임 상수 풀을 통해 해당 메서드나 필드의 실제 메모리상 주소를 찾아서 참조한다.
힙영역
인스턴스 또는 객체를 저장하는 공간으로 가비지 컬렉션 대상이다. JVM 성능 등의 이슈에서 가장 많이 언급되는 공간이다. 모든 스레드가 공유할 수 있다.
네이티브 메소드 스택
자바 외의 언어로 작성된 네이티브 코드를 위한 스택이다. 즉, JNI(Java Native Interface)를 통해 호출하는 C/C++ 등의 코드를 수행하기 위한 스택으로, 언어에 맞게 C 스택이나 C++ 스택이 생성된다
PC (Program Counter)
PC(Program Counter) 레지스터 : 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성된다. PC 레지스터는 현재 수행 중인 JVM 명령의 주소를 갖는다.
스택 (stack)
스레드마다 런타임 스택을 만들고 ,그 안에 메소드 호출을 하는 스택 프레임(메서드 콜)이라 부르는 블럭으로 쌓는다. 스레드 종료하면 런타임 스택도 사라진다. 또한 스택 프레임은 JVM 내에서 메서드가 수행될 때마다 하나의 스택 프레임이 생성되어 해당 스레드의 JVM 스택에 추가되고 메서드가 종료되면 스택 프레임이 제거된다.
스택 프레임에는 함수의 매개변수, 호출이 끝난 뒤 돌아갈 반환 주소값, 함수에서 선언된 지역 변수 등이 저장되어 있다.
정리
전체 스레드에서 공유 | 하나의 스레드에서만 공유 |
힙 영역, 메서드 영역 | 네이티브 메소드 스택, PC, 스택 |
실행 엔진
- 인터프리터 : 바이트 코드 명령어를 하나씩 읽어서 해석하고 실행한다. 하나씩 해석하고 실행하기 때문에 바이트코드 하나하나의 해석은 빠른 대신 인터프리팅 결과의 실행은 느리다는 단점을 가지고 있다. 흔히 얘기하는 인터프리터 언어의 단점을 그대로 가지는 것이다. 즉, 바이트코드라는 '언어'는 기본적으로 인터프리터 방식으로 동작한다.
- JIT 컴파일러 : 인터프리터의 단점을 보완하기 위해 도입된 것이 JIT 컴파일러이다. 인터프리터 방식으로 실행하다가 적절한 시점(반복되는 코드)에 바이트코드 전체를 컴파일하여 네이티브 코드로 변경하고, 이후에는 해당 메서드를 더 이상 인터프리팅하지 않고 네이티브 코드로 직접 실행하는 방식이다. 네이티브 코드를 실행하는 것이 하나씩 인터프리팅하는 것보다 빠르고, 네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 계속 빠르게 수행되게 된다.
- GC (Garbage Collection) : 더이상 참조되지 않는 객체를 모아서 정리한다. GC를 통해 개발자가 C, C+처럼 메모리를 해제(free 함수)를 하지 않아도 된다. 메모리 최적화(메모리 leak를 방지)를 위해 사용한다.
JNI (Java Native interface)
- 자바 애플리케이션에서 C, C++, 어셈블리로 작성된 함수를 사용할 수 있는 방법 제공
- Native 키워드를 사용한 메소드 호출
네이티브 메소드 라이브러리
- C, C++로 작성된 라이브러리
7. JDK와 JRE 차이
JRE (Java Runtime Environment)
- JRE = JDK + 라이브러리
- JVM과 핵심 라이브러리 및 자바 런타임 환경에서 사용되는 프로퍼티 세팅이나 리소스 파일을 가지고 있다.
- 개발 관련 도구는 포함하지 않는다. 이건 JDK에서 제공한다.
- JRE에서 가장 중요한 요소는 자바 바이트코드를 해석하고 실행하는 JVM(Java Virtual Machine)이다.
JDK (Java Development Kit) : JRE + 개발 툴
- JRE + 개발에 필요한 툴
- 예를 들어 컴파일 할 때 사용하는 자바 컴파일러가 없다. (javac)
- 오라클은 자바 11부터는 JDK만 제공하며 JRE를 따로 제공하지 않는다.
부족하거나 틀린 부분 피드백은 언제나 환영입니다.
이상으로 포스팅을 마칩니다. 감사합니다!!1
참고 자료
자바의 정석 (저자 : 남궁성)
더 자바, 코드를 조작하는 다양한 방법 (강사 : 백기선 https://www.inflearn.com/course/the-java-code-manipulation/dashboard)
https://d2.naver.com/helloworld/1230
https://ko.wikipedia.org/wiki/%EC%9E%90%EB%B0%94_%EA%B0%80%EC%83%81_%EB%A8%B8%EC%8B%A0
http://wiki.hash.kr/index.php/%EC%BB%B4%ED%8C%8C%EC%9D%BC
http://www.tcpschool.com/java/java_intro_programming
https://ko.wikipedia.org/wiki/%EC%9E%90%EB%B0%94_%EB%B0%94%EC%9D%B4%ED%8A%B8%EC%BD%94%EB%93%9C
https://ko.wikipedia.org/wiki/%EB%B0%94%EC%9D%B4%ED%8A%B8%EC%BD%94%EB%93%9C
https://ko.wikipedia.org/wiki/JIT_%EC%BB%B4%ED%8C%8C%EC%9D%BC
댓글