[Java] JVM 과 static 메모리 이해하기

안녕하세요

오늘은 자바가 어떻게 동작하는지 랑 JVM 관점에서 Static이 어떻게 작동하는지 알아보겠습니다.

 

자바에서 클래스를 실행시키기 위해서는 메인메소드가 필요합니다.

그러면 메인 메소드는 무엇일까요?

public static void main(String[] args) { } 를 메인 메소드라고 부릅니다.

 

정확하게 말하면 스프링 환경이 아닌, 일반 자바를 다루는 환경에서 클래스의 어떠한 값을 콘솔에 출력하기 위해서

꼭 있어야 하는 게 바로 메인메소드 입니다.

public class StaticTest {
	public static void main(String[] args) {
		System.out.println("Hello World");
	}
}

 

위 값이 없다면 출력을 할 수가 없죠.

 

그리고 값,메소드 등을 출력하기 위해선 메인 메소드 안에서

객체 생성을 통해서 출력을 할 수 있습니다.

 

그러면 왜 메인 메소드 안에서만 값을 출력할 수 있을까요?

 

일단 이 동작 과정을 알기 위해선

Java가 동작하는 원리를 간단하게 알고 있어야 합니다.

 

즉 Java가 실행되는 환경인 JVM을 동작 구조를 알고 있어야 합니다.

JVM을 동작 구조를 알고 있어야 JVM이 static을 처리하는 방식을 알 수 있기 때문입니다.

 

JVM 동작구조

JVM은 Java JDK를 깔면 그 안에 기본적으로 내장이 되어있습니다

 

JVM의 특징은 운영체제에 상관 없이 코드가 작동을 합니다.

아래 사진은 JVM을 동작 구조를 간단하게 설명한 사진 입니다.

https://github.com/devFancy/2023-CS-Study/blob/main/java/java_jvm_architecture.md

 

  • 자바로 개발된 프로그램 실행시 JVM은 OS로부터 메모리를 할당받습니다.
    • 메모리는 맥이든, Window, Linux 상관 없습니다.
  • 자바 컴파일러(Javac=JavaCompiler) 가 자바 소스코드(= .java) 파일을 자바 바이트 코드(= .class)로 컴파일을 시킵니다 ( .java → .class)
  • Class Loader를 통해 JVM Runtime Data Area로 로딩합니다.
  • 변환된 바이트 코드는 Runtime Data Area에서 각 영역에 맞게 배치되어 수행이 됩니다.
    • 이 과정에서 Execution Engine에 의해 가비지 컬렉션 작동 및 스레드 동기화가 이루어집니다.

이정도로 설명을 할 수 있습니다.

 

그러므로 면접에서 면접관님이 JVM 동작구조를 물어보면 위 과정을 대답을 하셔야 합니다.

 

❓그렇다면 컴파일은 무엇일까요❓

간단하게 설명을 하면 원시코드를 컴퓨터가 이해할 수 있는 코드(바이트 코드)로 바꾸어주는 것입니다.

 

컴퓨터는 0과1만 이해할 수 있기 때문에 내가 작성한 자바 코드를 바이트 코드(=기계어 코드, 어셈블리어) 로 바꾸어주는 과정을 컴파일 이라고 하는 것 입니다.

https://velog.io/@jjd/%EC%9E%90%EB%B0%94%EC%9D%98-%EC%BB%B4%ED%8C%8C%EC%9D%BC-%EA%B3%BC%EC%A0%95

 

간단하게 다시 설명을 하면

  1. 내가 작성한 코드를 자바 컴파일러가 JVM이 이해할 수 있도록 바이트 코드로 변환한다
  2. 컴파일된 바이트 코드를 JVM내부의 클래스 로더가 가져와 동적 로딩을 통해 JVM메모리 영역에 적재한다.
  • 동적 로딩 : 프로그램이 실행 중에 클래스를 로드하는 프로세스이다.
    • ex) 특정 요청이 들어올 때 필요한 클래스만 동적으로 로드해서 처리한다.
  1. JVM메모리 영역에서 실행엔진을 통해 실행한다.

 

❓그렇다면 클래스 로더는 무엇일까요❓

 

기본적으로 자바는 클래스 들로 이루어져 있습니다.

클래스 로더는 말 그대로, 클래스를 읽어오는 역할을 합니다.

 

즉 바이트 코드로 변환된 소스를, JVM메모리 영역인, Runtime Data Area로 적재를 합니다.

클래스 로더의 동작구조는 간단하게

Runtime 시 → load(적재) → links(연결) → initialize(초기화) 3가지 과정을 거쳐서 동작을 합니다.

자세한건 검색해보시면 알 수 있습니다..

 

❓그러면 JVM 메모리에는 무엇이 있을까요❓

앞서 자꾸 Runtime Data Areas 가 나왔습니다. 위 단어가 JVM의 메모리 구조 입니다.

JVM에 의해 프로그램이 실행될 때, 운영체제로부터 할당받은 메모리의 영역 입니다.

쉽게, 자바 프로그램이 실행될 때 사용되는 데이터들을 담는 공간 입니다.

아래 내용을 읽다보면, 또 나오는 내용입니다. 그만큼 중요하다는 거겠죠?

 

✅ Method Area

JVM이 동작하고, 클래스가 로드될 때 적재되서 프로그램이 종료될 때 까지 저장됩니다.

클래스 이름, 메소드, 변수를 저장합니다.

 

→ static-zone, none static-zone으로 나누어 집니다.

 

다른 Thread 사이에서 공유되는 자원 입니다.

Thread의 특징중 하나가 Process와 다르게 자원을 공유하는 특징이 있습니다.

그런 면에서 메소드 또한 공유 된다고 생각하면 될 것 같습니다.

 

✅ Heap Area

https://github.com/devFancy/2023-CS-Study/blob/main/java/java_jvm_architecture.md

 

→ 실제 객체가 생성되는 메모리 공간 입니다.(Ex, 배열,객체)

→ GC에 의해 메모리가 관리 됩니다.

 

위 사진은 Heap Area을 효율적인 GC를 위해 크게 3가지의 영역으로 나눈 것입니다.

  • Young Generation : 영역은 자바 객체가 생성되자마자 저장되고, 생긴지 얼마 안되는 객체가 저장됨
    • Heap 영역에 객체가 생성되며 최초로 Eden 영역에 할당된다.
    • 그리고 이 영역에 데이터가 어느정도 쌓이게 되면 참조정도에 따라 Service의 빈 공간으로 이동되거나 회수된다.
    • Young Generation 영역이 차게 되면 참조 정도에 따라 Tenured Generation(Old) 영역으로 이동되거나 회수됨Young Generation, Tenured Generation에서 GC를 Minor GC라고 한다.
  • Tenured Generation : Old 영역에 있는 모든 객체들을 검사하여 참조되지 않는 객체들을 한번에 삭제한다.
    • 시간이 오래 걸리는 작업이고 이때 GC를 실행하는 스레드를 제외한 모든 스레드는 작업을 멈추게 됨(Stop-the-World)Old영역의 메모리를 회수하는 GC를 Major GC라고 한다.

지금은 그냥 한번 쭉 읽어보고 넘어가면 될 것 같습니다. 나중에 깊게 공부해보고 싶을 때 한번 다른 내용을 찾아보는게 좋을 것 같습니다!

 

✅ Stack Area

→ stack은 Thread가 하나 만들어져서 실행이 된다.

→ 지역변수, 매개변수들이 만들어지는 공간이다.

→ LIFO 구조로 운영이 되는 메모리 공간이다.

→ 원통형 구조로 볼 수 있다. 계단식으로 하나씩 쌓인다. 맨위에 있는 것이 제일 먼저 빠져나간다.

→ 메소드의 호출 순서를 알 수 있다.

 

✅ Runtime Constant pool

→ Litreal Pool → 문자열 상수가 저장되는 공간이다 ex) String이 저장이됨

→ 상수 값 할당이 되는 메모리 공간입니다.

 

✅ PC 레지스터

→ Thread가 생성될 때마다 생성되는 영역이다. 프로그램 카운터로 현재 스레드가 실행되는 부분의 주소와 명령을 저장하고 있는 영역 입니다.

 

❔Gc 란❔

 

→ Heap 메모리 영역에서 사용하지 않는 메모리들을 자동으로 회수해 준다

→ 참조되지 않은 객체를 찾아서 제거한다

→ GC 역할을 수행하는 스레드를 제외한 나머지 모든 스레드들은 일시정지 상태가 된다.

 

모든 스레드가 공유해서 사용(가비지 컬렉션의 대상)

  • Hear Area
  • Method Area

스레드 마다 하나씩 생성

  • Stack Area
  • PC Register
  • Native Method Stack

이제 본론으로 넘어가 JVM이 Static을 처리하는 방식을 알아 보겠습니다.

 

1) JVM이 실행할 클래스를 찾습니다.

 

그럼 어떻게 실행할 클래스를 찾는지 궁금해 할 수 있습니다.

바로 main() 메소드를 찾아 시작점으로 삼습니다. 이 메소드가 포함된 클래스가 실행의 출발점 입니다.

public static void main(String[] args)

 

그리고 위 메인 메소드( =main() )에서

static 키워드가 붙어있는 멤버들을 **정해진 메모리 위치(static-zone)**에 한번 자동으로 로딩 한다.

 

→ static 멤버들은 클래스를 사용하는 시점에서 딱 한 번 메모리에 로딩된다는 점이 중요하다

→ 여기서는 main() 메서드가 static 이기 때문에 메모리에 자동으로 로딩한다.

 

이 말이 무슨 말이냐면, 다른 객체들은 new 를 사용해서 객체 생성을 해야지

메모리에 객체가 할당되고, 주소 값이 생깁니다.

 

그러나 static 메소드는 객체를 생성하는 그 과정이 필요가 없이

객체가 자동으로 JVM에 올라간다는 것 입니다.

 

static-zone은 static이 들어있는 애들끼리 만 모여있는 메모리 영역을 말합니다.

크게 분류를 한다면 JVM에서 영역은 static-zone 과 None-static-zone 두 개로 구분을 할 수 있습니다.

 

그렇다면 static 메소드를 쓰면은 객체 생성을 안하고 사용할 수 있으니

덜 귀찮고 개꿀 아닌가요??? 라고 생각할 수 있습니다.

 

하지만 static 메소드를 남용하면은 OOP에서 캡슐화를 어긋난다고 볼 수 있습니다.

언제나 가져와서 사용할 수 있기 때문에, 객체의 상태를 고려하지 않기 때문에 나중에

유지보수와 테스트에서 어려움을 겪을 수 있습니다.

 

2) JVM이 static-zone에서 main() 메소드를 호출한다

  • JVM이 static-zone에 가서 메소드를 호출을 합니다

 

3) 호출된 메소드를 Stack Area에 push 한 뒤 동작

  • 호출된 메인 메소드가 stack영역에 들어간다.
    • 최종적으로 stack영역에서 실행이 된다.

간단하게 static이 붙은 메소드들은 위 동작 과정으로 실행이 됩니다.

전체적인 흐름은 이렇습니다.

한번 코드를 보면서 이해를 도와보겠습니다.

public class StaticTest {
    public static void main(String[] args) {
        int a = 10;
        int b = 20;
        int sum = StaticTest.hap(a,b);
        System.out.println(sum);
    }
    public static int hap(int a, int b) {
        int v = a+b;
        return v;
    }
}

 

  • 프로그램 실행을 시킨다
  • JVM main() 메소드를 찾아 시작점을 설정한다.
    • 그리고 다음으로 static이 붙은 메소드를 찾는다 → hap() 메소드 찾음

  • 메소드 area에 static 메소드들이 올라간다 
  • 그 다음으로 main() 메소드가 호출이 된다.
    • 그러면 Stack Area로 main메소드가 push가 됩니다.
  • 그리고 push된 메소드는 stack area에서 각 주소를 가지는 영역을 가지게 됩니다.

 

그리고 각 메서드 안에 변수의 값이 할당되어있는데

그것들을 바로 지역 변수라고 합니다.

 

그 지역 안에서만 값이 할당이 되어있고 다른 지역에서는 전혀 다른 값을 가질 수 있습니다.

Stack 에서는 현재 프로그램이 실행되고 있는 상태를 파악할 수 있습니다.

 

Stack(=LIFO) 에 아무것도 없으면 프로그램이 종료가 됩니다.

이렇게 main() 및 static 동작 구조를 알아갈 수 있습니다.

 

그러면 static은 위 방법처럼 접근을 하는데

none-static 메소드는 어떻게 접근을 할까요?

 

일단 static처럼 객체에 바로 접근을 할 수 없으니, 객체를 생성을 해야합니다.

public class NoneStaticTest {
    public static void main(String[] args) {
        NoneStaticTest noneStaticTest = new NoneStaticTest();
        int a = 10;
        int b = 20;
        int sum = noneStaticTest.hap(a,b);
        System.out.println(sum);
    }
    public int hap(int a, int b) {
        return a+b;
    }
}

위 코드를 바탕으로 설명을 해보겠습니다.

 

1) 일단 클래스를 실행시킵니다

         → 실행 시키면 main() 메소드가 static-zone에 올라갑니다.

  • main()은 static-zone에 저장됨
  • hap()은 static이 없으니 none-static zone에 저장이 됩니다.

2) 그리고 실행 후 static이 없는 메소드는 Heap 영역에 생성이 됩니다.

 

static → Stack 영역

none static → Heap 영역

 

즉 객체가 생성(=new)되는 메모리 공간을 heap memory 라고 합니다.

그리고 none-static 메소드가 실행이 되고

메인 메소드가 더 이상 호출할게 없으니 종료가 됩니다.

 

그러면 이번에는 실무에서는 static 메소드를 어떻게 사용하는지를 알아보기 위해

2개의 메소드로 예시를 들어보겠습니다.

public class MyUtil {
    public static int hap(int a, int b) {
        int v = a+b;
        return v;
    }
}

public class StaticAccess {
    public static void main(String[] args) {
        int a = 10;
        int b = 20;
        int sum = MyUtil.hap(a,b);
        System.out.println(sum);
    }
}

 

📌 static 멤버 접근 방법

클래스이름.호출메소드

 

static 멤버는 클래스를 사용하는 시점에 자동으로 static-zone에 로딩이 됩니다.

따라서 new를 이용해서 객체를 생성할 필요가 없습니다.

 

static-zone에서 Stack영역으로 push가 되어 보관이 됩니다.

 

그러면 none-static 메소드는 어떻게 접근하나요?

평소 하던대로 new로 객체를 생성해서 메소드에 접근할 수 있습니다

public class MyUtil {
    public int hap(int a, int b) {
        int v = a+b;
        return v;
    }
}

public class StaticAccess {
    public static void main(String[] args) {
			MyUtil myutil = new MyUtil();
        int a = 10;
        int b = 20;
        int sum = myutil.hap(a,b);
        System.out.println(sum);
    }
}

위 방법을 사용해서 객체을 메소드에 접근 할 수 있습니다.

 

다음으로 제가 궁금했던 내용에 대해서 알아보겠습니다.

 

1) 만약에 객체를 생성할 수 없는 상황이라면?

 

객체를 생성할 수 없는 상황이라하면은, 생성자의 접근제어자가 public이 아닌, private, protected 둘 중 하나 일 경우 입니다.

 

자바에서 제공하는 API에는 private 생성자를 가지고 있는 클래스도 있습니다.

ex) System, Math 등등

System system = new System();

Error : 'System()' has private access in 'java.lang.System'

Math math  = new Math();

private Math() {}

 

Math API를 들어가보면 안에 생성자가 Math로 되어 있습니다.

→ 생성자는 반드시 public 이다?? 아님 니다 대부분이 그럴수는 있겠지만

객체에 접근을 못하게 하기 위해 의도적으로 private, protected로 접근을 막을 수 있습니다.

 

한번 코드로 보겠습니다.

public class AllStatic {

  private AllStatic() {
  }
  
  public static Integer getSum(int a, int b) {
    return a+b;
  }
  
  public static Integer getMinus(int a, int b) {
    return a-b;
  }
  
  public static Integer getMultiple(int a, int b) {
    return a*b;
  }
}

public class AllStaticTest {

  public static void main(String[] args) {
    System.out.println(AllStatic.getSum(10,20));
    System.out.println(AllStatic.getMinus(10,20));
    System.out.println(AllStatic.getMultiple(10,20));

  }
}

생성자를 private으로 만듬으로써 객체를 생성을 못하게 막아 둔 예시 입니다. 

결국 객체 생성을 못하는 클래스 이므로, static을 통해서 접근하겠다는걸 표시하는 느낌인 것 같습니다.

만약에 코드를 읽다가 생성자가 private,protected가 보이면 static이 있나 없나를 바로 찾아봐야 할 것입니다.

 

2) 그러면 static을 쓰면 객체 생성없이 메소드에 바로 접근할 수 있으니 간편하게 개꿀 아닌가요?

 

정답은 No 에 가깝습니다. (무조건 No는 아닙니다. 상황에 알맞게 사용해야 합니다)

 

No 일 것이라고 생각하나요?

 

static은 고정된 메모리 영역을 사용하기 때문에, 매번 인스턴스를 생성하며 낭비되는 메모리를 줄일 수 있다는 장점이 있습니다.

 

하지만 위 장점을 제외하고 단점이 더 많습니다

 

1) 프로그램 종료시 까지 메모리에 할당된 채로 존재합니다

-> GC가 따로 관리해주지 않아, 정적 메소드를 사용을 하든 안하든 메모리에 계속 존재합니다.

 

2) static은 객체지향적이지 못합니다

-> 캡슐화를 지키지 못합니다

-> 따로 객체를 생성하지 않고, 클래스에 접근하기 때문에, 객체 데이터들이 캡슐화 되야하는 법칙을 위반합니다.

 

3) static 은 interface를 구현하는데 사용될 수 없다

-> 객체지향적이지 못한 코드가 된다

-> 재사용성이 적다는 단점이 있다

 

그러면 static 장점보다 단점이 많은데 사용하면 안되나요?

 

또 그것은 아닙니다. 무작정 static을 사용하지 않고, 적절한 상황에 맞게 사용한다는 표현이 맞는 것 같습니다.

실제 실무에서도 static 을 사용한 내부 정적 클래스 및 정적 메소드를 사용합니다.

 

무분별하게 사용이 편하다는 이유로 사용을 하는게 아닌, 꼭 필요한 상황에서만 사용하는 것을 지향합니다.

 

이상 포스팅 마치겠습니다. 

 

📚

ref: https://github.com/devSquad-study/2023-CS-Study/tree/main/java
728x90