주제목차내용자바의 실행 과정인터프리터 & JIT 컴파일러JIT 컴파일러자바 메모리 영역(JAVA Runtime Data Areas)PC registerMethod areaStack areaHeap areaNative method areaHeap영역자바는 Call By Value일까? Call By Reference일까?Call By Value Vs Call By Reference어느 부분에서 헷갈릴까?swapObject()swapValue()swapValueInObject()음.. 아직도 잘 모르겠는 걸?why?결론
주제
자바 메모리영역, 자바는 Call By Value일까 Call By Reference 일까?
목차
- 자바 메모리영역
- 자바는 Call By Value일까 Call By Reference 일까?
내용
예전에 간단하게 JVM에 대해서 정리 해봤었는데, 이번에 팀원들과 스터디를 통해 다시 한번 공부를 하면서 복습도 하고 여러가지 새로운 것들을 알게되어 정리하고자 한다.
자바의 실행 과정
- .java파일을 자바 컴파일러가 바이트 코드인 .class 파일로 변환 시킨다.
- 실행 시, 클래스 로더는 바이트 코드로 변환된 클래스들을 로드, 링크하여 JVM에 탑재 시킨다.
- 실행 엔진은 클래스 로더로 메모리 영역에 탑재된 바이트 코드를 실행한다.
- 실행 엔진은 내부에서 바이트 코드를 기계어로 변환 시켜 실행을 한다.
인터프리터 & JIT 컴파일러
자바 실행 엔진은 런타임 시점에 인터프리터 방식으로 한 줄씩 바이트 코드를 읽어 실행 시킨다.한 줄씩 읽는 인터프리터의 특징 때문에 속도가 느리다. 그럼에도 기본 변환 방식을 컴파일 방식을 사용하지 않는 이유는, 컴파일러는 메모리와 CPU 스레드 사용에 대한 비용 때문인데, 실행 시점에서 모든 바이트 코드를 컴파일 하면 오히려 인터프리팅 방식보다 더 느릴 수 있기 때문이다.
이러한 단점을 커버하기 위해 JIT 컴파일러 방식이 도입되었는데, JIT 컴파일러는 런타임 시점에 적절한 때에 바이트 코드를 컴파일 방식으로 기계어로 변환 시킨다. 이러한 방식 덕분에 인터프리터의 느린 속도라는 단점을 커버할 수 있게 되었다.
JIT 컴파일러
JVM은 다음 두가지 카운트를 관리한다.
- method entry counter (JVM 내에 있는 메서드가 호출된 횟수)
- back-edge loop counter (메서드가 루프를 빠져나오기까지 돈 횟수)
이 카운트가 임계값을 넘었을 경우, JIT 컴파일러가 컴파일해 기계어로 변경된다. 이렇게 기계어로 변경된 후에도 지속적인 카운트를 체크해 일정 임계값을 다시 넘었을 경우 더 높은 수준의 최적화를 실행한다. 이 최적화는 가장 높은 수준의 최적화 단계까지 반복 된다.
실제로 최적화가 이루어지는지 테스트를 해볼 수 있었다.
public class JitTest { public static void main(String[] args) { final int CHUNK_SIZE = 1000; for (int i = 0; i < 500; ++i) { long s = System.nanoTime(); for (int j = 0; j < CHUNK_SIZE; ++j) { new Object(); } long e = System.nanoTime(); System.out.printf("%d\t%d\n", i, e - s); } } }



처음엔 실행시간이 20000 나노초대에 머물렀지만, 66~67회 실행 시점 부터 눈에 띄게 실행시간이 줄었다.그 후 218회 부터 한 번 더 크게 줄었다.
이 외에도 여러가지 최적화에 대한 자료를 찾을 수 있었다.ref - https://www.slideshare.net/dougqh/jvm-mechanics-understanding-the-jits-tricks-93206227
자바 메모리 영역(JAVA Runtime Data Areas)
PC register
스레드가 시작될 때 생성되며 현재 수행중인 JVM 명령의 주소를 가진다.
Method area
모든 스레드가 공유하는 영역으로, 클래스, 인터페이스, 메소드, 필드, Static 변수 등의 바이트 코드를 저장한다.
Stack area
스레드마다 하나의 Stack area를 가지며, 메서드 호출 시 메서드 단위로 스택 프레임이 생성된다. 호출된 메서드의 매개변수, 지역변수, 리턴 값, 연산 시 임시값 등을 저장하고메서드 종료 시 스택 프레임 단위로 제거된다.
Heap area
모든 스레드가 공유하는 영역으로 객체(instance)들을 위한 영역, new를 통해 생성된 객체, 배열, immutal 객체 등의 정보를 저장하고, Garbage Collector에 의해 관리되는 주요 메모리 영역이다.
Native method area
자바 언어가아닌 다른 언어로 작성된 네이티브 코드를 수행하기 위한 메모리 영역이다.
Heap영역
Heap 영역은 위에서 설명했듯이 GC에 의해 관리되는 영역이다.

최신 글에서도 여전히 과거의 힙 영역 구조를 가지고 설명하는 글이 많이 보이지만 자바 8에서는 힙 영역 구조가 아래와 같이 변경되었다.

이에 대한 자세한 글은 다음 글에서 확인할 수 있다.JAVA 8에서 perm 영역이 사라지고 metaspace 영역으로 대체된 이유?
스터디 항목 중 GC에 관해서도 있으니 빠른 시일내에 정리해서 포스팅할 수 있을 것 같다.
자바는 Call By Value일까? Call By Reference일까?
결론부터 말하자면 Call By Value 다. Call By Reference처럼 동작하는 것 같기도한데? 하며 헷갈릴 수 있는 부분을 정확히 그림을 통해 어떤 차이점이 있는지 비교 해보고자 한다.
Call By Value Vs Call By Reference
Call By Value란 메소드의 매개변수로 값이 전달될 때, 값이 복사되어 넘어가는 방식이다.즉, 매개 변수는 스택 영역에 새로운 변수로 할당되고 그 값을 복사 받는다.
반면 Call By Reference는 매개변수로 전달 될 때, 값이 아닌 참조가 넘어가는 방식이다.전달받는 변수가 메모리 영역에서 가르키는 주소 영역 자체를 넘겨받는다.
어느 부분에서 헷갈릴까?
예제와 그림을 통해서 어떤 부분이 Call By Reference로 착각하게 만드는지 알아본다.
다음와 같이 테스트 코드를 작성한 후, 세 가지의 케이스에 대해서 출력을 살펴 본다.
public class CallBy { public static void main(String[] args) { Person personA = new Person(27); Person personB = new Person(17); swapObject(personA, personB); System.out.println(personA.age +", " + personB.age); swapValue(personA.age, personB.age); System.out.println(personA.age +", " + personB.age); swapValueInObject(personA, personB); System.out.println(personA.age +", " + personB.age); } public static void swapObject(Person personC, Person personD) { Person tmp; tmp = personC; personC = personD; personD = tmp; } public static void swapValue(int valueA, int valueB) { int tmp2 = valueA; valueA = valueB; valueB = tmp2; } public static void swapValueInObject(Person personE, Person personF) { int tmp3; tmp3 = personE.age; personE.age = personF.age; personF.age = tmp3; } } class Person { int age; public Person(int age) { this.age = age; } }
코드를 실행해서 출력결과는 다음과 같다. 최종적으로 swapValueInObject() 함수를 실행했을 때만 swap이 발생했다. 자바 객체는 Reference타입이라면서 Call By Reference아니야?..라는 생각이 들 수도 있다. 비슷해 보이지만 어떻게 동작했길래 이런 결과가 나오는지 확인 해본다.

swapObject()
swapObject에서는 인수로 객체 인스턴스를 받아 통째로 바꿔보려고 시도를 했다. 메모리 구조에서는 어떤일이 일어나는지 확인 해본다.
처음 main 함수에서 personA, personB 인스턴스를 생성하면 다음과 같이 personA, personB는 스택 영역에 지역 변수가 할당되고, 힙영역에 할당된 인스턴스들을 가르킨다.

swapObject() 함수를 실행하면 다음과 같이 바뀐다.
스택 영역에 새로운 변수 personC, personD가 생성되고 똑같이 힙 영역에 있는 인스턴스들을 가르킨다.
tmp변수는 personC가 가르키는 인스턴스를 가르킨다.

tmp = personC; personC = personD; personD = tmp;
위의 코드를 실행시키면 다음과 같이 바뀐다. personC와 personD가 가르키는 화살표만 바뀌었을 뿐, personA와 personB는 여전히 각각 age가 27, 17인 인스턴스를 가르키고 있다.

메서드가 종료되고 나면 다음과 같이 처음 할당된 모습과 같은 모습 그대로 남는다.

swapValue()
swapValue()가 실행되면 메모리 영역은 다음과 같은 모습을 하게 된다.
primitive 타입의 지역 변수는 스택 영역에 할당된다.

int tmp2 = valueA; valueA = valueB; valueB = tmp2;
위의 코드를 실행시키면 다음과 같이 바뀐다. 메서드 내의 지역변수 valueA, valueB에 대해서만 바뀌었지 personA, personB의 age는 그대로다.

마찬가지로 메서드가 종료되면 할당된 메서드의 변수들은 스택영역에서 사라진다.
swapValueInObject()
swapValueInObject()가 실행되면 다음과 같은 메모리 영역 모습이 된다.
swapValue()에서와 같이 PersonE, PersonB 가 각각 힙 영역에 위치한 PersonA, PersonB의 인스턴스를 가르킨다. 그리고 tmp3은 PersonE의 age 값을 그대로 복사한다.

int tmp3; tmp3 = personE.age; personE.age = personF.age; personF.age = tmp3;
그렇다면 위의 코드를 실행 시키면perosnE가 가르키는 인스턴스의 age는 personF가 가르키는 인스턴스의 age인 17이 된다.personF가 가르키는 인스턴스의 age는 tmp3. 즉, 27이 된다.
결과적으로 아래와 같은 모습을 하게 된다.

결론은, 기본 타입 변수라면 우리가 흔히 아는 Call By Value의 동작을 하게되고, 참조 타입 변수라면 주소 값을 복사(Call by value)해서 넘긴다는 것이고. 참조형 변수는 주소값을 통해 힙 영역 내부의 인스턴스를 접근할 수 있게 해주는 것이다.
음.. 아직도 잘 모르겠는 걸?
이번에는 자바 코드와 Call By reference를 할 수 있는 C++ 코드를 비교 해본다.
각각 swap을 시도하는 코드를 작성하고 실행 결과를 살펴본다.
- java
public class CallBy2 { public static void main(String[] args) { Integer a = 10; Integer b = 20; func(a,b); System.out.println(a+", " + b); } static void func(Integer a, Integer b) { Integer tmp = a; a = b; b = a; } }

- c++
#include <iostream> using namespace std; void func(int &a, int &b) { int tmp = a; a = b; b = tmp; } int main() { int a = 10; int b = 20; func(a, b); cout<<a<<","<<b; }

why?
위에서 봤던 swapObject() 예제와 같이, 매개변수들은 값으로 주소값을 가지고 있을 뿐, 참조할 수 있는 주소를 제시할 뿐 참조하는 것은 아니다.추가적으로 자바의 Wrapper 클래스는 Object이므로 참조형이지만 immutable 하다. 즉, 새로운 값을 할당 시, 힙 영역의 새로운 주소를 가르킨다.(String도 마찬가지)
public class CallBy2 { public static void main(String[] args) { Integer a = 10; Integer b = 20; func(a,b); System.out.println(a+", " + b); } static void func(Integer a, Integer b) { Integer tmp = a; a = 20; b = 10; } }

반면에 c++에서의 Call by Reference는 메모리 영역의 참조를 전달한다. 그래서 값을 직접 바꿀 수 있는 것이다.
결론
자바는 Call By Value다. 참조 타입이라도 전달되는 것은 주소 값 일뿐이다.