본문 바로가기

항해 99/Java

Java - 컴파일, JVM 스택 / 힙 메모리, 클래스 / 인스턴스

 

Java 컴파일 과정

 

자바 컴파일 과정

  1. 개발자가 자바 소스코드(.java)를 작성합니다.
  2. 자바 컴파일러(Java Compiler)가 자바 소스파일을 컴파일합니다, 이때 나오는 파일은 자바 바이트 코드(.class)파일로 아직 컴퓨터가 읽을 수 없는 자바 가상 머신이 이해할 수 있는 코드입니다.
    • 바이트 코드의 각 명령어는 1바이트 크기의 Opcode와 추가 피연산자로 이루어져 있습니다.
  3. 컴파일된 바이트 코드를 JVM의 클래스로더(Class Loader)에게 전달합니다.
  4. 클래스 로더는 동적로딩(Dynamic Loading)을 통해 필요한 클래스들을 로딩 및 링크하여 런타임 데이터 영역(Runtime Data area), 즉 JVM의 메모리에 올립니다.
    • 클래스 로더 세부 동작 
      1. 로드 : 클래스 파일을 가져와서 JVM의 메모리에 로드합니다.
      2. 검증 : 자바 언어 명세(Java Language Specification) 및 JVM 명세에 명시된 대로 구성되어 있는지 검사합니다.
      3. 준비 : 클래스가 필요로 하는 메모리를 할당합니다. (필드, 메서드, 인터페이스 등등)
      4. 분석 : 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경합니다.
      5. 초기화 : 클래스 변수들을 적절한 값으로 초기화합니다. (static 필드)
  5. 실행엔진(Execution Engine)은 JVM 메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와서 실행합니다. 이때, 실행 엔진은 두가지 방식으로 변경합니다.
    1. 인터프리터 : 바이트 코드 명령어를 하나씩 읽어서 해석하고 실행합니다. 하나하나의 실행은 빠르나, 전체적인 실행 속도가 느리다는 단점을 가집니다.
    2. JIT 컴파일러(Just-In-Time Compiler) : 인터프리터의 단점을 보완하기 위해 도입된 방식으로 바이트 코드 전체를 컴파일하여 바이너리 코드로 변경하고 이후에는 해당 메서드를 더이상 인터프리팅 하지 않고, 바이너리 코드로 직접 실행하는 방식입니다. 하나씩 인터프리팅하여 실행하는 것이 아니라 바이트 코드 전체가 컴파일된 바이너리 코드를 실행하는 것이기 때문에 전체적인 실행속도는 인터프리팅 방식보다 빠릅니다.

기술면접 질문/답안

Java가 컴파일되는 과정은 어떻게 되는지 설명해주실 수 있을까요?

Java의 컴파일 과정은 크게 네 단계로 이루어집니다.
1. 소스 코드 작성: 개발자는 .java 확장자를 가진 파일에 Java 언어 규칙에 따라 소스 코드를 작성합니다.
2. 컴파일: Java 컴파일러(javac)는 .java 파일을 읽어들여 문법적 오류를 검사하고, 올바른 경우 바이트코드를 포함한 .class 파일을 생성합니다. 이 바이트코드는 JVM이 이해할 수 있는 중간 형태의 코드입니다.
3. 로딩: JVM은 .class 파일을 로드(load)하여 메모리에 적재합니다. 이 과정에서 클래스 로더(Class Loader)가 사용되며, 필요한 클래스들을 동적으로 메모리에 로딩합니다.
4.실행: 로드된 클래스 파일들은 실행 엔진(Execution Engine)에 의해 해석되고 실행됩니다. 실행 엔진은 바이트코드를 한 줄씩 읽어서 기계어로 변환(Just-In-Time 컴파일)하거나 직접 해석하여 실행합니다.

 

이 과정을 통해 Java 소스 코드는 먼저 플랫폼 독립적인 바이트코드로 변환되고, 이후 필요에 따라 JVM이 설치된 어떠한 시스템에서도 실행될 수 있습니다. 이러한 컴파일 과정은 Java가 '한 번 작성하면 어디서나 실행(WORA)'을 가능하게 하는 핵심 요소입니다.

 

 

JVM 스택 / 힙 메모리

자바의 메모리 영역

자바 프로그램이 실행되면 JVM(자바 가상 머신)은 OS로부터 메모리를 할당받고, 그 메모리를 용도에 따라서 여러 영역으로 나누어 관리를 한다.

 

JVM의 메모리 공간(Runtime Data Area)은 크게 Method(Static) 영역, Stack 영역, Heap 영역으로 구분되고 데이터 타입(자료형)에 따라 각 영역에 나눠서 할당 되게 된다.

 

스택(Stack)

  • 메서드 내에서 정의하는 기본 자료형에 해당되는 지역변수의 데이터 값이 저장되는 공간
  • 메서드가 호출될 때 스택 영역에 스택 프레임이 생기고 그 안에 메서드를 호출
  • primitive 타입의 데이터(int, double, byte, long, boolean 등)에 해당되는 지역변수, 매개 변수 데이터 값이 저장
  • 메서드가 호출 될 때 메모리에 할당되고 종료되면 메모리에서 사라짐
  • Stack은 후입선출 LIFO(Last-In-First-Out)의 특성을 가지며, 스코프(Scope)의 범위를 벗어나면 스택 메모리에서 사라진다
[스택 프레임(stack frame)]
하나의 메서드에 필요한 메모리 덩어리를 묶어서 스택 프레임(Stack Frame)이라고 한다.
하나의 메서드당 하나의 스택 프레임이 필요하며, 메서드를 호출하기 직전 스택프레임을 자바 Stack에 생성한 후 메서드를 호출하게 된다.
스택 프레임에 쌓이는 데이터는 메서드의 매개변수, 지역변수, 리턴값 등이 있다.
만일 메서드 호출 범위가 종료되면 스택에서 제거된다.

 

힙(Heap)

  • JVM이 관리하는 프로그램 상에서 데이터를 저장하기 위해 런타임 시 동적으로 할당하여 사용하는 영역
  • 참조형(Reference Type) 데이터 타입을 갖는 객체(인스턴스), 배열 등이 저장 되는 공간
  • 단, Heap 영역에 있는 오브젝트들을 가리키는 레퍼런스 변수는 stack에 적재
  • Heap 영역은 Stack 영역과 다르게 보관되는 메모리가 호출이 끝나더라도 삭제되지 않고 유지된다.그러다 어떤 참조 변수도 Heap 영역에 있는 인스턴스를 참조하지 않게 된다면, GC(가비지 컬렉터)에 의해 메모리에서 청소된다.
  • stack은 스레드 갯수마다 각각 생성되지만, heap은 몇개의 스레드가 존재하든 상관없이 단 하나의 heap 영역만 존재

힙과 스택 메모리의 차이점

  • 힙 메모리는 애플리케이션의 모든 부분에서 사용되며, 반면에 스택 메모리는 하나의 스레드가 실행될 때 사용.
    • 그래서 힙 과 메서드 공간에 저장된 객체는 어디서든지 접근이 가능하지만, 스택 메모리는 다른 스레드가 접근할 수 없다.
  •  언제든지 객체가 생성되면 항상 힙 공간에 저장되며, 스택 메모리는 힙 공간에 있는 객체를 참조만 한다.
    • 즉, 스택 메모리는 primitive 타입의 지역변수와 힙 공간에 있는 객체 참조 변수만 갖고 있다.
  • 스택메모리의 생명주기는 매우 짧으며, 힙 메모리는 애플리케이션의 시작부터 끝까지 살아남는다.
  • 자바 코드를 실행할때 따로 -Xms과 -Xmx 옵션을 사용하면 힙 메모리의 초기 사이즈와 최대 사이즈를 조절할 수 있다.
  • 스택 메모리가 가득차면 자바에서는 java.lang.StackOverFlowError를 발생.힙 메모리가 가득차면 java.lang.OutOfMemoryError : Java Heap Space 에러를 발생
  • 스택 메모리 사이즈는 힙 메모리와 비교했을 때 매우 적다. 하지만 스택 메모리는 간단한 메모리 할당 방법(LIFO)를 사용하므로 힙 메모리보다 빠르다.

 

기술면접 질문/답안

JVM의 스택과 힙메모리 영역에 대해 아는 만큼 설명해주실 수 있을까요?

스택 메모리 영역: 스택 메모리는 각 스레드별로 존재하며, 메서드 호출과 로컬 변수 저장에 사용됩니다. 스택은 후입선출(LIFO, Last In First Out) 구조를 가지며, 메서드가 호출될 때마다 메서드에 대한 스택 프레임이 생성됩니다. 스택 프레임은 로컬 변수, 연산 중간 결과, 메서드 호출과 리턴에 대한 정보를 포함합니다. 메서드가 종료되면 해당 스택 프레임은 스택에서 제거됩니다. 스택 메모리는 상대적으로 관리가 단순하고 접근 속도가 빠르지만, 크기가 고정되어 있으므로 오버플로우(스택 메모리 부족)가 발생할 수 있습니다.

힙 메모리 영역: 힙 메모리는 애플리케이션 전체에서 공유되는 영역으로, 객체와 배열과 같은 동적으로 할당된 데이터를 저장하는 데 사용됩니다. 힙은 스레드 간 공유되며, JVM의 가비지 컬렉터에 의해 관리됩니다. 객체가 생성되면 힙에 메모리가 할당되고, 더 이상 참조되지 않는 객체는 가비지 컬렉터에 의해 자동으로 회수됩니다. 힙 메모리는 런타임에 동적으로 확장될 수 있지만, 메모리 관리가 복잡하고 접근 속도가 스택에 비해 상대적으로 느립니다.

스택과 힙 메모리의 이해는 자바 애플리케이션의 성능 최적화와 메모리 관리에 중요합니다. 스택은 각 스레드의 메서드 호출 및 로컬 변수 처리에, 힙은 애플리케이션의 객체와 배열 저장에 사용되어, 자바 애플리케이션의 메모리 구조와 생명 주기를 결정합니다.

 

 

클래스와 인스턴스

클래스(Class)

객체를 정의한 설계도 또는 틀이라 정의할 수 있음, 클래스는 객체를 생성하는 데 사용된다

반대로 객체는 클래스에 의해 정의되고 설계된 내용을 기반으로 생성되며, 클래스로부터 객체를 만드는 과정을 인스턴스화(Instance ctiate)라 한다

클래스와 객체의 관계는 제품의 설계도와 제품과의 관계와 유사함, 제품 설계도 없이는 제품을 만들 수 없고, 제품 또한 설계도 없이 만들 수 없기 때문이다.

 

객체(Object)

객체는 클래스에 의해 정의되고 설계된 내용을 기반으로 생성된다, 객체는 속성과 행위(기능)를 가진다.

즉, 클래스를 통해 만들어진 객체가 실제로 사용할 수 있는 주체가 된다. 객체의 속성은 필드(변수), 행위는 메서드에 해당한다.

속성과 행위는 이너 클래스와 함께 객체의 멤버이다, 클래스를 통해 생성된 객체를 클래스의 인스턴스(instance)라고 부른다.

 

인스턴스(Instance)

객체와 인스턴스는 크게 차이를 보이지는 않는다, 따라서 두 용어를 혼용하여 사용하기도 한다.

객체는 모든 인스턴스를 포괄하는 넓은 의미를 가지고, 인스턴스는 해당 객체가 어떤 클래스로부터 생성된 것인지를 강조한다.

 

클래스의 구성요소

클래스는 class 키워드를 통해 정의할 수 있으며, 클래스의 이름은 대문자로 시작하는 것이 관례이다

public class ClassName { // 클래스
    int studentNumber = 10; // #1 필드

    public int MethodName{ // #2 메서드
    }
	
    ClassName { // #3 생성자
    }

    class ClassName2 { // #4 inner 클래스
    }
}
  • 클래스 안에는 필드, 메서드, 생성자, 이너 클래스 네 가지 구성 요소를 작성할 수 있다
    • 필드 : 클래스의 속성(변수)을 나타낸다. 객체의 데이터, 상태 정보 등을 저장하는 곳임(메서드와 생성자 전체에서 사용할 수 있음)
    • 메서드 : 클래스의 행위(메서드)를 나타낸다. 객체 간의 데이터 전달 수단으로 활용되고, 외부로부터 값을 받을 수 있으며, 메서드의 실행 이후 값을 반환할 수 있다.
    • 생성자 : 클래스의 객체를 생성하고 초기화하는 역할을 한다. new 연산자를 통해 호출되며 반환 타입이 없다.
    • 이너 클래스 : 클래스 내부에 존재하는 또 다른 클래스를 의미하고 중첩 클래스라고도 한다. 클래스나 인터페이스 내부에서 선언하고 외부 클래스의 멤버들에 접근할 수 있다.

객체의 속성과 행위(필드와 메서드)

예를 들어 자동차 한 대를 하나의 객체라고 바라본다면 객체의 속성과 행위는 다음과 같이 정의할 수 있음

  • 속성 : 모델, 문의 개수, 바퀴의 개수, 색상, ...  등
  • 행위 : 시동 걸기, 전진, 후진, 정지, ... 등
Class Car {
    // 필드
    String model; // 모델명(속성)
    int wheels; // 자동차 바퀴 개수(속성)
    int doors; // 자동차 문의 개수(속성)
    String color; // 자동차 색상(속성)

    void powerOn() { // 메서드
        // 전원을 켜는 행위
    }
    void accelerate() { // 메서드
        // 앞으로 가는 행위
    }
    void backwoards() { // 메서드
        // 뒤로 가는 행위
    }
    void stop() { // 메서드
        // 정지 행위
    }
}
  • 각 필드에는 알맞은 데이터 타입을 선언하여 자동차의 속성에 알맞도록 정의한다, 메서드를 통해 자동차가 실제 할 수 있는 행위를 코드로 작성한다.

객체의 생성

객체의 생성은 new  키워드를 통해 생성이 가능하다, 생성된 객체는 포인트 연산자(.)를 통해 해당 객체의 멤버에 접근이 가능하다.

이렇게 생성된 객체들은 인스턴스라 하며, 인스턴스는 여러 개 생성될 수 있다.

클래스이름 참조변수이름 = new 생성자(); // 객체의 생성, 생성된 객체는 인스턴스

class CarInstanceCreate {
    public static void main(String args) {

        Car tesla1 = new Car(); // Car 클래스를 기반으로 생성된 tesla1 인스턴스
        Car tesla2 = new Car(); // Car 클래스를 기반으로 생성된 tesla2 인스턴스

        Car benz1 = new Car(); // Car 클래스를 기반으로 생성된 benz1 인스턴스
        Car benz2 = new Car(); // Car 클래스를 기반으로 생성된 benz2 인스턴스

        Car genesis1 = new Car(); // Car 클래스를 기반으로 생성된 genesis1 인스턴스
        Car genesis2 = new Car(); // Car 클래스를 기반으로 생성된 genesis2 인스턴스

    }
}
  • 객체를 생성한 변수에는 실제 저장된 데이터의 주소 값을 가지고 있음, 생성한 객체의 실제 데이터는주소가 가리키는 힙 메모리 공간에 저장되어 있다.

객체의 활용

생성된 객체를 사용하는 방법은 포인터 연산자(.)를 사용하여 객체에 접근하는 것임

tesla1.powerOn();

 

기술면접 질문/답안

클래스와 인스턴스의 차이에 대해 설명해주실 수 있을까요?

클래스는 객체를 정의하는 템플릿 또는 설계도입니다. 속성(변수)과 행위(메서드)를 정의하여 객체의 기본 형태와 기능을 설명합니다. 예를 들어, '자동차' 클래스에는 색상, 브랜드, 모델 등의 속성과 가속하기, 정지하기 등의 행위가 정의될 수 있습니다. 클래스는 실제 데이터나 상태를 저장하지 않으며, 객체의 구조와 행동만을 정의합니다.

인스턴스는 클래스에 정의된 구조와 행동을 실제로 가지는 구체적인 객체입니다. 클래스를 기반으로 메모리에 할당되며, 실제 데이터와 상태를 포함합니다. 클래스를 사용하여 인스턴스를 생성하면, 그 인스턴스는 해당 클래스의 모든 속성과 행위를 상속받아 사용할 수 있습니다. 예를 들어, '자동차' 클래스의 인스턴스는 특정 색상, 브랜드, 모델을 가진 실제 자동차를 나타냅니다.

클래스와 인스턴스의 차이점은, 클래스는 객체의 추상적인 정의를 제공하는 반면, 인스턴스는 그 정의를 바탕으로 생성 된 실체라는 점입니다. 클래스는 설계도에 비유되고, 인스턴스는 그 설계도로 구축된 실제 건물에 비유될 수 있습니다. 클래스는 정적인 개념이며, 인스턴스는 동적인 실체입니다.

 

'항해 99 > Java' 카테고리의 다른 글

Array & LinkedList  (0) 2024.05.03
Java - Garbage Collector, Java Map  (1) 2024.04.08
Java - SOLID  (0) 2024.03.12
WIL-4  (0) 2024.03.03
객체지향 생활체조 9가지 원칙  (1) 2024.03.01