728x90
기존 언어들의 문제점
- 기존 언어들은 사용자의 프로그램 실행 환경에 맞춰서 다양한 버젼의 프로그램 개발이 필요로 했다
- 또한 Windows에서 컴파일하여 생성한 실행파일은 오직 Windows머신에서만 실행이 가능하다
- 즉, OS 별로 실행 파일이 따로 존재해야 한다는 것이다
Java의 등장
- 자바는 JVM(Java Virtual Machine) 덕분에 OS에 독립적인 특징을 가지고 있다
- Java는 Virtual Machine(가상머신)이라는 개념의 프로그램을 통해 Java프로그램을 실행한다
- 개발자들은 가상머신에 맞춰 개발을 진행하고, 사용자는 본인의 실행환경에 맞는 가상머신 프로그램만 구비하면 프로그램을 어디서든 똑같이 실행할 수 있는 것이다
- 따라서 어떠한 OS든 Java가 설치되어 있다면 JVM에 의해 .java 코드가 기계어로 해석될 수 있다
- Python과 같은 인터프리터 언어는 별도의 컴파일 과정이 없이 소스 코드를 한줄씩 읽어가며 실행을 하지만, Java의 경우 컴퓨터가 읽기 전에 컴파일 과정을 거친 후 컴퓨터가 이해를 하는 방식이다
Java 언어의 특징
1. 운영체제가 독립적이다
- 일종의 애뮬레이터인 JVM을 통해서 가능한 특징이다
- 자바 응용 프로그램은 운영체제나 하드웨어가 아닌 JVM 하고만 통신하고, JVM이 자바 응용 프로그램으로부터 전달 받은 명령을 해당 운영체제가 이해할 수 있도록 변환하여 전달한다
2. 객체지향 언어이다
- 상속, 캡슐화, 다형성이 잘 적용된 순수한 객체지향 언어이다
3. 비교적 배우기 쉽다
- 자바의 연산자와 기본 구문은 C++에서, 객체 지향 관련 구문은 스몰톡(small talk)이라는 객체지향 언어에서 가져왔다
- 이들 언어의 장점은 갖고 불필요한 부분은 제거하여 단순화함으로써 쉽게 배울 수 있고, 간결하고 이해하기 쉬운 코드를 작성할 수 있다
4. 자동 메모리 관리(Garbage Collection)
- 자바로 작성된 프로그램이 실행되면, 가비지 컬렉터(GC)가 자동적으로 메모리를 관리해주기 때문에 프로그래머는 메모리를 따로 관리하지 않아도 된다
5. 네트워크와 분산 처리를 지원한다
- 인터넷과 대규모 분산환경을 염두에 둔 언어여서 풍부하고 다양한 네트워크 프로그래밍 라이브러리(Java API)를 통해 비교적 짧은 시간에 네트워크 관련 프로그램을 쉽게 개발할 수 있도록 지원한다
6. 멀티쓰레드를 지원한다
- 일반적으로 멀티쓰레드의 지원은 사용되는 운영체제에 따라 구현 방법도 상이하며, 처리 방식도 다르다
- 그러나 자바에서 개발되는 멀티쓰레드 프로그램은 시스템과는 관계없이 구현 가능하며, 관련된 라이브러리(Java API)가 제공되므로 구현이 쉽다
- 그리고 여러 쓰레드에 대한 스케줄링(scheduling)을 자바 인터프리터가 담당한다
7. 동적 로딩(Dynamic Loading)을 지원한다
- 보통 자바로 작성된 애플리케이션은 여러 개의 클래스로 구성되어 있다
- 자바는 동적 로딩을 지원하기 때문에 실행 시에 모든 클래스가 로딩되지 않고 필요한 시점에 클래스를 로딩하여 사용할 수 있다는 장점이 있다
- 그 외에도 일부 클래스가 변경되어도 전체 애플리케이션을 다시 컴파일하지 않아도 되며, 애플리케이션의 변경사항이 발생해도 비교적 적은 작업만으로도 처리할 수 있는 유연한 애플리케이션을 작성할 수 있다
🚨 Java의 단점
- 자바는 속도가 느리다는 대표적인 단점이 있다
- 그러나 최근에는 바이트 코드를 하드웨어의 기계어로 바로 변환해주는 JIT 컴파일러와 Hostpot과 같은 기술의 도입으로 JVM의 기능이 향상됨으로써 속도 문제가 상당히 개선되었다!
Java 컴파일 순서
- 개발자가 자바 소스코드(.java)를 작성한다
- 자바 컴파일러(Java Compiler)가 자바 소스파일을 컴파일한다
- Java Compiler는 javac 라는 명령어를 사용하여 .class 파일을 생성한다
- 이 때 나오는 파일은 자바 *바이트 코드(.class) 파일로, 아직 컴퓨터가 읽을 수는 없지만 자바 가상 머신(JVM)이 이해할 수 있는 코드이다
- 바이트 코드의 각 명령어는 1바이트 크기의 Opcode와 추가 피연산자로 이루어져 있다
- 컴파일 된 바이트 코드(.class)를 JVM의 클래스 로더(Class Loader)에게 전달한다
- 클래스 로더는 *동적 로딩(Dynamic Loading)을 통해 필요한 클래스들을 로딩 및 *링크하여 런타임 데이터 영역(Runtime Data area), 즉 JVM의 메모리에 올린다
- 클래스 로더의 세부 동작
- 1) 로드 : 클래스 파일을 가져와서 JVM의 메모리에 로드한다
- 2) 검증 : *자바 언어 명세서(Java Language Specification) 및 JVM 명세에 명시된 대로 구성되어 있는지 준비한다
- 3) 준비 : 클래스가 필요로 하는 메모리를 할당한다 (필드, 메서드, 인터페이스 등)
- 4) 분석 : 클래스의 상수 풀 내 모든 *심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다
- 5) 초기화 : 클래스 변수들을 적절한 값으로 초기화한다 (static 필드)
- 실행 엔진(Execution Engine)은 JVM 메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와서 실행한다. 이 때 실행 엔진에서 바이트코드로 변환하는 방식에는 두 가지가 존재한다
- 인터프리터
- 바이트 코드 명령어를 하나씩 읽어서 해석하고 실행한다
- 하나하나의 실행은 빠르나, 전체적인 실행 속도가 느리다는 단점이 있다
- JIT 컴파일러 (Just-In Time Compiler)
- 인터프리터의 단점을 보완하기 위해 도입된 방식
- 바이트 코드 전체를 컴파일하여 바이너리 코드로 변경하고 이후에는 해당 메서드를 더이상 인터프리팅 하지 않고, 바이너리 코드로 직접 실행하는 방식이다
- 하나씩 인터프리팅하여 실행하는 것이 아니라 바이트 코드 전체가 컴파일 된 바이너리 코드를 실행하는 것이기 때문에 전체적인 실행속도는 인터프리팅 방식보다 빠르다
- 또한 JLT Compiler에 의해 해석된 코드는 캐시에 보관하기 때문에 한 번 컴파일된 후에는 빠르게 수행된다
- 그러나 인터프리팅 방식보다는 훨씬 오래 걸리므로 한번만 실행하면 되는 코드는 인터프리팅하는 것이 유리하다
- 인터프리터
💡 동적 로딩
Java는 동적 로딩을 지원하기 때문에 실행 시, 모든 클래스가 로딩되지 않고 필요한 시점에 클래스를 로딩하여 사용할 수 있다
💡 링크
- 소스코드의 양이 늘어남에 따라 한 파일에 모든 소스코드를 작성할 수 없게 되어 파일들을 분리되어 있는데, 이러한 여러 소스 파일들을 하나로 합치는 것
- 이를 수행하는 프로그램을 링커라고 한다
💡 자바 언어 명세서 (Java Language Specification)
- Java 언어의 명세서를 뜻한다
- JLS는 자바 언어를 위한 문법과 정상/비정상적인 규칙들을 보여준다
- 또한 정상적인 프로그램을 실행하기 위한 프로그램 방법들을 보여준다
💡 Symbolic Reference
- 실제 메모리 주소 값이 아닌, 참조하는 대상의 이름으로만 지정하고 있는 것이다
- JVM 클래스 로더가 Linking 단계의 분석(Resolving)에서 실제 주소값(다이렉트 레퍼런스)으로 변경한다
바이트 코드란?
- 바이트코드(bytecode)는 JVM이 이해할 수 있는 저수준 언어(반기계어)이다
- 각 바이트 코드는 1byte 크기의 opcode로 이루어져 있어서 바이트 코드라고 명명되었다
- .java파일을 컴파일 한 결과로 생성된 .class파일은 바이트코드로 이루어져있다
- 기존의 언어의 컴파일 결과로 생성되는 오브젝트파일(.obj)과 달리, 바이트코드(.class)는 모든 플랫폼의 JVM에서 실행가능하다
- Java만 설치되어 있다면 Windows환경에서 생성된 바이트코드는 Mac환경에서도 실행이 가능하다는 것이다
- 바이트 코드 변환을 통해서 CPU 및 OS에 독립적으로 수행되지만, 기계어 코드를 직접 읽는 것보다 느리다
- javap -c (.class file) 명령을 통해 바이트 코드를 확인할 수 있다
- javap 명령은 클래스 파일의 필드, 생성자, 메소드를 출력한다
- 위의 Java로 작성된 소스코드를 javac컴파일러를 통해 .class파일을 생성한다
- 생성된 클래스파일은 바이트코드로 작성되어있을 것이다
- 바이트 코드를 열어보면 다음과 같이 변환되어 있다
- 기존의 어셈블리어와 유사한 모습을 보인다
JVM의 역할
- 바이트 코드로 변환된 코드를 클래스 로더가 JVM의 메모리에 올리면, JVM의 실행엔진인 인터프리터 혹은 JIT 컴파일러가 해석하고 실행하는 역할을 한다
- 즉, JVM은 다른 프로그램을 실행시키는 것이 목적이다
- 자바 프로그램이 어느 기기나 운영체제 상에서도 실행될 수 있도록 하고, 프로그램 메모리를 관리하고 최적화한다
- JVM 에서의 실행 과정
- Class Loader를 통해 .class 파일들을 JVM에 올린다
- JVM에 있는 .class 파일들을 Execution Engine의 Interpreter와 JLT Compiler를 통해 해석된다
- 해석된 바이트 코드는 Runtime Data Area에 배치되어 실질적인 수행이 이루어진다
Runtime Data Area
Stack Area
- 클래스 내의 메소드에서 사용되는 정보들이 저장되는 공간이다
- 매개변수, 지역변수, 리턴값 등이 저장되며 LIFO(Last In First Out) 방식으로 메소드 실행 시 저장되었다가 실행이 완료되면 제거된다
- 임시 저장공간으로 생각하면 된다
Method(Class, Static) Area
- 클래스와 메소드, 멤버(클래스, 인스턴스)변수와 상수(final) 정보 등이 저장되는 공간이다
Heap Area
- new 명령어를 통해 생성한 인스턴스와 배열 등의 참조형 변수정보가 저장되는 공간이다
- 물론 Method Area에 올라온 클래스들만 생성이 가능하다
- GC(Garbage Collection)의 대상이 된다.
PC Register Area
- 쓰레드마다 하나씩 생성되는 공간
- JVM 명령의 주소값이 저장되는 공간이다
Native Method Stack Area
- 자바 외 다른 언어의 호출을 위해 할당되는 영역이다
- 자바에서 C/C++의 메소드를 호출할 때 사용하는 Stack 영역이라고 생각하면 된다
[메모리 실행 에시]
List의 CRUD 메소드가 있는 ListController가 있다고 가정해보자
- 클래스와 각 메소드의 정보는 실행엔진에 의해 Method 영역에 올라간다
- 클래스의 메소드 호출이 발생하면 Method 영역의 정보를 읽어 해당 메소드의 매개변수, 지역변수 리턴값 등이 Stack 영역에 올라가 처리된다
- 그리고 메소드의 실행이 끝나면 Stack 영역에서 자동으로 제거된다
- 만약 메소드 내에 new 명령어로 생성한 인스턴스나 배열이 있는 경우, 해당 값은 Heap 영역에 저장되고, Stack 영역에서는 Heap 영역의 값을 참조할 수 있는 메모리 주소 값만 저장된다
- 배열을 System.out.println(); 을 통해 출력하면 메모리 주소값이 출력되는 이유가 바로 이것이다!
Garbage Collector(GC)
- GC는 Heap 영역의 메모리를 관리한다
- 자바에서는 메모리를 명시적으로 지정하여 해제하지 않기 때문에 GC의 역할이 중요하다
- GC는 간단하게 말하자면, reachability라는 개념을 사용하여 참조되지 않는 객체들의 메모리를 회수하는 역할을 한다
- System.gc(); 로 GC를 실행하는 것처럼 할 수는 있으니, 실제로 바로 실행되는 것은 아니고 실행을 요청하는 것으로 이는 안티패턴이라 사용을 권장하지 않는다
- Heap 영역은 위와 같이 크게 3가지 영역으로 나뉜다
- Java 8부터는 Permanent 영역 대신에 Java Heap 영역이 아닌 Metaspace로 호출되는 네이티브 영역에 저장된다
- Minor GC
- Heap 영역에 객체가 생성되면 최초로 Eden 영역에 할당된다
- 그리고 이 영역에 데이터가 어느정도 쌓이면 참조 정도에 따라 Servivor1, Servivorr2 중 빈 공간으로 이동되거나 회수된다
- New Gemeration(Eden+Servivor) 영역이 차게 되면, 또 참조 정도에 따라 Old 영역으로 이동되거나 회수된다
- 이렇게 New Generation에서의 GC를 Minor GC라고 한다
- Major GC
- Old 영역에 할당된 메모리가 허용치를 넘게 되면, Old 영역에 있는 모든 객체들을 검사하여 참조되지 않는 객체들을 한꺼번에 삭제하는 GC가 실행된다
- 시간이 오래 걸리는 작업이고 이 때 GC를 실행하는 쓰레드를 제외한 모든 쓰레드는 작업을 멈춘다
- 이를 Stop-the-World 라고 한다
- Stop-the-World가 발생하고 Old 영역의 메모리를 회수하는 GC를 Major GC라고 한다
- Major GC가 실행되면 이것이 종료될 때까지 다른 모든 쓰레드가 멈추기 때문에 성능에 영향을 끼칠 수 밖에 없다
참고)
https://velog.io/@woo00oo/%EC%9E%90%EB%B0%94-%EC%BB%B4%ED%8C%8C%EC%9D%BC-%EA%B3%BC%EC%A0%95
https://93jpark.tistory.com/54
https://kwangkyun-world.tistory.com/entry/Java-%EC%BB%B4%ED%8C%8C%EC%9D%BC-%EA%B3%BC%EC%A0%95
https://aljjabaegi.tistory.com/387
728x90
'야미스터디 > Java' 카테고리의 다른 글
[Java] Java 버전 별 차이점📌 (0) | 2022.09.06 |
---|---|
[Java] JDK, JRE, JVM (0) | 2022.09.04 |
[Java] 스네이크, 카멜, 파스칼 케이스 📌 (0) | 2022.09.03 |
[Java] 인터페이스와 추상클래스 📌 (0) | 2022.08.18 |
[Java] 오버라이딩 vs 오버로딩 📌 (0) | 2022.08.17 |
댓글