[Java] 자바, 너는 누구냐?

김태현·2023년 10월 10일

0. 시작하기에 앞서...

세상에는 다양한 프로그래밍 언어가 존재한다. 분류하는 기준에 따라서 저수준-고수준 언어로 나뉘기도 하고 절차적-객체지향-함수형 언어로도 나뉘고 빌드 과정 유무에 따라서 컴파일-인터프리터 언어로 나뉘기도 한다.

이런 많은 언어 중에서 나는 C, C++, Java, Python을 다뤄봤다. 전공자라면 누구나 다들 접해봤을 언어들이다. 이중에서도 내가 가장 많이 다뤄봤고 앞으로도 많이 다룰 Java에 대해 파해쳐 보자.


1. Java의 역사

Java는 초기에 리모콘, 전자레인지와 같이 소형 가전제품에 사용되느 ㄴ임베디드 소프트웨어를 만들기 위해 만들어졌다. 이미 C++라는 언어가 존재하였지만 이는 해당 환경에 적합한 컴파일러가 필요했기 때문에 개발비용과 시간이 많이 요구되었다. Java는 이를 해결하기위해 세상에 나오게 되었으며 OS에 상관없이 + CPU에 상관없이 여러 환경에서 실행이 가능한 Cross Platform언어로써 개발이 되었다.

🕯️Complier / Interpreter
* Complier
컴파일러는 프로그램 전체를 스캔하여 이를 모두 기계어로 번역한다. 전체를 스캔하기 때문에 초기에 속도가 오래 걸린다. 하지만 전체 실행속도의 경우 초기 스캔을 마치면 실행파일을 만들어 놓고 다음에 실행할때 이전에 만들어 놓았던 실행파일을 실행하기 때문에 빠르다. 오류 메시지를 생성할 때 전체 코드를 검사한 후 실행하므로 실행 전에 오류 발견 가능하다. 주요 언어로 C, Java, C++가 있다.
* Interpreter
프로그램 실행시 한문장씩 번역한다. 실행 시간이 더 걸리지만 메모리 효율이 훨씬 좋다. 오류 메시지 검출의 경우 한번에 한문장식 번역하므로 오류 발생시 해당 직전까지 실행 후 종료된다. 주요 언어로 Python, Rubby, Javascript가 있다.

초기에 Oak라 명명되었으나 1.0.2 버전 이후 인도네시아 자바섬에서 재배되는 커피 원두에서 영감을 받아 이후 Java라는 이름을 사용하게 되었다.

Java는 WWW(인터넷)의 출현으로 사용도가 크게 상승하였다. 이식 가능한 언어를 요구하는 Web의 출현으로 당시 컴퓨터 언어 설계 프로젝트의 최선두로 부상헤게 되었다. 구체적으로 인터넷은 다양한 컴퓨터, OS, CPU를 모두 지원해야 하는 광대한 분산형 시스템이었기 때문에 이식성이 매우 중요하여 Java의 인기가 급상습하였다. 1998년 J2SE 1.2에서 웹에서 자바를 돌릴 수 있게 해주는 Java Applet이 추가되어 전성기가 시작되었다.

이 후 자바는 버전이 업데이트 될때마다 여러 기능들이 추가되었고 2009년 Oracle와 인수합병되어 현재 Oracle에서 지속적으로 업데이즈를 진행중이다.


2. 자바의 특징

핵심 정리 : 객체지향, 플랫폼에 독립적, 객체지향적, 네트워크 분산환경 지원, 멀티쓰레드 지원, 동적로딩 지원

자바는 여러 기능을 지원하고 강력한 장점들이 존재한다. 그중에서도 플랫폼(OS)와 CPU에 독립적이라는 점이 큰 장점이다. Java Compiler는 Java code를 Java bytecode라는 특수한 바이너리 형태로 변환한다. Java bytecode를 실행하기 위새허는 JVM(Java Virtual Machine)이라는 특수한 가상 머신이 필요한데 해당 가상 머신은 Java bytecode를 어떠한 플랫폼에서도 동일하게 실행시킬 수 있다. 이를 통해 플랫폼에 독립적인 성격을 지원한다.

🕯️Java Complier 이란?

  • .java 파일을 .class 라는 Java bytecode로 변환
  • JDK를 설치하면 bin 에 존재하는 javac.exe를 말함(즉, JDK에 Java compiler가 포함되어 있음)
  • 변환된 Java bytecode는 기계어가 아니라 OS에서 바로 실행되지 않음

🕯️JVM 이란?

  • 자바를 실행하기 위한 가상 기계
  • OS에 종속받지 않고 CPU 가 Java를 인식, 실행할 수 있게 하는 가상 컴퓨터

몇가지 더 자세히 살펴보자면 Java는 숫자나 논리 값을 제외한 모든 것들을 Object(객체)로 구성하며 객체지향 언어의 특징인 상속, 캡슐화, 다형성 등이 적용되어 있다. Java는 Object 클래스에서 모든 클래스가 파생되며 사용자는 클래스를 기반으로 만들어지는 객체를 생성해서 실행한다.


3. 세부 동작 과정

  1. 작성한 Java Source(.java)파일을 Java Compiler(javac)를 통해 Java Bytecode(.class)로 변환한다.
  2. 컴파일된 bytecode를 JVM의 Class Loader에 전달한다.
  3. Class Loader는 동적 로딩을 통해 필요한 클래스들을 로딩 및 링크하여 Runtime Data Area, 즉 JVM의 메모리에 올린다.
  4. 실행엔진은 JVM메모리에 올라온 Bytecode들을 명령어 단위로 하나씩 가져와 실행한다.

3.1 Class Loader

  • 클래스 로더는 런타임 중에 JVM의 메소드 영역에 동적으로 Java 클래스를 로드하는 역할, 클래스 로더에는 로딩, 링크, 초기화 단계로 나뉘어져 있다.

  • 클래스 로더의 주요 특징 : 계층구조, 위임모델, 가시성제한, 언로드 불가, 이름공간

  • 계층구조 : 클래스 로더는 클래스 로더 끼리 부모-자식관계를 가지는 계층적인 구조로 되어있다. 부트스트랩 클래스 로더는 최상위 클래스 로더로 유일하게 Java가 아니라 네이티브 코드로 구현되어 있어 JVM이 실행될 때 같이 메모리에 올라가며 Object클래스를 비롯한 Java API들을 로드한다. 익스텐션 클래스 로더는 기본 Java API를 제외한확장 클래스 들을 로드한다. 시스템 클래스 로더는 부트스트랩과 익스텐션 클래스 로더가 JVM 자체의 구성요소들을 로드한다면 시스템 클래스 로더는 애플리케이션의 클래스 들을 로드한다. 즉 사용자가 지정한 $CLASSPATH 내의 클래스들을 로드한다. 사용자 정의 클래스 로더는 사용자가 직접 코드상에서 생성하여 사용하는 클래스 로더 이다.

  • 위임모델 : 처음 bytecode를 넘겨받은 클래스 로더가 필요한 클래스를 로드할 떄 혹은 실행 엔진에서 명령어 단위로 bytecode를 실행하다가 처음으로 참조하는 클래스에 대해 클래스 로더에게 로드를 요청할 떄 로드를 요청받은 클래스 로더는 클래스 로더 캐시 -> 상위 클래스 로더 -> 자기자신 순서대로 요청받은 클래스가 있는지 확인한다. 만약 파일 시스템에서도 찾지 못했다면 클래스를 찾지 못했다는 예외가 발생한다.

  • 가시성 제한 : 클래스 로더가 클래스 로드를 요청 받았을 떄 위임 모델에 의해 클래스 로더 캐시를 확인하고 없으면 상위 클래스 로더를 확인하는데 이때 하위 클래스 로더에 있는 클래스는 확인이 불가능한 특성

  • 언로드 불가 : 클래스를 로드하는 것은 가능하지만 반대로 언로드 하는 것은 불가능한 특성

  • 이름공간 : 각각의 클래스 로더들이 가지고 있는 공간

3.2 Runtime Data Area

  • Runtime Data Area는 JVM이 OS위에서 실행되면서 할당받는 메모리 영역
  • 다음과 같이 총 6개의 영역으로 나뉨, PC 레지스터, JVM 스택, 네이티브 메소드 스택은 Thread 마다 하나씩 생성되며 Heap, 메소드 영역은 모든 Thread가 공유한다.
  • PC 레지스터 : 현재 수행중인 명령어의 주소를 가지며 Thread가 시작될 떄 생성되고 각 Thread마다 1개씩 존재
  • JVM 스택 : 스택 프레임이라는 구조체를 저장하는 스택, Thread가 시작될 때 생성되며 Thread마다 1개씩 존재
  • 네이티브 메소드 스택 : Java 이외의 언어로 작성된 네이티브 코드를 위한 스택
  • Heap : 인스턴스 혹은 객체를 저장하는 공간, GC(Garbage Collection)대상이다
  • 메소드 영역 : 모든 Thread가 공유하는 영역으로 JVM이 시작될 때 생성, 필드와 메소드에 대한 정보나 Static변수 혹은 메소드으 ㅣ바이트 코드 등을 보관한다.
  • 런타임 상수 pool : JVM동작에서 가장 핵심적인 역할을 수행하는 곳, 각 클래스와 인터페이스의 상수 뿐만 아니라 메소드와 필드에 대한 모든 레퍼런스까지 담고 있는 테이블로 어떤 메소드나 필드를 참ㄷ조할 떄 JVM은 런타임 상수 pool을 통해 해당 메소드나 실제 메모리상 주소를 찾아서 참조한다.

3.3 Execution Engine

  • 실행엔진은 클래스 로더를 통해 런타임 데이터 영역에 배치된 바이트 코드를 명령어 단위로 읽어서 실행
  • 바이트코드의 각 명령어는 1byte 크기의 OpCode와 추가 피연산자로 이루어져 있음
  • 실행엔진은 하나의 OpCode를 가져와서 피연산자와 작업을 수행한 다음 다음 OpCode를 가져온다.
  • 수행 과정에서 실행엔진은 바이트코드를 기계가 실행할 수 있는 형태로 변경하는데 인터프리터, JIT 컴파일러 2가지 방식으로 변경한다.
  • 인터프리터 : 바이트 코드 명령어를 하나씩 읽어서 해석하고 실핸한다, JVM안에서 바이트코드는 기본적으로 인터프리터 방식으로 동작한다.
  • JIT 컴파일러 : 인터프리터 방식의 단점 보완하기위해 도입됨, 바이트코드 전체를 컴파일하여 네이티브 코드로 변경하고 해당 메소드를 더이상 인터프리팅 하지 않고 네이티브 코드로 직접 실행


4. Java는 그래서 컴파일 언어? 인터프리터 언어?

  • Java는 다른 컴파일 언어들이 작동하듯이 컴파일러를 이용해 전체 코드를 한번에 번역한다. 여기서 사용하는 컴파일러를 Java Compiler라고 하며, Java Compiler는 우리가 작성한 Java 코드를 JVM이 실행시킬 수 있는 자바 바이트 코드로 번역한다. -> 컴파일 언어
  • 바이트 코드는 JVM의 Java Interpreter를 이용해 한 줄씩 실행된다. 바이트 코드로 작성되어 있는 실행 프로그램을 자바 인터프리터가 한 줄씩 읽으면서 컴퓨터가 이해할 수 있는 2진 코드로 번역한 후 실행시킨다 -> 인터프리터 언어
  • 즉 자바는 컴파일 언어와 인터프리터 언어를 혼합한 하이브리드 언어의 형태를 가진다. (처음에는 인터프리터 언어로 사용되다가 성능 향상을 위해 컴파일 언어의 장점을 가져옴)

1개의 댓글

comment-user-thumbnail
2023년 10월 10일

유익한 글 잘 보았읍니다 ^^

답글 달기