1. 개념
1) garbage collection
Java 언어에서 new 연산자를 사용하여 생성된 객체가, 자동으로 제거되는 것을 garbage collection 이라 한다.
어떤 객체를 참조하는 변수가 하나라도 있다면, 그 객체는 사용되고 있는 것이고,
반대로 어떤 객체를 참조하는 변수가 하나도 없다면, 그 객체는 사용될 수 없으니 garbage collection 된다.
garbage collection은 JVM(Java virtual machine)에 의해서 자동으로 실행되는데,
(방문해보면서 mark 해놨는데 가리키고 있는게 아무것도 없는데 존재하면 그게 garbage 이다.)
이걸 확인하려면 메모리를 다 둘러봐야해서 시간이 꽤 걸리는 작업이기 때문에, 가끔 실행된다.
-
stop-the-world
garbage collection이 실행될 때, 그 JVM(Java virtual machine)에서 애플리케이션은 모두 정지되고,
오로지 garbage collection 작업만 실행된다.
즉 application thread는 모두 정지되고, garbage collection thread만 실행된다.
성능이 매우 중요한 애플리케이션의 경우에, garbage collection 동안 애플리케이션이 정지되는 것이 문제가 될 수 있다. -
System.gc()
garbage collection 작업을 즉시 실행하도록 명령하는 메소드이다. 이 메소드를 호출하지 말자. virtual machine 스스로 garbage collection 할 때를 결정하도록 놔두자.
System은 클래스이다. gc()는 System 클래스의 static 메소드이다.
Q. 그렇다면 garbage collection이 자동으로 잘 처리 되도록 하려면 어떻게 해야할까?
A. 더 이상 사용하지 않는 객체를 참조하고 있는 변수에 null을 대입하자.
변수에 의해 참조되지 않는 객체는 자동으로 garbage collection이 되기 때문이다.
2) weak generational hypothesis
많은 경우에 Java 객체는 다음과 같은 특징을 갖는다.
- 대부분의 객체는 짧은 시간 안에 garbage가 된다. -> 지역변수
- 오래된 객체의 멤버변수가 젊은 객체를 참조하는 경우는 아주 드물다. -> 영역이 다르다.
대부분의 객체는 짧은 시간 안에 garbage가 된다.
메소드 내에서 잠깐 사용되고 끝나는 객체가 많다.
이런 객체는 지역 변수에 대입하여 사용한다.
메소드가 리턴될 때, 지역 변수가 없어지면, 그 지역 변수에 의해서만 참조되던 객체는 garbage가 되고, garbage collection 대상이 된다.
오래된 객체
지역 변수가 아니라, 멤버 변수에 의해 참조되는 객체는 일찍 제거되지 않고 꽤 오래 살아남는다.
static 멤버 변수에 의해 참조되는 객체는 더욱 오래 살아남는다.
즉 지역 변수가 참조하는 객체들은 금방 garbage collection 되지만,
멤버 변수가 참조하는 객체들은 꽤 오래 살아남는다.
오래된 객체의 멤버변수가 젊은 객체를 참조하는 경우는 아주 드물다.
오래된 객체의 멤버 변수가 참조하는 객체도 오래된 객체일 확률이 높고, 일찍 제거될 객체일 확률은 낮다.
일찍 제거될 객체는 대부분 지역 변수에 대입되어 사용되기 때문이다.
위 특징들을 활용하여 Java virtual machine의 garbage collection 기능이 구현되었다.
garbage collection의 성능이 좋아진 원인은 이러한 특징을 잘 잡았기 때문이다.
3) Java 컴파일
- HotSpot
Oracle이 만든 Java virtual machine의 이름이 Java HotSpot Performance Engine 이다.
줄여서, HotSpot virtual machine 이라고 부르거나, 그냥 HotSpot 이라고 부른다.
즉 HotSpot은 Oracle이 만든 Java virtual machine의 제품명이다.
- C/C++ 컴파일
C/C++ 컴파일러는 C/C++ 소스 코드를 기계어 코드로 변환한다.
C/C++ 소스 파일을 컴파일하여 생성된 *.exe 실행 파일에는 기계어 코드가 들어있다.
*.exe 파일은 직접 CPU에서 실행된다.
실행 파일의 구조는 운영체제에 따라 다르다.
- 표준 Java 컴파일
표준 Java 컴파일러는 Java 소스 코드를 bytecode로 컴파일한다.
*.java 소스 파일을 컴파일하여 생성된 *.class 파일에는 bytecode가 들어있다.
Java virtual machine이 *.class 파일을 읽어서 bytecode를 실행한다.
표준 Java 컴파일러 실행 파일은 javac.exe 이다.
표준 Java 가상 기계 실행 파일은 java.exe 이다.
- bytecode로 컴파일하는 이유
기계어 코드로 컴파일 하지 않고 bytecode로 컴파일하는 방식의 장단점은 다음과 같다.
- 단점: 약간 느리다.
CPU가 기계어 코드를 직접 읽어서 실행하는 방식이 가장 빠르다.
- 장점: CPU에 독립적이고, 운영체제에 독립적이다.
실행파일에 들어있는 기계어 코드는 CPU마다 다르다.
실행파일 구조는 운영체제마다 다르다.
그래서 Windows 실행파일을 맥에서 실행할 수 없다.
Java 소스파일을 컴파일하여 생성된 *.class 파일의 구조는 운영체제에 무관하다.
그리고 bytecode도 CPU에 무관하다.
따라서 Java로 개발한 앱은, 특정 운영체제나 CPU에 무관하게 배포될 수 있고,
Java virtual machine만 있으면 실행될 수 있다.
- 단점: 약간 느리다.
- JIT 컴파일 (Just In Time compilation)
Java는 JIT 컴파일 방식을 사용한다.
JIT 컴파일이란, bytecode를 실행하기 직전에 기계어 코드로 컴파일 하는 방식을 말한다. Java virtual machine은 실행하기 직전에, bytecode를 기계어 코드로 변환(컴파일)해서 실행한다.
bytecode를 해석해서 실행하는 것보다(인터프리터 방식), 기계어 코드로 변환해서 실행하는 쪽이 훨씬 더 빠르기 때문이다.
Java virtual machine 내부에는 JIT 컴파일러가 내장되어 있다.
이 JIT 컴파일러는 bytecode를 기계어 코드로 변환한다.
- JVM JIT 컴파일 방식의 장단점
JVM(Java virtual machine)- 단점: 변환된 기계어 코드를 따로 저장하지는 않기 때문에, 실행할 때마다 매번 다시 JIT 컴파일 해야 한다.
- 장점: dynamic optimization 가능
- dynamic optimization
컴파일러가 소스코드를 컴파일할 때, 소스코드와 동일한 순서로 기계어 코드를 생성하지 않고,
좀 더 빠르고 효율적으로 실행될 수 있도록, 실행 순서를 재배치하고 조정하여, 기계어 코드를 생성하는 것을,
컴파일러 최적화(compiler optimization)라고 한다.
소스코드를 컴파일할 때가 아니고, 실행하는 시점에, 실행 순서를 재배치하고 조정하여 기계어 코드를 생성하는 것을, 동적 최적화(dynamic optimization)라고 한다.
JVM의 JIT 컴파일러는 dynamic optimization을 수행한다.
4) process와 virtual memory
애플리케이션이 운영체제 메모리로 로드(load)되어 실행될 때, 이것을 프로세스(process)라고 부른다.
즉 운영체제 메모리로 로드되어 실행되는 애플리케이션을 프로세스라고 부른다.
운영체제는 프로세스에게 자원을 제공한다. 제공되는 자원은, 메모리, 파일, 소켓(socket), 세마포어(semaphore), 파이프(pipe) 등이다.
운영체제에 의해서, 각각의 프로세스는 서로 격리된다.
어떤 프로세스의 메모리나 자원을, 다른 프로세스가 건드릴 수 없다.
- virtual memory
프로세스가 사용하는 메모리 주소는, 실제 메모리의 물리적 주소가 아니고, 가상의 주소이다. (virtual memory 주소)
이 가상의 주소는, 운영체제와 CPU에 의해서, DRAM 부품의 물리적 주소로 변환되어 실행된다.
어느 프로세스가 다른 프로세스나 운영체제 커널(kernel)의 물리적 메모리 주소를 사용하는 것은 불가능하다.
운영체제와 CPU가 그런 주소 변환을 허용하지 않기 때문이다.
즉, 어느 프로세스는 자신에게 주어진 virtual memory 주소만 사용할 수 있고, 자신에게 주어진 메모리에만 접근할 수 있다.
- Intel CPU meltdown 결함
2017년에 문제가 되었던 인텔 CPU의 meltdown결함은, virtual memory 주소를 물리 주소로 변환하는 인텔 CPU의 기능에 헛점이 있다는 것이다.
이 결함을 이용하면, 운영체제 커널의 메모리 주소나, 다른 프로세스의 메모리 주소를 알아내고, 그 메모리에 접근하는 것이 가능하게 된다.
그래서, 결함이 있는 Intel CPU의 주소 변환 기능을 사용하지 않고, CPU가 담당했던 주소 변환까지 운영체제가 수행하도록 수정하는 것이, meltdown 결함 패치(patch)의 내용이다.
CPU가 하던 기능을 운영체제 SW가 해야 하므로, 이 패치를 적용하면, 컴퓨터가 조금 느려진다.
5) stack, heap, data, text segment
프로세스에게 주어진 메모리 공간은 세그먼트들로 나눠 사용된다.
- text segment (code segment)
기계어 코드가 위치한 영역을 text segment라 한다.
소스코드에 포함된 문자열 상수가 숫자 상수들도 여기에 위치하는 경우도 있는데,
특정 컴파일러나 운영체제에서는 다를 수 있다.
- heap segment
동적으로 할당되고 반납되는 메모리가 위치한 영역이다.
C언어의 malloc/free 함수를 사용하여 할당된 메모리는 heap에 위치한다.
Java 언어의 new 연산자를 사용하여 생성한 객체/배열은 heap에 위치한다.
Java에서 참조형 값들은 전부 heap에 위치한다.
heap 영역에서 불필요한 객체를 찾아 제거하는 작업이 garbage collection 이다.
Java의 garbage collection은 JVM에 의해서 자동으로 실행된다.
- data segment
프로세스가 시작할 때부터 종료될 때까지 존재하는 변수들이 위치한 영역이다.
전역변수, static 지역 변수, static 멤버 변수가 여기에 위치한다. (C/C++ 언어)
Java 언어에는 전역변수, static 지역 변수가 없다.
Java 언어는 data segment라고 부르지 않는다.
Java 언어의 static 멤버 변수는 metaplace 영역에 위치한다.
- stack segment
stack segment는 함수나 메소드 호출 과정에서 사용되는 메모리 영역이다.
함수가 호출될 때 생성되는 지역 변수, 파라미터 변수가 stack segment에 생성된다.
함수의 리턴 값이나, 함수가 리턴할 때 되돌아갈 기계에 코드의 주소도 여기에 저장된다.
- stack overflow 공격
함수가 리턴될 때 되돌아갈 기계에 코드의 주소가 stack segment에 저장되고,
지역 변수도 stack segment에 저장된다는 점을 노린 해킹 공격이다.
공격할 SW의 기계어 코드를 분석하여, 네트워크로 전송된 데이터가 어떤 함수의 지역 변수 배열에 저장되는 경우를 찾는다.
그 지역 변수 배열의 크기보다 더 큰 데이터를 전송해서,
데이터가 저장될 때, 배열의 뒤까지 넘치도록(overflow) 한다.
이렇게 넘쳐서 저장된 데이터가 함수가 리턴 주소가 저장된 곳까지 덮어쓰도록 한다.
함수가 리턴할 때, overflow 되어 변경된 주소로 리턴하게 된다.
즉 해커가 원하는 곳으로 리턴하게 되어 해커가 심어 놓은 기계어 코드가 실행하게 된다.
- stack overflow 공격 막기
배열에 데이터를 저장하거나 복사할 때, 배열의 범위를 벗어나서 넘치게(overflow) 저장하지 말아야 한다.
Java 언어의 경우에는, VM이 이것을 검사하기 때문에, overflow가 가능하지 않다.
따라서 Java는 stack overflow 공격에 안전하다.
배열의 크기를 벗어난 곳에 접근하려 하는 경우에, Java에서는 IndexOutOfBoundsException이 발생한다.
C/C++ 언어에서는 overflow가 가능하므로, 코딩할 때 주의해야 한다.
그리고 overflow를 검사하지 않는 표준 함수의 사용을 자제하고 (예: strcpy, strcat, memcpy, sprinf)
overflow를 검사하는 표준 함수를 사용해야 한다. (예: strncpy, strncat, memcpy, snprintf)
6) process와 thread
process 내부에 thread가 생성된다.
process가 시작될 때, main thread가 자동으로 생성되고,
다른 스레드는 스레드 생성 명령을 실행하여 생성해야 한다.
스레드는 stack segment를 따로 소유한다.
스레드를 한 개 생성할 때마다, stack segment도 한 개 생성해서, thread에게 할당해야 한다.
heap segment, data, text segment는 thread들 사이에 공유된다.
- ThreadLocal
Java 표준 라이브러리에 ThreadLocal 클래스가 있다.
thread들 사이에 공유되지 않는 객체를 생성하기 위해 ThreadLocal 클래스를 사용한다.
7) HotSpot java virtual machine 구조
HotSpot JVM의 주요 구성요소는 다음과 같다.
- Class Loader : 클래스 파일(*.class)을 메모리에 불러오는 역할을 담당
- Runtime Data Area : 데이터를 보관하는 메모리 영역
- Execution Engine : 실행 엔진
runtime data areas 내부에서
method area
이영역에서 위치한 항목들은 다음과 같다.
- method의 bytecode
- static 멤버 변수
- constant 값
method area 이름은 자주 사용되는 이름이 아니다.
이 영역의 대표적인 이름은, permanenet generation 영역이다. (Java7까지)
Java8 부터 이 영역의 이름이 metaplace로 바뀌었다. (세부 구조도 변경되었다.)
- heap
Java 객체와 배열이 생성되는 영역이다. (heap segment)
garbage collection은, 더 이상 사용되지 않는 객체를 메모리에서 삭제하는 작업이다.
Java 객체들은 heap 영역에 위치한다. 즉 garbage collection 작업은 heap 영역을 청소하는 작업이다.
- Java stack
지역 변수와 파라미터 변수가 생성되는 영역이다.
Java 메소드가 호출될 때, 그 메소드의 지역 변수와 파라미터 변수가 생성될 공간이 할당되어야 한다.
이 공간은, 메소드가 리턴될 때 반납된다. stack segment에 이 공간이 할당된다.
Java 메소드가 호출될 때 사용되는 stack segment를 Java stack 이라고 한다.
각 thread 마다 stack segment를 따로 소유한다.
즉 새 thread가 생성되면, 그 thread가 사용할 stack segment를 할당해야 한다.
- Program Counter Registers
java thread가 현재 실행하고 있는 명령(bytecode)의 주소가 program counter register에 저장된다.
CPU 코어(core)의 address register와 유사한 역할을 한다.
각 java thread 마다 program counter register를 따로 소유해야 한다.
- Native stack
Java 표준 라이브러리의 클래스의 메소드들 중에서, Java가 아니고 C로 구현된 메소드도 있다.
C로 구현된 메소드를 native method라고 부른다.
C로 구현된 메소드가 호출될 때 사용될 stack segment를 Native stack 이라고 한다.
8) Java thread 와 메모리 영역
각 Java thread 마다 따로 소유하는 메모리 영역
- Program Counter Register
- Java Stack (지역 변수, 파라미터 변수)
- Native Stack
여러 Java thread들이 공유하는 메모리 영역
- Heap (객체, 배열)
- Method Area (상수, static 멤버 변수)