목표
-
자바 소스 파일(.java)을 JVM으로 실행하는 과정 이해하기.
학습할 것
-
JVM이란 무엇인가
-
컴파일 하는 방법
-
실행하는 방법
-
바이트코드란 무엇인가
-
JIT 컴파일러란 무엇이며 어떻게 동작하는지
-
JVM 구성 요소
-
JDK와 JRE의 차이
-
javac 옵션(추가과제)
-
라이브 피드백
JVM이란 무엇인가
JVM은 Java Virtual Machine의 줄임말로 OS 환경에 상관없이 자바 프로그램을 실행할 수 있도록 도와주는 프로그램이다.
좀더 정확히 말하자면, 자바 소스코드(*.java)를 컴파일 한 경우 바이트코드로 이루어져 있는 클래스파일(*.class)이 생성된다. JVM은 클래스파일을 운영체제가 이해할 수 있도록 기계어로 해석해준다. JVM의 해석을 거치기 때문에 C나 C++과 같은 네이티브 언어에 비해 속도가 느렸지만 JIT(Just In Time)컴파일러를 구현해 한계를 극복했다. 현재는 거의 차이가 없다고 봐도 무방하다.
컴파일 하는 방법
자바를 개발하는 사람들은 대부분 이클립스나 인텔리제이 등 IDE툴을 이용해서 빌드를 통해 컴파일을 한다.
(사실 IDE툴도 PC에 설치된 Java의 경로에서 javac.exe를 툴 내부적으로 javac라는 명령어를 이용해서 컴파일한다.)
하지만 리눅스에 톰캣으로 올린 스프링부트 프로젝트의 소스를 수정 후 재컴파일을 해야하는 순간이 존재하기 때문에 알아야 한다.
※ javac에 대한 옵션은 맨아래에 있다.
터미널에서 자바를 실행하려면 환경설정이 필요한데 구글링하면 자료가 무수히 쏟아지므로 이글에서는 생략하겠다.
과정은 아래의 내용과 같다
1. 메모장을 통해 자바코드를 작성한다. 확장자는 .java로 끝나야 한다.
2. 터미널을 통해 작성한 파일의 경로로 들어간 후 javac Hello.java 치면 컴파일 되어 Hello.class파일이 생성된 것을 확인할 수 있다.
3. Hello.class 파일을 열어보면 괴상한 문자들이 존재한다.
이는 컴파일된 바이트코드로서 JVM이 해석할 수 있는 언어로 변경된것이다.
4. javap -c "클래스명" 명령어로 해석된 바이트코드를 확인할 수 있다.
int a = 1; 한줄 짜리 코드가 바이트코드에서는 두줄이다.
0: iconst_1 상수 0을 stack에 푸시
1: istroe_1 push한 값을 로컬 변수 1에 저장
...
실행하는 방법
1.컴파일된 클래스 파일을 java Hello 명령어를 통해 실행한다.
※C언어와 자바 컴파일 과정 차이
(1)C언어
c언어는 전체코드를 컴파일
C/C++ 소스를 컴파일하고 나면 OS상에서 바로 실행될 수 있는 실행 파일이 생성된다.
그러나 이렇게 생성된 실행 파일에는 OS에 종속적인 코드가 있으며, OS가 바뀌면 다시 소스를 컴파일하고 링크해야 한다.
(2)자바
링크과정 없이 컴파일러가 바로 바이트코드를 생성
컴파일된 클래스파일(바이트코드)이 JVM에서 실행되어 OS에 종속적인 코드를 갖지 않으므로
OS가 달라져도 다시 컴파일할 필요없이 OS에 맞는 JVM만 설치하면 된다.
바이트코드란 무엇인가
위키백과에서 정의한것에 따르면 바이트코드란 특정 하드웨어가 아닌 가상 컴퓨터에서 돌아가는 실행 프로그램을 위한 이진 표현법이라 한다. 초심자(나)가 이해하기에는 어렵다.. 쉽게 말하자면 우리가 흔히 코드를 작성하는 파일인 .java 파일을 컴파일 후 생기는 .class파일 안의 내용을 말한다. 코틀린, 스칼라, 그루비 등 여러가지 언어로 코드를 작성한 다음 특정 컴파일러를 통해 바이트코드로만 만들어주면 JVM을 통해 어플리케이션을 실행할 수 있다.
※ src 디렉토리에 있는 .java파일을 컴파일 하면
이클립스는 해당 프로젝트 루트경로의 target - classes
인텔리제이는 build - classes에 class파일들이 존재한다.
바이트코드는 1Byte로 이루어진 명령 코드(opcode)였기 때문에 바이트코드라 불리게 되었다.
자바의 Interpreter인 JVM 안에서 .class파일에 대한 Interpret이 수행이 되는데, 소스코드 전체를 한번에 번역하는 컴파일러와는 달리 소스 코드를 한 행씩 중간 코드로 번역 후 실행한다. OS에 독립적인 장점을 얻었지만 중간과정이 추가됨으로서 느리다는 비판을 받아왔지만 JIT Compiler가 등장함으로써 한계를 극복했다.
JIT 컴파일러란 무엇이며 어떻게 동작하는지
JIT 컴파일(Just-in-time-compilation) 또는 동적 번역(dynamic-translation)은 프로그램을 실제 실행하는 시점(Runtime)에 기계어로 번역하는 컴파일 기법이다.(JIT 컴파일 = 인터프리터 방식 + 컴파일 방식)
일반적으로 기계어로 만드는 방법은 인터프리터 방식과 정적 컴파일 방식이 있다.
인터프리터 방식 : 한줄씩 읽어내려 가며 기계어로 변환
정적 컴파일 방식 : 실행하기전에 프로그램 코드를 기계어로 번역
위에서 말한 실행속도를 증가시키기 위해 JIT컴파일러는 런타임시에 JVM과 상호작용하여 적절한 바이트코드 시퀀스를 기계어로 컴파일한다. 즉, 번역한 코드를 캐싱하여, 동일한 코드가 있을 경우 번역작업을 거치지 않고 캐싱해둔 값을 사용함으로서 매번 기계어 코드 생성을 방지하여 인터프리팅 시간을 단축시킨다.
그림을 보면 JIT컴파일러는 JRE안에 존재한다.
포함관계가 헷갈리므로 다시정리해보자면
JDK 안에 JRE가 포함되어 있다
JRE안에 JVM이 있다
JVM안에 JIT가 있다.
JIT 컴파일러는 JRE의 구성요소로서 런타임에 자바 애플리케이션의 성능을 향상시킨다. 런타임에 JVM은 클래스 파일을 로드하고, 각 개별 바이트코드의 의미를 결정하며 적절한 계산을 수행한다.
JIT 컴파일러는 메서드 호출 내내 활성화 되며, 바이트코드를 기계어로 컴파일 하여 실행한다. JVM은 컴파일된 코드를 해석하는 대신 이전에 JIT컴파일러에 의해 컴파일된 코드를 직접 호출한다.
이는 메모리와 프로세스를 사용하지 않는다는 전제하에, 자바 애플리케이션의 속도가 네이티브 애플리케이션의 속도에 근접할 수 있도록 도움을 준다. 하지만 JIT컴파일 또한 프로세스시간과 메모리 사용량이 필요하며 JVM에서 JIT를 처음 사용할 경우 수천가지의 방법이 호출된다.
※ 다른 설명(이부분에서 컴파일이란 바이트코드 -> 기계어)
JIT Compiler가 처음 바이트 코드를 읽을 때 한번 번역하고 저장소에 저장한다. 즉, 반복되는 코드를 매번 해석하지 않고 런타임 때 컴파일 하면서 해당 코드를 캐싱한다. 이후엔 바뀐 부분만 컴파일하고 나머지는 캐싱된 코드를 사용한다. 그래서 인터프리터는 읽을 때, 반복되는 코드는 컴파일된 코드를 바로 사용할 수 있어서 속도가 개선된다.
인터프리터가 안읽고 캐싱된 컴파일된 코드를 바로 사용
뒤에 나오겠지만 아래의 그림에서
자바 인터프리터랑 JIT컴파일러가 동시에 동작되는 것이다.
성능을 좌우하는 중요한 코드(메소드)는 JIT컴파이러가 기계어로 미리 정적 컴파일을 수행하고 캐싱한다.
후에 똑같은 코드(메소드)가 실행되면 캐싱된 기계어를 가져와서 그대로 사용한다.
나머지 덜 중요한 코드들이 인터프리터 방식으로 수행되는것이다.
JVM 구성 요소
JVM은 크게 4가지 구성요소로 이루어져 있다.
ClassLoader System
Runtime 시점에 Runtime Data Area의 Method Area에 .class파일들을 검사 후 로드하고
링크를 통해 배치하는 작업을 수행
Runtime Data Area
프로그램이 실행되기 위해선 메모리가 필요하다. 자바 애플리케이션도 마찬가지다. OS가 관리하는 메인메모리인 RAM의 일부 영역을 JVM이 필요한 만큼 OS로부터 할당받는다.
OS로부터 받은 메모리 공간을 Runtime Data Area라고 부르며 5개의 영역으로 용도별로 나누어서 관리한다. 초심자들은 Method Area, Heap, Stack영역만 숙지하고 있어도 될것 같다.(나)
Method Area(=Class Area=Static Area)
클래스 파일의 바이트코드가 로드되는 곳
클래스별로 필요 정보(필드, 메소드, 생성자 등)를 적재한 공간
이 영역에 할당된 변수(전역, static)는 프로그램이 종료될 때까지 계속 존재한다.
클래스의 정보(런타임 상수 풀, 생성자, 클래스, 클래스안의 메소드, 변수 등)와 클래스 변수가 저장되는 영역
※ Runtime Constant pool
- 해당 클래스의 메소드, 필드, 문자열 상수 등의 레퍼런스를 가지고 있다.
- 실제 물리적 메모리 위치를 참조할 때 사용
여기서 잠깐!!! String 클래스에 대해 잠깐 알아가보자
String a = "choi";에서 "choi"라고 하는 리터럴을 a에 대입시킨 개념으로 생각했다.
하지만 잘못된 생각이였다. "choi"는 상수풀을 참조하는 것이다.
문자 리터럴은 상수의 한 종류로서 변하지 않는 중요한 특성이 있다.
String b = "choi";에서 b 또한 a와같이 상수풀에 생성된 동일한 "choi"를 참조하고 있는 것이다.
"choi"문자열의 주소값을 참조한거지 문자열 자체를 참조한건 아니다
문자 리터럴은 내부적으로 intern()이라는 메소드를 사용한다
intern()메소드는 해당 문자열이 상수풀에 이미 있는 경우에는 그 문자열의 주소값을 반환하고 없다면
상수풀에 문자열을 새로 집어넣고 해당 문자열의 주소값을 반환한다.
String str1 = new String("PARK");
String str2 = new String("PARK");
String str3 = "PARK";
str1 == str1.intern(); // false (str1.intern()의 "PARK"라는 문자열이 str3에 의해 이미 상수풀에 저장되었으므로
상수풀에 있는 주소를 반환), str1은 힙메모리에 생성되었으므로 서로 다른 주소값
str3 == str1.intern(); // true
intern() 메소드는 Heap 메모리에 있는 String 객체를 String 상수풀로 이동시키는 역할을 함으로서, 위의 비교결과값은 각각 false, true가 나온다.
str3과 같이 리터럴을 이용하여 생성할 경우 Heap영역안의 String constant pool이라는 영역에 존재하게 된다. (Java6까지는 Perm영역에 저장되었지만 고정된 용량을 가지고 있어 런타임 중 OutOfMemoryException을 발생시킬 수 있는 문제가 있었다고 한다)
그래서 String constant pool의 모든 문자열도 GC의 대상이 된다.
요약
Heap : 인스턴스가 생성되는 공간. 객체를 생성하면 Heap영역의 메모리에 할당되어 사용된다. Garbage Colleciton의 대상이 된다.
Stack : 프로그램 실행중에 메서드 호출시, 각각의 메소드를 위한 메모리가 할당되는 영역
호출한 메소드의 지역 변수와 매개변수 등을 저장
메소드 호출과 함께 할당되며, 메소드 호출이 완료되면 소멸
PC Register : 현재 실행되고 있는 부분의 주소를 가지고 있음. 현재 실행되고 있는 명령이 종료되면 카운트 값을 증가시켜 다음 명령을 실행
Native Method Stack : 자바 외 언어(C, C++ 등)을 수행하기 위한 Stack 영역. 프로그램 실행 도중 호출 된 메서드가 Native방식을 사용하는 메서드일 경우, 이 영역에 저장되어 처리
Execution Engine
Interpreter : 바이트코드 명령어를 하나씩 읽어 해석하고 실행
JIT Compiler : 런타임시에 JVM과 상호작용하여 적절한 바이트코드 시퀀스를 기계어로 컴파일을 실행
Garbage Collector : 참조되지 않는 객체를 우선적으로 메모리에서 제거하여 메모리 공간을 확보하는 Garbage Colleciton을 실행시켜주는 주체(?)
※ Garbage Collection을 딥하게 설명해준 블로그
yaboong.github.io/java/2018/06/09/java-garbage-collection/
자바 중급이상 찍고 읽어보자...
JDK와 JRE의 차이
openJDK8을 다운받으면 JDK설치 후에 JRE 설치창이 뜨는 것을 확인할 수 있다.
JRE(Java Runtime Environment)
JVM + Java class Libraries + Java Class Loader가 포함된다.
※ 클래스 로더는 런타임 환경에서 바이트코드(.class)를 최초로 메모리에 로드한다.
즉, 모든 클래스 파일이 한 번에 JVM 메모리에 로딩되지 않고, 요청 순간 로딩이 되게끔 조율하는 역할을 한다.
그림을 확인하면 바이트코드는 JRE(JVM + Java class Libraries + Java Class Loader) 위에서 동작하는 것을 알 수 있다.
JRE는 자바 애플리케이션을 실행하는데 필요한 요소만 들어있지 자바를 개발하는 필요한 툴은 포함되어 있지 않다.
JDK(Java Development kit)
JRE뿐만 아니라 컴파일러, 디버거 등 자바 애플리케이션을 개발하는데 필요한 도구가 포함되어 있다.
Javac 옵션
생김새 : javac <option> <source files>
(1) -classpath(-cp)
컴파일러가 컴파일 할때 필요로 하는 라이브러리나 클래스들의 경로를 지정해주는 옵션
스프링 프로젝트에서 리눅스에서 직접 javac로 컴파일할 때 자주 사용되는 옵션이다.
위와 같은 에러가 뜬다면
javac -classpath 프로젝트경로/WEB-INF/lib/* 프로젝트경로/WEB-INF/classes/SomeFile.java
이런식으로 명령어를 작성하여 파일을 탐색할 수 있도록 경로만 잘 맞춰주면 된다.
※ classpath를 구분하는 기호는 리눅스는 :(콜론) / 윈도우는 ;(세미콜론)
(2) -source [자바버전] -target [자바버전]
하위 jdk 버전에서도 작성한 자바애플리케이션이 실행되도록 할 때 사용
예를 들어 java 1.6에서 컴파일했지만 java 1.5에서 실행이 가능하도록 할 때 사용한다.
(3) -d
클래스 파일을 생성할 루트 디렉터리를 지정
이 옵션을 주지 않으면 소스파일이 위치한 디렉터리에 클래스 파일을 생성
※ javap -c "클래스 이름".class
.class의 파일의 바이트코드를 opcode로 해석한다.
라이브 피드백
-
인터넷에서 가져온 이미지 바로 밑에 출처를 달아라
-
그림도 직접 그리는게 도움이 된다.
-
직접 그린그림은 출처를 달지 않아도 된다.
-
텍스트도 복사-붙여넣기 하지말고 자신이 이해한것을 토대로 직접 기록하라
-
사람마다 공부한 깊이가 다르기 때문에 다른사람들이한 과제도 참고해라
-
에러메시지를 정독해라
상위버전의 바이트코드는 하위버전의 자바프로그램으로 실행 불가능
하위버전의 바이트코드는 상위버전의 자바프로그램으로 실행 가능
스프링프레임워크는 자바15버전으로 컴파일되지만 -source, -target 옵션으로 java8버전에서도
돌아가게끔 만들어둠
그러나 가끔 일부 메이븐 플러그인이 이런 사항을 고려하지 않고 만들어서 하위버전의 자바로 돌아가지 않는 일이 종종 있다.
java.lang.UnsupportedClassVersionError: Hello has been compiled by a more recent version of the
Java Runtime(class file version 58.0), ...52.0
중요한 에러
58은 java14, 52는 java8
에러를 소개해줄려고 한거지 -source, -target 사용을 권장하지는 않음
버전이 올라갈수록 컴파일러의 기능이 상승하기 때문에, 최신 자바버전을 사용하는 것이 좋음
하위버전으로 낮출시 바이트코드가 효율적이지 않을수 있음
References
github.com/league3236/startJava/blob/master/live_study/week1.md
www.notion.so/1-71a17cda52624af393a551a867f5a010
'Language > Java' 카테고리의 다른 글
[자바 스터디 5주차] 클래스 (0) | 2020.12.19 |
---|---|
switch문 동작방식 (0) | 2020.12.09 |
[자바 스터디 4주차] 제어문 (0) | 2020.12.02 |
[자바 스터디 3주차] 연산자 (4) | 2020.11.24 |
[자바 스터디 2주차] 자바 데이터 타입, 변수 그리고 배열 (0) | 2020.11.17 |