학습 목표
JVM에 대하여 제대로 알기
자바 소스파일 .java를 JVM으로 실행하는 과정 살펴보기
목차
- 바이트코드란?
- JVM이란?
- JVM 구성요소
- GC란?(링크참조)
- JIT 컴파일러란 무엇이며 어떻게 동작하는지
- 자바 소스파일(.java)을 JVM으로 실행 과정
- 컴파일 하는방법
- 컴파일 후 실행하는 방법
- 더 자세히 알아보기
바이너리코드와 바이트코드란 무엇인가
프로그램을 실행하는 것은 컴퓨터이다. 즉, 프로그램은 컴퓨터가 이해할 수 있는 형태로 작성되어 있어야 한다.
1) 바이너리 코드란?
C언어는 컴파일러에 의해 소스파일(.c)이 목적파일(.obj)로 변환될 때 0과 1로 이루어진 바이너리코드로 변환된다.
즉, 컴파일 후에 이미 컴퓨터가 이해할 수 있는 이진(바이너리)코드로 변환되는 것이다.
목적파일은 기본적으로 컴퓨터가 이해할 수 있는 바이너리 코드 이지만 실행될 수는 없다.
그 이유는 완전한 기계어가 아니기 때문이다.
변화된 목적 파일은 링커에 의해 실행 가능한 실행파일(.ex)로 변환 될 때 100% 기계어가 될 수 있다.
2) 기계어란 ?
기계어는 컴퓨터가 이해할수 있는 0과 1로 이루어진 바이너리 코드이다.
기계어가 바이너리코드로 이루어졌을 뿐이지 모든 이진코드가 기계어인 것은 아니다
3) 바이트코드란 ?
c언어와 다르게 Java에서는 컴파일러(javac)에 의해 소스파일(.java)이
컴퓨터가 바로 인식할 수 없는 바이트코드(.class)로 변환된다.
컴퓨터가 이해할 수 있는 언어가 바이너리코드라면 바이트코드는 가상 머신이 이해할 수 있는 언어이다.
고급언어로 작성된 소스코드를 가상 머신이 이해할 수 있는 중간 코드로 컴파일한 것을 말한다.
이러한 과정을 거치는 이유는 어떠한 플랫폼에도 종속되지 않고 JVM에 의해 실행 될수 있도록 하기 위함이다.
여기서 플랫폼이란?
개발환경 실행환경 등 어떠한 목적을 수행할 수 있는 환경
ex) 프로그램이 실행되는 환경인 운영체제의 종류(Window, Mac, Linux 등)
개발이 수행되는 환경의 종류(visual studio 등)
JVM이란?
Java Virtual Machine 은 자바를 실행하기 위한 자바 가상머신이다.
자바와 운영체제 사이에서 중계자 역할을 하며, 자바가 운영체제 종류에 영향을 받지 않고 실행 가능하도록 한다.
운영체제 위에서 동작하는 프로세스로 자바 코드를 컴파일 해서 얻은 바이트 코드를
해당 운영체제가 이해할 수 있는 기계어로 바꿔실행 시켜주는 역할을 한다.
GC(Garbase Collection)을 이용하여 자동으로 메모리 관리를 해준다.
- Java Compiler는 .java파일을 .class라는 Java byte code로 변환
- Byte 코드는 기계어가 아니기 때문에 OS에서 바로 실행이 안됨
- JVM은 OS가 ByteCode를 이해할 수 있도록 해석해 주는 역할을 함
- 따라서 JVM은 c언어 같은 네이티브 언어에 비해 속도가 느렸지만 JIT(Just In Time)컴파일러 구현을 통해이점을 극복
- Byte코드는 JVM위에서 OS상관없이 실행된다.
- JVM은 OS에 독립적이지만 의존적이다
JVM 구성요소
JVM의 구성요소 4가지
1. ClassLoader
2. Execution Engine
3. Garbage Collector
4. Runtime data Area
1. Java Compiler 컴파일러
java source(.java)파일은 ByteCode(.class)로 변환된다.
2. Class Loader
변환된 ByteCode(.class)파일을 JVM이 운영체제로 부터 할당 받은 메모리 영역인
Runtime Data Area로 적재하는 역할을 한다.
3. Execution Engine
Class Loader 를 통해 JVM 내부로 넘어와 Runtime Data Area(JVM 메모리)에 배치된 ByteCode들을
기계어로 변경하게 되는데 이때 두가지 방식을 사용하게 된다.
1) 인터프리터 방식
2) JIT 컴파일러 방식
실행 엔진 내부적으로는 인터프리터, JIT 컴파일러, GC가 있다.
1) Interpreter(인터프리터)
기존 바이트 코드를 실행하는 방법은 인터프리터 방식이 기본이다.
실행엔진은 자바 바이트 코드를 명령어 단위로 읽어서 실행한다.
이 방식은 한줄씩 수행하기 때문에 느리다는 인터프리터 언어의 단점을 그대로 가지고 있다.
2) JIT(Just-In-Time)
JIT 컴파일러는 실행 시점에 인터프리터 방식으로 기계어 코드를 생성 하면서 그 코드를 캐싱한다.
그리고 같은 함수가 여러 번 불릴 때 매번 기계어 코드를 생성하는 것을 방지한다.
전체 컴파일 후 캐싱 -> 이후 변경된 부분만 컴파일 하고 나머지는 캐시에서 가져다가 바로 실행 한다.
변경된 부분만 컴파일 하기 때문에 수행속도가 인터프리터 방식에 비해 빠르다.
하지만, 한번만 실행해도 되는 코드에 대해서는 인터프리팅이 유리하다.
(참고) 캐시, 캐싱이란?
캐싱이란 저장한다는 뜻이다. 캐싱이란 오랜시간이 걸리는 작업의 결과를 저장해서
시간과 비용을 필요로 회피하는 기법을 의미한다.
ex)파일을 미리 받아놓고, 그 내용을 보거나 웹서버에서도 매번 로딩을 해야 하는 파일들을 미리 로딩해두고,
응답을 주기도 한다. 데이터 베이스를 매번 확인해야 하는것도 캐시서버를 이용한다면 빠른 응답을 해줄 수 있다.
3) GC (Garbage Collection)
자바에서 개발자는 힙을 사용할 수 있는 만큼 자유롭게 사용하고,
더이상 사용되지 않는 오브젝트들은 GC에 의해 자동으로 메모리에서 제거된다.
GC에 대한 부분을 자세히 알아보고 싶다면 아래 링크 클릭
https://wonyong-jang.github.io/java/2020/03/14/Java-GC.html
[Java] Garbase Collection - SW Developer
GC 프로그래머는 힙을 사용할 수 있는 만큼 자유롭게 사용하고, 더 이상 사용되지 않는 오브젝트들은 가비지 컬렉션을 담당하는 프로세스가 자동으로 메모리에서 제거하도록 하는 것이 가비지
wonyong-jang.github.io
4. Runtime Data Area
프로그램을 수행하기 위해 운영체제로부터 할당받은 메모리 공간
1) PC Register
Thread가 시작될 때 생성되는 공간으로 Thread마다 하나씩 존재한다.
Thread가 어떤 부분을 어떤 명령으로 실행해야 할 지에 대한 기록을 하는 부분
2) Stack Area
프로그램 실행과정에서 임시로 할당되었다가 메소드를 빠져나가면 바로 소멸되는 특성의 데이터를 저장하기 위한 영역
Stack 영역은 Thread별로 각각 독립적으로 생성된다.
각종 형태의 변수나 임시데이터, 스레드나 메소드의 정보를 저장한다.
호출된 메서드의 매개변수, 지역변수, 리턴 값 및 연산 시 일어나는 값들을 임시로 저장한다.
3) Heap Area
new 연산자로 생성된 객체와 배열을 저장하는 메모리 공간
Stack과 Heap에 대한 부분을 자세히 알아보고 싶다면 아래 링크 클릭
[Java] Stack And Heap - SW Developer
이때 원시타입(byte, short, int, long, double, float, boolean, char)의 데이터들의 참조값이 아닌 실제 값을 stack에 저장!
wonyong-jang.github.io
4) Native method stack
Java가 아닌 다른 언어로 작성된 코드를 위한 공간
자바 프로그램이 컴파일되어 생성되는 바이트 코드가 아닌
실제 수행할 수 있는 기계어로 작성된 프로그램을 실행 시키는 영역
5) Method Area
클래스와 인터페이스의 정보를 처음 메모리 공간에 올릴 때 초기화 되는 대상을 저장하기 위한 메모리 공간
Method Area는 모든 Thread에 의해 공유되는 영역이며, JVM이 시작될 때 생성된다.
런타임 상수풀(runtime constant pool), Field, Method, constructor 등
클래스와 인터페이스와 관련된 데이터들을 분류하고 저장한다.
자바 소스파일(.java)을 JVM으로 실행 과정
1. 프로그램이 실행되면 JVM은 운영체제로 부터 이 프로그램이 필요로 하는 메모리를 할당 받음
JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리
2. 자바 컴파일러(javac)가 자바 소스코드(.java)를 읽어들여 자바 바이트코드(.class)로 변환
.java -> .class
3. Class Loader를 통해 class파일을 JVM 메모리에 적재
4. JVM 메모리 영역에 적재된 class 파일을 Execution engine을 통해 해석
자바 컴파일 및 실행 방법
자바 소스를 컴파일 및 실행하기 위해서는 기본적으로 아래의 두 프로그램이 필요하다.
1. javac.exe
2. java.exe
javac.exe는 자바 소스코드를 컴파일 할때 사용하는 프로그램이며
컴파일된 바이트코드를 실행할 때 java.exe를 사용한다.
javac.exe는 JDK, java.exe 는 JRE에 포함되어 있기에 JDK과 JRE를 설치해야 하지만
과거와 다르게 요즘은 JDK에 JRE가 포함된 형태로 배포되고 있기에 JDK만 설치해도 무관하다.
사진참조
1. 컴파일 하는 방법
1. 자바 소스 파일 작성 .java
2. javac.exe를 사용하여 .java 파일 컴파일
javac 소스파일명.java 으로 컴파일한다.
컴파일이 정상적으로 완료되면 해당 경로에 소스파일명 .class 생성
1) 컴파일 옵션
JDK 상위 버전으로 컴파일 하고 하위버전 JDK(또는 JRE)로 실행 했을 경우 아래 에러가 발생한다.
Exception in thread "main" java.lang.UnsupportedClassVersionError
이를 해결하기 위해서는 상위버전 JDK로 실행하거나
javac 옵션을 추가하여 하위 버전의 JDK도 실행 가능하도록 만들어야한다.
1-1) Compiling old Java with a newer compiler
버전이 오래된 자바 코드를 버전이 높은곳 에서 재 컴파일 하려는 경우,
일반적인 경우는 특정 컴파일 옵션을 줄 필요는 없다.
하지만, 몇가지 경우에는 옵션이 필요한데 아래를 예로 들 수 있다.
enum이 자바 5부터 추가 되면서 아래와 같이 변수명으로 enum은 불가능해 졌다.
하지만, 오래된 코드에서 아래처럼 작성되었다면 새로운 컴파일러로 재컴파일 했을 경우는 에러가 발생한다.
public class OldSyntax {
private static int enum; // invalid in Java 5 or later
}
따라서 아래와 같이 컴파일러 옵션을 이용하여 소스코드가 특정 버전으로 컴파일 될 수 있도록 지정한다.
javac -source 1.4 OldSyntax.java
1-2) Compiling for an older execution platform
오래된 자바 플랫폼에서 높은 버전의 자바 소스를 실행 하려 할 때 에러가 발생한다.
해결책으로는 JDK를 자바 소스코드에 맞춰 업그레이드를 하는게 가장 쉬운 방법이다.
그렇지 않다면 컴파일 옵션을 주어 해결 가능하다.
하지만 여기서도 버전을 낮추는 경우 버전 보다 높은 문법을 사용 했을 경우는 에러가 발생한다.
javac -target 1.4 SomeClass.java
2. 실행하는 방법
java.exe 파일을 사용하여 바이트코드로 컴파일된 .class 실행
java 소스파일명
실행시에는 소스파일명 뒤에 확장자명(.java)를 붙이지 않는다.
더 자세히 알아보자
- Class Loader
- Runtime 시점에 .class에서 바이트코드를 읽고 메모리에 저장
- 로딩: 클래스를 읽어오는 과정
- 링크: 레퍼런스를 연결하는 과정
- 초기화: static 값들을 초기화 및 변수에 할당
- Runtime Data Areas
- Heap 과 Method는 모든 쓰레드가 공유 나머지는 쓰레드 마다 생성
- JVM이 프로그램을 수행하기 위해 OS로 부터 별도로 할당받은 메모리 공간
- PC Register: CPU가 Instruction을 수행하는 동한 필요한 정보를 저장
- JVM Stack: Thread가 시작될 때 생성되며 Method와 Method 정보 저장
- Navtive Method Stack: Java 이외의 언어로 작성된 native 코드를 위한 Stack(JNI)
- Method Area: 모든 쓰레드가 공유하는 메모리 영역(클래스, 인터페이스, 메소드, 필드, Static 변수등의 바이트 코드 등을 보관)
- Heap: 런타임시 동적으로 할당하여 사용하는 영역 class를 통해 instance를 생성하면 Heap에 저장됨
- Heap의 경우 명시적으로 만든 class와 암묵적인 static 클래스(.class 파일의 class)가 담긴다.
- 또한 암묵적인 static 클래스의 경우 클래스 로딩 시 class 타입의 인스턴스를 만들어 힙에 저장한다. 이는 Reflection에 등장한다.
- Execution Engine
- Load된 Class의 ByteCode를 실행하는 Runtime Module
- Class Loader를 통해 JVM 내의 Runtime Data Areas에 배치된 바이트 코드는 Execution Engine에 의해 실행(바이트 코드를 명령어 단위로 읽어서 실행)
Class Loader
Class Loader
- 로딩, 링크, 초기화 순으로 진행된다.
- 로딩
- 클래스 로더가 .class 파일을 읽고 그 내용에 따라 적절한 바이너리 데이터를 만들고 “메소드” 영역에 저장
- 이때 메소드 영역에 저장하는 데이터
- FQCN(Fully Qualified Class Name)
-
클래스 인터페이스 이늄 인지를 저장 - 메서드와 변수
- 로딩이 끝나면 해당 클래스 타입의 Class 객체를 생성하여 “힙” 영역에 저장
- 링크
- Verify, Prepare, Resolve(Optional) 세 단계로 나눠져 있다.
- 검증: .class 파일 형식이 유효한지 체크한다.
- Preparation: 클래스 변수(static 변수)의 기본값에 따라 필요한 메모리
- Resolve: 심볼릭 메모리 레퍼런스를 메서드 영역에 있는 실제 레퍼런스로 교체한다.
- 초기화
- Static 변수의 값을 할당한다.(static 블럭이 있다면 이때 실행된다.)
- 클래스 로더는 계층 구조로 이뤄져 있으면 기본적으로 세가지 클래스 로더가 제공된다.
- 부트 스트랩: JAVA_HOME/lib에 있는 코어 자바 API를 제공한다. 최상위 우선순위를 가진 클래스 로더
- 플랫폼: JAVA_HOME/lib/ext 폴더 또는 java.ext.dirs 시스템 변수에 해당하는 위치에 있는 클래스를 읽는다.
- 애플리케이션: 앱 ClassPath(앱을 실행 할 때 주는 -classpath 옵션 또는 java.class.path 환경변수의 값에 해당하는 위치)에서 클래스를 읽는다.
Java Compiler
흔히 우리가 JIT Compiler라고 부르는 Compiler는 실행중에 바이트 코드를 여러가지 다양한 테크닉을 사영하여 JVM 해석 엔진 없이 바로 수행되는 기계어 코드를 만들어 낸다. 그럼으로서 바이트코드가 가지는 장점과 기계어가 가지는 장점을 결합할 수 있다.
과정
- Java Compiler(javac 명령어 실행)에 의해 Java Source(.java 확장자)로부터 Byte Code(.class 확장자)가 생성된다.
- JVM에 있는 Class Loader에 의해 Byte Code는 JVM내로 로드되고 실행엔진에 의해 기계어로 해석되어 메모리 상(Runtime Data Area)에 배치된다.
- 실행엔진에는 Interpreter와 JIT(Just-In-Time) Compiler가 있는데, Interpreter에 의해 Byte Code를 한 줄씩 읽어 실행하다가 적절한 시점에 Byte Code 전체를 컴파일하고 더이상 인터프리팅하지 않고 해당 코드를 직접 실행한다.
- JIT Compiler에 의해 해석된 코드는 캐시에 보관하기 때문에 한 번 컴파일된 후에는 빠르게 수행할 수 있다는 장점이 있습니다. 하지만 코드 전체를 컴파일하기 때문에 인터프리팅하는 것보다 시간이 오래 걸리므로 한 번만 실행해도 되는 코드에 대해서는 인터프리팅하는 것이 유리합니다.
- Interpreter : 자바 Byte Code를 한 줄씩 실행. 전체 성능면에서 불리.
- JIT Compiler : 전체 Byte Code를 컴파일하고 캐시에 보관해놓고 직접 실행. 한 번만 실행해도 되는 코드에 대해서는 Interpreter가 유리.
장점
- 생성되는 코드의 안정성
- Java가 수행중 만들어내는 기계어 코드는 안전한 공간(sandBox)안에서 돌아가기 때문에 외부 해킹에 안전
- 동작하는 메모리 공간의 안전성
- 모든 자바 객체들은 Heap이라는 독립적인 공간에서만 수행
- 다른 Process 와 다른 메모리 공간을 사용하기 때문에 Stack overflow에 강함
- 최적화 재사용에 유일한 관련 클래스간 상속구조
- 메모리 위치상 가깝게 관련된 객체와 메소드들을 위치시킨다.
- method inlining 같은 성능을 높이기 위한 테크닉들이 자바에서 효율적으로 작동
- 동적 최적화와 그것에 대한 취소, 재 최적화 가능
- static 언어와 다르게 dynamic class loading으로 어떤 방식으로든 수행중 변경 가능 compiler를 통한 최적화가 수시로 이루어 진다.
Compiler 기술들
- Hot Spot Detection
- JVM이 ByteCode를 해석하다가 루프등을 만나 몇번이나 중복적인 해석이 이루어진다고 판단되면 Byte코드를 기계어로 직접 컴파일하는 방식
- 기존의 모든 기본 코드를 수행전에 컴파일 하는 방식은 수행 자체는 빠르지만 프로그램 크기가 커지고 기기별 이식성이 떨어지기도 한다.
- Method inlining
- 클래스 안에서 사용된 다른 클래스에 대해 method inlining을 수행함으로서 다른 메모리 공간에 있는 메소드에 대해 호출하는 것을 피할 수 있다.
- 이걸 취소할 수도 있다.
- reflection
- 객체를 명시적으로 코드에서 new하지 않아도 임의의 객체를 동적으로 생성하고 메소드를 호출할수 있는 reflection은 자바 동적 클래스로딩의 핵심
JIT
핫스팟 JVM의 핵심 컴파일 방법중 하나로 먼저 인터프리터가 동작하여 코드를 실행
일정시간 동안 인터프리터가 코드를 해석하며 컴파일하기에도 충분할 정도로 자주 호출되는 메소드가 무엇인지 알아내고 해당 메소드만 컴파일
위의 컴파일 장점이 되는 모든 기능들은 자바 1.6 이후의 HotSpot 엔진의 성능향상의 덕이다.
HotSpot이란 Sun Microsystem사의 Java 엔진 이름이다. Java VM의 엔진은 다양하지만 크게 HotSpot(Sun/Oracle), J9(IBM), JRocket(Oracle) 정도가 기업용자바 환경에서 주로 사용되는 것들이다. JRocket은 서버쪽 성능개선에 집중된 것으로 HostSpot에 그 핵심기능이 옮겨져있기에 현재 대새는 Oracle Hotspot이다. HotSpot은 오픈소스 자바 프로젝트인 OpenJDK의 JVM엔진이기도 하다.
–Tips–
interpreter가 수행된다음에는 프로그램에 대해 더욱 많은 정보를 저장하면서 분석할수 있다는 점이다. 어느 지점이 hot spot인지도 monitor링 될 뿐만아니라 어느 함수가 어느 함수를 부르고 있는지도 명확하게 파악될수 있다. 자바 기반의 어플리케이션이 디버깅에 매우 유리한 이유이기도 하다.
Client Compiler
클라이언트 모드에서 동작하는 컴파일러는 주로 프로그램의 시작시간을 최소화하는데에 집중한다.
- 클라언트 모드 총 세단계
- 바이트코드를 해석해서 최적화를 쉽게 하기 위해, HIR이라고 하는 정적인 바이트코드 표현을 만듬
- HIR로부터 플랫폼에 종속적인 중간표현식 (LIR) 을 만듬
- LIR을 사용해 기계어 생성
- 클라이언트 모드 JIT의 특징은 바이트코드로부터 최대한 많은 정보를 뽑아내어 실제 동작하는 코드 블럭에 대해 최적화를 집중하는 것이다.
- 전체적인 최적화에는 큰 관심이 없다.
Server Compiler
서버모드의 Jit compiler는 부분적인 코드 실행보다 전체적인 성능 최적화에 관점을 둔다.
- 일반적인 컴파일러 최적화 기술들을 이용해 일단 코드들을 최적화 한다.
- 죽은 코드 삭제(Dead Code Elimination), loop 변수의 끌어올리기(Loop invariants hoisting), 공통 부분식 제거(Common Subexpression Elimination), 상수 지연(Constant propagation), 전역 코드 이동(Global Code motion) 등
- 자바에 최적화된 최적화를 수행한다.
- Null Check 삭제, 배열의 Range Check 삭제, 예외처리 경로 최적화.
- 대단위 RICS 레지스터들을 최대한 활용하기 위한, Graph연산을 통한 register할당
- 이런 과정을 통해 상대적으로 느린 속도로 JIT이 수행된다. 하지만 코드의 수행은 더욱 빠르다.
자세히 알아보기 출처 : https://jeongjin984.github.io/posts/JVM/
'프로그래밍 공부 > JAVA' 카테고리의 다른 글
GC(Garbage Collection)란? (0) | 2022.12.26 |
---|---|
애너테이션(어노테이션) (0) | 2021.12.05 |
빌드와 실행, Ant, Maven, Gradle 이란 무엇인가? (0) | 2021.09.03 |
JDK, JRE, JVM은 무엇인가? (0) | 2021.09.03 |
JAVA중급강의 정리 람다식 (0) | 2021.08.30 |