-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
65f13be
commit 2e0b445
Showing
2 changed files
with
46 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
--- | ||
layout: post | ||
title: "JVM 밑바닥까지 파헤치기" | ||
author: 김연우 | ||
date: '2024-10-05' | ||
thumbnail: /assets/img/posts/jvmbook.jpeg | ||
keywords: 책, JVM, 스터디 | ||
--- | ||
|
||
요즘 회사에서 팀원분들이랑 같이 “JVM 밑바닥까지 파헤치기”라는 책을 읽고 있다. 아직 초반부지만, 내가 2부 2장 “자바 메모리 영역과 메모리 오버플로”의 사회자를 맡게 되어 평소보다 좀 더 자세하게 책의 내용을 정리할 기회가 있었는데, 이 부분을 블로그에도 정리해놓으려고 한다. | ||
|
||
꽤나 오랫동안 자바, 코틀린 등 JVM 환경에서 돌아가는 언어를 써왔음에도 JVM을 제대로 공부한 적이 거의 없는것 같다. 그만큼 JVM은 꽤나 잘 추상화된 영역이라 관련 지식 없이도 대부분의 역할 수행이 가능하기 때문에 이러한 지식의 중요성을 망각하기 쉬운것 같다. 하지만 때로는 이런 영역에 대한 무지함이 큰 리소스 낭비나 주기적인 시스템 크래시를 유발하기도 하며, 회사 생활하면서도 심심치 않게 CPU / 메모리 사용량과 같은 리소스 이슈로 SRE (Software Reliability Engineer) 분들이 서비스에 문제가 없는지 확인 요청을 하시기도 한다. 그럴때 당황하지 않고 SRE분들과 원활히 소통하며 문제를 해결해나가고, 더 나아가 평상시에 좀 더 안전한 코드를 작성하기 위해서도 JVM에 대한 지식은 필수라는 사실을 나도 공부하며 다시금 통감했다. | ||
|
||
### JVM 런타임 데이터 영역 | ||
2장에선 먼저 자바 가상머신 명세에 나와있는 JVM의 메모리 구조에 대해 다룬다. 읽으면서 다시한번 느낀건, JVM 메모리 구조는 C 프로그램과 같은 네이티브 프로세스의 메모리 구조를 많이 따르고 있으며, JVM은 어떤 의미로는 **중첩되어 있는 구조**를 가지고 있다고 표현할 수 있을거 같다. 여기서 하나씩 이러한 메모리 영역들을 살펴보겠다. | ||
|
||
#### 프로그램 카운터 | ||
JVM의 메모리영역은 크게 두종류로 나뉘는데, 하나는 쓰레드 로컬한 메모리 영역이고, 다른 하나는 쓰레드들이 공유하는 데이터 영역이다. 그 중 쓰레드 로컬한 메모리 영역부터 살펴보면, 먼저 프로그램 카운터가 있다. | ||
|
||
프로그램 카운터는 특정 쓰레드가 현재 어떤 라인의 바이트 코드를 실행하고 있는지를 나타내는 메모리 영역으로, 그 안에는 실행해야하는 바이트코드의 주소가 담겨있다. 쓰레드마다 실행하고 있는 코드가 다 다르므로, 당연히 쓰레드마다 있어야 하는 영역이라고 이해할 수 있다. 원래 프로그램 카운터라는 것은 OS 레벨에서는 메모리 영역이 아닌 특정 CPU 레지스터이다. OS가 프로세스를 실행할때, 마찬가지로 실행해야하는 기계어의 주소를 저장하기 위해 사용한다. 이 정보는 쓰레드의 컨텍스트에 같이 저장되어 있으며, 쓰레드간 context switching이 발생할때 같이 CPU 레지스터에 로드된다. | ||
|
||
그렇다면 자바 프로그램을 실행할때는 어떤식으로 동작하는걸까? 상상하기 힘들지만 추측해보자면, 아마도 자바 바이트코드를 실행할때는 CPU 레지스터에 해당하는 PC는 JVM 코드의 어딘가를 가리키고 있고, JVM내에서 관리되는 PC 데이터 영역이 바뀌면서 프로그램이 실행될 것이다. 하지만 JNI (Java Native Interface)등을 통해서 네이티브 코드를 호출했을 때는 더이상 PC 데이터 영역이 사용되지 않고, CPU 레지스터 PC를 사용하여 네이티브 코드를 순차 실행한 후, 바이트코드 상으로 돌아왔을때 다시 PC 데이터 영역이 사용되면서 프로그램 실행을 이어갈 것이라고 생각한다. 책에서도 `한편 스레드가 네이티브 메서드를 실행 중일때 프로그램 카운터 값은 Undefined다` 라고 되어있었으니 위와 같은 흐름이 맞지 않을까 싶다. | ||
|
||
#### 스택 | ||
자바가상머신 명세에 따르면 원래 스택은 가상머신 스택과 네이티브 메서드 스택으로 나뉘어 있지만, 우리가 많이 쓰는 핫스팟 가상머신은 이 둘을 합쳐서 구현하므로 나도 뭉뚱그려서 스택이라고 표현해보았다. 스택도 쓰레드 로컬한 메모리 영역 중 하나로, 쓰레드가 호출하는 메소드 콜을 저장하고 관리한다. 이 또한 우리가 익히 아는 콜 스택과 많이 닮아있는데, 아까 얘기했듯이 JVM이 프로세스로써 가지는 자체 콜스택과는 다른 개념으로 JVM이 생성하고 관리하는 메모리 영역이다. | ||
|
||
자바 프로그램에서 메서드가 호출될때마다 새로운 스택 프레임이 스택에 들어가고, 이 스택 프레임 안에는 지역 변수 테이블, 피연산자 스택, 동적 링크, 메서드 반환값 등이 들어있다. 이후 해당 메서드가 반환(return)될때 스택 프레임도 같이 pop되면서 reclaim되는 형태이다. 재밌는건 스택 프레임의 지역변수 테이블에 들어가는 변수들은 컴파일 타임에 크기를 정확하게 알 수 있기 때문에 이 데이터 공간의 크기는 컴파일 과정에 정해지고 런타임에 절대 변하지 않는것이다. 또한, 핫스팟 가상 머신의 경우 스택 용량의 동적 확장을 허용하지 않기 때문에 스택 용량이 설정한 용량을 넘어갔을때는 OOM이 아니라 StackOverflowError 가 난다. | ||
|
||
#### 힙 | ||
힙은 모든 쓰레드들이 공유하는 메모리 영역으로, 아마도 자바 개발자가 가장 많이 접하게 되는 메모리영역이 아닐까 싶다. 자바 힙은 생성된 객체가 관리되는 메모리 영역이며, GC (Garbage Collection) 대상이 되는 메모리 영역이기도 하다. 계속 얘기해왔던 구분은 여기에도 적용되는데, 자바 힙과 JVM이 프로세스로서 가지는 힙(네이티브 힙)은 distinct한 메모리 영역이다. | ||
|
||
옛날에는 “모든 객체는 힙에 저장된다”라는 말이 사실이었지만, 요즘은 탈출 분석 기법을 통해 특정 객체가 특정 메소드 안에서만 쓰이는 경우에는 해당 객체를 힙이 아닌 스택에 저장해서 힙 공간과 GC overhead를 아끼는 기술도 발전을 많이 했다고 한다. 또한 힙 공간은 세대별 GC 이론에 따라 에덴 공간, 생존자 공간, 구세대, 영구세대 등으로 나누기도 하는데, 이또한 오늘날에는 무조건 세대별 이론을 GC에 적용하지는 않는다고. (자세한건 뒷장에서 설명하는듯 하다.) | ||
|
||
#### 다이렉트 메모리와 네이티브 메모리 | ||
이 부분은 스터디하면서 많이 혼란을 겪었던 부분인데, 다시 명확하게 정리해보고자 한다. | ||
|
||
네이티브 메모리는 JVM 프로세스 주소 공간 (address space) 중 JVM에 의해 관리되는 메모리를 제외한 공간으로, 네이티브 힙 공간과 스택 공간 등이 모두 포함된다. 다시 말해 JVM 코드에서 malloc을 하는 등 시스템 콜을 통해 메모리를 할당하면 할당되는 메모리는 네이티브 메모리의 일부다. 이는 JVM 런타임 영역 밖에 위치하므로 JVM이 관리하는 영역이 아니며, 당연히 GC 대상도 아니다. 일반적으로 64bit 프로세서에서 JVM에 할당될 수 있는 주소공간은 굉장히 크고, 이는 시스템 레벨에서 관리자가 직접 설정하고 관리하게 된다. 따라서 JVM이 사용할 수 있는 메모리를 따로 제한하지 않는다면 네이티브 메모리의 크기는 하드웨어에 의해서만 제한된다고 이해할 수 있다. | ||
|
||
다이렉트 메모리는 네이티브 메모리의 일부이고, 그 중 IO 시스템 콜로 JVM 네이티브 힙에 받아온 데이터를 자바 힙에 복사 없이 사용하는 맥락을 가진 공간을 의미한다. JVM이 IO 시스템 콜을 호출하면, OS로 컨트롤이 넘어가서 커널 버퍼에 데이터를 받아오고, 다시 이 커널버퍼에서 JVM 네이티브 힙으로 데이터가 복사되면서 JVM으로 컨트롤이 넘어온다. 기존 IO의 경우에는 이 네이티브 힙에 존재하는 데이터를 JVM 런타임에 포함시키기 위해 한번 더 복사해서 JVM heap에 넘겨주는 과정이 필요했는데, NIO를 도입하면서 네이티브 힙에 있는 데이터를 직접 접근해서 사용할 수 있게 개선되었다. | ||
|
||
#### 메서드 영역 | ||
메서드 영역에는 타입 정보, 상수, 정적 변수, 코드 캐시 등이 저장되는데, 이는 JDK6 이전까지만 해도 자바 힙의 일부인 영구세대에 저장되었으나, 영구세대 공간의 제약으로 인해 OOM이 난다거나 하는 문제로 인해 현재는 네이티브 메모리로 옮겼다고 한다. 드물게 메서드 영역에서 OOM이 나기도 하는것 같지만, 보편적인 개발 업무로는 잘 발생하지 않는듯 하다. | ||
|
||
#### 정리 | ||
오랜만에 JVM 관련 지식을 재정립할 수 있어서 정말 시간 가는줄 모르고 공부했다. 나도 아직 읽는 중이지만, 유익한 내용도 많고 내용도 차근차근 단계별로 정리되어 있어 많이 어렵지 않으니 “JVM 밑바닥까지 파헤치기” 책을 읽어보기를 권한다. | ||
|
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.