자바에서 일반적으로 구성되는 메모리의 구조는 다음과 같이
Stack 과 Heap 메모리로 구성된다.
Stack
스택 메모리는 heap 영역에 존재하는 객체들에 대한 참조를 가지고 있다.
또한 객체들 뿐만 아니라 기본형 타입 , main() 함수를 포함한 각 함수의 지역변수도 Stack 메모리 영역에 저장된다.
Stack에 존재하는 변수들은 유효범위(scope)라고 하는 특정한 가시성을 가진다.
오직 활성화 된 범위의 객체들만 사용이 가능하다.
scope란 무엇일까?
유효범위는 쉽게 말하면 명명충돌을 방지하는 기능이라고 생각하면 되겠다.
public class DemoScope {
static void a () {
int i = 0;
}
public static void main(String[] args) {
for(int i = 0; i < 5; i++) {
a();
System.out.println(i);
}
}
}
일부로 충돌이 일어나게 작성한 클래스이다.
유효범위를 제외하고 생각한다면
for문이 돌아갈 때 메소드 a가 호출되면 int i = 0 이 되므로 무한으로 0이 찍히는 상황이 발생 할 수도 있겠다.
하지만 scope가 존재하기 때문에 이 코드는 0,1,2,3,4가 찍히고 바로 종료된다.
메소드에서 다른 메소드의 지역변수(i)를 호출한다 하더라도, 유효범위의 우선순위는 지역변수 - 멤버변수 이다.
또한 메소드에서 다른 메소드의 지역변수의 값은 참조 할 수 없기 때문에 ( 매개변수로 넣어주지 않는 한.. ) ,
main메소드의 지역변수 i의 값은 무한루프가 되지 않는 것이다.
만약 전역변수로 int i 를 선언한다면 무한루프가 실행된다.
위의 사진을 다시 보면, Stack메모리가 여러겹으로 중첩되어 있는데,
Stack 메모리 영역이 스레드 별로 할당이 되기 때문이다.
그렇기 때문에 프로세스의 스레드가 생성되고 시작 될 때 마다 각각의 stack 메모리를 가지게 되며,
다른 스레드의 스텍 메모리에 액세스 할 수 없다.
Heap
Heap 메모리 영역에는 실제 객체가 저장된다.
Heap 영역에 존재하는 객체들은 Stack 영역의 변수들에 의해 참조된다. ( Stack 엔 주소값만 )
실행 중인 각 JVM 프로세스에 대해서 Stack과 달리 Heap 메모리 영역은 단 하나만 존재하기 때문에
실행 중인 스레드의 수에 관계없이 Heap 영역에 존재하는 메모리는 공유된다.
힙 영역에는 임의의 위치에 객체가 생성되고 따라서 어떤 객체의 프로퍼티에 값을 저장하거나 저장된 값을
가져오고 싶다면 그 객체의 힙 영역상 좌표 ( 주소값 ) 만 알고 있으면 되겠다.
이제 스택과 힙이 뭔지 알겠다. 그런데 프로세스와 스레드는 무엇일까?
프로세스는 실행되고 있는 프로그램의 인스턴스 ( 독립적인 개체 ) 이다.
운영체제로 부터 시스템 자원을 할당받는 작업의 단위이고
각각의 독립된 메모리 영역 ( Code , Data , Stack , Heap의 구조 ) 를 할당받는다.
기본적으로 프로세스는 최소 1개의 스레드를 가지고 있다.
각 프로세스는 별도의 주소 공간에서 실행되고, 한 프로세스는 다른 프로세스의 변수나 자료구조에
접근할 수 없다.
가장 위의 사진인 스택과 힙의 관계표 는 위 사진의 하나의 프로세스인 것 이다.
각 메모리에 대해 알아보자.
프로세스의 메모리 구조이다. 위와 같이 4가지 영역으로 구성된 주소공간을 가상메모리라고 한다.
프로그램의 실행으 두가지 중요한 의미를 가진다.
- 파일 시스템에 존재하던 실행파일이 메모리에 적재된다는 의미
- 프로그램이 CPU를 할당받고 명령을 수행하고 있는 상태
파일 시스템에 있는 실행 파일이 메모리에 적재될 때, 실행파일 전체가 메모리에 올라가지 않는다.
일부분만 메모리에 올라가고 나머지는 디스크이 특정영역인 스왑영역에 존재한다.
특정 프로그램을 실행시켰을 때, 작업관리자의 성능 항목이나 기타 벤치마크 프로그램을 이용해서 보면
메모리수치와 함께 디스크사용량도 변하는 것을 확인 할 수 있다.
Code 영역
- 사용자가 작성한 프로그램 함수들의 코드가 CPU에서 수행 할 수 있는 기계어 명령 형태로 변환되어 저장되는 공간
- 0과 1로 된 2진수 ( binary ) 영역
- static 변수 / 메서드는 class가 실행되기 전에 미리 메모리가 확보된다
- 프로그램이 실행되면 모든 코드가 저장되어 있는 상태가 아니고 new 키워드로 객체를 생성하기 전엔 텍스트이다
Data 영역
- 전역변수 또는 static 변수 등 프로그램이 사용하는 데이터를 저장하는 공간
- 전역변수 또는 static 값을 참조한 코드는 컴파일이 완료되면 Data 영역의 주소값을 가르키도록 바뀐다.
Stack 영역
- 선입후출
- main() 메서드가 가장 먼저 호출된다. ( 지역변수를 저장 할 메모리가 필요하기 때문에 )
- 메서드가 생성될 때 마다 하나씩 생성되고 호출 시 메서드와 메서드의 정보가 stack area에 쌓인다.
- 메서드 호출이 종료될 때 제거된다.
- 호출 된 함수의 수행을 마치고 복귀 할 주소 및 데이터( 지역변수, 매개변수, 리턴값 등 ) 를 임시로 저장
Heap 영역
- 객체 인스턴스는 무조건 Heap 영역에 만들어진다. ( 실제 객체가 저장되는 공간 )
- 호출되는 클래스 내의 메서드들도 저장된다.
- 사용자가 관리하는 인스턴스가 생성되는 공간
- 객체를 동적으로 생성하면 인스턴스가 Heap메모리의 공간에 할당되어 사용된다.
- 객체지향언어인 JAVA에서는 C와 달리 해당 Heap 영역에 메모리를 사용해도 Garbage Collector 가 주기적으로 메모리를 검사하여 메모리공간의 사용하지 않는 공간 ( stack 에서 참조하지 않는 공간 ) 을 제거해준다.
- Stack과 달리 단 하나만 존재 ( 크기가 훨씬 크다 )
스택에 쌓이는 참조변수의 경우 함수가 끝나면 스택에서 해제되지만,
참조변수가 가리키고 있는 객체는 힙에서 지워지지 않는다.
즉, 힙 영역에 생성한 변수는 어느 블록에서 생성했던 힙에 유지되며
힙변수는 스택변수처럼 지역변수가 아닌 전역변수이다.
힙 영역을 두면 하나의 객체를 여러 참조변수에서 공유하는 형태로 사용 할 수 있어서
훨씬 메모리공간을 절약 할 수 있게 된다.
하지만 힙은 쓸 수 있는 변수크기에 제한이 없기 때문에 메모리누수 발생의 위험이 있다.
Garbage Collector
객체의 위치를 기억하는 참조 변수가 호출되고 종료되어 사라지면 ( 스택의 특성 )
힙에 객체의 위치는 사막 한 가운데에 볼펜을 떨어뜨린 것 과 같은 상황이 된다.
객체는 힙 어딘가에 존재하겠지만 위치를 모르기 때문에 다시 찾을 수 없다.
이렇게 미아가 된 객체는 JVM의 가비지 컬렉션이라는 기능에 의해 소멸한다.
JAVA의 경우 힙 영역의 모든 객체는 JVM이 관리하며, 객체가 참조되는 한 ( 스택에 해당 객체의 위치가 있다면 )
JVM은 객체가 살아있다고 여기고, 참조되지 않아 접근불가능이 될 때 해당 객체를 삭제하고 메모리 공간을 넓힌다.
객체의 사용 여부를 판단하기 위해 mark-and-sweep 알고리즘 프로세스를 실행하며
그 과정은 간단하게 다음과 같다.
1. Mark : GC roots부터 시작하여 모객 객체 참조를 검토하고, 살아있는 객체를 마킹한다.
2. Sweep : 힙 메모리에서 마킹되지 않은 객체가 차지하는 영역을 해제한다.
해당 사진에서 빨간색으로 표시된 힙 영역의 객체들은 GC에 의해서 수거된다.
GC프로세스의 특징은 JAVA에 의해 자동적으로 실행되며, 언제 작동할 지는 JAVA가 결정한다.
GC는 비용이 많이 드는 과정이고, 실행된다면 실행중인 애플리케이션의 모든 스레드가 일시중지된다.
GC프로세스는 GC의 기능인 가비지수집과 메모리해제보다 훨씬 복잡하게 작동하는 프로세스이다.
때문에 성능에 영향을 줄 수있어, 위의 알고리즘 방식을 사용한다.
쓰레기가 더 많아지고, 살아있는 객체로 Mark 되는 객체들이 적을수록 프로세스는 더욱 빨라지기 때문에,
Heap 메모리는 위에서는 하나의 공간이라고 말했지만 공간은 여러 부분으로 구성된다.
객체가 생성되면, 해당 객체는 1번 영역인 Eden 영역에 할당된다.
Eden 영역은 크지 않기 때문에, 빠른 속도로 가득 차게되고 GC는 Eden 공간에서 객체를 Mark 한다.
만약 객체가 GC 프로세스에서 살아남는 다면 생존공간( Survivor )이라 불리는 S0 ( 2번 ) 으로 이동하게 되고
GC가 Eden 공간에서 다시 수행되었을 때, 살아남은 모든 객체들은 S1 ( 3번 ) 으로 이동한다.
또한 S0에 존재하는 모든 객체도 S1으로 이동한다.
객체가 여러번의 GC 프로세스 동안 생존하는 경우,
해당 객체는 영원히 생존할 가능성이 높기 때문에 Old 영역 (4번)으로 넘어간다.
6번을 보면 GC를 실행할 때 마다 객체가 생존 공간으로 이동하고 Eden 공간이 확보되는 것을 볼 수 있다.
Garbage Collector Types
JVM에는 3가지의 GC가 있으며, 이들 중 어떤 것을 사용 할 지 선택할 수 있고, 기본적으로 JAVA는 기본 하드웨어를 기반으로 GC를 선택한다.
- Serial GC (직렬 GC) - 단일 스레드 수집기(single thread collector)입니다. 주로 데이터 사용량이 적은 소규모 애플리케이션에 적용됩니다.
- Parallel GC (병렬 GC) - 여러 스레드를 사용하여 GC 프로세스를 수행합니다. 해당 유형의 GC를 throughput(스루풋) collector라고도 부릅니다.
- Mostly concurrent GC (주로 동시성 GC..?) - 이전에 GC 프로세스가 비싸며, 실행될 때 모든 스레드가 일시중지 된다고 했었던 것이 기억나시나요? 해당 유형의 GC는 애플리케이션과 동시에 작동한다고 명시되어 있습니다. 그러나 대부분(Mostly) 동시성인 데이는 이유가 있습니다. 응용 프로그램과 100% 동시에 작동하지는 않습니다. 스레드가 일시 중지되는 기간이 있지만, 그래도 최고의 GC 성능을 달성하기 위해 가능한 짧게 유지됩니다. 실제 Mostly concurrent GC는 다음 두 가지 유형이 있습니다.
- Garbage First - 합리적인 어플리케이션의 일시 중지 시간으로 높은 스루풋을 가집니다.
- Concurrent Mart Sweep - 어플리케이션의 일시 중지 시간이 최소로 유지됩니다. 그러나 JDK 9부터 이 GC 유형은 더이상 사용되지 않습니다.
메모리누수를 좀 더 효율적으로 관리해야 할 때는
강한참조 ( StrongReference ) / 약한참조 ( WeakReference )를 사용 할 수도 있다.
이번 포스팅에선 자바의 메모리구조와 가비지 컬렉터에 대해서 알아보았다.
원래 가비지 컬렉터에 대한 글만 작성하려다가 조금 더 넓은 범위로 포스팅을 해보았다.
현재는 멀티스레드 환경을 제대로 구현해본 적이 없지만,
후에 언젠간 분명 사용할 같다는 느낌이 온다.
그때 이런 메모리구조나 관리가 분명 필요한 정보일테니 공부해두도록 하자.
면접질문 ex ) 가비지컬렉터에 대해 설명해보세요.
메모리를 관리하는 OS에는 프로그램이 프로세스단위로 연산되고,
프로세스의 스레드에 따라 나뉘는 스택이 있습니다.
스택에는 기본형 변수와 , main()을 포함한 함수, 지역변수가 들어가고 객체에 대한 주소값(참조값)을 가지고 있습니다.
객체는 하나의 힙 메모리에 저장되고 , 호출이 끝나면 삭제되는 스택 메모리와 달리
생성된 객체가 힙메모리에 계속 남아있게 됩니다.
그래서 스택에서 객체에 대한 주소값을 가지고 있지 않은 힙의 객체를
가비지 컬렉터가 수집하여 삭제해 메모리용량을 관리해줍니다.
관리자가 메모리를 직접 할당하고 닫아줘야하는 C언어와 다르게
java는 JVM의 가비지 컬렉터가 그 역할을 대신 해줍니다.
가비지 컬렉터를 효율적으로 사용하기 위해 mark-and-sweep 알고리즘과
하나의 힙메모리를 여러 부분으로 나눠 Eden 영역과 Old 영역으로 나누게 됩니다.
가비지 컬렉터는 관리자가 호출을 요청 할 수는 있지만 쓰레기 수집을 임의로 책정할 수 없으며,
쓰레기 수집은 java의 관리 아래 실행됩니다.
java는 하드웨어에 따라 가비지컬렉터의 3가지의 타입 중 자동으로 선택하며, 사용자가 임의로 지정 할 수 있습니다.
'JAVA > 자바' 카테고리의 다른 글
[JAVA] SOLID 객체지향 설계 5원칙 (2) | 2022.02.26 |
---|---|
[JAVA] Static과 Final (0) | 2022.02.26 |
[JAVA] String에서 ==와 equals()의 차이점 (0) | 2022.02.20 |
Java 문자열 나누기 - substring , indexOf , charAt (0) | 2021.12.29 |
Java 배열 정렬하기 (0) | 2021.12.11 |