들어가기에 앞서
• 왜 JVM을 공부해야 할까?
◦ 메모리 구조를 알아야 메모리 관리가 잘 되는 코드를 작성할 수 있기 때문
• 왜 메모리 관리를 해야 하는가?
◦ 메모리 관리에 따라 프로그램의 성능은 월등하게 차이날 수 있다.
◦ 메모리 부족으로 인한 성능저하 현상은 대부분 메모리에 대한 이해없이 코드를 설계한 경우 발생한다.
JVM의 개념과 특징
• Java는 특정 운영체제에 종속되지 않도록 JVM이라는 가상머신 위에서 실행되게끔 만들어진 언어
◦ 운영체제가 바뀌면? Java 코드는 그대로, JVM 만 다른것을 사용하면 OK
◦ 리눅스 환경에서 만든 자바 파일을 윈도우에서 실행하고 싶다면 윈도우용 JVM을 설치하면 된다.
• JVM(Java Virtual Machine) 자바와 운영체제 사이에서 중재자 역할을 수행하는 가상 머신
◦ 자바가 운영체제에 구애 받지 않고 프로그램을 실행할 수 있도록 도와준다.
◦ 자동으로 메모리 관리를 해준다. (GC)
◦ 다른 하드웨어와 달리 레지스터 기반이 아닌 스택 기반으로 동작한다는 점이 특징
.java 프로그램이 실행되는 과정
• 자바 컴파일러에 의해 자바 소스 파일이 바이트 코드 형태의 클래스 파일로 변환된다.
• 클래스 로더가 해당 클래스 파일을 읽어들여 JVM 내에 객체를 로드하고 배치한다.
JVM 구성 요소
• Class Loader
◦ 컴파일 결과로 만들어진 .class 바이트코드 파일을 읽어들여 메모리에 배치
◦ 로딩, 링크, 초기화 세 가지 과정을 거친다.
- 런타임 시에 동적으로 클래스를 로드한다.
• Execution Engine
◦ 클래스 로더를 통해 Runtime Data Area에 배치된 바이트 코드들을 명령어 단위로 읽어서 실행한다.
◦ 초기 JVM은 인터프리터 방식이었기 때문에 속도가 느리다는 단점이 있었지만,
바이트 코드를 어셈블러 같은 네이티브 코드로 바꿔주는 JIT 컴파일러 방식을 통해 이를 보완했다.
- 그러나 JIT를 통해 코드를 변환하는데에도 비용이 발생함
- 때문에 JVM은 모든 코드를 JIT 컴파일러 방식으로 실행하지 않고,
인터프리터 방식을 사용하다가 일정 기준이 넘어가면 JIT 컴파일러 방식으로 실행한다.
• Garbage Collector
◦ 힙 메모리 영역에 저장된 객체들 중 더이상 참조되지 않는 객체들을 탐색 및 제거하는 역할을 수행
◦ GC 동작하는 정확한 시간은 예측할 수 없다.
• Runtime Data Area
◦ JVM 메모리
◦ Java 어플리케이션이 실행되는 와중에 할당받은 메모리영역이다.
◦ Runtime Data Area 의 주요 영역
Method area (Meta Space)
• 모든 쓰레드가 공유하는 메모리 영역
• 클래스, 인터페이스, 메소드, 필드, Static 변수 등의 바이트 코드를 보관
Heap area
• 모든 쓰레드가 공유하는 메모리 영역
• new 키워드로 생성된 객체와 배열이 생성되는 영역
• Method area에 로드된 클래스만 생성 가능
• Garbage Collector가 작동하는 영역
Stack area (Thread Stack)
• Method를 호출할 때 마다 각각의 스택 프레임(그 메서드만을 위한 공간)이 생성된다.
• Method의 parameter, 반환값, 지역변수등을 저장하는 용도로 사용된다.
• Method의 동작이 끝나면 해당 스택 프레임은 삭제된다.
Code Cache
• JIT 컴파일러가 데이터를 저장하는 영역
• 자주 접근하는 '컴파일된 코드 블록'이 저장된다.
• 일반적으로 JVM은 바이트 코드를 기계어로 변환하는 작업을 수행하는데,
이곳에 저장된 코드는 기계어로 이미 변환된 채 캐시되어 있으므로 빠르게 실행할 수 있다.
Shared Library
• 애플리케이션에서 사용할 공유 라이브러리가 기계어로 변환되어 저장되는 영역
• OS에서 프로세스당 한 번씩 로드된다.
PC Register
• 쓰레드가 시작될 때 생성되는 메모리로, 쓰레드마다 PC Register가 하나씩 존재한다.
• 쓰레드가 어떤 부분을 무슨 명령으로 실행해야할 지에 대한 정보를 기록하는 부분
• 현재 수행중인 JVM 명령의 주소를 가진다.
Native method stack
• Java 외의 언어(C/C++)로 작성된 네이티브 코드를 위한 메모리 영역
◦ Java Native Interface를 이용하여 네이티브 코드 작성 가능
◦ 네이티브 코드를 사용하는 이유
- 하나의 Java 파일은 해당 운영체제 환경에 상관없이 똑같은 결과를 반환해야 한다.
(운영체제에 따라 서로 다른 JVM이 존재하기 때문에 이것이 가능)
- 단점 : 운영체제의 모든 기능을 JVM이 담지 못한다
→ 리눅스에서만 돌아가는 코드를 허용하면 윈도우즈에서는 안돌아가는 상황 발생
- 따라서 운영체제의 고유한 일부 기능은 Java 언어만으로 구현이 불가능하다.
Class Loader의 개념과 동작 원리
• 자바는 동적 로드, 즉 컴파일 타임이 아니라 런타임(바이트 코드를 실행할 때)에 클래스 로드하고 링크하는 특징이 있다.
• 이 동적 로드를 담당하는 부분이 JVM의 클래스 로더
• 로딩, 링크, 초기화 단계로 나누어진다.
◦ 로딩
- 자바 바이트 코드(.class)를 메소드 영역에 저장한다.
- 각 자바 바이트 코드(.class)는 JVM에 의해 메소드 영역에 다음 정보들을 저장한다.
1) 로드된 클래스를 비롯한 그의 부모 클래스의 정보
2) 클래스 파일과 Class, Interface, Enum의 관련 여부
3) 변수나 메소드 등의 정보
- 로딩이 끝나면, 해당 클래스 타입의 class 객체를 생성해 Heap 영역에 저장한다.
→ 객체이름.class 또는 인스턴스의 getClass() 형태로 호출했을 때 리턴되는 값을 말함
◦ 링크
- Verfiy : 읽어 들인 클래스가 자바 언어 명세 및 JVM 명세에 명시된 대로 잘 구성되어 있는지 검사
- Prepare : 클래스가 필요로 하는 메모리를 할당하고, 클래스에서 정의된 필드, 메소드, 인터페이스를 나타내는 데이터 구조를 준비
- Resolve : Symbolic Memory Reference 를 Method Area에 있는 실제 Reference로 교체
→ Book book = new Book(); 이라는 코드가 있다고 할 때, book이라는 참조변수가 Heap에 저장된 실제 Book 클래스를
가리킬 수 있도록 연결하는 과정이 바로 Resolve 과정이다.
◦ 초기화
- static 필드들을 설정된 값으로 초기화된다.
- static으로 선언된 변수와 메소드에 메모리를 할당하고 초기값을 채우는 과정.
static final String name = "staticName";
클래스 로더의 동작
• 클래스 로더는 새로운 클래스를 로드해야 할 때, 위와 같은 방식으로 로드를 수행한다
부트스트랩 클래스 로더 (Bootstrap Class Loader)
• JVM 시작 시 가장 최초로 실행되는 클래스 로더
• 자바 자체의 클래스 로더와 최소한의 자바 클래스(java.lang.Object, Class, ClassLoader)만을 로드한다.
확장 클래스 로더 (Extension Class Loader)
• 부트스트랩 클래스 로더를 부모로 갖는 클래스 로더
• 확장 자바 클래스들을 로드한다. java.ext.dirs 환경 변수에 설정된 디렉토리의 클래스 파일을 로드하고,
이 값이 설정되어 있지 않은 경우 ${JAVA_HOME}/jre/lib/ext 에 있는 클래스 파일을 로드한다.
시스템 클래스 로더 (System Class Loader)
• 자바 프로그램 실행 시 지정한 Classpath에 있는 클래스 파일 혹은 jar 에 속한 클래스들을 로드
• 작성자가 직접 만든 .class 확장자 파일을 로드한다.
클래스 로더의 동작 과정
• JVM의 메소드 영역에 클래스가 로드되어 있는지 확인한다. 만일 로드되어 있는 경우 해당 클래스를 사용
• 메소드 영역에 클래스가 로드되어 있지 않을 경우, 시스템 클래스 로더에 클래스 로드를 요청
• 시스템 클래스 로더는 확장 클래스 로더에 요청을 위임
• 확장 클래스 로더는 부트스트랩 클래스 로더에 요청을 위임
• 부트스트랩 클래스 로더는 부트스트랩 Classpath(JDK/JRE/LIB)에 해당 클래스가 있는지 확인.
클래스가 존재하지 않는 경우 확장 클래스 로더에게 요청을 넘긴다.
• 확장 클래스 로더는 확장 Classpath(JDK/JRE/LIB/EXT)에 해당 클래스가 있는지 확인한다.
클래스가 존재하지 않을 경우 시스템 클래스 로더에게 요청을 넘긴다.
• 시스템 클래스 로더는 시스템 Classpath에 해당 클래스가 있는지 확인한다.
클래스가 존재하지 않는 경우 ClassNotFoundException을 발생시킨다.
동적 클래스 로딩
• 자바의 클래스 로딩은 클래스 참조 시점에 JVM에 코드가 링크되고, 실제 런타임 시점에 로딩되는 동적 로딩을 거친다.
• 런타임에 동적으로 클래스를 로딩한다는 것은, JVM이 미리 모든 클래스에 대한 정보를 메소드 영역에 로딩하지 않는다는 것을 의미
• 로드 타임 동적 로딩 (Load-time Dynamic Loading)
public class HelloWorld {
public static void main(String[] args) {
System.out.println("안녕하세요!");
}
}
◦ JVM이 시작되고 부트스트랩 클래스 로더가 생성된 후에 모든 클래스가 상속받고 있는 Object 클래스를 읽어온다.
◦ 클래스 로더는 명령 행에서 지정한 HelloWorld 클래스를 로딩하기 위해, HelloWorld.class 파일을 읽는다.
◦ HelloWorld 클래스를 로딩하는 과정에서 필요한 클래스인 java.lang.String과 java.lang.System을 로딩한다.
◦ 이처럼 하나의 클래스를 로딩하는 과정에서 동적으로 다른 클래스를 로딩하는 것 : 로드 타임 동적 로딩
• 런타임 동적 로딩 (Run-time Dynamic Loading)
public class RuntimeLoading {
public static void main(String[] args) {
try {
Class cls = Class.forName(args[0]);
Object obj = cls.newInstance();
Runnable r = (Runnable) obj;
r.run();
} catch (Exception e) {
e.printStackTrace();
}
}
}
◦ Class.forName() 메소드가 실행되기 전까지는 RuntimeLoading 클래스에서 어떤 클래스를 참조하는지 알 수 없다.
◦ 따라서 RuntimeLoading 클래스를 로딩할 때는 어떤 클래스도 읽어오지 않고, RuntimeLoading 클래스의 main() 메소드가
실행되고 Class.forName(args[0]) 를 호출하는 순간에 비로소 args[0] 에 해당하는 클래스를 로딩한다.
◦ 클래스를 로딩할 때가 아닌, 코드를 실행하는 순간에 클래스를 로딩하는 것 : 런타임 동적 로딩
클래스가 로딩되지 않는 경우
• 클래스에 접근하지 않을 때
• 클래스의 정적 변수 사용 (final 키워드)
Java 메모리 할당 방식
public static void main(String[] args) {
int number = 12;
Mydata mydata = new Mydata(15);
}
• JVM은 기본적으로 Stack과 Heap 두가지 저장 공간에 메모리를 할당한다.
• 간단한 변수인 Primitive Type의 변수들은 Stack에 저장이 가능하지만, 복잡한 변수는 Heap 공간에 저장되고
Stack에는 해당 Heap 공간을 가리키는 변수가 저장된다.
◦ JVM은 코드 한 줄 씩 읽으며 객체를 메모리에 할당한다.
◦ 읽어들인 줄에 저장해야 할 객체가 있을때, 객체가 Primitive Type이면 값 자체가 Stack에 저장된다.
◦ 그 외는 Stack에는 객체의 주소값만 저장되고, 실제 값은 Heap 영역에 저장된다.
call by value 와 메모리 할당
• Java는 call by reference가 존재하지 않는다.
• call by reference 같은 call by value 만 존재할 뿐
• call by reference 같은 call by value = 주소값을 복사하여 가져가는 call by value
◦ 실제로 해당 주소를 가리키고 있는 것이 아니다!
◦ 단지 주소값을 복사해서 가지고 있을 뿐이다.
◦ 하나의 공간을 여러 객체가 동시에 가리키고 있을 수 없다!
◦ 단지 한 공간의 주소를 여러 객체가 동시에 가지고 있을 수 있을 뿐이다.
• 참조하고 있는 값을 단순히 바꿀수도 있고, 실제로 가리키고 있는 주소의 값을 바꿀수도 있다.
◦ 개발자가 선택적으로 수행할 수 있음 (신기하네)
• arg2 = arg1 을 수행해도 이는 run 메서드 내에 존재하는 arg2가 arg1이 가진 주소값을 복사하여 저장하는 것일 뿐,
원본 a2와는 독립된 변수이기 때문에 원본 a2는 변경되지 않는다.
public static void main(String args[]) {
class A {
public int value;
A(int value) { this.value = value; }
void run(int x, int y) {
x.value = 111; // 주소에 저장된 값이 달라짐
x = y; // 주소에 저장된 값에는 영향 X
}
}
A a1 = new A(1);
A a2 = new A(2);
run(a1,a2); // a1 = 111, a2 = 2
}
#include<iostream>
using namespace std;
class A {
public:
int value;
A(int value) { this->value = value; }
};
void run(A* arg1, A* arg2) {
arg1->value = 111;
*arg2 = *arg1;
};
int main() {
A* a1 = &A(1);
A* a2 = &A(2);
run(a1, a2);
cout << a1->value; // 111
cout << a2->value; // 111
system("pause");
return 0;
}
참고자료
'Java' 카테고리의 다른 글
[Effective Java] ITEM9 : try-finally 보다는 try-with-resources를 사용하라 (0) | 2022.07.24 |
---|---|
[Effective Java] ITEM7 : 다 쓴 객체 참조를 해제하라 (0) | 2022.07.24 |
[Effective Java] ITEM5 : 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2022.07.24 |
[Effective Java] ITEM3 : private 생성자 혹은 enum 타입으로 싱글톤임을 보증하라 (0) | 2022.07.23 |
[JAVA] GC(Garbage Collection) 톺아보기 (0) | 2022.07.10 |