Synchronized로 검색한 결과 :: 시소커뮤니티[SSISO Community]
 
SSISO 카페 SSISO Source SSISO 구직 SSISO 쇼핑몰 SSISO 맛집
추천검색어 : JUnit   Log4j   ajax   spring   struts   struts-config.xml   Synchronized   책정보   Ajax 마스터하기   우측부분

회원가입 I 비밀번호 찾기


SSISO Community검색
SSISO Community메뉴
[카페목록보기]
[블로그등록하기]  
[블로그리스트]  
SSISO Community카페
블로그 카테고리
정치 경제
문화 칼럼
비디오게임 스포츠
핫이슈 TV
포토 온라인게임
PC게임 에뮬게임
라이프 사람들
유머 만화애니
방송 1
1 1
1 1
1 1
1 1
1

Synchronized로 검색한 결과
등록일:2008-04-13 16:48:52
작성자:
제목:자바를 더 빠르게 만들자


제가 아는 분이 자동차 튜닝을 하고 있습니다. 같은 자동차이지만 엔진을 튜닝하면 시속 100km까지 속도를 내는데 걸리는 시간이 획기적으로 단축된다고 합니다. 그러나 그렇게 튜닝한 차를 잘못 몰아서 교통사고를 당하는 경우도 많다고 들었습니다. 자동차를 빠르게 움직이려면 자동차도 좋아야 하고 운전 기술도 뛰어나야 합니다. 아무리 운전을 잘해도 차가 안 따라주면 속도가 날 수 없으며, 반대로 차가 아무리 좋아도 왕초보가 몰면 잘 움직일 수가 없을 것입니다.
마찬가지로 자바 가상머신(Java Virtual Machine, JVM) 위에서 작동하는 자바 프로그램을 운전에 비유하면, 자바 프로그램은 운전 기술이고, JVM은 자동차에 비유될 수 있습니다. 따라서 자바의 성능 향상은 두가지 관점에서 접근할 수 있습니다. 첫 번째는 JVM의 성능향상이고, 두 번째는 자바 프로그래밍 기법적 성능 향상입니다. 먼저 JVM의 작동과 구조를 살펴보고 성능 향상을 위한 해결책들을 살펴보겠습니다.


JVM과 퍼포먼스
JVM 은 이미 7월호 첫 연재에서 설명했다시피 운영체제에 상관없이 어디서든지 컴파일된 자바 클래스를 실행해 줍니다. 다시 말하자면 JVM이 실행되는 동안(runtime instance, 런타임 인스턴스) 해야 하는 것은 바로 자바 프로그램을 수행하는 것입니다. 자바 프로그램이 시작되면 런타임 인스턴스가 하나 생겨나고, 그 프로그램이 완료되면 인스턴스는 사라집니다. 만일 같은 컴퓨터에서 세 개의 자바 프로그램을 동시에 실행하면 세 개의 JVM 런타임 인스턴스가 생깁니다. 각 자바 프로그램은 각각의 가상머신에서 돌아가는 것입니다.
JVM의 인스턴스는 자신이 수행할 프로그램의 시작 클래스의 main() 메쏘드를 호출하면서 시작합니다. 그래서 main() 메쏘드는 public이어야 하고, static으로 선언되어야 하며, void를 러턴해야 하며, String 배열을 파라미터로 받아들여야 합니다. 즉 public static void main(String[] args)로 정의돼야 합니다. 그래서 어떤 클래스든지 main() 메쏘드를 가지면 자바 프로그램의 시작점으로 사용될 수 있습니다.

JVM의 구조
JVM 은 앞에서 언급했듯이 자바 클래스를 실행합니다. 이를 위해선 먼저 클래스를 읽어들이고(로딩하고), 읽어들인 클래스의 데이터를 저장하고, 클래스의 코드를 실행할 수 있어야 합니다. 그래서 JVM은 클래스 로더, 런타임 데이터 영역, 실행 엔진으로 구조를 파악할 수 있습니다.

클래스 로더
클래스 로더는 JVM이 수행할 클래스를 찾아서 클래스의 바이너리 데이터를 메모리에 적재합니다. 이때 클래스를 검증하고(verification), 클래스 변수를 위한 메모리를 초기화해서 기본값으로 할당하며(preparation), 클래스의 참조를 직접참조로 변환(resolution)합니다.

런타임 데이터 영역
JVM 이 프로그램을 실행하면, 바이트 코드, 로드된 클래스로부터의 정보, 객체들, 메쏘드 파라미터, 리턴 값, 로컬변수, 연산중간 결과 값 등을 메모리로 저장합니다. JVM은 메모리를 몇 개의 런타임 데이터 영역으로 논리적으로 나누어서 이 데이터들을 저장하고 프로그램을 수행합니다. 이 데이터 영역은 <그림 1>과 같이 나눌 수 있습니다.

◆ 메쏘드 영역(method area) : 프로그램이 수행되는 동안 클래스의 정보를 참조하는 곳으로서, 로딩된 클래스의 정보, 멤버변수 정보, 메쏘드 정보, static 변수정보 등이 저장됩니다.
◆ 힙(heap) : 클래스의 인스턴스나 배열, 자바 프로그램 실행중 생성되는 객체들이 저장되는 메모리 공간입니다. 힙은 JVM에서 하나뿐이므로 여러 쓰레드가 공유해서 사용합니다. 이때 힙에 저장된 객체가 더 이상 사용되지 않을 경우 메모리 해제는 프로그래머가 할 수 없고, 가비지 컬렉터가 수행합니다.
◆ 스택(stack) : 메쏘드가 호출될 때마다 스택 프레임이라는 데이터 영역이 생성되며, 이것이 쌓여 스택을 구성합니다. 스택에는 수행되는 메쏘드 정보, 로컬변수, 매개변수, 연산중 발생하는 임시 데이터 등이 저장됩니다.
◆ 네이티브 메쏘드 스택(native method stack) : 네이티브 메쏘드 즉 자바가 아닌 다른 언어(예 : C 언어)로 쓰여진 메쏘드가 호출될 경우 사용되는 메모리 공간입니다.
◆ 레지스터(register) : 쓰레드가 시작할 때 생성되며, JVM이 수행할 명령어를 저장하는 공간입니다.


실행 엔진
JVM의 핵심으로서 적재된 클래스의 메쏘드 내의 명령, 즉 바이트 코드를 인터프리트하여 수행합니다. 실행 엔진이 어떻게 수행되느냐에 따라 성능이 달라지며, 이에 따른 가상머신의 종류도 다양할 수 있습니다.

JVM과 성능저하 요인
< 그림 2>에서 JVM의 작업 비율을 보면 코드 인터프리테이션(interpretation)이 제일 많이 차지하고 있고, 쓰레드 동기화(thread synchronization)와 가비지 컬렉션의 순으로 나타납니다. 자바의 작동상 나타나는 구체적인 성능 저하의 원인을 분석해 보면 다음과 같습니다.

* 인터프리팅 과정 : 자바는 컴파일러형 언어와 달리 소스 프로그램이 바이트 코드로 바뀐 후 이것이 인터프리트 되는 형태로 실행됩니다. 바이트 코드를 인터프리트하는 것은 원시 코드를 실행하는 것보다 10배 내지 30배 느립니다. 


* 가비지 컬렉션에서의 성능 저하 : 가비지 컬렉션은 더 이상 객체를 참조하지 않을 경우 필요없게 된 메모리를 알아서 수거하고, 힙 단편화(fragmentation)를 제거해주는 것입니다. 그런데 이는 백그라운드 쓰레드로 수행되기 때문에 어떤 객체가 참조되고 있고, 어떤 객체가 참조되지 않는지 항상 지켜보고 있다가 그것들을 할당 해제해야 하므로 커다란 오버헤드를 초래합니다. 이는 작업시간의 15∼25%를 차지합니다. 또한 언제, 어떻게 가비지 컬렉터가 수행되는지에 대해서 알 수 없기 때문에 큰 데이터 구조를 필요로 하거나 메모리에 민감한 프로그램 개발시 주의가 필요합니다.


* 쓰레드 동기화 : 자바 프로그램에서는 여러 쓰레드들이 공유하는 힙에서의 쓰레드간 충돌을 방지해 공유 자원에 대한 일관성을 유지하기 위해 Synchronized라는 키워드를 사용해 동기화합니다. 자바에서 제공하는 많은 메쏘드들이 Synchronized로 선언되어 있기 때문에 쓰레드 모니터는 백그라운드로 계속 관리 작업을 수행해야 하므로 심각한 오버헤드를 초래하며 많은 경우 쓰레드들이 동기화된 메쏘드나 블럭에 들어가지 못하고 대기하고 있게 됩니다. 이 또한 작업 시간의 15∼25%를 차지합니다.


* 동적 바인딩(dynamic binding)과 동적 클래스 로딩(dynamic class loading) : 동적 바인딩은 함수 호출이 발생하는 곳에 해당 함수의 포인터를 기록해두는 정적 바인딩(static binding)과 달리, 프로그램이 실행되는 중간에 특정 심볼릭 참조가 사용되면 그것을 직접적인 메모리 참조로 변경시키는 것을 말합니다. 즉 클래스의 상속 관계에서 발생하는 오버라이딩(overriding), 오버로딩(overloa ding)된 메쏘드 호출을 풀기 위한 작업으로 이로 인해 프로그램 수행 시간이 길어지게 됩니다. 동적 클래스 로딩은 실행중인 클래스가 다른 클래스를 참조하였을 경우 직접 참조로 바꾸는 과정에서 직접 그 클래스들을 로딩하는 것을 말합니다. 이때, 로드된 클래스 파일들은 JVM에 의해 안전한지 검증받은 후 메모리 영역을 할당받고 링킹, 초기화가 이뤄지므로 역시 성능 저하를 유발합니다.

<표 1>을 보면 각종 자바의 연산을 하는 데 걸리는 시간(ms)이 나타나 있습니다. 이는 오래된 JDK 버전(1.0.2)의 결과이지만 자바에서 각종 연산에 있어서 상대적인 부하를 파악할 수 있습니다. 동기화 연산, 객체 생성, 배열 생성에 많은 시간이 걸린다는 것을 알 수가 있습니다. 따라서 자바 프로그램을 짤 때 이러한 사항을 반드시 고려해야 하겠습니다.

자바 퍼포먼스 튜닝
앞 서 자바의 성능 저하 요인을 알아봤습니다. 이제 자바 퍼포먼트 튜닝을 위한 다양한 해결 방법을 살펴볼까요. 자바의 성능 향상을 위한 기술적인 해결책은 꾸준히 나오고 있습니다. 먼저 컴파일러의 성능 향상을 위해 JIT 컴파일러가 나왔습니다. 또한 JVM의 성능을 월등히 향상시킨 Hotspot 가상머신이 있습니다. 이외에도 원시 메쏘드 호출(JNI), 하드웨어로 만들어진 JVM을 통한 성능 향상 방법이 있습니다. 특히 가상머신의 경우 IBM, 오라클, 마이크로소프트를 비롯한 여러 회사에서 개발이 되고 있습니다.

초보자와 숙련자의 차이점은?
제 가 프로젝트를 하면 늘 느끼는 것이지만, 초보자와 숙련자가 짠 프로그램은 항상 차이가 나더군요. 숙련자의 프로그램은 같은 동작을 하는 프로그램이라도 초보자 보다는 코드의 라인 수도 적고, 간결하고 빠르게 작동합니다. 그래서 프로젝트가 끝나갈 즈음이면 코드를 왕창 뜯어고치기 마련입니다. 초보자와 숙련자의 코드의 차이점은 과연 어디에 있을까요?
바로 ‘불필요한 동작을 하지 않는 코드’, ‘최단 거리로 동작하는 프로그램 로직’이 답입니다. 즉 같은 프로그램이라도 어떤 알고리즘을 택하느냐, 얼마나 리소스를 사용해서 코딩하느냐에 달려 있습니다. 이를 위해 먼저 프로그램의 최적화 원칙과 일반적으로 적용될 수 있는 프로그램의 성능 향상 방법을 살펴보고, 자바 프로그램에 적용할 수 있는 방법을 살펴보겠습니다.

프로그램의 최적화 원칙
* 80/20 룰 : 이것은 프로그램 수행 시간의 80%를 실제 프로그램 코드의 20%가 잡아먹는다는 것을 의미합니다. 때문에 실제 프로그램 코드 중 수행 시간의 80%를 차지하는 그 일부 코드를 찾는 일이 중요하다는 것입니다.


* 빠른 알고리즘 : 같은 프로그램이라도 열번 수행하는 프로그램을 한 번만 수행해도 되도록 코딩한다면 훨씬 빠르겠죠. 평소에 다양한 로직으로 프로그램을 짜 보고, 어떻게 하면 더 간결하고 빠르게 돌아가는 코드를 만들 수 있을지를 많이 궁리해야 하겠습니다.


* 가벼운 데이터 구조 : 리소스를 적게 사용하는 것이 훨씬 빠르게 작동한다는 것은 당연한 이치일 것입니다. 불필요하게 메모리를 많이 쓰는 코딩은 지양합니다.


* 가독성과 최적화 : 프로그램의 최적화를 더 중요시 할지, 가독성을 더 중요시 할지를 잘 판단해야 합니다. 다음 예제 1은 가독성과 최적화의 딜레마라 볼 수 있습니다.

·예제 1
x >> 2 또는 x / 4, x << 1 또는 x * 2

일반적 최적화 기법
* Strength reduction : 앞의 예제 1처럼 동일한 값이 나오지만 더 빠르게 작동하는 수식을 사용하는 것을 말합니다. Shift 연산은 비트 단위로 값을 이동하는 것이지만 좌로 1비트 이동하면 곱하기 2, 우로 1비트 이동하면 나누기 2가 됩니다. 이때 곱하기나 나누기보다 Shift 연산이 훨씬 빠릅니다.


* Common sub expression elimination : 불필요한 수식을 제거하는 것을 말합니다. 다음의 예제 2를 보면 d * (lim/max)의 계산이 2번 수행되지만, 예제 3은 d * (lim/max)이 한 번 수행됩니다. 만약 이러한 계산이 복잡하고 오래 걸리는 것이라면 그 차이는 더욱 커지겠죠?

·예제 2
double x = d * (lim / max) * sx;
double y = d * (lim / max) * sy;

· 예제 3
double depth = d * (lim / max);
double x = depth * sx;
double y = depth * sy;

* Code motion : 이는 변하지 않는 값을 가진 코드 부분을 반복 수행되는 곳의 바깥으로 이동시키는 것을 말합니다. 예제 4에서 Math 클래스의 PI, cos가 계속 반복해서 호출되는데, 예제 5에선 이것을 picosy 변수에 담아서 반복문 내부에서 곱해 더합니다. 이는 Math 클래스의 PI, cos가 계속 반복해서 호출 회수를 한 번으로 줄여줍니다.

·예제 4
for (int i = 0; i < x.length; i++)
x[i] *= Math.PI * Math.cos(y);

·예제 5
double picosy = Math.PI * Math.cos(y);
for (int i = 0; i < x.length; i++)
x[i] *= picosy;

* Unrolling loops : 반복문 내에서 한 번 이상의 연산을 수행함으로써 반복 회수를 줄여 반복 제어의 부하를 줄여 주는 것을 말합니다. 앞서 예제 코드 4에서 x배열은 항상 2의 배수이기 때문에 예제 6처럼 제어변수 i를 2씩 증가시키면서 내부에서는 연산을 2번 수행합니다. 이는 결과적으로 연산 회수는 같으나 반복제어 회수가 줄어든다는 이점이 있습니다.

·예제 6
double picosy = Math.PI * Math.cos(y);
for (int i = 0; i < x.length; i += 2) {
x[i] *= picosy;
x[i+1] *= picosy;
}

메쏘드 호출 감소
같 은 프로그램이라도 메쏘드 호출이 잦으면 더 많은 연산을 수행하게 되며, 이는 실행 시간의 증가를 의미합니다. 게다가 자바에서는 동적인 메쏘드 호출을 지원하기 때문에 잦은 메쏘드 호출은 프로그램의 실행 속도를 더욱 저하시킵니다. 또한 메쏘드를 통해서만 데이터를 사용할 수 있는 데이터 캡슐화는 잦은 메쏘드 호출을 발생시켜 성능의 저하 요인이 됩니다.
이를 위해서는 컴파일시 인라인(inline)을 하는 -O 옵션을 사용합니다. 메쏘드 인라인은 코드 부분을 호출한 메쏘드로 이동시켜서 컴파일함으로써 메쏘드 호출의 부하를 줄여주는 것을 말합니다. 주의할 점은 static, private, final로 선언된 메쏘드만 인라인의 대상이 된다는 것입니다. 즉 static, private, final이라는 키워드를 가진 메쏘드는 정적으로 바인딩되어 인라인화될 수 있습니다. 메쏘드 인라인에 의한 메쏘드 호출 감소법을 살펴봅시다.

·예제 7
public class InlineMe{
int counter=0;
public void method1(){
for(int i=0;i<1000;i++){
addCount();
System.out.println("counter="+counter);
}
public int addCount(){
counter=counter+1;
return counter;
}
public static void main(String args[]){
InlineMe im=new InlineMe();
im.method1();
}
}
예제 7 코드에서 addCount() 메쏘드를 다음과 같이 수정해 볼까요.

public void addCount(){
counter=counter+1;
}
이렇게 수정할 경우 addCount() 메소드는 컴파일시 인라인되어서 실제 메쏘드를 호출하지 않고 같은 결과를 반환한다. 즉 method1()이 실제 수행될 때는 예제 8과 같이 수행됩니다.

·예제 8
public void method1(){
for(int i=0;i<1000;i++){
counter=counter+1;
System.out.println("counter="+counter);
}

객체를 적게 생성하자
임 시 객체를 빠른 캐시에 저장하는 C나 C++와 달리, 자바에서는 메모리의 힙 영역에 객체를 저장합니다. 따라서 임시 객체를 생성할 때마다 힙에 액세스해야 하므로 속도가 느려집니다. 또한 임시 객체에 대한 가비지 컬렉션의 오버헤드가 큽니다. 이를 위해서는 다음과 같이 코딩할 것을 권장합니다. 

* 될 수 있는 한 불필요한 임시 객체의 생성을 줄입니다. 
* new를 통한 생성자 호출대신 static 메쏘드를 사용합니다. 다음의 예제 9에서는 Integer 클래스를 생성한 다음 string에서 정수 값을 추출해냈습니다. 예제 10에서는 Object의 Instance가 필요없는 static 메쏘드를 사용했습니다.

·예제 9
String string="55";
int theInt=new Integer(string).intValue();

·예제 10
String string="55";
int theInt=Integer.parseInt(string);

*  루프문이나 자주 불리워지는 메쏘드에서의 객체 생성을 피합니다. 다음의 예제 11에서는 반복문 내부에서 무려 1000번이나 Date라는 객체를 만들지만 예제 12에서는 한 개의 Date 객체에 계속 값을 할당하고 지우고 하는 식으로 사용합니다.

·예제 11
for(i=0;i<1000;i++){
Date a=new Date();
................
}

·예제 12
Date a;
for(i=0;i<1000;i++){
a=new Date();
.....
a=null;
}

* 디폴트 값으로 저절로 초기화가 되는 인스턴스 변수들을 프로그램에서 또다시 디폴트 값으로 초기화하여 객체 생성 시간을 더 늘리지 않도록 합니다.

필요할 때만 동기화
동 기화를 위해서 쓰레드 모니터는 백그라운드로 계속 관리작업을 수행합니다. 이때 많은 경우 쓰레드들이 동기화된 메쏘드나 블럭에 들어가지 못하고 대기하고 있게 되어 수행시간이 길어지게 됩니다. 따라서 꼭 필요할 때가 아니면 동기화를 사용하지 않습니다. 동기화되지 않은 메쏘드를 호출하는 것이 동기화된 메쏘드 호출보다 10배 정도 빠릅니다.

보다 빠른 연산자의 사용
같은 일을 하더라도 더 빠른 연산자를 사용합니다. 증감연산은 ++나 --가 +1, -1보다 훨씬 빠르고, shift 연산이 곱셈이나 나눗셈보다 빠릅니다.

캐스팅의 지향
캐 스팅을 하면 컴파일 시간에 그 타입이 결정될 수 없기 때문에 실행 시간을 느리게 만듭니다. 인터페이스를 캐스팅할 경우 더욱 많은 실행 시간을 필요로 하게 됩니다. 이를 위해서 같은 객체를 여러 번 캐스팅할 필요가 있을 경우 지역 변수에 저장해서 사용합니다. 캐스팅을 될 수 있는 한 피하고, 가장 빠른 변수 타입은 int이기 때문에 불가피한 경우를 제외하고는 int를 사용하는 것이 좋습니다.

빠른 변수 타입의 사용
변수의 성능은 그것의 범위와 타입에 의해서 결정됩니다. 가장 빠른 변수는 지역 메쏘드 변수이며, 가장 빠른 변수 타입은 int와 참조 변수입니다. 또한 어떻게 배열을 초기화시킬지가 중요한 요소가 됩니다. 다차원 배열로 정의할 경우 매번 생성자를 호출하기 때문에 꼭 필요한 경우가 아니면 다차원 배열로 정의하지 않습니다. 배열이 지역변수일 경우 메쏘드 호출시 매번 초기화를 수행하므로, 배열을 static으로 선언하면 초기화가 반복되는 것을 제거할 수 있습니다.

반복의 최적화
루프의 실행 시간은 얼마나 많이 루프 구문을 반복하느냐와 한 번 반복시 얼마나 많은 양의 일을 하는지에 따라 달라집니다. 루프에서의 인덱스는 로컬 int를 사용하는 것이 좋습니다. 또한 필요없는 메쏘드 호출이나 객체 생성을 루프문 안에서 사용하지 않습니다.
반복문 내에서 배열의 바운드를 체크해야 할 경우, try-catch 구문을 써서 ArrayIndexOutOfBounds Exception이라는 예외 상황(Exception)을 사용하면 비교를 수행할 필요가 없기 때문에 실행 시간이 줄어듭니다. 이는 배열의 바운드 체크가 비용이 드는 동작이기 때문입니다.
또한 반복문 내의 비교 상대값을 메쏘드 호출해서 계산해 오는 것이라면 다음처럼 미리 지역 변수에 그 값을 저장한 후에 그것을 사용하는 것이 좋습니다.

for(int k=0; k< s.length(); k++) 보다는 int limit = s.length(); for(int k=0; k

스트링보다는 스트링 버퍼를 사용
자 바에서 String은 한 번 생성되면 변하지 않는 성질(immutable)을 가지고 있습니다. 따라서 스트링들 사이에 + 연산을 수행하면 새로운 스트링을 생성하고, 양쪽 스트링의 내용을 복사한 후 앞의 스트링을 가비지 컬렉션합니다. 이에 따른 부하가 많아지므로 스트링의 연산이 필요할 경우, String 대신 고정적이지 않은 Stri ngBuffer를 사용하는 것이 좋습니다. 다음의 예제 13은 + 스트링 연산의 예이며, 에제 14는 StringBuffer를 이용한 append 연산을 통해 코드를 개선한 것입니다.

·예제 13
String a="Hello";
a=a+"World";
System.out.println(a);

·예제 14
StringBuffer a=new StringBuffer();
a.append("Hello");
a.append("World");
System.out.println(a.toString());

입출력 버퍼렁
즉 데이터를 입출력할 경우 Buffer를 사용해서 많은 양의 데이터를 내부 버퍼에 저장해서 사용하면 더욱 성능을 올릴 수 있습니다. InputStream을 리턴하는 호출을 할 경우, InputStream은 read()를 호출하고 이것은 하나의 byte나 문자를 리턴하는데, 이것에 대한 BufferedInputStream을 호출하면 이는 Input Stream과 똑같이 수행하지만, 많은 양의 데이터를 내부 버퍼에 저장함으로써 나중에 데이터를 읽어올 때 디스크 등과 같은 느린 소스로부터 데이터를 읽어올 필요가 없게 됩니다. 마찬가지로 BufferedOutputStream은 쓰여질 값을 버퍼에 저장했다가 버퍼가 가득찼을 때나 flush()가 호출되었을 때 실제로 쓰기 작업을 수행함으로써, 매번 직접 쓸 때 발생하는 오버헤드를 줄일 수 있습니다. 예제 15는 버퍼를 사용하지 않은 경우이고 예제 16은 버퍼를 사용한 코딩의 예입니다.

·예제 15
InputStream in=null;
OutputStream out=null;
try{
in=new FileInputStream(from);
out=new FileOutputStream(to);

·예제 16
InputStream in=null;
OutputStream out=null;
try{
in=new BufferedInputStream(
new FileInputStream(from));
out=new BufferedOutputStream(
new FileOutputStream(to));

객체 재사용
객 체를 미리 필요한 만큼 준비해두고서 이를 관리하는 클래스를 통해서 얻어오는 것을 말합니다. 실제 프로젝트에서는 커넥션 풀을 많이 사용하는데 이것도 객체 재사용에 해당합니다. 예제 17과 같이 사용할 경우 하나의 인스턴스 변수를 사용하기는 하지만, 두 번의 초기화 과정을 거치게 됩니다. 예제 18을 보면 setLength라는 메쏘드를 통해서 객체를 초기화하지 않고서 계속 사용할 수 있습니다.

·예제 17
StringBuffer sb=new StringBuffer();
sb.append("Hello");
out.println(sb.toString());
sb=null;
sb=new StringBuffer();
sb.append("World");
out.println(sb.toString());

·예제 18
StringBuffer sb=new StringBuffer();
sb.append("Hello");
out.println(sb.toString());
sb.setLength(0);
sb.append("World");
out.println(sb.toString());

빠르고 세련되게 코딩하자
이 번 연재에서는 자바 코드를 더욱 빠르고 세련되게 하기 위한 방법을 살펴보았습니다. 빠르고 세련된 코드는 곧 프로그래머의 습관입니다. 항상 머릿속에 두고서 어떻게 하면 더 간결하고 빠르게 작동할 수 있도록 하는 코딩을 할 것인가를 궁리하면 여러분들의 코드는 점점 나아지리라 확신합니다. 다음 연재에서는 자바 코드를 다시 사용하기 위한 방법, 즉 객체지향적으로 자바를 다루는 방법을 살펴보도록 하겠습니다.

정리 : 이종림 nowhere@sbmedia.co.kr

성능 향상된 컴파일러, JIT

< 그림 1>을 보면 JIT(Just-In-Time) 컴파일러는 JVM의 기능을 수행하면서 한 번 기계어로 번역된 코드를 기억하고 있어서 다시 실행할 경우에는 번역과정을 거치지 않고 바로 기계어를 실행해 줍니다. 즉 어떤 명령을 처음 실행할 때 최적의 기계어로 컴파일해둔 다음, 그 명령이 다시 사용될 때마다(컴퓨터 프로그램에는 많은 루프가 있지요) 그 최적화된 것을 참조하게 되므로 엄청난 속도 향상이 이루어지게 됩니다. 한 번 생성된 코드는 다음번 해당 바이트 코드가 또 실행될 경우 다시 사용할 수 있으므로, 여러 번 수행하는 계산 프로그램 등에 적합하나, 초기화 과정이 많고 반복 수행이 없는 프로그램이라면 오히려 인터프리터보다 더 느릴 수도 있습니다.
JIT 컴파일러는 처음으로 JDK 1.1.6가 나왔을 때 성능 향상을 위해 사용되었고 현재 나오는 JVM 대부분에 JIT 포함되어 있습니다. 자바 명령어에 -Djava.compiler=NONE 옵션을 사용함으로써 JIT 컴파일러를 사용하지 않도록 할 수도 있습니다.

JVM 성능 개선한 Hotspot 가상머신
자 바 Hotspot 가상머신은 동적 컴파일러로서 JIT 컴파일러와 인터프리터를 결합한 것입니다. 이것은 프로그램 실행중에 그 실행 형태를 분석하여 병목이 발생하는 부분을 찾아내어 최적화시켜 줍니다. 자바 Hots pot은 자바 2 가상머신과 같은 바이트 코드를 사용하나, JVM의 속도를 향상시키기 위해서 적응적인 최적화 기법(adaptive optimization)을 적용하였고, 가비지 컬렉션과 쓰레드 동기화에서 속도를 개선하였습니다.
적응적인 최적화 기법은 프로그램이 작동할 때 Hotspot 가상머신은 계속 그 프로그램의 성능을 메쏘드별로 모니터링하면서 시간이 많이 걸리는 메쏘드들을 빠르게 찾아내 최적화를 수행하는 것입니다. 가비지 컬렉션에서 성능 향상은 직접적인 메모리 참조를 통해서 가비지 컬렉션에서 메모리 할당 및 해제에 소요되는 부하를 감소시킴으로써 가능하게 되었습니다. 또한 generational memory-management을 사용하며 메모리 누수를 줄여줍니다. 또한 쓰레드 동기화를 보다 빠르게 수행하여 쓰레드 동기화에 드는 시간을 줄여줍니다.
<그림 2>에서 Hospot 가상머신은 자바 컴파일러에 의해 생성된 바이트 코드는 JVM에 의해 인터프리트되고, 이것이 실행되면, 프로파일러가 성능정보를 추적하고 컴파일을 위한 메쏘드를 선택합니다. 컴파일된 메쏘드들은 순수 기계어 캐시에 저장됩니다. 메쏘드가 호출될 때, 그것이 존재하면 순수 기계어 코드 버전이 사용됩니다. 반면에 바이트 코드들은 재번역됩니다.

이 달의 숙제

지금까지 배운 것을 활용하는 프로그램을 만들어 봅시다.

* 여러분이 코딩한 프로그램을 최적화의 수행 시간을 측정해 보세요. 코드의 시작과 끝에 다음의 코드를 넣고 수행해 보세요.

long start = System.currentTimeMillis();
long end = System.currentTimeMillis() -
start;

* 다음의 메쏘드를 사용하여 여러분이 코딩한 프로그램의 메모리 사용량을 측정해 보세요.

long freemem () {
System.gc();
return Runtime.getRuntime().freeMemory();
}