달력

092017  이전 다음

  •  
  •  
  •  
  •  
  •  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
Core Java Technologies Technical Tips
 Core Java Technologies Tech Tip에 오신 여러분을 환영합니다
Core Java Technologies
TECHNICAL TIPS
2006년 2월 8일자
 이번 호에서는,

» Singleton 패턴
» Observer 패턴

을 다룹니다.

이 문서는 Java 2 Platform, Standard Edition Development Kit 5.0 (JDK 5.0)을 기반으로 개발되었습니다. PDF 파일받기    

Singleton 패턴
 

설계 패턴은 소프트웨어 설계에서 상에서의 공통 문제에 대한 일반적인 솔루션이라 할 수 있는데, 그 기본 개념은 솔루션을 코드로 변환하면 그 코드를 다양한 문제 상황에 적용할 수 있다는 것이다. 설계 패턴에 관한 논의는 Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides 등이 공동 집필한 Design Patterns: Elements of Reusable Object-Oriented Software에서 시작되었다. 이 책에서는 패턴을 생성, 구조, 동작의 세 가지 주요 영역이 포함하여 다양한 주제 영역으로 분류하고 있다. 생성 패턴(Creational patterns)은 객체가 생성되는(객체 지향 용어로는 ‘예시되는(instantiated)’) 방식을 기술한다. 구조 패턴(Structural patterns)은 객체들을 연결하고 결합하는 방식을 도와주고, 동작 패턴(Behavioral patterns)은 알고리즘 또는 통신 메커니즘을 기술한다. 몇 가지 공통 패턴의 이름으로는 생성 영역의 Singleton, 동작 영역의 Observer, 구조 영역 Facade를 들 수 있다. 본 테크 팁에서는 자바 프로그래밍 언어의 맥락에서 Singleton 패턴을 면밀히 살펴보도록 한다.

Singleton 패턴은 흔히 사용되는 생성 패턴의 하나이다. 이는 하나의 클래스에서 오직 하나의 인스턴스만 생성되도록 보장하는 기법을 기술한다. 이는 본질적으로, 클래스 외부의 누구도 객체의 인스턴스를 생성하지 못하게 하는 접근법을 사용한다. 일반적으로 Singleton은 느리게 생성되어 필요 시점까지 메모리의 요구를 줄여준다. 이 접근법은 다양한 방식으로 구현이 가능하다.

생성되고 있는 하나의 인스턴스가 서브클래스가 되리라는 것을 알고 있다면, 상위(parant) 클래스를 추상화하고 메소드를 제공하여 현재의 인스턴스를 얻도록 한다. 그 일례로 AWT 패키지의 Toolkit 클래스를 들 수 있으며, Toolkit을 위한 생성자는 public(이 특정 경우에는 디폴트 생성자)이다.
public Toolkit()그리고 클래스는 특정 서브클래스(이 경우에는 플랫폼을 따름)를 얻기 위한 getDefaultToolkit() 메소드를 가진다.
public static Toolkit getDefaultToolkit()썬 자바 런타임을 탑재한 Linux 플랫폼 상에서 특정 서브클래스는 sun.awt.X11.XToolkit 타입이다. 하지만 사용자는 공통 추상 상위 클래스인 Toolkit을 통해 클래스에 액세스할 뿐이므로 이 부분까지 알아야 할 필요는 없다.

Collator 클래스는 이 패턴의 또 다른 예로, 약간의 차이가 있다. 이 클래스는 2개의 getInstance() 메소드를 제공하고, 무인자(no-argument) 버전은 디폴트 locale을 위한 Collator를 얻는다. 이 때, 사용자는 자체 locale을 전달하여 해당 locale을 위한 Collator 인스턴스를 얻을 수 있다. 동일 locale에 대한 Collator를 여러 차례 요청해도 동일한 Collator 인스턴스를 돌려받게 되며, 생성자 자체는 보호된다. 한편, J2SE 표준 라이브러리 전반에 걸쳐 클래서 생성을 제한하는 유사한 방식을 발견할 수 있다.

이 시점에서 클래스 생성자에 대한 액세스를 제한하면 자동적으로 Singleton이 된다고 생각할 수도 있겠으나, 그렇지 않다. 문제가 되는 케이스는 Calendar 클래스인데, Calendar 클래스 생성자는 보호되며, 이 클래스는 클래스 인스턴스를 얻기 위한 getInstance() 메소드를 제공한다. 그러나 getInstance() 메소드를 호출할 때마다 새로운 클래스 인스턴스가 얻어짐으로 결국 이는 Singleton이 아닌 것이다.

사용자의 자체 Singleton 클래스 생성 시에는 오직 하나의 인스턴스만 생성되도록 유의해야 한다.
public class MySingleton { private static final MySingleton INSTANCE = new MySingleton(); private MySingleton() { } public static final MySingleton getInstance() { return INSTANCE; } }정적 메소드 getInstance()는 단일 클래스 인스턴스를 리턴한다. 단일 인스턴스가 서브클래스일 필요가 있더라도 API를 변경할 필요는 없다는 점에 주목할 것.

이론적으로는 INSTANCE 변수가 public일 수 있으므로 getInstance() 메소드는 필요치 않으나 getInstance() 메소드는 향후에 시스템을 변경할 경우 유연성을 제공한다. 바람직한 가상 머신 구현이라면 정적 getInstance() 메소드에 대한 호출을 즉시 처리(inline)해야 한다.

Singleton 생성 작업은 이것으로 그치지 않는다. 즉, Singleton 클래스를 Serializable로 만들 필요가 있다면 반드시 readResolve() 메소드를 제공해야 한다.
/** * Ensure Singleton class */ private Object readResolve() throws ObjectStreamException { return INSTANCE; }readResolve() 메소드가 갖추어진 상태에서 deserialization은 단일(오직 하나의) 객체(getInstance() 메소드에 대한 호출에 의해 생성되는 것과 동일한 객체이다)로 귀결되는데, 사용자가 readResolve() 메소드를 제공하지 않을 경우에는 객체를 deserialize할 때마다 객체 인스턴스가 생성된다.

Singleton 패턴은 오직 단일한 리소스만 가지고 있고 그 단일 리소스의 상태 정보에 대한 액세스를 공유할 필요가 있음을 알고 있을 경우에 유용하다. 설계 시에 Singleton 패턴의 필요성을 파악하면 개발을 간소화할 수 있다. 하지만, 때로는 성능 문제로 코드를 refactor하고 나중에 패턴을 사용하게 되기까지 필요성을 인식하지 못하는 수가 있다. 예를 들어, 프로그램이 동일한 클래스의 인스턴스를 반복 생성하여 상태 정보와 함께 전달하기 때문에 시스템 성능이 저하되는 경우가 발생할 수 있다. Singleton 패턴으로 변경하면 동일한 객체가 반복되는 것을 방지할 수 있는데, 이는 시스템이 객체를 재생성하는데 드는 시간을 제거해줄 뿐 아니라 garbage collector가 인스턴스들을 삭제하는 데 소요되는 시간을 줄여준다.

간단히 말해서, 단일의, 그리고 오직 하나의 클래스 인스턴스만 생성되도록 하고자 할 때 Singleton 설계 패턴을 이용하면 된다. 생성자가 연산을 요구하지 않을 경우에는 빈 private 생성자(또는 서브클래스가 필요할 경우에는 보호된 생성자)를 제공한다. 그렇지 않으면 디폴트값으로 시스템이 public 생성자를 제공하게 되는데, 이는 Singleton으로 작업 시 바람직하지 않은 결과라 할 수 있다.

Singleton은 주어진 클래스 로더 내에서만 고유성이 보장된다는 점에 유의할 것. 복수의 서로 다른 엔터프라이즈 컨테이너에 걸쳐 동일한 클래스를 사용할 경우에는 각 컨테이너에 대해 하나의 인스턴스를 얻게 된다.

Singleton 패턴은 종종 Factory 패턴이라 불리는 다른 패턴과 함께 사용되는데, Factory 패턴도 Singleton 패턴과 마찬가지로 생성 패턴의 일종이다. 이 패턴은 특정 객체의 서브클래스, 또는 보다 일반적으로 특정한 인터페이스의 구현이 어떻게 실제로 객체를 생성하는지 기술한다. Factory 패턴의 좋은 보기로 Swing BorderFactory 클래스를 들 수 있다. 이 클래스는 다양한 종류의 Border 객체를 리턴하는 일련의 정적 메소드를 가지는데, 서브클래스의 구현 세부사항을 숨겨서 factory가 인터페이스 구현을 위한 생성자를 직접 호출할 수 있게 해준다. 다음은 BorderFactory의 사용 예제이다.
Border line = BorderFactory.createLineBorder(Color.RED); JLabel label = new JLabel("Red Line"); label.setBorder(line);여기서 BorderFactoryLineBorder를 생성한다는 사실이나 그 생성 방법은 숨겨져 있다. 이번 특정 예제에서는 LineBorder 생성자를 직접 호출할 수 있지만 Factory 패턴을 이용하는 대부분의 경우에는 직접 호출이 불가능하다.

Singleton 패턴을 구현하는 클래스는 다른 클래스의 인스턴스를 생성하기 위해 Factory로 사용할 객체를 리턴하는 경우가 흔히 있는데, 이는 PopupFactory 클래스에 의한 Popup 객체 생성 방식에 의해 예증된다.

Singleton factory를 얻으려면 PopupFactorygetSharedInstance() 메소드를 호출한다.
PopupFactory factory = PopupFactory.getSharedInstance();그런 다음 factory의 getPopup() 메소드를 호출하여 factory에서 Popup 객체를 생성하고, 상위 컴포넌트, 그 콘텐츠, 포지션을 전달한다.
Popup popup = factory.getPopup(owner, contents, x, y);보안 컨텍스트에서 Factory 패턴이 자주 사용되는 것을 볼 수 있을 것이다. 다음 예제에서는 특정 알고리즘에 대한 certificate factory를 획득한 다음 stream certificate를 생성한다.

FileInputStream fis = new FileInputStream(filename); CertificateFactory cf = CertificateFactory.getInstance("X.509"); Collection c = cf.generateCertificates(fis);BorderFactory에서 본 것처럼, Factory 패턴이 반드시 Singleton 패턴과 함께 사용되어야 하는 것은 아니지만 실제로는 두 패턴이 함께 사용되는 경우도 종종 볼 수 있다.

맨 위로

Observer 패턴
 

앞서 다룬 Singleton 패턴과 마찬가지로, Observer 패턴은 자바 프로그램에서 많이 사용되는 설계 패턴이다. 이 패턴은 동작 설계 패턴으로, 클래스가 느슨하게 연결되는 방식과 타 클래스 업데이트 시 하나의(또는 다수의) 클래스가 통지되는 방식을 정의한다. 기본적으로, 이는 특정 장소에서 무슨 일이 발생할 경우, 이를 보고있었거나 관심을 가지고 있던 사람들에게 상황을 통지하는 것을 의미한다.

Observer 패턴을 취급하는 방식에는 두 가지가 있는데, 첫 번째 방식은 java.util package에서 볼 수 있는 ObserverObservable 클래스를 수반하는 것이고, 두 번째 방식은 컴포넌트에 이벤트 리스너를 등록하는 JavaBeans 컴포넌트 모델을 따르는 것이다.

JavaBeans 이벤트 모델 생성에 앞서 ObserverObservable 클래스는 Observable 패턴의 구현을 기술한다. 달리 말해서 이 클래스들은 Java 플랫폼 1.0 버전 때부터 사용되어 왔고, 기술적으로 하자가 없었으며 여전히 라이브러리에 존재하고 있는 것이다. 또한, 이 클래스들은 여전히 Observable 패턴 구현에 사용할 수는 있지만, 두 번째 모델인 JavaBeans 컴포넌트 모델이 일반적으로 사용된다. Observable 패턴 구현을 위해 이 클래스들을 사용하는 데 따른 한 가지 중요한 문제는 Observable 확장이 필요하다는 점인데, 이 경우 자바 플랫폼의 단일 상속 세계에서는 불가능할 수도 있는 클래스 하이어라키 구조를 강요 받게 된다.

이벤트 리스너를 등록하는 JavaBeans 컴포넌트 모델은 일련의 add 및 remove 메소드를 수반하는데, 여기서 리스너 타입은 메소드 이름에 내장되어 있다. 예를 들어, 버튼의 선택을 관찰하기 위해서는 컴포넌트에 ActionListener를 등록한다.
ActionListener listener = new ActionListener() { public void actionPerformed(ActionEvent actionEvent) { ... } }; JButton button = new JButton("Pick Me"); button.addActionListener(listener);시스템 정의 클래스를 위한 Observer 패턴은 이것이 전부라고 할 수 있는데, 리스너 인터페이스를 구현하고, 그것을 관찰 Subject에 첨부한 다음 기다린다. Subject는 관찰되는 대상으로, 누가 관찰하고 있는지를 기억하는 일을 책임진다. JavaBeans 컴포넌트 모델의 경우, Observer 객체를 첨부, 분리하기 위한 인터페이스로 add/remove 리스너 네이밍 패턴이 사용된다. Subject의 상태가 변경되면 이를 Observer 객체에 통지한다.

패턴의 주된 목표 중 하나는 Subject와 Observer의 느슨한 연결을 가능케 하는 것이다. JButton이 선택되면, ButtonNotification이라 불리는 가상 서브클래스의 특정 메소드를 호출하는 대신 누구나 구현할 수 있는 인터페이스로 통지가 추상화된다. JButton은 첨부된 Observer(리스너)가 어떤 클래스인지 전혀 개의치 않는데, 실제로 버튼은 구현 클래스가 수정되더라도 오로지 Observer가 리스너를 구현한다는 사실에만 관심을 가진다.

Observer 패턴 사용 시 주의할 필요가 있는 여러 가지 복잡한 문제들이 있다. 첫째는 메모리 누출의 가능성이다. Observer에 대한 레퍼런스는 Subject에 의해 관리되는데, Subject가 레퍼런스를 해제할 때까지는 garbage collector로 Observer를 제거할 수 없다. 이런 가능성에 유의하여 적절한 상황에서 Observer를 제거해야 한다. 아울러 (적어도 이벤트 리스너를 등록할 때는) 일련의 Observer 객체가 unordered collection에서 관리된다는 점에 주목할 것. 먼저 등록된 리스너가 먼저 통지되는지 마지막에 통지되는지를 반드시 알아야 할 필요는 없으나 반드시 객체 A가 먼저 통지되고 이어서 객체 B가 통지되는 cascading 방식의 통지가 필요한 경우에는 중간 객체를 도입하여 순서가 지켜지도록 해야한다. 단지 Observer를 특정 순서로 등록한다고 해서 반드시 그 순서에 따라 통지가 이루어지는 것은 아니다.

Observer 패턴을 모델링하는 자바 플랫폼의 또 다른 영역으로는, guaranteed delivery, non-local distribution, persistence 등의 이점을 갖춘 JMS(Java Message Service)를 들 수 있다. JMS publish-subscribe 메시징 모델은 무한한 수의 가입자가 관심 있는 주제를 청취할 수 있게 해주는데, publish된 주제에 대한 메시지가 생성되면 모든 가입자들에게 내용이 통지된다.

그 밖에도 자바 플랫폼에는 Observer 패턴을 모델링하는 여러 다른 분야가 있으며, 이 패턴은 자바 플랫폼 전반에 걸쳐 자주 사용된다.

1995년설계 패턴(Design Patterns)이 출간된 이래로 이 패턴들에 대해 다양한 관점을 제시하고 추가 패턴을 소개하는 다른 책들이 발표된 바 있다. 그 중 다음 두 가지가 인기를 끌었다. 또한, 보다 심도 있는 설계 패턴에 관심이 있는 독자를 위해 기업 애플리케이션 아키텍처의 패턴(Patterns of Enterprise Application Architecture) 같은 책들도 나와 있다.

설계 패턴 일반에 관한 자세한 내용을 보려면 Wikipedia 엔트리(영문)을 참조하기 바란다.


맨 위로

본 메일은 수신을 동의한 회원님에게만 발송됩니다.
본 메일의 수신을 거부하거나 수신주소를 변경하려면 SKDN@Sun.com으로 문의 주시기 바랍니다.

SKDN(Sun Korea Developers Network)에서 J2EE/J2SE 테크팁 등 다양한 아티클들을 참고하세요.

Copyright 2003-2006 Sun Korea, Ltd. All rights reserved.


신고
Posted by Tornado tornado
  Core Java Technologies Tech Tip에 오신 여러분을 환영합니다
Core Java Technologies
TECHNICAL TIPS
2006년 4월 21일자
  이번 호에서는,

» 애플릿 콘텍스트 스트림으로 작업하기
» Singleton 패턴에 대한 재고찰

에 대해 다룹니다.

이 문서는 Java 2 Platform, Standard Edition Development Kit 5.0 (JDK 5.0)을 기반으로 개발되었습니다. PDF 파일받기   

애플릿 콘텍스트 스트림으로 작업하기
 

요즘 애플릿에 대한 논의가 많지는 않으나 J2SE 1.4에서는 java.applet 패키지의 AppletContext 클래스에 세 가지 메소드가 추가되었다. 많은 사람들이 추가된 사실을 알아채지 못할 수도 있지만, 이 메소드들은 유용한 기능을 제공한다. 즉, 데이터를 스트림에 저장하고 각 스트림을 지정된 키(named key)에 매핑하는 것이다.

정보 저장을 위한 메인 메소드는 setStream()이다.
public void setStream(String name, InputStream stream) 스트림은 저장 시 키값 (Map과 유사한) 구조의 키에 연결되고, 매핑은 애플릿의 코드베이스에 한정된다. 이 말은 하나의 호스트에서 나온 애플릿이 다른 호스트의 스트림에 액세스하지 못한다는 것을 의미한다.

스트림이 저장된 후에는 싱글 스트림 또는 스트림 전체를 이용하여 이를 검색할 수 있다. 싱글 스트림을 얻으려면 getStream() 메소드를 이용하여 이름별로 요청을 한다.
public InputStream getStream(String name) getStreamKeys() 메소드를 이용하여 모든 스트림을 검색하는데, 이 작업을 수행할 때는 Map으로 돌아가지 않는다. 대신, 다음과 같은 String 이름의 Iterator를 얻는다.
public Iterator getStreamKeys() 사용자가 원하는 특정 스트림의 키 이름을 얻은 후에는 getStream() 메소드를 이용하여 해당 스트림을 얻는다. 이 때, 일반적으로 사용되는 패턴은 다음과 같다.
Iterator iter = getAppletContext().getStreamKeys(); if (iter != null) { while (iter.hasNext()) { String name = iter.next(); InputStream stream = getAppletContext().getStream(name); // read stream... } } 이들이 인풋 스트림이라는 점을 유념할 것. 문자로 작업하고자 하는 경우에는 반드시 문자 세트를 이용해야 한다. 예를 들어 String 오브젝트를 저장하려면 바이트를 얻어 AppletContext 내의 ByteArrayInputStream에 저장한다.
String message = ...; ByteArrayInputStream bais = new ByteArrayInputStream(message.getBytes("UTF-8")); getAppletContext().setStream("key-name", bais); 이 오브젝트를 읽으려면 스트림을 얻어 이를 InputStreamReader로 변환할 때 동일 문자 세트를 전달한다. Reader 오브젝트를 얻은 후에는 다음 예제처럼 문자를 읽을 수 있다.
InputStream stream = getAppletContext().getStream("key-name"); InputStreamReader isr = new InputStreamReader(stream, "UTF-8"); BufferedReader reader = new BufferedReader(isr); String line = reader.readLine(); API는 사실상 이것이 전부라 할 수 있다. 'setStream() 메소드를 이용하여 새로운 스트림을 저장하고, getStream()을 이용하여 스트림을 되돌린다. getStreamKeys()를 이용하여 스트림 키 세트를 얻는다. ' 이것이 스트림에 관해 알아 두어야 할 전부인 것처럼 보일지 모르지만, 그렇다면 스트림 컨텐츠를 제거하려면 어떻게 해야 할까? 그 답은 다음과 같다. 특정 키와 연결된 InputStream으로 null을 전달한다. 이렇게 하면 스트림의 컨텐츠가 시스템에서 제거된다.
getAppletContext().setStream("key-name", null); 이번에는 API의 데모를 살펴보기로 하자. 먼저, 프로그램 로더인 HTML 파일을 생성해야 한다. 200x200의 디스플레이 면적을 필요로 하는 StreamsApplet으로 명명된 애플릿의 경우, HTML 파일에는 다음과 같은 애플릿 태그가 포함되어야 한다.
<applet code=StreamsApplet width=200 height=200> </applet> StreamsApplet 애플릿은 키값 쌍을 위해 2개의 텍스트 필드를 제공하는데, 이 때 키는 lookup name이며 값은 저장될 InputStream 컨텐츠이다. 아울러 애플릿은 2개의 버튼을 디스플레이한다. 첫 번째 버튼은 명명된 스트림을 추가하고(또는 기존의 것을 업데이트한다), 두 번째 버튼은 명명된 스트림을 제거한다. 애플릿은 JList에 현재의 이름 세트를 표시한다.

Streams Demo


다음은 사용자 인터페이스 생성에 사용되는 코드이다.
import javax.swing.*; import javax.swing.event.*; import java.awt.*; import java.awt.event.*; import java.io.*; import java.util.*; public class StreamsApplet extends JApplet { private static final String CHARSET = "UTF-8"; JButton add; JButton remove; JList list; JTextField key; JTextField value; public void init() { JLabel keyLabel = new JLabel("Key"); keyLabel.setDisplayedMnemonic('K'); key = new JTextField(); keyLabel.setLabelFor(key); JLabel valueLabel = new JLabel("Value"); valueLabel.setDisplayedMnemonic('V'); value = new JTextField(); valueLabel.setLabelFor(value); JPanel topPanel = new JPanel(new GridLayout(2,2)); topPanel.add(keyLabel); topPanel.add(key); topPanel.add(valueLabel); topPanel.add(value); add(topPanel, BorderLayout.NORTH); list = new JList(); list.setSelectionMode (ListSelectionModel.SINGLE_SELECTION); JScrollPane pane = new JScrollPane(list); add(pane, BorderLayout.CENTER); add = new JButton("Add/Update"); add.setDisplayedMnemonic('A'); remove = new JButton("Remove"); remove.setDisplayedMnemonic('R'); JPanel bottomPanel = new JPanel(); bottomPanel.add(add); bottomPanel.add(remove); add(bottomPanel, BorderLayout.SOUTH); } } 이제 스트림 추가와 업데이트를 위한 액션을 추가해 보자. Add/Update 버튼 뒤의 ActionListener는 각각의 텍스트 필드에서 이름과 스트림 컨텐츠를 획득한 다음 이를 AppletContext에 저장해야 한다. 스트림을 추가한 후에는 JList에 스트림 목록이 표시되고 ActionListener가 텍스트 필드를 소거해야 한다.
String keyText = key.getText(); String valueText = value.getText(); try { ByteArrayInputStream bais = new ByteArrayInputStream(valueText.getBytes(CHARSET)); getAppletContext().setStream(keyText, bais); } catch (IOException ioe) { JOptionPane.showMessageDialog(StreamsApplet.this, "Unable to save", "Error", JOptionPane.ERROR_MESSAGE); } updateList(); key.setText(""); value.setText(""); updateList() 메소드는 상당히 간단한데, 단순히 이름 목록을 얻어 이를 JList에 넣기만 하면 된다.

DefaultListModel model = new DefaultListModel(); Iterator<String> iter = getAppletContext().getStreamKeys(); if (iter != null) { while (iter.hasNext()) { model.addElement(iter.next()); } } list.setModel(model); Remove 버튼 뒤의 ActionListener는 키 텍스트 필드의 모든 이름에 대해 단순히 스트림을 null로 설정한다. 이 경우에도 제거 후에 이름 목록을 업데이트하고 텍스트 필드를 소거해야 한다.

String keyText = key.getText(); try { getAppletContext().setStream(keyText, null); } catch (IOException ioe) { JOptionPane.showMessageDialog(StreamsApplet.this, "Unable to clear", "Error", JOptionPane.ERROR_MESSAGE); } updateList(); key.setText(""); value.setText(""); 이것으로 작업이 모두 완료된 것은 아니다. 목록에서 이름을 선택하면 현재의 값이 표시되는데, 이 작업은 ListSelectionListener를 통해 이루어진다. 리스너는 JList에서 선택된 값을 획득한 다음 애플릿 컨텍스트에서 스트림을 룩업한다. 이름을 찾을 수 없으면 getStream() 메소드는 null을 리턴한다. 단, JList에는 스트림과 일치하는 이름만 포함되므로 확인 작업이 필요하지는 않다.
String selection = (String)list.getSelectedValue(); try { InputStream stream = getAppletContext().getStream(selection); InputStreamReader isr = new InputStreamReader(stream, CHARSET); BufferedReader reader = new BufferedReader(isr); String line = reader.readLine(); key.setText(selection); value.setText(line); } catch (IOException ioe) { JOptionPane.showMessageDialog(StreamsApplet.this, "Unable to read", "Error", JOptionPane.ERROR_MESSAGE); } 이것을 모두 합치면 애플릿 컨텍스트에 명명된 스트림을 저장할 완벽한 애플릿이 구현된다. 다음은 모든 리스너를 각각의 해당 컴포넌트에 첨부하기 위한 소스의 전체 내용이다.
import javax.swing.*; import javax.swing.event.*; import java.awt.*; import java.awt.event.*; import java.io.*; import java.util.*; public class StreamsApplet extends JApplet { private static final String CHARSET = "UTF-8"; JButton add; JButton remove; JList list; JTextField key; JTextField value; public void init() { JLabel keyLabel = new JLabel("Key"); keyLabel.setDisplayedMnemonic('K'); key = new JTextField(); keyLabel.setLabelFor(key); JLabel valueLabel = new JLabel("Value"); valueLabel.setDisplayedMnemonic('V'); value = new JTextField(); valueLabel.setLabelFor(value); JPanel topPanel = new JPanel(new GridLayout(2,2)); topPanel.add(keyLabel); topPanel.add(key); topPanel.add(valueLabel); topPanel.add(value); add(topPanel, BorderLayout.NORTH); list = new JList(); list.setSelectionMode( ListSelectionModel.SINGLE_SELECTION); JScrollPane pane = new JScrollPane(list); add(pane, BorderLayout.CENTER); add = new JButton("Add/Update"); add.setMnemonic('A'); remove = new JButton("Remove"); remove.setMnemonic('R'); JPanel bottomPanel = new JPanel(); bottomPanel.add(add); bottomPanel.add(remove); add(bottomPanel, BorderLayout.SOUTH); ActionListener addListener = new ActionListener() { public void actionPerformed(ActionEvent e) { String keyText = key.getText(); String valueText = value.getText(); try { ByteArrayInputStream bais = new ByteArrayInputStream( valueText.getBytes(CHARSET)); getAppletContext().setStream(keyText, bais); } catch (IOException ioe) { JOptionPane.showMessageDialog(StreamsApplet.this, "Unable to save", "Error", JOptionPane.ERROR_MESSAGE); } updateList(); key.setText(""); value.setText(""); } }; add.addActionListener(addListener); ActionListener removeListener = new ActionListener() { public void actionPerformed(ActionEvent e) { String keyText = key.getText(); try { getAppletContext().setStream(keyText, null); } catch (IOException ioe) { JOptionPane.showMessageDialog(StreamsApplet.this, "Unable to clear", "Error", JOptionPane.ERROR_MESSAGE); } updateList(); key.setText(""); value.setText(""); } }; remove.addActionListener(removeListener); ListSelectionListener selectListener = new ListSelectionListener() { public void valueChanged(ListSelectionEvent e) { String selection = (String)list.getSelectedValue(); try { InputStream stream = getAppletContext().getStream(selection); InputStreamReader isr = new InputStreamReader(stream, CHARSET); BufferedReader reader = new BufferedReader(isr); String line = reader.readLine(); key.setText(selection); value.setText(line); } catch (IOException ioe) { JOptionPane.showMessageDialog(StreamsApplet.this, "Unable to read", "Error", JOptionPane.ERROR_MESSAGE); } } }; list.addListSelectionListener(selectListener); updateList(); } private void updateList() { DefaultListModel model = new DefaultListModel(); Iterator<String> iter = getAppletContext().getStreamKeys(); if (iter != null) { while (iter.hasNext()) { model.addElement(iter.next()); } } list.setModel(model); } } Java 플러그인 기술 덕분에, 대부분의 데스크톱에서도 애플릿을 브라우저에서 실행할 수 있게 되었다. 최신 자바 소프트웨어를 확인하고 새로운 애플릿을 시험해보고 싶다면 java.com을 방문하기 바란다.

맨위로

Singleton 패턴에 대한 재고찰
 

2006년 2월 8일자 테크 팁 Singleton 패턴에 대해 많은 피드백이 접수되었다. 이 피드백 중에는 Singleton 클래스가 단일 클래스 로더를 통해 공유되지 않으면 Singleton이 아니라는 사실을 강조하는 내용도 포함되어 있다.

사실인즉, 서로 다른 클래스 로더에서 로드된 클래스들은 이름이 같고 동일한 패키지에 속하더라도 동일한 클래스가 아니다.

이를 이해하는 것이 중요한 이유는 무엇일까? 일부 환경에서는 흔히 복수 클래스 로더를 사용하고 있기 때문이다. 예를 들어, Java 2 Platform, Enterprise Edition (J2EE) 애플리케이션 서버는 클래스가 더 이상 필요치 않게 되면 클래스를 언로드할 수 있도록 복수의 클래스 로더를 사용한다(클래스에 대한 클래스 로더를 제거하면 메모리에서 클래스가 제거된다). 이 외에도, J2EE 애플리케이션 서버는 보안상의 이유로 클래스 격리를 위해 복수의 클래스 로더를 사용하기도 한다.

따라서, Singleton 클래스가 단일 클래스 로더(가령 시스템 클래스 로더)를 통해 공유되지 않는다면 그것은 Singleton이라고 할 수 없다.

맨위로

본 메일은 수신을 동의한 회원님에게만 발송됩니다.
본 메일의 수신을 거부하거나 수신주소를 변경하려면 SKDN@Sun.com으로 문의 주시기 바랍니다.

SKDN(Sun Korea Developers Network)에서 J2EE/J2SE 테크팁 등 다양한 아티클들을 참고하세요.

Copyright 2003-2006 Sun Korea, Ltd. All rights reserved.


신고
Posted by Tornado tornado
Core Java Technologies Technical Tips
 Core Java Technologies Tech Tip에 오신 여러분을 환영합니다
Core Java Technologies
TECHNICAL TIPS
2006년 5월 12일자
 
<5월 개발자 이벤트>

썬 자바 스튜디오 크리에이터 무료 체험 및 웍스테이션 파격가 한정 판매

   설문에 참여하시고 PSP, 아이팟 등 푸짐한 선물을 받아가세요!!!
   자세한 내용 보기 »

5월의 Sun Technology Day - University Sun Day

   일시: 2006년 5월 23일(화) 10:00-14:00
   장소: 서울 대학교 제2공학관(302동) 105호
   주제: 자바의 현재와 미래, 오픈 솔라리스, 코드 캠프
   자세한 내용 보기 »

  이번 호에서는,

» 빈에서 Indexed Property 변경 사항 보고하기
» J2SE 5.0에서의 Java 2D API 기능 강화

에 대해 다룹니다.

이 문서는 Java 2 Platform, Standard Edition Development Kit 5.0 (JDK 5.0)을 기반으로 개발되었습니다. PDF 파일받기    

빈에서 Indexed Property 변경 사항 보고하기
 

J2SE 5.0의 JavaBeans 컴포넌트 API에 여러 다양한 기능들이 추가되었는데, IndexedPropertyChangeEvent에 대한 지원도 그 중 하나이다. JavaBeans 컴포넌트 API는 JavaBeans 컴포넌트(JavaBean 또는 그냥 '빈'이라고도 한다)의 Regular Property에 대한 변경 내용을 보고하는 수단을 제공했었는데, IndexedPropertyChangeEvent에 대한 지원으로 빈의 Indexed Property 변경에 관한 추가 정보를 보고하는 기능이 부가되었다.

대부분의 개발자들은 JavaBean 컴포넌트 속성에 관해 익숙한 편이다. 즉, 각자의 클래스에 set과 get 메소드를 간단히 추가하기만 하면 set과 get 다음의 이름에 의해 정의되는 읽기/쓰기 속성을 설정할 수 있다. 다시 말해, 클래스가 setName()getName()으로 명명된 메소드를 가지고 있다면 이 클래스는 name이라는 이름의 JavaBean 컴포넌트 속성을 가지게 된다.

컴포넌트 속성에는 Regular와 Indexed의 두 종류가 있는데, Indexed Property는 각각의 값이 Index에 의해 액세스되는 복수의 값을 가진다는 점에서 Regular Property와 구분된다. 한편 name과 같은 Regular Property를 위한 set과 get 메소드는 다음과 같은 형태를 띠게 된다.

Regular Property:
  • public void setName(String name)
  • public String getName()
name이 Indexed Property인 경우에 메소드는 다음과 같다.

Indexed Property:
  • public void setName(int index, String name)
  • public String getName(int index)
  • public void setName(String[] names)
  • public String[] getName()
빈은 속성값의 변경사항이 사용자에게 통지되도록 설계될 수 있으며, addPropertyChangeListener() 메소드를 이용하여 PropertyChangeListener 오브젝트를 빈에 등록할 수도 있다. 이 경우 PropertyChangeEvent 오브젝트나 IndexedPropertyChangeEvent 오브젝트를 통해 리스너에 변경 사항이 통지된다. 값 변경 시 PropertyChangeEvent를 생성하는 속성을 Bound Property라고 한다.

PropertyChangeListener 클래스는 다음과 같이 하나의 메소드를 가진다.
public void propertyChange(PropertyChangeEvent pce)그렇다면 propertyChange()에 대한 인자가 PropertyChangeEvent 타입인 경우에는 IndexedPropertyChangeEvent를 어떻게 통지받게 되는가? 그 대답은 IndexedPropertyChangeEvent 클래스가 PropertyChangeEvent의 서브클래스라는 사실에서 찾아볼 수 있다. 따라서, propertyChange()의 내부에서는 어떤 타입의 인자를 얻는지 확인하기 위한 instanceof 체크가 필요하다.

public void propertyChange(PropertyChangeEvent pce) { String name = pce.getPropertyName(); if (pce instanceof IndexedPropertyChangeEvent) { IndexedPropertyChangeEvent ipce = (IndexedPropertyChangeEvent) pce; int index = ipce.getIndex(); System.out.println("Property: " + name + "; index: " + index); } else { System.out.println("Property: " + name); } System.out.println("; value: " + pce.getNewValue()); }IndexedPropertyChangeEvent를 사용하는 예제 프로그램을 살펴보기에 앞서 Bound Indexed Property에 대한 변경을 보고하는 방법에 초점을 맞추어 보도록 하자. 다음은 앞서 살펴본 Name Indexed Propterty의 업데이트를 보고하는 코드이다.

private PropertyChangeSupport changeSupport; public ClassConstructor() { changeSupport = new PropertyChangeSupport(this); } public void setName(int index, String name) { String oldName = this.name; this.name = name; changeSupport.fireIndexedPropertyChange("name", index, oldName, name); }PropertyChangeSupport 클래스는 java.beans 패키지(JavaBeans 컴포넌트 API 패키지)에 들어 있는 지원 클래스이고, fireIndexedPropertyChange() 메소드는 Bound Indexed Property(이 경우에는 name)에 대한 변경을 addPropertyChangeListener() 메소드를 통해 등록된 모든 리스너에게 보고한다.

다음은 전체 예제의 내용이다.
import java.beans.*; import java.util.*; public class IndexedSampleBean { private PropertyChangeSupport changeSupport; private Map<Integer, String> names; private String title; public IndexedSampleBean() { changeSupport = new PropertyChangeSupport(this); names = new HashMap<Integer, String>(); } public void setTitle(String title) { String oldTitle = this.title; this.title = title; changeSupport.firePropertyChange("title", oldTitle, title); } public String getTitle() { return title; } public void setName(int index, String name) { String oldName = names.get(index); names.put(index, name); changeSupport.fireIndexedPropertyChange("name", index, oldName, name); } public String getName(int index) { return names.get(index); } public void addPropertyChangeListener( PropertyChangeListener l) { changeSupport.addPropertyChangeListener(l); } public void removePropertyChangeListener( PropertyChangeListener l) { changeSupport.removePropertyChangeListener(l); } public static void main(String[] args) throws Exception { IndexedSampleBean bean = new IndexedSampleBean(); PropertyChangeListener listener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent pce) { String name = pce.getPropertyName(); if (pce instanceof IndexedPropertyChangeEvent) { IndexedPropertyChangeEvent ipce = (IndexedPropertyChangeEvent) pce; int index = ipce.getIndex(); System.out.print("Property: " + name + "; index: " + index); } else { System.out.print("Property: " + name); } System.out.println("; value: " + pce.getNewValue()); } }; bean.addPropertyChangeListener(listener); bean.setName(1, "John"); bean.setName(2, "Ed"); bean.setName(3, "Mary"); bean.setName(4, "Joan"); bean.setTitle("Captain"); System.out.println("Name at 3 is: " + bean.getName(3)); System.out.println("Title is: " + bean.getTitle()); } }클래스는 name으로 명명된 Indexed Property를 정의하고 4개의 위치에서 name을 설정한다. 이와 함께 title로 명명된 Regular Bound Property 역시 생성되어 설정된다. 리스너는 이름이나 타이틀을 설정하고 이어서 속성 이름, 인덱스(해당될 경우), 값을 프린트하기 위한 각 호출을 통지받는다. 그러면 클래스는 특정 위치에서 이름을 얻고 단일 타이틀을 프린트하기 전에 이를 먼저 프린트한다.

프로그램을 실행하면 다음과 같은 결과가 나온다.
>java IndexedSample Property: name index: 1 value: John Property: name index: 2 value: Ed Property: name index: 3 value: Mary Property: name index: 4 value: Joan Property: title; value: Captain Name at 3 is: Mary Title is: CaptainJavaBeans component API에서의 IndexedPropertyChangeEvent 지원에 관한 자세한 내용은 J2SE 5.0의 JavaBeans Component API에 대한 API 강화를 참조하기 바란다. 자바 튜토리얼의 JavaBeans Trail도 함께 참조할 것.

맨위로

J2SE 5.0의 Java 2D API 기능 강화
 

자바 플랫폼 스탠다드 에디션의 버전이 새로 나올 때마다 크고 작게 기능이 개선되었다. 대부분의 사람들은 Generics나 새로운 병행 유틸리티(Concurrency Utilities) 패키지처럼 중요한 신기능에 관해서는 충분히 들어 내용을 잘 알고 있는 반면 자잘한 신기능들에 관한 정보를 입수할 수 있는 기회는 그다지 흔하지 않다. 또한 이런 기능들은 중요한 특성에 비해 소수의 사용자에게 유용하기 때문에 자주 언급되지도 않는다. 따라서 본 팁에서는 J2SE 5.0에 추가된 작지만 새로 강화된 Java 2D API 신기능들을 몇 가지 소개하고자 한다. 이러한 신기능은 각자의 운영체제와 하드웨어에 따라 실제 성능이 좌우되는 경우가 많으며, 경우에 따라서는 이러한 기능을 사용하는 것이 별 효과가 없을 수도 있다.

Bicubic Interpolation

Java 2D API에서 강화된 기능 중 하나는 이미지 스케일링에 관한 것으로, 회전을 비롯한 어파인 변형(Affine Transformation)에도 영향을 미친다. RenderingHints 클래스는 다양한 보간법(interpolation) 옵션을 처리하는 다음 2개의 상수를 가진다: VALUE_INTERPOLATION_BILINEARVALUE_INTERPOLATION_BICUBIC. KEY_INTERPOLATION 힌트를 2개 옵션 중 하나로 설정하면 기본 시스템이 이미지를 스케일링할 때 따라야 할 규칙이 지정된다. 한편, J2SE 5.0 이전까지는 2개의 힌트 값이 동일한 결과를 산출했다. RenderingHints는 단순한 힌트일 뿐이므로 무시해도 상관 없는데, 실제로 Bicubic 설정은 항상 무시되어 왔고, 대신 Bilinear Interpolation이 사용되었다.

2개의 보간법 옵션에 익숙치 않을 경우에는 이미지를 스케일업할 때 어떤 일이 벌어지는지 이해하면 상당한 도움이 된다. 예를 들어, 스케일업된 이미지의 새로운 픽셀들은 모두 어디서 나오는 것일까? Bilinear 스케일링의 경우 픽셀은 스케일된 픽셀에 가장 가까운 소스 이미지의 2x2 픽셀 직사각형에서 공급된다. 픽셀 직사격형은 X와 Y의 선형 함수를 이용하여 섞이게 되는데, Bicubic 스케일링의 경우 이들은 스케일된 픽셀을 둘러싼 4x4 면적에서 공급된다. 한편, 픽셀 직사각형은 X와 Y의 3차 함수를 이용하여 섞이게 된다. Bicubic 스케일링을 이용할 경우, 증가된 면적과 알고리즘의 복잡성으로 인해 스케일된 이미지의 품질을 향상되지만 대신 성능이 저하된다.

J2SE 5.0에서는 Bicubic 설정이 상당히 중시되는데, 다음의 테스트 프로그램을 실행해보면 이 사실을 알 수 있다. 프로그램을 실행할 때 이미지 파일을 지정한다. 프로그램은 RenderingHints를 2개의 상수 VALUE_INTERPOLATION_BILINEARVALUE_INTERPOLATION_BICUBIC으로 설정하고 보간법 알고리즘의 차이(있을 경우)를 보고한다. 먼저 JDK 1.4로 프로그램을 실행한 다음 JDK 5.0로 실행한다. 보간법 알고리즘의 차이는 신 버전의 JDK에서만 보고된다는 것을 알 수 있다.
import java.awt.*; import java.awt.image.*; import java.io.*; import javax.swing.*; import javax.imageio.*; public class Bicubic { public static void main(String args[]) throws IOException { if (args.length == 0) { System.err.println( "Provide image name on command line"); System.exit(-1); } Image image = ImageIO.read(new File(args[0])); int w = image.getWidth(null); int h = image.getHeight(null); BufferedImage bilinear = new BufferedImage(2*w, 2*h, BufferedImage.TYPE_INT_RGB); BufferedImage bicubic = new BufferedImage(2*w, 2*h, BufferedImage.TYPE_INT_RGB); Graphics2D bg = bilinear.createGraphics(); bg.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); bg.scale(2, 2); bg.drawImage(image, 0, 0, null); bg.dispose(); bg = bicubic.createGraphics(); bg.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); bg.scale(2, 2); bg.drawImage(image, 0, 0, null); bg.dispose(); for(int i=0; i<2*w; i++) for(int j=0; j<2*h; j++) if (bilinear.getRGB(i, j) != bicubic.getRGB(i, j)) System.out.println("Interpolation algo differ"); } } JDK 1.4 환경에서는 프로그램을 실행해도 아웃풋이 생성되지 않는다.
> java Bicubic image.jpg [No Output] JDK 1.5 환경에서는 차이가 있는 모든 픽셀이 보고된다.
> java Bicubic image.jpg Interpolation algo differ Interpolation algo differ Interpolation algo differ Interpolation algo differ Interpolation algo differ .... repeated many timesOpenGL 가속

2005년 3월 30일자 테크 팁 JOGL 개요에서는 OpenGL 3D 그래픽 API를 위한 자바 프로그래밍 언어 바인딩에 관해 설명한 바 있다. J2SE 5.0에는 2D API의 핵심 기능을 가속화하는 데 이용할 수 있는 몇 가지 GL 옵션이 포함되어 있는데, 이 옵션들은 '시트 밑에' 감추어져 있으므로 사용자는 OpenGL로 프로그래밍하는 방법을 알 필요가 없다. 가속 옵션은 기본적으로 "사용하지 않음" 상태로 설정되어 있고, 옵션을 사용하려면 sun.java2d.opengl 시스템 속성을 true로 설정하면 된다. OpenGL 기반 파이프라인과 관련하여 전송된 아웃풋을 verbose하고 싶으면 t 대신 True(대문자T) 값을 이용하고, 결과를 보려면 JDK에 포함된 Java 2D 데모 코드를 이용하면 된다.
java -Dsun.java2d.opengl=True -jar Java2Demo.jarOpenGL 기반의 파이프라인에 관한 자세한 내용은 Behind the Graphics2D: The OpenGL-based Pipeline을 참조할 것.

폰트 생성하기

J2SE 5.0 이전에는 Font 클래스를 이용하여 createFont() 메소드를 통해 InputStream에서 트루타입 폰트를 생성할 수 있었다.
public static Font createFont(int fontFormat, InputStream fontStream)이 때, fontFormatFont.TRUETYPE_FONT이다. J2SE 5.0의 경우 이 지원 항목에서 두 가지의 변경 사항이 있는데, 첫째는 Adobe Type 1 폰트를 생성할 수 있고, 두번째는 새로운 createFont() 메소드 시그니처를 통해 File 오브젝트에서 직접 폰트를 생성할 수 있다는 점이다.
public static Font createFont(int fontFormat, File fontFile)단순히 폰트를 설치하고자 한다면, 즉 어느 메소드로도 폰트를 생성할 생각이 없다면, $JREHOME/lib/fonts 디렉토리에 폰트 파일을 복사하면 된다. 그러면 폰트는 해당 디렉토리에 설치될 때 런타임 환경에 의해 자동으로 픽업된다.

이미지 가속(Acceleration)

Java 2D의 새로운 특징 중에서 마지막으로 살펴볼 내용은 이미지 가속에 관한 것이다. 이 기능들은 각 시스템의 기본 지원 여부에 의해 좌우되는데, 시스템이 이 기능들을 지원하지 않는다면 단순히 무시되어 버린다.

첫 번째 이미지 가속 기능은 버퍼된 이미지 캐싱인데, 비디오 메모리가 관리 이미지의 캐싱을 지원한다. 일반적으로, 관리 가능한 종류의 이미지를 사용하는 프로그램의 경우 관리되지 않는 이미지를 사용하는 프로그램에 비해 성능이 뛰어난 편이다. J2SE 5.0 이전에는 ComponentcreateImage() 메소드 또는 GraphicsConfigurationcreateCompatibleImage() 메소드를 통해 생성된 이미지만 Java 2D 구현에 의해 관리되었지만 이제는 BufferedImage 생성자 중 하나로 생성된 모든 이미지를 Java 2D 구현을 통해서도 관리할 수 있게 되었다.

또 하나의 새로운 이미지 가속 기능으로 이미지의 하드웨어 가속을 제어하는 능력을 들 수 있다. 하지만 J2SE 5.0에서는 이미지의 하드웨어 가속을 제어하는 데 사용되는 메소드가 완전하게 작동하지 않는다는 점에 유의할 필요가 있다. 자세한 내용은 이미지의 하드웨어 가속 제어를 위한 메소드를 참조하기 바란다.

이 지원의 가용성 여부는 이미지 컨텐트의 플랫폼과 타입에 의해 좌우되며, Microsoft Windows 플랫폼 상에서 결과를 보려면 경우에 따라 sun.java2d.translaccel 시스템 속성을 true로 설정해 주어야 한다. 플래그에 관한 자세한 내용을 보려면 Microsoft Windows 플랫폼을 위한 시스템 속성translaccel에 관한 설명을 참조할 것.

자바 프로그램 작성 시, 특정 이미지 연산을 최적화하기를 원한다는 것을 기본 시스템에 알려줄 수 있다. 이 작업은 시스템이 가속화된 메모리에 특정 이미지를 저장하는 방식의 우선순위 정함으로써 가능한데(가능한 경우 ), 여기서 '가능한 경우'라는 단서가 중요하다. J2SE 5.0 이전에는 VolatileImage 오브젝트가 가속화된 메모리에 저장되도록 요청할 수 있었는데, J2SE 5.0에서는 추가로 레귤러 Image 오브젝트를 그곳에 배치하도록 요청할 수 있게 되었다. 또한, 투명한 VolatileImage 오브젝트를 생성할 수도 있다 (이전에는 불투명한 오브젝트 생성만 가능했음).

getCapabilities() 메소드는 이제 VolatileImage로만 정의되는 것이 아니라 ImageVolatileImage 모두에 의해 정의되며, 이 메소드는 특정 이미지가 현재 가속화되고 있는지 확인하는 데 이용될 수도 있다. 또한 setAccelerationPriority() 메소드로 가속 우선순위를 지정하거나 getAccelerationPriority() 메소드로 우선순위 설정을 확인할 수 있다. 가속 우선순위가 0으로 되어 있는 경우에는 특정 이미지의 가속화 기능은 사용되지 않는다. 하지만 우선순위 설정은 단지 힌트일 뿐이며, JDK 구현은 적절한 판단 기준에 따라 이를 중시(또는 무시)할 수 있다.

투명한 VolatileImage 오브젝트를 생성하려면 두 가지의 새로운 createCompatibleVolatileImage() 메소드 중 하나를 이용해야 한다.
  • createCompatibleVolatileImage( int width, int height, int transparency)
  • createCompatibleVolatileImage( int width, int height, ImageCapabilities caps, int transparency)
요약

자바 플랫폼이 계속 발전함에 따라, 크게 주목을 끄는 주요 핵심 기능 이외의 사소한 특성들에 대해서도 관심을 가질 필요가 있다. 실례로, 솔라리스 및 Linux를 위한 CUPS(Common Unix Printer System) 프린터 지원 같은 작은 기능들이 소리 소문 없이 추가되었는데, 만약 사용자가 CUPS 프린터를 사용하고 있다면 이 새로운 기능이 상당히 중요한 역할을 하게 될 것이다. 따라서 평소에 원하던 기능이 새로 추가되지는 않았는지 릴리즈 노트를 잘 살펴보기 바란다. 아울러, J2SE 5.0의 새로운 특징과 강화된 기능에 대한 설명도 함께 참조할 것.

맨위로

본 메일은 수신을 동의한 회원님에게만 발송됩니다.
본 메일의 수신을 거부하거나 수신주소를 변경하려면 SKDN@Sun.com으로 문의 주시기 바랍니다.

SKDN(Sun Korea Developers Network)에서 J2EE/J2SE 테크팁 등 다양한 아티클들을 참고하세요.

Copyright 2003-2006 Sun Korea, Ltd. All rights reserved.


신고
Posted by Tornado tornado

http://eclipsecolorer.sourceforge.net/index_profiler.html

 

신고
Posted by Tornado tornado

http://logging.apache.org/log4j/docs/chainsaw.html

 

 

신고
Posted by Tornado tornado

http://www.cloudgarden.com

 

한번도 안써본것!

 

뭐만 새로 보면 신기해서 해봐야 하는 이 성질 ㅡㅡ;

 

신고
Posted by Tornado tornado

[링크] jndc

JAVA/JSE 2005.10.24 14:56

https://jdnc.dev.java.net

 

잘 사용하면 쓸데가 많을듯.... 

DataSet 같은 거는 jsf 용으로 좀 개량좀 해주징~ 무지 편리한데..

 

신고
Posted by Tornado tornado

가비지 콜렉션

2004년 1월의 테크팁이었던 Monitoring Class Loading and Garbage Collection에서는 자바 커맨드 라인 툴 (자바 애플리케이션 런쳐)를 이용하여 커맨드 라인 옵션인 -verbose:gc에 대해서 알아보았다. 표준이 아니라는 이유로 -XX 옵션에 대해 신경 쓰지 않는다거나, 옵션의 균질성을 중요시한다거나, 또는 현재 사용하고 있는 옵션에 대해서 알고 싶다면 다음과 같이 -verbose:gc 를 입력해보자.

     -XX:+PrintGC -XX:+TraceClassUnloading

이것은 -verbose:gc 옵션이 어떻게 번역되는지를 보여준다.

이 옵션은 애플리케이션이 실행되는 동안 가비지 콜렉션 이벤트의 리포트를 요구한다. J2SE v1.4.2에서는 가비지 콜렉터를 컨트롤하는 다른 많은 커맨드 라인이 있다. 코드의 어느 라인도 변경하지 않고 애플리케이션의 성능을 최대한으로 활용하려면, 이 옵션들을 최소한 하나 이상 사용할 수가 있다. 이번 팁에서는 가비지 콜렉터를 컨트롤하는 부수적인 많은 자바 커맨드 라인을 다룰 것이지만, 가비지 콜렉션 튜닝 옵션의 전체를 말하지는 않는다.

자바 애플리케이션 런쳐는 표준 커맨드-라인 스위치와 비표준 커맨드-라인 스위치를 모두 동반한다. -verbose:gc 옵션에 관한 정보에 대해 알아보려면, 특정 플랫폼별로 다음 웹페이지를 참고하자.

위의 참고 페이지에는 몇몇 비표준 옵션들에 대해서도 나와있다. 자바 커맨드 라인 툴에서 표준 옵션인 -X을 실행하면 사용자의 플랫폼을 위한 비표준 옵션을 보여준다.

솔라리스 플랫폼이라면, java -X 을 실행했을 경우 출력되는 내용은 다음과 같다.

   -Xmixed           mixed mode execution (default)   -Xint             interpreted mode execution only   -Xbootclasspath:<directories and zip/jar files separated by :>                     set search path for bootstrap classes and                      resources   -Xbootclasspath/a:<directories and zip/jar files separated by :>                     append to end of bootstrap class path   -Xbootclasspath/p:<directories and zip/jar files separated by :>                     prepend in front of bootstrap class path   -Xnoclassgc       disable class garbage collection   -Xincgc           enable incremental garbage collection   -Xloggc:<file>    log GC status to a file with time stamps   -Xbatch           disable background compilation   -Xms<size>        set initial Java heap size   -Xmx<size>        set maximum Java heap size   -Xss<size>        set java thread stack size   -Xprof            output cpu profiling data   -Xrunhprof[:help]|[:<option>=<value>, ...]                     perform JVMPI heap, cpu, or monitor profiling   -Xdebug           enable remote debugging   -Xfuture          enable strictest checks, anticipating future                      default   -Xrs              reduce use of OS signals by Java/VM (see                      documentation)   -Xcheck:jni       perform additional checks for JNI functions     The -X options are non-standard and subject to change without   notice.

위의 출력값의 마지막 줄을 주목하자. 그러한 비표준 옵션들을 사용하는 것은 사용자의 책임 하에 있음을 분명히 나타내고 있다.

가비지 콜렉션을 위한 세 가지 특정한 비표준 옵션은 -Xnoclassgc, -Xincgc, 그리고 -Xloggc:<file>이다. -Xnoclassgc 옵션을 지정한다면, 콜렉션이 발생하지만 클래스들은 permanent generation으로부터 수집되지 않는다. 이는 콜렉션이 수거하는 메모리가 없음을 의미한다. 만약 클래스를 로딩하려고 할 때 이미 로딩된 클래스들을 위해 메모리를 써버렸다면 콜렉션은 문제를 해결하지 않을 것이다.

썬의 1.4.2 구현에서 -Xincgc-XX:+UseTrainGC 옵션과 동등한 값을 가진다. 즉, -Xincgc은 old generation을 위해서 디폴트 값에서 주어진 "serial" 콜렉터를 사용하기 보다는 "train"콜렉터를 사용한다. ("generations"의 개념은 이번 팁의 후반에서 다루게된다.) train 콜렉터는 약 10%의 퍼포먼스 부하를 발생시킨다. 또한 old generation 전체를 한 번에 수집할 수 없기 때문에 약간의 공간적인 부하(space overhead)도 발생한다. train 콜렉터는 점진적으로 작동하는 장점을 가지기 때문에 휴지 시간이 상당히 짧다. J2SE 1.5.0 베타 릴리즈에서는 -Xincgc 플래그는 train 콜렉터보다는 CMS(concurrent mark sweep) 콜렉터를 인보킹하게 된다. 그 이유는, CMS 콜렉터가 train 콜렉터보다 더 균일하기 때문이다.

-Xloggc 옵션을 지정해줌으로써 -verbose:gc 출력값의 등가를 파일로 출력할 수 있다. -verbose:gc 에 의한 출력값과 비교해 보면, -Xloggc 에 의한 출력값은 한 가지 추가적인 항목을 포함하는데, 그것은 애플리케이션에서의 첫 콜렉션이 일어난 시간에서부터 가비지 콜렉션이 발생한 시간에 대한 정보이다. 다음의 -Xloggc 샘플 출력값에서 살펴보자.

   0.000: [GC 512K->153K(1984K), 0.0198483 secs]   0.372: [GC 506K->281K(1984K), 0.0206428 secs]   0.393: [Full GC 281K->281K(1984K), 0.0888926 secs]   0.519: [GC 947K->941K(1984K), 0.0045715 secs]   0.524: [Full GC 941K->941K(1984K), 0.0797666 secs]   0.650: [GC 2107K->1597K(2808K), 0.0013546 secs]   0.838: [GC 2107K->1700K(2808K), 0.0116557 secs

파일에 재전송할 필요없이 -XX:+PrintGCTimeStamps 옵션을 지정해줌으로써 -Xloggc 출력값에 시간 소인을 나타낼 수 있다.

-Xnoclassgc, -Xincgc-Xloggc:<file> 이외에도 가비지 콜렉터를 컨트롤하는 다른 옵션들이 존재한다. 예를 들자면, 콜렉터가 실행할 때 간접적으로 영향을 끼치도록 -Xms -Xmx 옵션을 사용하여 메모리 할당의 풀 사이즈를 변경할 수 있다. -Xms의 값을 크게 지정해주면, 그만큼 큰 자바 객체 힙으로 작업하게 된다. 이것은 힙을 채우는 데에 더 많은 시간이 걸린다는 것을 의미하고 결과적으로 콜렉션을 피하지는 않지만 미루게 된다는 것이다. 큰 -Xms 값은 또한 더 많은 시스템 리소스를 소모하게 된다. 반면 -Xmx의 값을 크게 지정하면, 자바 객체 힙의 사이즈가 필요 시에 커질 수 있게 된다. 이렇게 값이 큰 객체 힙은 조금 덜 빈번하게 가비지 콜렉트된다는 점 외에는 모든 점이 동일하다. 따라서 이 옵션을 통해 가비지 콜렉션이 빈번하지만 짧은 시간동안 일어나는 것과, 횟수는 적지만 오랜 시간동안 가비지 콜렉션이 일어나는 것 중 하나를 선택할 수 있다. 하지만 각각의 콜렉션이 부하를 발생하기 때문에, 사용자가 정의하는 요구조건에 따라 더 나은 접근법은 달라지게 되어있다.

-Xms-Xmx 외에도 가비지 콜렉션에 영향을 주는 비표준 옵션들이 존재한다. 그 옵션들은 java -X 도움말에서 볼 수 없는데, 그 이유는 이 옵션들은 비표준 옵션이기 때문에 두개의 X를 사용해야하기 때문이다.

만약 힙의 최소값과 최대값을 지정해주고자 한다면 (이 때, 디폴트 값의 범위는 40%~70%), -XX:MinHeapFreeRatio=<Minimum> 옵션과 -XX:MaxHeapFreeRatio=<Maximum> 옵션을 사용하면 된다. 그러나 최소값을 너무 작게 설정해놓으면, 힙은 콜렉션 후에 충분한 여유공간을 확보하지 못하게 되므로, 단기간 내에 또다시 콜렉팅을 해야할 수도 있다. 반면, 최소값을 크게 설정하면, 더 많은 "head room"이 생기므로 다음 콜렉션 때까지 시간을 지연시킬 수 있을 것이다. -XX:MinHeapFreeRatio 의 값을 크게 했을 때의 단점은 시스템 메모리가 이 가상 장치에 고정되기 때문에 사용자의 또 다른 애플리케이션에 사용될 수 없다는 점이다.

-XX 세트내의 다른 비표준 옵션들은 가비지 콜렉터의 작동 방법에 영향을 끼친다. -XX:+UseConcMarkSweepGC 커맨드 라인 플래그("concurrent mark sweep garbage collection"의 줄임말)는 병행(concurrent) 가비지 콜렉터를 실행시킨다. -XX:+UseConcMarkSweepGC 콜렉터는 old generation을 동시에 콜렉팅한다. 그 이유는 전반적인 콜렉션 같은 old generation 들은 콜랙팅할 때 대체적으로 시간이 오래 걸려서 병행 가비지 콜렉터를 실행시키는 것이 더 효과적이기 때문이다. 이는 애플리케이션에서 가비지 콜렉션 휴지 시간을 짧게 할 수 있도록 프로세싱 파워를 활용한다는 것을 말한다. -XX:+UseParallelGC 커맨드 라인 플래그는 병렬 가비지 콜렉팅을 가능하게 한다. -XX:+UseParallelGC 콜렉터는 오직 다중 프로세서를 이용한 young generation만을 콜렉팅한다. 이 접근법은 처리율은 높이지만, 전반적인 콜렉션의 휴지 시간을 줄이지는 못한다.

또 하나의 흥미로운 옵션인 -XX:+PrintGCDetails 는 각각의 콜렉션에서 각 generation에 어떤 일이 발생했는지를 보여준다. (단, permanent generation 에 대한 자세한 사항들은 보여주지 않는다. 이 점은 JDK 1.5.0.에서 수정되었다.)

SwingSet2의 데모에 사용된 -XX:+PrintGCDetails의 예제를 살펴보자. (출력값의 라인들은 테크팁 페이지의 경계선에 맞추기 위해 나누어주었다.)

   java -Xloggc:details.out -XX:+PrintGCDetails -jar    /home/eo86671/j2sdk1.4.2/demo/jfc/SwingSet2/SwingSet2.jar   0.000: [GC 0.000: [DefNew: 511K->64K(576K), 0.0182344 secs]         511K->153K(1984K), 0.0185255 secs]   1.387: [GC 1.387: [DefNew: 417K->64K(576K), 0.0192086 secs]        1.407: [Tenured: 217K->281K(1408K), 0.0725645 secs]         506K->281K(1984K), 0.0923346 secs]   1.559: [GC 1.559: [DefNew: 10K->3K(576K), 0.0044098 secs]        1.564: [Tenured: 937K->941K(1408K), 0.0741569 secs]        948K->941K(1984K), 0.0790573 secs]   1.703: [GC 1.703: [DefNew: 510K->0K(576K), 0.0011627 secs]         2107K->1597K(2808K), 0.0013820 secs]   2.210: [GC 2.210: [DefNew: 509K->64K(576K), 0.0112637 secs]         2107K->1710K(2808K), 0.0115942 secs]   2.927: [GC 2.927: [DefNew: 575K->64K(576K), 0.0170128 secs]        2222K->1841K(2808K), 0.0173293 secs]   8.430: [GC 8.430: [DefNew: 576K->64K(576K), 0.0142839 secs]         2353K->2025K(2808K), 0.0156266 secs]   8.823: [GC 8.823: [DefNew: 494K->64K(576K), 0.0164915 secs]         2456K->2243K(2808K), 0.0166323 secs]   8.856: [GC 8.856: [DefNew: 569K->41K(576K), 0.0058505 secs]        8.862: [Tenured: 2341K->1656K(2360K), 0.1464764 secs]         2749K->1656K(2936K), 0.1526133 secs]

변경내용이 그 결과에 어떤 영향 (예를 들어서, 콜렉션간의 시간 연장이나 각각 특정 휴지 시간의 감소)을 주는지를 알아보기 위해 -Xms 옵션과 -Xmx 옵션을 조정해보자.

앞서 'generations'의 힙 공간에 대해 언급한 바 있다. 이 말이 무슨 의미인지 궁금하다면, 여기 짤막한 설명을 참조하자. JRE 1.4.2 에서 힙은 young, old, 그리고 permanet와 같이 세 개의 generation으로 구분된다. young generation은 객체가 생성된 장소이다. 이 generation의 사이즈는 -XX:NewSize 옵션과 -XX:MaxNewSize 옵션에 의해 컨트롤된다. 새로운 객체를 보유하게 되면, old generation으로 승격된다. old generation의 사이즈는 전체 힙 사이즈 (-Xms-Xmx) 에서 young generation의 사이즈 (-XX:NewSize-XX:MaxNewSize)를 감한 사이즈로 계산된다. young generation은 명시적 크기보다는 -XX:NewRatio=를 이용하여 더 잘 지정할 수 있는데, -XX:NewRatio= 옵션은 힙 설정의 전체 사이즈를 설정해주기 때문이다. 선택한 사이즈는 콜렉팅 되는 시간에 대해 콜렉션의 횟수를 결정하게 된다. 어떤 애플리케이션들은 짧은 휴지 시간을 요구하는 한편, 콜렉터의 효율성을 필요로 하는 애플리케이션들도 있다. (많은 애플리케이션에서 이 단계의 튜닝을 해줄 필요는 전혀 없다.)

마지막으로 언급할 GC옵션은 -XX:+DisableExplicitGC이다. System.gc()의 호출을 이용하여 가비지 콜렉터를 실행하라는 명령을 했을 때, 시스템이 이를 무시하기를 원한다면 이 옵션을 사용해보자. 가비지 콜렉터는 프로그래머가 명령했을 때가 아닌, 필요시에 계속해서 실행되고 있을 것이다. 하지만 프로그래머의 명령을 무시하는 것이 좋은 생각일까? 아마도 아닐 것이다. 왜냐하면, 프로그래머가 가비지 콜렉터를 실행하기 적당한 시점이라고 생각할 수 있기 때문이다. 물론, 만약 공유 라이브러리를 사용하고 있고, 실행 콘텍스트가 프로그래머의 원래의 계획과 다르다면, 이러한 새로운 상황에 유효하지 않을 수도 있다.

가비지 콜렉션에 관련된 옵션들에 대해 좀더 자세한 정보를 원한다면, Tuning Garbage Collection with the 1.4.2 Java Virtual Machine를 읽어보기 바란다. 튜닝을 할 때는 튜닝의 목적이 성능 향상인지, 짧은 휴지 시간인지 아니면 작은 범위의 ㄴ메모리 사용을 위한 것인지 그 목적을 확실히 해야 한다. JVM이 업데이트 되면서 선택할 수 있는 튜닝 옵션들도 다시 한번 살펴 보아야 한다. 이번 팁에서 다룬 옵션들의 대부분은 썬의 런타임 환경 1.4.2 릴리즈에 한정됨을 기억하자. 1.5.0 릴리즈는 고유의 컨트롤 셋과 디폴트 셋을 가진다.

신고
Posted by Tornado tornado

애플리케이션 서버의 성능과 확장성을 개선하기 위한 Java HotSpot VM vl.4.x Turbo-Charging (1)

 

 
 
 

Java 프로그래밍 언어는 많은 데이터 중심적인 애플리케이션용 선택 언어로 널리 받아들여지고 있다. Java의 성공은 언어의 초기 단계부터 설계된 다음과 같은 중요 기능을 토대로 만들어졌기 때문이다.

- WORA : Write Once Run Anywhere
- 자동 메모리 할당과 컬렉션
- 객체 지향 디자인
- 상속

그러나 결정적으로 소스 코드를 컴파일해서 만들어진 Java 바이트 코드를 하위단인 JVM이 실행시키는 구조는 시간을 다투는 환경에서 Java를 선택하는 데 있어 고민을 안겨주었던 것도 사실이다. 전통적으로 가비지 컬렉션(GC)은 사용자의 애플리케이션을 일시적으로 중단시키고 시스템이 메모리를 재순환시킨다. 이런 GC 중단은 밀리초에서 심한 경우에는 수초일 수도 있다.

통신 회사 장비 공급자는 통신 회사가 기업 고객과 엄밀한 수준의 서비스 협약이 돼 있을 뿐더러 부동산 시장에서도 기본적인 수준의 신뢰성과 반응 시간을 가지고 있어야 한다는 것을 잘 알고 있다. 수화기를 든 다음 10 또는 20초를 기다려야 신호음이 난다는 것은 용납될 수 없다.

통신 회사들은 여러 가지 이유로 자사의 환경에 Java를 사용하길 원한다.


특히 이미 존재하는 광범위하고 풍부한 애플리케이션을 생성할 수 있는 능력있는 수백만의 프로그래머들과 회사들이 Java를 기본 기술로 표준화하고 있다. 이는 새로운 서비스와 기술을 많이 구할 수 있다는 것이다. 거기에다가 J2EE에는 보편적인 표준 환경과 기하급수적으로 확장할 수 있는 잠재력이 있다. 그렇지만 예측할 수 없는 ‘중단’과 그로 인해 애플리케이션의 반응이 지연되는 것 때문에 어떤 회사라도 자사 네트웍의 핵심 기술로 Java를 사용하는 것이 타당한지 묻게 한다.

통신 회사는 수익을 얻기 위해서 완전한 통화와 서비스 세션에 의존한다. 불완전하거나 버림받은 세션은(예를 들어 통화와 인스턴트 메시지) 네트웍 자원과 전혀 사용되지 않는 대역폭을 제공하는 비용을 낭비하는 것이다.

이러한 환경에서 통신 회사뿐만 아니라 차세대의 통신 기반을 선도하는 3rd generation(3G) 회사들에게도 표준으로 채용된 SIP 프로토콜이 이상적인 선택일 것이다. SIP는 UDP와 TCP를 기반으로 신호를 보내고, 패킷(UDP) 재전송이 내장돼 있는 프로토콜이다. 만약 UDP 요청 패킷이 특정 시간 내(주로 500ms)에 반응이 없으면 재전송된다. 네트웍 불능이나 패킷 손실 등 다른 에러들에 대한 계산도 돼 있어야 한다.

즉 JVM은 500ms에 근접한 시간만큼 애플리케이션을 중단시키면 안된다는 의미다. 왜냐하면 네트웍 패킷 지연(가는 데만 50~100ms)과 CPU의 프로세싱 시간이 계산되면 재전송이 일어나기까지 시간이 많지 않는다. 계단식(Cascade) 실패는 주로 애플리케이션 서버에 높은 로드에 대한 대비를 하지 않은 불완전한 디자인이나 단일 쓰레드, 애플리케이션 쓰레드를 모두 중지시키는 ‘Stop-the-world 가비지 컬렉션’ 정책을 사용하는 JVM(J2SE 1.4 버전 이하)을 사용할 때 일어난다. 그리고 이 JVM들은 단일 쓰레드이고, 여러 CPU가 있더라도 오직 하나의 CPU에서만 가비지 컬렉션을 실행시킨다.

Sun HotSpot JVM(JDKTM 1.3+)에 사용된 알고리즘은 메모리 재사용과 객체 노화에 효과적이다. JVM heap은 객체의 나이에 따라 ‘신세대’와 ‘구세대’로 나뉘어진다.

신세대는 ‘에덴’과 2개의 ‘생존 공간(Survivor)’으로 나눠졌다. 대부분의 애플리케이션에서는 2/3의 객체는 ‘단기 객체’로 빨리 죽고, ‘신세대’에서 컬렉션할 수 있다. 주로 신세대의 객체는 총 heap 크기에 비해 작다. 따라서 신세대에서 짧은 멈춤이 빈번하게 일어나지만 한번의 컬렉션에 더 많은 메모리를 컬렉션할 수 있다. 그러나 신세대에서 여러 번 생존 공간에 있게 되면, 그것은 ‘오래된’ 또는 ‘장기 객체’로 인식하고 ‘구세대’로 ‘승진(promoted)’ 또는 ‘보유(tenured)’하게 된다. 비록 대개 ‘구세대’의 크기가 크기만, 결국은 모두 사용하게 되면 컬렉션이 필요하게 된다. 이로 인해 구세대에서 빈도수는 낮지만, 장시간의 멈춤이 일어난다. 보다 자세한 것은 ‘Tuning Garbage Collection with the 1.3.1 Java Virtual Machine’이라는 Sun HotSpot JVM 가비지 컬렉션 테크놀로지를 참조한다.

Ubiquity의 SIP 애플리케이션 서버인 Application Services Broker(ASB)는 100% 순수 Java 기술이며, JVM과 가비지 컬렉션에 대한 완벽한 테스트 환경을 제공한다. SIP 애플리케이션 서버로서, ASB는 신호가 오는 네트웍으로부터 서비스 요청을 받은 다음 수정하고, 최종적으로 답변해주는 책임이 있다. SIP 트래픽을 신호가 오는 네트웍으로부터 서비스 요소(SIP Servlets)로 연결해 책임을 이행한다. 이 서비스 요소는 특정 신호 메시지에 관심이 있다고 ASB에 등록한다. ASB는 대규모 호출을 다양한 로드 분산으로 생성한다. 이는 클러스터 기반으로 구현되어 한 대나 여러 대에서나, 여러 프로세서에서 잘 확장된다. 이런 때 JVM 디자인이 한계점까지 압력을 받으며, 이런 상태를 관찰하면서 다중 쓰레드의 JVM을 개선시키는 데 많은 통찰력을 얻게 된다.

예를 들어 Ubiquity ASB는 호출되는 하나의 프로세스당(일반적으로 한 호출은 하나의 세션이다) 약 220KB의 가비지를 생성한다. 10%의 데이터는 40초 가까이 생존하고, 나머지 90%는 곧바로 죽어버린다. 이 10%의 데이터는 단시간이지만 평균 초당 100번을 호출한다고 하면 대단히 많은 메모리를 사용하게 된다. 40초 안에 최소한 88MB의 활성 데이터를 갖게 된다. 만약 가비지 컬렉션이 5분에 한번씩 ‘구세대’에서 일어난다면 heap의 크기는 최소한 660MB가 돼야 한다. 이전의 가비지 컬렉션 엔진이 스캔하기에 매우 커 컬렉션하는 데 100~150ms 정도 걸린다.

통신 애플리케이션 서버는 애플리케이션을 멈추는 시간을 엄격히 제한해줄 수 있는 결정적인 GC 모델을 요구하고 여러 프로세스에서도 잘 확장돼야 한다.

J2SE 1.2와 1.3에서 GC는 단일 쓰레드이며, stop-the-world 형식이었다. 이 JVM의 가비지 컬렉션으로 인해 생기는 멈춤은 애플리케이션에 지연을 더해 처리량과 확장성으로 본 성능이 떨어지게 된다. 이런 아키텍처는 한대의 컴퓨터의 여러 CPU에서 확장하기 어럽게 만든다. 가비지 컬렉션이 여러 프로세서로 분산되지 않기 때문에, 매우 다중 쓰레드인 애플리케이션이 JVM에서 실행될 때, 애플리케이션 레벨의 작업을 이행하기에 충분한 프로세서를 갖고 있음에도 불구하고 JVM으로부터 컨트롤을 얻기 위해 기다리며, JVM은 다중 CPU 환경에서 많은 압력을 받는다. 다중 프로세서 시스템에서 단일 쓰레드 GC의 영향은 병렬 애플리케이션일수록 크게 나타난다. 만약 GC를 제외하고 애플리케이션이 완벽하게 확장한다고 가정하면, GC가 일어나는 동안 작업을 수행할 수 있는 다른 프로세서가 유휴 상태에 있게 됨으로써, GC가 확장성을 저해하는 원인(Bottleneck)이 되는 것이다.

이런 내용에서 Java를 Turbo-charge한다는 것은 많은 CPU 사용과 많은 메모리에 접근, 많은 동시 발생 소켓 연결의 처리를 의미한다.

J2SE 1.4는 새로운 기능과 버퍼 관리, 네트웍 확장성, 파일 I/O의 성능을 개선한 논블록킹 new I/O API를 구현했다.
- 새로운 네트웍 I/O 패키지는 접속할 때마다 쓰레드를 배정하는 방식을 제거함으로써 서버에 동시 접속할 수
  있는 수를 극적으로 증가시켰다.
- 새로운 파일 I/O는 읽기와 쓰기, 복사, 이동 작업에서 현재의 파일 I/O보다 2배 가까운 속도를 낸다.
  새 것은 file locking과 memory-mapped files, 동시 여러 읽기/쓰기 작업을 지원한다.


J2SE 1.4의 64비트의 JVM은 4G 이상 큰 heap을 가질 수 있을 뿐더러 300G 이상 가질 수도 있다. 만약 가비지의 생성과 배당하는 비율이 일정하다면 heap이 클수록 GC로 인한 멈춤은 더욱 빈도수가 낮아지지만 그 시간은 길어질 것이다.

애플리케이션의 효율성과 확장성을 정하는 중요한 요소는 ‘GC 순차 비용(GC sequential overhead)’이다. 이는 애플리케이션의 실행 시간 중에 애플리케이션이 멈춘 상태에서 JVM에서 GC가 실행되는 시간의 비율을 뜻한다. 다음과 같이 계산할 수 있다.

Avg. GC pause * Avg. GC frequency * 100 %.
‘GC 빈도수’는 주기적인 GC나 한 단위의 시간에 일어나는 GC수이다. ‘신세대’와 ‘구세대’의 평균 GC 멈춤과 빈도수가 매우 다르기 때문에 GC 순차 비용을 구하는 계산이 다른데, 즉 둘을 합해서 애플리케이션의 총 GC 순차 비용을 구할 수 있다. GC 순차 비용은 다음과 같이 계산될 수 있다.

Total GC time/Total wall clock run time.

총 GC 시간은 다음과 같이 계산할 수 있다.

Avg. GC pause * total no. of GCs

분명 적은 GC 순차 비용은 애플리케이션의 높은 처리량과 확장성을 의미한다.

J2SE 1.4.1은 JVM이 더 많은 CPU와 메모리를 사용할 수 있게 해, 애플리케이션의 성능과 확장성을 높일 수 있게 디자인한 두 개의 새 가비지 컬렉터를 소개했다. 이 컬렉터는 통신 업체의 도전과제를 충족할 수 있게 해준다.

- 최대의 처리량을 위해 시스템의 자원을 확장성과 최적화로 사용하기 위해, 시스템의 GC 순차 비용은 10% 이상
  될 수 없다.
- 클라이언트와 서버 간에 정해진 프로토콜에 정의된 지연에 부합되고 서버의 양호한 답변 시간을 보장하기
  위해서, 애플리케이션에서 발생되는 모든 GC 멈춤은 200ms 이상이 되면 안된다.


J2SE 1.4.1에서 소개한 두 개의 새로운 컬렉터는 Parallel Collector와 Concurrent mark-sweep(CMS) Collector이다.

-Parallel collector: Parallel collector는 신세대를 대상으로 구현되었다. 이는 다중 쓰레드이며, stop-the-world이다. 이 컬렉터는 다중 프로세서 컴퓨터에서 더 좋은 성능을 내도록 다중 쓰레드에서 GC를 할 수 있게 한다. 비록 모든 애플리케이션 쓰레드를 중지시키지만 시스템의 모든 CPU를 사용해 주어진 양의 GC를 더욱 빠르게 처리할 수 있다. 신세대 부분에서의 GC 멈춤을 많이 감소시킨다. 따라서 Parallel collector는 여러 CPU뿐만 아니라 더 많은 메모리에 애플리케이션이 확장할 수 있게 해준다.

-Concurrent mark-sweep(CMS) collector: CMS collector는 구세대를 대상으로 구현되었다. CMS collector는 애플리케이션과 함께 ‘mostly concurrently’하게 실행되어, 때로는 ‘mostly-concurrent garbage collector’로 불린다. GC 멈춤 시간을 짧게 하기 위해서 애플리케이션을 위해 사용할 처리 능력을 이용한다. CMS collection은 Initial mark와 Concurrent marking, Remark, Concurrent sweeping의 4단계로 나뉜다.

‘initial mark’와 ‘remark’ 단계는 CMS collector가 모든 애플리케이션 쓰레드를 일시적으로 중지시키는 stop-the-world 단계이다. initial mark 단계는 시스템의 ‘roots’에서 곧바로 접근가능하게 모든 객체를 기록한다. ‘concurrent marking’ 단계에서 모든 애플리케이션 쓰레드는 재시작되고 concurrent marking 단계가 초기화된다. ‘remark’ 단계에서 애플리케이션 쓰레드는 다시 중지되고 마지막 마킹을 끝낸다. ‘concurrent sweeping’ 단계에서 애플리케이션 쓰레드는 다시 시작되며 heap을 동시 스위핑하며 표기되지 않은 모든 객체는 회수된다. initial mark와 remark 단계는 상당히 짧다. 구세대의 크기가 1G라고 해도 200ms 이하의 시간이 걸린다. concurrent sweeping 단계는 mark-compact collector 만큼의 시간이 걸릴 것이지만 애플리케이션 쓰레드가 중지되지 않기 때문에, 멈춤이 숨겨져 있다.

 
‘mostly concurrent’인 CMS collector는 JVM이 큰 heap과 여러 CPU에 확장할 수 있게 해, 지연과 mark-compact stop-the-world collector로 발생한 처리량 문제를 해결한다.

표 1은 J2SE 1.4.1에 있는 여러 컬렉터의 기능을 비교해본다.
그림 1과 2, 3은 다른 여러 가비지 컬렉터를 그림으로 나타낸 것이다. 초록 화살표는 다중 CPU에서 실행되는 다중 쓰레드 애플리케이션을 뜻한다. 빨간 화살표는 GC 쓰레드를 뜻한다. GC 쓰레드의 길이는 GC 멈춤 시간을 대략적으로 나타낸다.

표 1.다른 여러 가비지 컬렉터의 기능 요약
신세대 컬렉터구세대 컬렉터
Copying collector collector
Default
Stop-the-
world
Single
threaded
All J2SEs
Mark-compact collector
Default
Stop-the-world
Single threaded
All J2SEs
Parallel collectorConcurrent mark-sweep collector
Stop-the-
world
Multi-
threaded
J2SE 1.4.1+
Mostly-concurrent
Single threaded
J2SE 1.4.1+

그림 1과 2, 3이 보여주듯, Parallel collector를 신세대에서 concurrent mark-sweep collector를 구세대에서 같이 사용하면 중지 시간과 GC 순차 비용을 줄일 수 있다. 이 두 컬렉터는 애플리케이션이 더 많은 프로세서와 메모리에 확장할 수 있도록 도와준다.

신고
Posted by Tornado tornado

애플리케이션 서버의 성능과 확장성을 개선하기 위한 Java HotSpot VM vl.4.x Turbo-Charging (2)

 

 
 



J2SE 1.3의 Sun HotSpot JVM에 있는 heap 크기와 GC 튜닝에 잘 알려진 스위치들은 다음과 같다. 스위치에 대한 더 자세한 것은 JVM HotSpot VM options을 참조한다.

일반 스위치들
-server
-Xmx, -Xms
-XX:NewSize=, -XX:MaxNewSize=
-XX:SurvivorRatio=

J2SE 1.4.1에서 2개의 새로운 가비지 컬렉터를 활성화시키기 위한 스위치는 다음과 같다.


Parallel Collector
-XX:+UseParNewGC : 이 플래그는 신세대에 병렬 가비지 컬렉션을 실행시키게 한다. 구세대에 CMS collector와 함께 활성화할 수 있다.

 

-XX:ParallelGCThreads=n : 이 스위치는 JVM이 신세대 GC를 실행할 때 몇개의 병렬 GC 쓰레드를 사용할 것인지 지정해준다. 기본값은 시스템의 CPU 개수와 같다. 그렇지만 어떤 경우에는 이 수를 변경해 성능을 개선할 수 있다는 것을 관찰할 수 있다. 예를 들면 다중 CPU 컴퓨터에서 여러 JVM 인스턴트를 실행할 때이다. 이런 경우, 각 JVM이 사용하는 병렬 GC 쓰레드수를 이 스위치를 사용해 전체 CPU수보다 적게 지정해줘야 할 것이다.

-XX:+UseParallelGC : 이 플래그도 신세대에 병렬 가비지 컬렉션 정책을 사용하게 하지만, 이것은 구세대에서 CMS collector를 같이 사용하지 못한다. 이는 매우 큰 신세대 heap을 사용하는 기업환경용 애플리케이션에 적합하다.

Concurrent Collector
-XX:+UseConcMarkSweepGC : 이 플래그는 구세대에 동시 수행 가비지 컬렉션을 사용하게 한다.

-XX:CMSInitiatingOccupancyFraction=x : CMS collection을 동작시키는 구세대 heap의 threshold 비율을 지정한다. 예를 들어 60으로 지정하면, CMS collector는 구세대의 60%가 사용될 때마다 실행될 것이다. 이 threshold는 실행시 기본값으로 계산되며, 대개의 경우 구세대의 heap이 80~90%가 사용될 때만 CMS collector가 실행된다. 이 값은 튜닝을 통해 대부분의 경우 성능을 개선할 수 있다. CMS collector는 스위핑하고 메모리를 반환할 때 애플리케이션 쓰레드를 중지시키지 않기 때문에, 많은 메모리를 요구하는 애플리케이션의 경우 신세대로부터 넘어오는 객체를 위한 여유 메모리 공간을 최대한 확보할 수 있도록 해준다. 이 스위치가 최적화되지 않았을 때, CMS collection은 수행에 실패하는 경우가 생길 수도 있으며, 이 경우 기본인 stop-the-world mark-compact collector를 실행하게 된다.
다음과 같이 다른 스위치를 사용해 성능을 튜닝할 수도 있다.

-XX:MaxTenuringThreshold=y : 이 스위치는 신세대의 객체가 얼마만큼의 시간이 지나야 구세대로 승진되는지 지정할 수 있다. 기본값은 31이다. 꽤 큰 신세대와 ‘생존 공간’은 오래된 객체는 구세대로 승진되기까지 생존 공간 사이에 31번 복사가 이뤄진다. 대부분의 통신 애플리케이션에서는 호출 또는 세션마다 만들어진 80~90%의 객체는 생성 즉시 죽고, 10~20%정도가 호출이 끝날 때까지 생존한다. XX:MaxTenuringThreshold=0는 애플리케이션의 신세대에 배정한 모든 객체가 한번의 GC 주기를 생존하면 신세대의 생존 공간 사이에 복사되지 않고 곧바로 구세대로 옮겨진다. 구세대에서 CMS collector를 사용할 때, 이 스위치는 두 가지로 도움이 된다.

- 신세대 GC는 10~20%의 장기 객체를 생존 공간 사이에 여러 번 복사할 필요가 없고 곧바로 구세대로 승진시킨다.
- 추가적으로 이 객체는 구세대에서 대부분의 동시발생적인 컬렉션을 행할 수 있다. 이것은 GC 순차 비용을 추가적으로 감소시킨다.

이 스위치가 사용되면 -XX:SurvivorRatio를 128 정도의 매우 큰 값으로 정하길 권한다. 이는 생존 공간이 사용되지 않고, 매 GC 주기마다 신생에서 곧바로 구세대로 승진되기 때문이다. 생존 비율을 높임으로써 신세대의 heap은 대부분 신생에 배정된다.

-XX:TargetSurvivorRatio=z : 이 플래그는 객체가 구세대로 승진되기 전에 사용해야 하는 생존 공간 heap의 희망 비율이다. 예를 들어 z를 90으로 지정하면 생존 공간의 90%를 사용해야 신세대가 꽉 찼다고 생각해 객체가 구세대로 승진된다. 이것은 객체가 승진되기까지 신세대에서 더 오래 머무를 수 있게 한다. 기본값은 50이다.

 

JVM은 GC에 관련된 많은 유용한 정보를 파일에 로그할 수 있다. 이 정보는 애플리케이션과 JVM을 GC 시각에서 튜닝하고 측정하는 데 사용할 수 있다. 이 정보를 로그하기 위한 스위치는 다음과 같다.

verbose:gc : 이 플래그는 GC 정보를 기록하게 한다.
-Xloggc=filename : 이 스위치는 ‘verbose:gc’로 인한 정보를 표준 출력 대신 기록할 로그 파일명을 지정한다.
-XX:+PrintGCTimeStamps : 애플리케이션의 시작을 시점으로 GC가 언제 실행되었는지 출력한다.
-XX:+PrintGCDetails : GC에 대해 GC하기 전과 후의 신세대와 구세대의 크기, 총 heap 크기, 신세대와 구세대에서 GC가 실행되기까지의 시간, GC 주기마다 승진된 객체 크기와 같은 자세한 정보를 준다.
-XX:+PrintTenuringDistribution : 신세대에서 배정된 객체의 나이 분포를 알려준다. 앞서 설명한 -XX:NewSize, -XX:MaxNewSize, -XX:SurvivorRatio, -XX:MaxTenuringThreshold=0의 튜닝은 바로 이 스위치에서 나오는 데이터를 사용해서 객체가 구세대로 승진할 정도로 시간이 지났는지 판단하게 된다.

 

다음의 커맨드라인 스위치를 사용해 나타나는 GC 관련 출력을 살펴보자,

java -verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -XX:+PrintTenuringDistribution -XX:+UseParNewGC -XX:+UseConc MarkSweepGC -Xmx512m -Xms512m -XX:NewSize=24m -XX:MaxNew Size=24m -XX:SurvivorRatio=2 <app-name>
 
신세대 GC
311.649: [GC 311.65: [ParNew Desired survivor size 4194304 bytes, new threshold 3 (max 31)
- age 1: 1848472 bytes, 1848472 total
- age 2: 1796200 bytes, 3644672 total
- age 3: 1795664 bytes, 5440336 total
: 21647K->5312K(24576K), 0.1333032 secs] 377334K->362736K(516096K), 0.1334940 secs]
 
앞서의 GC 스냅샷은 신세대의 GC에 대한 설명이며, 이는 다음 정보를 제공한다.

- 이 GC를 실행할 때, 총 JVM heap은 516096K이며 신세대의 heap 크기는 24576K이다.
- 이 GC가 실행될 때, 구세대의 heap 크기는 516096K-24576K=491520K이다.
- 이 GC 주기의 마지막에 애플리케이션의 신세대에 배정받은 객체의 ‘나이 분포’를 설명한다. 이 GC 주기는 new threshold=3으로 설정돼 있으며, 이는 다음 GC 주기가 되기 전에 최대 3번 시간이 지나야 한다는 뜻이다.

- Desired survivor size가 4194304바이트라는 것은 SurvivorRatio=2를 통해 결정되었다. 이는 Eden/Survivor space=2로 지정한 것이다. 신세대의 heap 크기가 24MB이면, 24MB=2*Survivor space+Survivor space+Survivor space 또는 Survivor space=8MB를 뜻한다. 기본값인 TargetSurvivorRatio=50의 희망 생존 크기는 8*.5=4MB이다.
- 애플리케이션 시작을 시점으로 이 GC가 실행된 시간은 311.649초이다.
- 이 GC가 신세대에서 만든 멈춤 시간은 0.1334940초이다.
- GC를 하기 전에 사용중이던 신세대의 크기는 21647K이다.
- GC를 한 후 사용되는 신세대의 크기는 5312K이다.
- 신세대에서 377334K-362736K=14598K의 데이터가 GC되었다.
- 21647K-5312K-14598K=1737K가 구세대로 승진되었다.
- GC를 하기 전에 사용되고 있던 총 heap 크기는 377334K이었다.
- GC를 한 후에 사용되고 있는 총 heap 크기는 362736K이다.
- GC를 하기 전에 구세대에 사용중인 heap 크기는 377334K-21647K= 355687K이었다.
- GC를 한 후 구세대의 사용하는 heap 크기는 362736K-5312K= 357424K이다.
- 이 GC에 구세대로 승진한 객체의 총 크기는 357424K-355687K=1737K이다.
- ParNew 표기는 신세대에 Parallel collector를 사용했다는 것을 나타낸다. 기본인 Copying collector를 사용하면 ParNew 대신 DefNew로 표기한다.

verbose:gc 로그 형식에서 사용된 관례는 특정 세대에 대한 보고가 있다면 그 세대의 이름이 우선시되어 보고될 것이다.

[GC [gen1 info1] [gen2 info2] info]

info는 GC의 일반적인 것이고, 신세대와 구세대를 합한 것이다. info<n>는 gen<n>에 한정돼 있다. 그러므로 각 괄호의 배치를 잘 봐야 할 것이다.
 

구세대 GC

CMS Collector
구세대에서 concurrent mark-sweep collector를 사용하는 것을 스냅샷한 것이다. 앞서 언급했듯이 이 부분은 4단계로 나눠진다.

513.474: [GC [1 CMS-initial-mark: 335432K(491520K)] 340897K (516096K), 0.0482491secs]

- stop-the-world인 initial mark 단계는 0.048초 걸렸다.
- 애플리케이션 시작 시점에서 513.474초에 시작되었다.
- 사용중이었던 구세대의 heap 크기는 335432K이다.
- 신세대에서 사용하는 heap을 포함한 총 사용중인 heap 크기는 491520K이다.
- 구세대의 heap 총 크기는 340897K이다.
- 신세대 heap을 포함한 총 heap 크기는 516096K이다.
- 이 단계는 메모리를 재순환하지 않는다.
- ‘[GC’ 접두사는 stop-the-world 단계를 뜻한다.

513.523: [CMS-concurrent-mark-start]
514.337: [CMS-concurrent-mark: 0.814/0.814 secs]
514.337: [CMS-concurrent-preclean-start]
514.36: [CMS-concurrent-preclean: 0.023/0.023 secs]

이 concurrent mark 단계는 일초(0.814 + 0.023초)도 걸리지 않았다. 그러나 GC와 함께 애플리케이션도 동시에 실행되었다. 이 단계 또한 어떠한 가비지도 컬렉션되지 않았다.

514.361: [GC 514.361: [dirty card accumulation, 0.0072366 secs]
514.368: [dirty card rescan, 0.0037990 secs]
514.372: [remark from roots, 0.1471209 secs]
514.519: [weak refs processing, 0.0043200 secs] [1 CMS-remark: 335432K(491520K)] 352841K(516096K), 0.1629795 secs]

- stop-the-world인 remark 단계는 0.162초 걸렸다.
- 구세대에 사용중이던 heap 크기는 335432K이다.
- 구세대의 총 heap 크기는 491520K이다.
- 신세대를 포함한 총 사용중이던 heap 크기는 352841K이다.
- 신세대를 포함한 총 heap 크기는 516096K이다.
- 이 단계에서 어떠한 메모리도 재순환되지 않는다.
- 이미 말했듯이 ‘[GC’ 표기는 stop-the-world 단계라는 것을 알려준다.

514.525: [CMS-concurrent-sweep-start]
517.692: [CMS-concurrent-sweep: 2.905/3.167 secs]
517.693: [CMS-concurrent-reset-start]
517.766: [CMS-concurrent-reset: 0.073/0.073 secs]

이 concurrent sweep은 3초 걸렸다. 그러나 여러 프로세스가 있는 시스템에서 이 단계는 GC와 함께 애플리케이션 쓰레드가 실행될 수 있다. 4개의 CMS 단계에서 이 단계만이 heap을 스위핑하고 컬렉션한다.

Default Mark-Compact Collector
만약 CMS collector 대신에 구세대에서 기본인 mark-compact collector가 사용되었다면 GC 스냅샷은 다음과 같을 것이다.

719.2: [GC 719.2: [DefNew: 20607K->20607K(24576K), 0.0000341 secs]719.2: [Tenured: 471847K->92010K(491520K), 2.6654172 secs] 492454K->92010K(516096K), 2.6658030 secs]

- GC가 시작한 시작은 애플리케이션 시작 시점으로 719.2초였다.
- DefNew 표기는 신세대에서 기본인 copying collector가 사용되었음을 알려준다.
- ‘[GC’ 표기는 JVM이 stop-the-world GC를 사용했다는 것을 나타낸다. 구세대의 GC에서, 애플리케이션에서 System.gc()를 통해 시스템에 GC를 요청할 수 있는데, 이것은 ‘Full GC’로 표기된다.
- 신세대의 총 heap 크기는 24576K이다.
- 신세대에서 컬렉션은 단지 시도만 이뤄졌다. 구세대에서 잠재적인 데이터를 모두 흡수할 수 있다는 보장이 없어 컬렉션할 수 없다는 것을 알았기 때문이다. 그 결과 신세대의 컬렉션은 일어나지 않았고, 신세대에서는 어떠한 메모리 재순환도 이뤄지지 않았다고 보고했다(순간의 ms 내에 종료했다는 것을 주시한다). 이는 신세대에서 컬렉션이 이뤄지지 못한다는 것을 결정하는 것 외에는 별달리 한 것이 없기 때문이다.

[DefNew: 20607K->20607K(24576K), 0.0000341 secs]

신세대에서 승진되는 것을 흡수하지 못해 구세대가 꽉 찼기 때문에 GC가 필요하다는 알게 되었다. 그래서 full mark-compact collection이 실행되었다.

- 보유하고 있는 표기는 구세대에 full mark-compact GC가 실행되었다는 것을 나타낸다. ‘구세대’는 ‘Tenured generation’이라고도 한다.
- GC를 하기 전에 구세대의 사용중인 heap 크기는 471847K이었다.
- GC 후의 구세대의 사용 heap 크기는 92010K이다.
- GC가 실행될 때의 구세대의 총 heap 크기는 491520K이었다.
- GC 전에 신세대와 구세대의 합인 총 사용중인 heap 크기는 492454K이었다.
- GC 후에 신세대와 구세대의 합인 총 사용 heap 크기는 92010K이다.
- GC를 통해 컬렉션된 총 크기는 492454K-92010K=399837K이다.
- JVM의 총 heap 크기는 516096K이다.
- 이 GC로 총 2.6658030초동안 애플리케이션이 중지되었다.

 

verbose:gc 로그로부터 데이터 마이닝을 통해 애플리케이션
모델링하기


이 로그를 통해 가비지 컬렉션 시각에서 애플리케이션과 JVM 행동에 대한 다양한 정보가 파생될 수 있다. 이들은 다음과 같다.

- 신세대와 구세대에서의 평균 GC 멈춤 시간(Average GC pauses) : JVM에서 가비지 컬렉션이 이뤄지는 동안 애플리케이션이 중지되는 평균 시간이다.
- 신세대와 구세대에서의 평균 GC 빈도수(Average GC frequency) : 신세대와 구세대에서 실행된 가비지 컬렉터의 주기수이다. 이 값은 각 GC가 로그에 기록된 시간을 보면 구할 수 있다.
- GC 순차 비용(GC sequential overhead) : 가비지 컬렉션이 이뤄지면서 애플리케이션이 중지된 시스템 시간의 비율이다. Avg. GC pause*Avg. GC frequency*100%로 계산할 수 있다.
- GC concurrent overhead : 애플리케이션과 함께 가비지 컬렉션이 일어난 시스템 시간의 비율이다. Avg. concurrent GC time (sweeping phase) * Avg. concurrent GC frequency / no. of CPUs로 계산할 수 있다.
- 신세대와 구세대에서 각 GC에서 재순환된 메모리 : 각 GC에서 컬렉션된 총 가비지
- 할당 비율 : 애플리케이션의 신세대에서 데이터가 할당받는 비율을 말한다. 만약 heap이 occupancy_at_start_of_current_gc=x , occupancy_at_end_of_ previous_gc = y, GC 빈도수가 초당 1이면, 할당 비율은 약 초당 x-y이다.
- 승진 비율(Promotion rate) : 데이터가 구세대로 승진되는 비율. ‘신세대 GC’에서 설명한 것처럼 GC당 승진된 객체의 크기가 계산되었고, 예를 들어 신세대 GC 빈도수가 초당 1일 때 그 스냅샷에 의하면 승진 비율은 초당 1737K이다.
- 호출당 애플리케이션에 의해 할당된 총 데이터 : 이것은 앞서와 같이 할당 비율을 구하고, 애플리케이션 서버의 로드가 호출 비율이라고 할 때, 할당 비율/호출 비율(Allocation Rate/Call rate)로 계산될 수 있다. 즉 호출 비율은 요청 또는 오는 호출을 서버가 처리하는 비율을 말한다.
- 모든 데이터는 단기 데이터(short term)와 장기 데이터(long term data)를 나눌 수 있다 : 장기 데이터는 신세대 GC에서 생존해서 구세대로 승진한 것을 말한다. 승진 비율/호출 비율(Promotion Rate/Call Rate)로 계산할 수 있다.
- 호출당 단기 데이터(Short Term) : 단기 데이터는 매우 빨리 소멸하며, 신세대에서 컬렉션할 수 있다. 이것은 총 데이터-장기 데이터(Total Data-Long Term Data)로 계산할 수 있다.
- 호출당 총 활성화 데이터 : JVM heap 크기를 결정하는 데 매우 중요한 정보이다. 예를 들어 이 SIP 애플리케이션의 경우와 같이, 초당 100 호출의 로드에 초당 50K의 장기 데이터가 최소 40초 이상 지속되면 구세대의 최소 메모리 크기는 50K*40s*100=200M이어야 한다.
- ‘메모리 누출(Memory leaks)’ : 이것은 감지할 수 있으며 로그가 보여주는 각 GC를 모니터링에서 나오는 ‘out of memory’ 에러로 보다 쉽게 이해할 수 있을 것이다.

이런 종류의 정보는 GC의 시각에서 애플리케이션과 JVM 행동을 보다 잘 이해하고 가비지 컬렉션의 실행 성능을 튜닝하는 데 사용할 수 있다.

 

PrintGCStats(http://developer.java.sun.com/servlet/Turbo1EntryServlet에서 다운받을 수 있다)는 ‘verbose:gc’ 로그를 마이닝하는 쉘 스크립트이고 신세대와 구세대의 GC 중단 시간(총, 평균, 최대, 표준편차)의 통계를 낸다. 이는 GC 순차적 오버헤드, GC 동시발생 오버헤드, 데이터 배정, 승진 비율, 총 GC와 애플리케이션 시간과 같은 다른 중요한 GC 매개변수도 계산한다. 통계 외에도 PrintGCStats는 사용자가 설정한 주기에 맞춰 애플리케이션 실행에 대해 GC를 시간에 따라 분석한다.

 

입력
이 스크립트의 입력은 HotSpot VM이 다음의 하나 이상의 플래그를 사용했을 때 나오는 출력이다.

-verbose:gc : 최소의 출력을 생성한다. 제한적인 통계를 내지만, 모든 JVM에서 사용할 수 있다.
-XX:+PrintGCTimeStamps : 시간 단위의 통계를 활성화한다. 예를 들어 할당 비율과 주기 등이 있다.
-XX:+PrintGCDetails : 더 많은 통계 정보를 얻을 수 있다.
J2SE 1.4.1 이후부터 다음 명령어를 권한다.
java -verbose:gc -XX:+PrintGCTimeStamps -XX:+ PrintGCDetails ...

 

사용법
PrintGCStats -v ncpu=<n> [-v interval=<seconds>] [-v verbose=1] <gc_log_file >
·ncpu
Java 애플리케이션이 실행된 컴퓨터의 CPU 개수이다. 사용할 수 있는 CPU 시간과 GC ‘로드’ 요소를 계산하는 데 사용된다. 기본값이 없기 때문에 명령어에서 필히 지정해줘야 한다(기본값 1은 틀릴 때가 많다).

·interval
시간 단위 분석을 위해서 매 주기의 마지막에 통계를 출력한다 : -XX:+PrintGCTimeStamps의 출력이 필요하다. 기본값은 0(비활성화)

·verbose
만약 non-zero이면 통계 요약 후 추가적으로 각 아이템은 다른 줄에 출력한다.

출력 통계치
표 1은 PrintGCStats로부터의 통계치를 설명한 것이다.

 
표 1. PrintGCStats이 출력한 통계 요약
항목명 정의
  gen0(s) 신세대의 GC 시간을 초단위로 나타낸다.
  cmsIM(s) CMS의 initial mark 단계의 멈춤 시간을 초단위로 나타낸다.
  cmsRM(s) CMS의 remark 단계의 멈춤 시간을 초단위로 나타낸다.
  GC(s) 모든 stop-the-world GC의 멈춤 시간을 초단위로 나타낸다.
  cmsCM(s) CMS concurrent mark 단계를 초단위로 나타낸다.
  cmsCS(s) CMS concurrent sweep 단계를 초단위로 나타낸다.
  alloc(MB) 신세대에 객체 할당한 용량(MB)
  promo(MB) 구세대에 승진된 객체 용량(MB)
  elapsed_time(s) 애플리케이션이 실행되며 경과된 총 시간(초)
  tot_cpu_time(s) 총 CPU 시간 = CPU 개수 * 경과 시간
  mut_cpu_time(s) 애플리케이션이 사용한 총 CPU 시간.
  gc0_time(s) 신세대를 GC할 때 총 멈춰진 시간
  alloc/elapsed_time(MB/s) 경위 시간당 할당 비율(MB/초)
  alloc/tot_cpu_time(MB/s) 총 CPU 시간당 할당 비율(MB/초)
  alloc/mut_cpu_time(MB/s) 애플리케이션 총 시간당 할당 비율(MB/초)
  promo/gc0_time(MB/s) GC 시간당 승진 비율(MB/초)
  gc_seq_load(%) stop-the-world GC에 사용된 총 시간의 비율
  gc_conc_load(%) 동시발생적 GC에 사용된 총 시간의 비율
  gc_tot_load(%) GC 시간(순차적과 동시발생적)의 총 비율

- Solaris 8 이후로 사용할 수 있는 alternate thread library를 사용해라.
Solaris 8에서 LD_LIBRARY_PATH=/usr/lib/lwp:/usr/lib을 설정해서 사용할 수 있고, Solaris 9에서는 기본 라이브러리로 설정돼 있다. 이는 Java 쓰레드와 커널 쓰레드 사이에 ‘one-to-one threading model’을 사용한다. 이 라이브러리를 사용해서 대부분의 경우에 처리량이 5~10% 또는 그 이상으로 성능이 향상됐다.

- prstat -Lm -p <jvm process id>로 light-weight-process(LWP) 단위로 한 프로세스의 자원 사용을 분석하고 확장성과 성능 면에서의 병목 지점을 찾을 수 있게 한다. 예를 들어 애플리케이션의 확장성에 있어 병목 지점이 GC인지 확인하는 데 도움을 준다. 대부분의 경우, 애플리케이션이 자체적으로 제대로 쓰레드화하지 못해 큰 시스템으로 확장되지 못한다. 이 명령어는 시스템 자원 사용과 주어진 프로세스에서 LWP 단위의 활동을 보고한다. 더 자세한 사항은 man prstat에서 참조한다. 이 명령어의 결과 출력물의 단점은 보고된 LWP id가 어떤 Java 쓰레드인지 확인하기 어렵다는 것이다.

- ThreadAnalyser는 Java 프로세스를 분석하는 쉘 스크립트이고, Solaris의 LWPIDs 대신 prstat의 출력물에 있는 ‘쓰레드명’을 생성한다. Java 애플리케이션의 시스템 사용을 ‘쓰레드명’ 단위로 확인할 수 있게 해준다. 이 스크립트를 앞서 언급한 alternate thread library와 함께 사용하는 것이 가장 이상적이다.

ThreadAnalyser는 이미 실행돼 있는 Java 프로세스에 붙일 수 있거나(파일로 전환된 프로세스의 stderr 출력에 접근할 수 있는 한) Java 프로세스를 시동시킬 수 있는 스크립트 파일을 사용해 동작시킬 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 
사용 예
Java 프로세스를 시작하고 표준 에러를 java <app_name>>/ tmp/java.out 2>&1 으로 파일로 전환시킨다. Java 프로세스의 PID를 주의하고 다음과 같이 ThreadAnalyser(http://developer.java.sun.com/servlet/Turbo2EntryServlet에서 다운받을 수 있다.)을 붙인다.

ThreadAnalyser -f /tmp/java.out -p PID <interval>
또는
ThreadAnalyser -f /tmp/java.out <interval>


이 컴퓨터에 다른 Java 프로세스가 없으면 ps(1)을 사용해 PID를 구한다. ThreadAnalyser로 Java 프로세스를 실행하고자 한다면 다음과 같이 한다.

ThreadAnalyser -s <app_start_script_file> <interval>

이것은 이 시스템에 다른 Java 프로세스가 동작하고 있지 않을 때만 사용할 수 있다. app_start_script_file이 Java 프로세스을 실행하는 실행 스크립트 파일명이다. Interval은 스크립트로부터 출력되는 정보 갱신 주기이고, 기본값은 5이다.

보다 많은 옵션과 사용법은 ThreadAnalyser 스크립트의 README 부분을 읽어보기 바란다.
 

man prstat 명령어를 입력하면 다음과 같은 출력의 각 열이 의미하는 것을 자세히 알 수 있다.



- 실행되는 Java 애플리케이션의 모든 쓰레드의 덤프(dump)를 얻으려면, SIGQUIT 신호를 JVM 프로세스에 보낸다. 이는 kill - QUIT <JVM process pid> 명령어로 신호를 보낼 수 있다.
- JVM 프로세스 안의 각 LWP의 hex와 symbolic stack trace를 얻으려면 명령어 pstack <JVM process id>을 사용하면 된다. 이 출력은 LWP id와 Java 애플리케이션 쓰레드 id와의 관계를 알려준다. 만약 alternate thread library를 JVM에서 사용하면, 출력물은 프로세스의 각 LWP와 해당 묶여 있는 Java 애플리케이션 쓰레드의 일대일 관계를 보여준다. 명령어 man pstack을 입력하면 보다 자세한 것을 알 수 있다.
- JVM에 있는 -Xrunhprof 플래그를 사용해 불필요한 객체 보유(때로는 막연하게 ‘메모리 누수’라 부른다.)를 확인할 수 있다.
- UDP 기반의 애플리케이션에서는 DatagramSocket.connect (InetAddress address, int port)와 DatagramSocket.disconnect()의 호출 횟수를 최소화한다. J2SE 1.4 버전 이상의 JVM부터는 이것들이 네이티브한 호출이기 때문에, 여러 다른 이점을 가져다 주지만, 잘못된 사용은 애플리케이션의 성능 저하를 야기한다. API를 잘못 사용하면 애플리케이션의 애플리케이션 ‘시스템 시간’ 시점에서 매우 비싼 비용을 지불하게 된다.

 

다음에 보여줄 결과값은 2개의 새로운 GC 정책과 앞서 언급한 튜닝 기법을 사용해 얻은 것이다. 이 사례를 위해서 Ubiquity의 Application Services Broker(ASB)인 SIP Application Server가 사용되었다. 지난 호에 언급했듯이 ASB는 GC 시점에서 JVM을 광범위하게 사용하는 일반적인 통신 애플리케이션 서버를 대표한다.

 

실험적인 플랫폼
4개의 900MHz UltraSPARC Ⅲ 프로세서를 장착하고, Solaris 8이 설치된 Sun Fire V480을 사용했다. 물론 논의된 튜닝 기술들은 Linux와 Windows, Solaris(인텔 아키텍처)와 같은 다른 플랫폼에도 적용된다.
구세대 heap의 크기는 ASB 서버의 최대 로드에 기반해서 결정되었고, JVM은 튜닝이 적용되었다. 신세대 heap의 크기는 12MB에서 128MB 사이에 경험을 통해 계속적으로 변경해보았다. GC 순차적 오버헤드가 가장 낮고 신세대에 허용할 수 있는 GC 멈춤 정도를 갖는 최적의 크기는 24MB였다. 24MB 이상의 신세대는 GC 멈춤이 증가했고 성능의 차이가 없었다. 24MB 이하는 신세대의 GC 빈도수가 많아지면서 GC 순차적 오버헤드가 급증하고 짧은 생명주기를 갖은 데이터도 구세대로 승진되고 만다. 그림 1은 신세대의 heap 크기에 따른 다양한 GC 순차적 오버헤드와 GC 멈춤, GC 빈도수를 보여준다.

512MB의 구세대와 24MB의 신세대에서 여러 가지의 GC 정책과 튜닝 기술을 사용해 나온 결과값을 보여준다.

그림 1. 신세대의 다양한 GC 순차적 오버헤드와 GC 멈춤, GC 빈도수를 신세대의 heap 크기에 비례해서 보여준다.

단계 1 : J2SE 1.4.1에서 신세대와 구세대의 기본 GC를 사용할 때 가장 좋은 결과다.

java -Xmx512m -Xms512m -XX:MaxNewSize=24m -XX:NewSize=24m -XX:SurvivorRatio=2 <application>

구세대의 평균 GC pause: 3 secs.
신세대의 평균 GC pause: 110 ms.
GC sequential overhead: 18.9%

단계 2 : J2SE 1.4.1에서 구세대의 GC를 CMS 컬렉터로 설정하고 나온 가장 좋은 값이다.

java -Xmx512m -Xms512m -XX:MaxNewSize=24m -XX:NewSize=24m -XX:SurvivorRatio=128 -XX:+UseConcMarkSweepGC -XX:MaxTenuring Threshold=0 -XX:CMSInitiatingOccupancyFraction=60 <application>

-XX:MaxTenuringThreshold=0로 설정하면 성능이 증가한다. -XX:CMSInitiatingOccupancyFraction의 적절한 값은 60이다. 이보다 작으면 더 많은 CMS 가비지 컬렉션이 일어나고 많으면 CMS 컬렉션의 효율이 떨어진다.

구세대의 평균 GC 멈춤(stop-the-world init mark와 동시발생 컬렉션의 remark phase) : 115ms
신세대의 평균 GC 멈춤 : 100ms
GC 순차적 오버헤드 : 8.6%

단계 3 : J2SE 1.4.1에서 구세대는 CMS 컬렉터를 신세대는 새로운 패러럴 컬렉터를 사용할 때 가장 좋은 값이다.

java -Xmx512m -Xms512m -XX:MaxNewSize=24m -XX:NewSize=24m -XX:SurvivorRatio=128 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=0 -XX:CMSInitiatingOccupancyFraction=60 <application>

구세대의 평균 GC 멈춤(stop-the-world init mark와 동시발생 컬렉션의 remark phase) : 142ms
신세대의 평균 GC 멈춤 : 50ms
GC 순차적 오버헤드 : 5.9%

 
표 2. 사례를 통해 얻은 값을 정리
Summary Table 구세대의 평균
GC 멈춤(ms)
신세대의 평균
GC 멈춤(ms)
순차적
오버헤드(%)
  기본 컬렉터3000 110 18.9%
  CMS 컬렉터1151008.6%
  CMS와 패러럴 컬렉터142 50 5.9%
 

결과
CMS 컬렉터를 사용함으로써 구세대에서 GC 멈춤은 2000% 감소하고 GC 순차적 오버헤드는 220% 감소했다. GC 순차적 오버헤드의 감소는 애플리케이션 처리량의 성능 향상에 직접적으로 영향을 준다. 패러럴 컬렉터를 사용해 신세대에서 GC 멈춤은 100% 감소했다. 4개의 CPU가 장착된 시스템에서 4배 성능의 패러럴 컬렉터를 기대했을지도 모르겠다. 그러나 패러럴 컬렉션에 연관된 간접 비용에 의해, 가속도는 선형을 그리지 않지만 JVM이 선형을 그리도록 연구하고 있다.

기업에 적용
성능 튜닝 기술과 소개된 새로운 GC 정책은 통신업체에만 국한된 것이 아니고 웹 서버와 포털 서버, 애플리케이션 서버와 같이 비슷한 요구사항을 갖는 대기업용으로도 적용될 수 있다.

미래에는...
썬 JVM에서 하위 레벨의 명령어 플래그를 없애려고 연구중이다. 신세대와 구세대의 크기와 GC 정책 등을 설정하기보다는 최고 멈춤 시간 또는 지연, JVM의 메모리 사용량, CPU 사용법 등과 같은 상위 레벨의 요구사항을 설정하면 JVM이 자동적으로 그 설정에 맞게 애플리케이션을 위해 여러 하위 레벨의 값과 정책을 변경한다.

HotSpot JVM에 JVM의 조작 면에서 상위 레벨 단의 성능 정보를 제공할 수 있는 가볍고 항상 실행될 수 있는 것을 포함하려고 한다. 향후 출시되는 JVM에서 사용할 수 있겠지만, 현재로서는 툴과 기술은 아직 실험적이다.

Java Specification Request(JSR) 174는 JVM을 모니터링하고 관리하는 API 스펙이고, JSR 163은 시간과 메모리에 대한 프로파일링의 지원으로 실행되는 JVM의 프로파일링 정보를 추출해내는 API 스펙이다. 이 API는 프로파일에 영향을 주지 않는 구현이 되도록 설계될 것이다. 이 API는 프로파일링의 상호 운용성과 향상된 가비지 컬렉션 기술과 최대한 많은 JVM에서도 안정적으로 동작하는 구현을 고려할 것이고, Java 애플리케이션, 시스템 관리 툴, RAS 관련 툴에 JVM의 상태를 모니터할 수 있는 능력을 부여하며, 많은 런타임 컨트롤을 관리할 수 있게 할 것이다.

통신 애플리케이션 서버의 특수한 요구사항을 충족하기 위한 연구가 Sun ONE Application Server 7에서도 진행되고 있다. 통신업체의 경우 서비스와 데이터에 더 높은 가용성이 요구되는 것과 서비스 업그레이드를 제외하면 대기업과 유사하다. Sun ONE Application Server Enterprise Edition 7은 상태 확인 저장소와 세션, 빈 상태의 높은 유용성을 주는 Clustra ‘Always ON’ 기술이 포함돼 있다. Sun ONE Application server 7는 JAIN과 OSA/Parlay 프레임웍 기반의 호출 프로세싱 서버를 구축하는 데 사용할 수 있다. EJB 컨테이너 향상을 위해 SLEE 구현은 Sun ONE Application server에 통합될 수 있다. 추후 EJB를 사용할 때 지속적인 유용성과 어느 정도의 실시간, QoS 능력을 제공하기 위해 JSR 117 API 뿐만 아니라 통합된 SIP 스택도 포함될 수 있다.

통신 업체 사이에 Java의 인기가 증가하면서 썬 JVM은 그들의 요구 사항에 맞추어 발전해가고 있다. 대형 서버의 요구사항을 충족시키는 데 필요한 해결책으로 64비트 JVM과 대형 하드웨어에서(4G에 제한되지 않은) 대용량의 heap 크기를 설정할 수 있다. 여러 CPU 시스템에서 패러럴 컬렉터는 신세대에서 멈춤을 줄일 수 있고 동시발생 컬렉션은 구세대에 mark-compact collector로 인해 생긴 큰 GC 멈춤을 숨길 수 있다. 이로 인해 GC 순차적 오버헤드를 상당히 감소시켜서, 다수의 CPU가 장착된 시스템에서 애플리케이션의 성능과 확장성을 높여줄 수 있다.

JVM의 가비지 컬렉션은 verbose:gc의 로그를 보고 모니터하고, 분석하고 튜닝할 수 있다. GC 로그의 정보는 JVM의 크기를 조정하고 애플리케이션의 최적의 성능을 내기 위한 스위치와 매개변수를 설정하기 위해 분석된다.

머지않아 Sun ONE Application Server 7 Enterprise Edition은 최적의 통신 애플리케이션 서버로 구현될 것이다.

참고로 이글은 썬의 Alka Gupta 씨와 Ubiquity Software Corporation의 CTO인 Michael Doyle 씨가 쓴 ‘Turbo-Charging the Java HotSpot VM, 1.4.X to Improve the Performance and Scalability of Application Servers’를 정리한 것이다.

 

신고
Posted by Tornado tornado

A look inside the Glassbox Inspector with AspectJ and JMX

 

Level: Intermediate

Ron Bodkin (ron.bodkin@newaspects.com), Founder, New Aspects of Software

13 Sep 2005

Say goodbye to scattered and tangled monitoring code, as Ron Bodkin shows you how to combine AspectJ and JMX for a flexible, modular approach to performance monitoring. In this first of two parts, Ron uses source code and ideas from the Glassbox Inspector open source project to help you build a monitoring system that provides correlated information to identify specific problems, but with low enough overhead to be used in production environments.
About this series

The AOP@Work series is intended for developers who have some background in aspect-oriented programming and want to expand or deepen what they know. As with most developerWorks articles, the series is highly practical: you can expect to come away from every article with new knowledge that you can put immediately to use.

Each of the authors contributing to the series has been selected for his leadership or expertise in aspect-oriented programming. Many of the authors are contributors to the projects or tools covered in the series. Each article is subjected to a peer review to ensure the fairness and accuracy of the views expressed.

Please contact the authors individually with comments or questions about their articles. To comment on the series as a whole, you may contact series lead Nicholas Lesiecki. See Resources for more background on AOP.

Modern Java™ applications are typically complex, multithreaded, distributed systems that use many third-party components. On such systems, it is hard to detect (let alone isolate) the root causes of performance or reliability problems, especially in production. Traditional tools such as profilers can be useful for cases where a problem is easy to reproduce, but the overhead imposed by such tools makes them unrealistic to use in production or even load-test environments.

A common alternative strategy for monitoring and troubleshooting application performance and failures is to instrument performance-critical code with calls to record usage, timing, and errors. However, this approach requires scattering duplicate code in many places with much trial and error to determine what code needs to be measured. This approach is also difficult to maintain as the system changes and is hard to drill into. This makes application code challenging to add or modify later, precisely when performance requirements are better known. In short, system monitoring is a classic crosscutting concern and therefore suffers from any implementation that is not modular.

As you will learn in this two-part article, aspect-oriented programming (AOP) is a natural fit for solving the problems of system monitoring. AOP lets you define pointcuts that match the many join points where you want to monitor performance. You can then write advice that updates performance statistics, which can be invoked automatically whenever you enter or exit one of the join points.

In this half of the article, I'll show you how to use AspectJ and JMX to create a flexible, aspect-oriented monitoring infrastructure. The monitoring infrastructure I'll use is the core of the open source Glassbox Inspector monitoring framework (see Resources). It provides correlated information that helps you identify specific problems but with low enough overhead to be used in production environments. It lets you capture statistics such as total counts, total time, and worst-case performance for requests, and it will also let you drill down into that information for database calls within a request. And it does all of this within a modest-sized code base!

In this article and the next one, I'll build up from a simple Glassbox Inspector implementation and add functionality as I go along. Figure 1 should give you an idea of the system that will be the end result of this incremental development process. Note that the system is designed to monitor multiple Web applications simultaneously and provide correlated statistical results.


Figure 1. Glassbox Inspector with a JConsole JMX client
Glassbox Inspector monitoring multiple applications

Figure 2 is an overview of the architecture of the monitoring system. The aspects interact with one or more applications inside a container to capture performance data, which they surface using the JMX Remote standard. From an architectural standpoint, Glassbox Inspector is similar to many performance monitoring systems, although it is distinguished by having well-defined modules that implement the key monitoring functions.


Figure 2. The Glassbox Inspector architecture
Glassbox Inspector architecture

Java Management Extensions (JMX) is a standard API for managing Java applications by viewing attributes of managed objects. The JMX Remote standard extends JMX to allow external client processes to manage an application. JMX management is a standard feature in Java Enterprise containers. Several mature third-party JMX libraries and tools exist, and JMX support has been integrated into the core Java runtime with Java 5. Sun Microsystems's Java 5 VM includes the JConsole JMX client.

You should download the current versions of AspectJ, JMX, and JMX Remote, as well as the source packet for this article (see Resources for the technologies and Download for the code) before continuing. If you are using a Java 5 VM, then it has JMX integrated into it. Note that the source packet includes the complete, final code for the 1.0 alpha release of the open source Glassbox Inspector performance monitoring infrastructure.

The basic system

I'll start with a basic aspect-oriented performance monitoring system. This system captures the time and counts for different servlets processing incoming Web requests. Listing 1 shows a simple aspect that would capture this performance information:


Listing 1. An aspect for capturing time and counts of servlets
/** * Monitors performance timing and execution counts for  * <code>HttpServlet</code> operations */public aspect HttpServletMonitor {   /** Execution of any Servlet request methods. */  public pointcut monitoredOperation(Object operation) :     execution(void HttpServlet.do*(..)) && this(operation);   /** Advice that records statistics for each monitored operation. */  void around(Object operation) : monitoredOperation(operation) {      long start = getTime();       proceed(operation);       PerfStats stats = lookupStats(operation);      stats.recordExecution(getTime(), start);  }   /**   * Find the appropriate statistics collector object for this   * operation.   *    * @param operation   *            the instance of the operation being monitored   */  protected PerfStats lookupStats(Object operation) {      Class keyClass = operation.getClass();      synchronized(operations) {          stats = (PerfStats)operations.get(keyClass);          if (stats == null) {                              stats = perfStatsFactory.                createTopLevelOperationStats(HttpServlet.class,                        keyClass);              operations.put(keyClass, stats);          }      }      return stats;          }   /**   * Helper method to collect time in milliseconds. Could plug in   * nanotimer.   */  public long getTime() {      return System.currentTimeMillis();  }   public void setPerfStatsFactory(PerfStatsFactory     perfStatsFactory) {      this.perfStatsFactory = perfStatsFactory;  }   public PerfStatsFactory getPerfStatsFactory() {      return perfStatsFactory;  }   /** Track top-level operations. */   private Map/*<Class,PerfStats>*/ operations  =     new WeakIdentityHashMap();  private PerfStatsFactory perfStatsFactory;} /** * Holds summary performance statistics for a  * given topic of interest * (e.g., a subclass of Servlet). */ public interface PerfStats {  /**   * Record that a single execution occurred.   *    * @param start time in milliseconds   * @param end time in milliseconds   */  void recordExecution(long start, long end);   /**   * Reset these statistics back to zero. Useful to track statistics   * during an interval.   */  void reset();   /**   * @return total accumulated time in milliseconds from all   *         executions (since last reset).   */  int getAccumulatedTime();   /**   * @return the largest time for any single execution, in   *         milliseconds (since last reset).   */  int getMaxTime();   /**    * @return the number of executions recorded (since last reset).    */  int getCount();} /** * Implementation of the *  * @link PerfStats interface. */public class PerfStatsImpl implements PerfStats {  private int accumulatedTime=0L;  private int maxTime=0L;  private int count=0;   public void recordExecution(long start, long end) {      int time = (int)(getTime()-start);      accumulatedTime += time;      maxTime = Math.max(time, maxTime);      count++;  }   public void reset() {      accumulatedTime=0L;      maxTime=0L;      count=0;  }   int getAccumulatedTime() { return accumulatedTime; }  int getMaxTime() { return maxTime; }  int getCount() { return count; }} public interface PerfStatsFactory {    PerfStats       createTopLevelOperationStats(Object type, Object key);} 

As you can see, this first version is fairly basic. HttpServletMonitor defines a pointcut called monitoredOperation that matches the execution of any method on the HttpServlet interface whose name starts with do. These are typically doGet() and doPost(), but it also captures the less-often-used HTTP request options by matching doHead(), doDelete(), doOptions(), doPut(), and doTrace().

Managing overhead

I'll focus on techniques to manage the monitoring framework's overhead in the second half of the article, but for now, it's worth noting the basic strategy: I'll do some in-memory operations that take up to a few microseconds when something slow happens (like accessing a servlet or database). In practice, this adds negligible overhead to the end-to-end response time of most applications.

Whenever one of these operations executes, the system executes an around advice to monitor performance. The advice starts a stop watch, and then it lets the original request proceed. After this, it stops the stop watch and looks up a performance-statistics object that corresponds to the given operation. It then records that the operation was serviced in the elapsed time by invoking recordExecution() from the interface PerfStats. This simply updates the total time, the maximum time (if appropriate), and a count of executions of the given operation. Naturally, you could extend this approach to calculate additional statistics and to store individual data points where issues might arise.

I've used a hash map in the aspect to store the accumulated statistics for each type of operation handler, which is used during lookup. In this version, the operation handlers are all subclasses of HttpServlet, so the class of the servlet is used as the key. I've also used the term operation for Web requests, thus distinguishing them from the many other kinds of requests an application might make (e.g., database requests). In the second part of this article, I'll extend this approach to address the more common case of tracking operations based on the class or method used in a controller, such as an Apache Struts action class or a Spring multiaction controller method.



Back to top


Exposing performance data

Thread safety

The statistics-capturing code for the Glassbox Inspector monitoring system isn't thread safe. I prefer to maintain (potentially) slightly inaccurate statistics in the wake of rare simultaneous access to a PerfStats instance by multiple threads, rather than adding extra synchronization to program execution. If you prefer improved accuracy, you can simply make the mutators synchronized (for example, with an aspect). Synchronization would be important if you were tracking accumulated times more than 32 bits long, since the Java platform doesn't guarantee atomic updates to 64-bit data. However, with millisecond precision, this would give you 46 days of accumulated time. I recommend aggregating and resetting statistics far more frequently for any real use, so I've stuck with the int values.

Once you've captured performance data, you have a wide variety of options for how to make it available. The easiest way is to write the information to a log file periodically. You could also load the information into a database for analysis. Rather than add the latency, complexity, and overhead of summarizing, logging, and processing information, it is often better to provide direct access to live system performance data. I'll show you how to do this in this next section.

I want a standard protocol that existing management tools can display and track, so I'll use the JMX API to share performance statistics. Using JMX means that each of the performance-statistics instances will be exposed as a management bean, thus yielding detailed performance data. Standard JMX clients like Sun Microsystems's JConsole will also be able to show the information. See Resources to learn more about JMX.


Figure 3 is a screenshot of the JConsole showing data from the Glassbox Inspector monitoring the performance of the Duke's Bookstore sample application (see Resources). Listing 2 shows the code that implements this feature.


Figure 3. Using Glassbox Inspector to view operation statistics
Using Glassbox Inspector to view operation statistics

Traditionally, supporting JMX involves implementing patterns with boilerplate code. In this case, I'll combine JMX with AspectJ, which enables me to write the management logic separately.


Listing 2. Implementing the JMX management feature
/** Reusable aspect that automatically registers *  beans for management */public aspect JmxManagement {    /** Defines classes to be managed and      *  defines basic management operation     */    public interface ManagedBean {        /** Define a JMX operation name for this bean.          *  Not to be confused with a Web request operation.         */        String getOperationName();        /** Returns the underlying JMX MBean that          *  provides management         *  information for this bean (POJO).         */        Object getMBean();    }     /** After constructing an instance of      *  <code>ManagedBean</code>, register it     */    after() returning (ManagedBean bean):       call(ManagedBean+.new(..)) {        String keyName = bean.getOperationName();        ObjectName objectName =           new             ObjectName("glassbox.inspector:" + keyName);        Object mBean = bean.getMBean();        if (mBean != null) {            server.registerMBean(mBean, objectName);        }    }    /**      * Utility method to encode a JMX key name,      *  escaping illegal characters.     * @param jmxName unescaped string buffer of form      * JMX keyname=key      * @param attrPos position of key in String     */     public static StringBuffer       jmxEncode(StringBuffer jmxName, int attrPos) {        for (int i=attrPos; i<jmxName.length(); i++) {            if (jmxName.charAt(i)==',' ) {                jmxName.setCharAt(i, ';');            } else if (jmxName.charAt(i)=='?'                 || jmxName.charAt(i)=='*' ||                 jmxName.charAt(i)=='\\' ) {                jmxName.insert(i, '\\');                i++;            } else if (jmxName.charAt(i)=='\n') {                jmxName.insert(i, '\\');                i++;                jmxName.setCharAt(i, 'n');            }        }        return jmxName;    }    /** Defines the MBeanServer with which beans     *   are auto-registered.     */    private MBeanServer server;    public void setMBeanServer(MBeanServer server) {        this.server = server;    }    public MBeanServer getMBeanServer() {        return server;    }}

JMX tools

Several good JMX implementation libraries support remote JMX. Sun Microsystems provides reference implementations of JMX and JMX Remote under a free license. Some open source implementations also exist. MX4J is a popular one that includes helper libraries and tools like a JMX client. Java 5 integrates JMX and JMX remote support into the virtual machine. Java 5 also introduced management beans for VM performance in the javax.management package. Sun's Java 5 virtual machines include the standard JMX client JConsole.

You can see that this first aspect is reusable. With it, I can automatically register an object instance for any class that implements an interface, ManagedBean, using an after advice. This is similar to the AspectJ Marker Interface idiom (see Resources) in that it defines the classes whose instances should be exposed through JMX. However, unlike a true marker interface, this one also defines two methods.

The aspect provides a setter to define which MBean server should be used for managing objects. This is an example of using the Inversion of Control (IOC) pattern for configuration and is a natural fit with aspects. In the full listing of the final code, you'll see I've used a simple helper aspect to configure the system. In a larger system, I would use an IOC container like the Spring framework to configure classes and aspects. See Resources for more about IOC and the Spring framework and for a good introduction to using Spring to configure aspects.


Listing 3. Exposing beans for JMX management
/** Applies JMX management to performance statistics beans. */public aspect StatsJmxManagement {    /** Management interface for performance statistics.      *  A subset of @link PerfStats     */    public interface PerfStatsMBean extends ManagedBean {        int getAccumulatedTime();        int getMaxTime();        int getCount();        void reset();    }        /**      * Make the @link PerfStats interface      * implement @link PerfStatsMBean,      * so all instances can be managed      */    declare parents: PerfStats implements PerfStatsMBean;    /** Creates a JMX MBean to represent this PerfStats instance. */     public DynamicMBean PerfStats.getMBean() {        try {            RequiredModelMBean mBean = new RequiredModelMBean();            mBean.setModelMBeanInfo              (assembler.getMBeanInfo(this, getOperationName()));            mBean.setManagedResource(this,               "ObjectReference");            return mBean;        } catch (Exception e) {            /* This is safe because @link ErrorHandling            *  will resolve it. This is described later!            */            throw new               AspectConfigurationException("can't                 register bean ", e);        }    }    /** Determine JMX operation name for this     *  performance statistics bean.      */        public String PerfStats.getOperationName() {                StringBuffer keyStr =           new StringBuffer("operation=\"");        int pos = keyStr.length();        if (key instanceof Class) {            keyStr.append(((Class)key).getName());        } else {            keyStr.append(key.toString());        }                JmxManagement.jmxEncode(keyStr, pos);                keyStr.append("\"");        return keyStr.toString();    }    private static Class[] managedInterfaces =       { PerfStatsMBean.class };    /**      * Spring JMX utility MBean Info Assembler.      * Allows @link PerfStatsMBean to serve      * as the management interface of all performance      * statistics implementors.      */    static InterfaceBasedMBeanInfoAssembler assembler;    static {        assembler = new InterfaceBasedMBeanInfoAssembler();        assembler.setManagedInterfaces(managedInterfaces);    }}

Listing 3 contains the StatsJmxManagement aspect, which defines concretely which objects should expose management beans. It outlines an interface, PerfStatsMBean, that defines the management interface for any performance-statistics implementation. This consists of the statistics values for counts, total time, maximum time, and the reset operation, which is a subset of the PerfStats interface.

PerfStatsMBean itself extends ManagedBean, so that anything that implements it will automatically be registered for management by the JmxManagement aspect. I've used the AspectJ declare parents form to make the PerfStats interface extend a special management interface, PerfStatsMBean. As a result, the JMX Dynamic MBean approach manages these objects, which I prefer to using JMX Standard MBeans.

Using Standard MBeans would require me to define a management interface with a name based on each implementation class of performance statistics, such as PerfStatsImplMBean. Later, when I add subclasses of PerfStats to the Glassbox Inspector, the situation would get worse because I would be required to create corresponding interfaces such as OperationPerfStatsImpl. The standard MBeans convention would make the interfaces depend on the implementations and represents a needless duplication of the inheritance hierarchy for this system.

Deploying these aspects

The aspects used in this article need to be applied only to each application they are monitoring, not to third-party libraries or container code. As such, you could integrate them into a production system by compiling them into an application, by weaving into an already compiled application, or by using load-time weaving, which I prefer for this use case. You'll learn more about load-time weaving in the second half of this article.

The rest of the aspect is responsible for creating the right MBean and object name with JMX. I reuse a JMX utility from the Spring framework, the InterfaceBasedMBeanInfoAssembler, which makes it easier to create a JMX DynamicMBean that manages my PerfStats instance using the PerfStatsMBean interface. At this stage, I've exposed only PerfStats implementations. This aspect also defines helper methods using inter-type declarations on managed bean classes. If a subclass of one of these classes needed to override the default behavior, it could do so by overriding the method.

You may be wondering why I've used an aspect for management rather than simply adding support directly into the PerfStatsImpl implementation class. While adding management to this one class wouldn't scatter my code, it would entangle the performance-monitoring system's implementation with JMX. So, if I wanted to use the system in an application without JMX, I would be forced to include the libraries and somehow disable the service. Moreover, as I expand the system's management functionality, I expect to expose more classes for management with JMX. Using aspects keeps the system's management policy modular.



Back to top


Database request monitoring

Distributed calls are a common source of slow application performance and errors. Most Web-based applications do a significant amount of database work, making monitoring queries and other database requests an especially important area for performance monitoring. Common issues include ill-written queries, missing indexes, and excessive numbers of database requests per operation. In this section, I'll expand the monitoring system to track database activity, correlated with operations.

Distributed calls

In this section, I present an approach to handling distributed calls to a database. While databases are typically hosted on a different machine, my technique also works for a local database. My approach also extends naturally to other distributed resources, including remote object invocations. In the second part of the article, I'll show you how to apply this technique to Web services invocations using SOAP.

To start with, I'll monitor database connection times and the execution of database statements. To support this effectively, I need to generalize my performance monitoring information and allow for tracking performance nested within an operation. I'll want to extract the common elements of performance into an abstract base class. Each base class is responsible for tracking performance before and after certain operations and will need to update system-wide performance statistics for that information. This lets me track nested servlet requests and will be important to support tracking controllers in Web application frameworks (discussed in Part 2).

Because I want to update database performance by request, I'll use a composite pattern to track statistics held by other statistics. This way, statistics for operations (such as servlets) hold performance statistics for each database. The database statistics hold information about connection times and aggregate additional statistics for each individual statement. Figure 4 shows how the overall design fits together. Listing 4 has the new base monitoring aspect that supports monitoring different requests.


Figure 4. Generalized monitoring design
Revised monitoring design

Listing 4. The base monitoring aspect
/** Base aspect for monitoring functionality.  *  Uses the worker object pattern. */public abstract aspect AbstractRequestMonitor {    /** Matches execution of the worker object     *  for a monitored request.     */    public pointcut       requestExecution(RequestContext requestContext) :        execution(* RequestContext.execute(..))           && this(requestContext);        /** In the control flow of a monitored request,     *  i.e., of the execution of a worker object.      */    public pointcut inRequest(RequestContext requestContext) :        cflow(requestExecution(requestContext));    /** establish parent relationships     *  for request context objects.     */     // use of call is cleaner since constructors are called    // once but executed many times    after(RequestContext parentContext)       returning (RequestContext childContext) :       call(RequestContext+.new(..)) &&         inRequest(parentContext) {        childContext.setParent(parentContext);    }    public long getTime() {        return System.currentTimeMillis();    }    /** Worker object that holds context information     *  for a monitored request.     */    public abstract class RequestContext {        /** Containing request context, if any.          *  Maintained by @link AbstractRequestMonitor         */        protected RequestContext parent = null;                /** Associated performance statistics.          *  Used to cache results of @link #lookupStats()         */        protected PerfStats stats;        /** Start time for monitored request. */         protected long startTime;        /**          * Record execution and elapsed time          * for each monitored request.         * Relies on @link #doExecute() to proceed         * with original request.          */        public final Object execute() {            startTime = getTime();                        Object result = doExecute();                        PerfStats stats = getStats();            if (stats != null) {                stats.recordExecution(startTime, getTime());            }                        return result;        }                /** template method: proceed with original request */        public abstract Object doExecute();        /** template method: determines appropriate performance         *  statistics for this request          */        protected abstract PerfStats lookupStats();                /** returns performance statistics for this method */         public PerfStats getStats() {            if (stats == null) {                stats = lookupStats(); // get from cache if available            }            return stats;        }        public RequestContext getParent() {            return parent;                    }                public void setParent(RequestContext parent) {            this.parent = parent;                    }    }}

As you might expect, I had a number of choices for how to store the shared performance statistics and per-request state for the base monitoring aspect. For example, I could have used a singleton with lower-level mechanisms like a ThreadLocal holding a stack of statistics and context. Instead, I chose to use the Worker Object pattern (see Resources), which allows for a more modular, concise expression. While this imposes a bit of extra overhead, the additional time required to allocate a single object and execute the advice is generally negligible compared to the time servicing Web and database requests. In other words, I can do some processing work in the monitoring code without adding overhead because it runs only relatively infrequently and generally is dwarfed by the time spent sending information over networks and waiting for disk I/O. This would be a bad design for a profiler, where you would want to track data about many operations (and methods) per request. However, I'm summarizing statistics about requests, so the choice is a reasonable one.

In the above base aspect, I've stored transient state about the currently monitored request in an anonymous inner class. This worker object is used to wrap any execution of monitored requests. The worker object, a RequestContext, is defined in the base class and provides a final execute method that defines the flow for monitoring any request. The execute method delegates to an abstract template method, doExecute(), which is responsible for proceeding with the original join point. The doExecute() method also acts as a natural point to set up statistics before proceeding with the monitored join point based on context information, such as the data source being connected to, and to associate returned values such as database connections after returning from the join point.

Each monitor aspect is also responsible for providing an implementation of an abstract method, lookupStats(), to determine what statistics object is updated for a given request. lookupStats() needs to access information based on the monitored join point. In general, the context captured must vary in each monitoring aspect. For example, in HttpServletMonitor, the context needed is the class of the currently executing operation object. For a JDBC connection, the context needed is the data source being acquired. Because the requirements will differ according to context, the advice to set up the worker object is best contained in each sub-aspect rather than in the abstract base aspect. This arrangement is cleaner, it allows type checking, and it performs better than writing one piece of advice in the base class that passes the JoinPoint to all the children.



Back to top


Servlet request tracking

The AbstractRequestMonitor does contain one concrete after advice to track the parent contexts of request contexts. This allows me to associate the operation statistics for nested requests with the statistics of their parents (for example, what servlet request caused this database access). For my monitoring example system, I explicitly do want nested worker objects and do not limit myself to handling just top-level requests. For example, all the Duke's Bookstore Servlets call a BannerServlet as part of rendering a page. It is useful that I can break out the times for these calls separately, as Listing 5 shows. I don't show the supporting code for looking up nested statistics in an operation statistics here (you can see it in the article source code). In part two, I'll revisit this topic to show how I update the JMX support to display nested statistics like this.


Listing 5. Updated servlet monitoring
Listing 5 should now readpublic aspect HttpServletMonitor extends AbstractRequestMonitor {    /** Monitor Servlet requests using the worker object pattern */  Object around(final Object operation) :     monitoredOperation(operation) {      RequestContext requestContext = new RequestContext() {          public Object doExecute() {              return proceed(operation);          }                    public PerfStats lookupStats() {                              if (getParent() != null) {                  // nested operation                  OperationStats parentStats =(OperationStats)getParent().getStats();                  returnparentStats.getOperationStats(operation.getClass());              }              return lookupStats(operation.getClass());          }        };        return requestContext.execute();    } ... 

Listing 5 shows the revised monitoring advice for servlet request tracking. All the rest of the code remains the same as in Listing 1: it is either pulled up into the base AbstractRequestMonitor aspect or remains the same.



Back to top


JDBC monitoring

Having set up my performance monitoring framework, I am now ready to track database connection times and the time for database statements. Moreover, I want to be able to correlate database statements with the actual database I have connected to (in the lookupStats() method). To do this, I create two aspects to track information about JDBC statements and connections: JdbcConnectionMonitor and JdbcStatementMonitor.

One of the key responsibilities for these aspects is to follow chains of object references. I would like to track requests by the URL I used to connect to the database, or at least the database name. This requires me to track the data source used to acquire a connection. I would further like to track performance of prepared statements by the SQL strings that was prepared prior to executing to them. Finally, I need to track the JDBC connection associated with statements that are executing. You'll note that JDBC statements do provide an accessor for their connection; however, application servers and Web application frameworks frequently use the decorator pattern to wrap JDBC connections. I want to make sure I can correlate statements with the connection I have a handle to and not a wrapped connection.

The JdbcConnectionMonitor is responsible for measuring performance statistics for connections to the database, and it also associates connections with their metadata (for example, a JDBC URL or a database name) from data sources or connection URLs. The JdbcStatementMonitor is responsible for measuring performance statistics for executing statements, tracks the connections used to acquire statements, and tracks the SQL strings associated with prepared (and callable) statements. Listing 6 shows the JdbcConnectionMonitor aspect.


Listing 6. The JdbcConnectionMonitor aspect
/**  * Monitor performance for JDBC connections,  * and track database connection information associated with them.  */ public aspect JdbcConnectionMonitor extends AbstractRequestMonitor {        /** A call to establish a connection using a      *  <code>DataSource</code>      */    public pointcut dataSourceConnectionCall(DataSource dataSource) :         call(Connection+ DataSource.getConnection(..))           && target(dataSource);    /** A call to establish a connection using a URL string */    public pointcut directConnectionCall(String url) :        (call(Connection+ Driver.connect(..))  || call(Connection+           DriverManager.getConnection(..))) &&         args(url, ..);    /** A database connection call nested beneath another one     *  (common with proxies).     */        public pointcut nestedConnectionCall() :         cflowbelow(dataSourceConnectionCall(*) ||           directConnectionCall(*));        /** Monitor data source connections using     *  the worker object pattern      */    Connection around(final DataSource dataSource) :       dataSourceConnectionCall(dataSource)         && !nestedConnectionCall() {        RequestContext requestContext =           new ConnectionRequestContext() {            public Object doExecute() {                                accessingConnection(dataSource);                 // set up stats early in case needed                Connection connection = proceed(dataSource);                return addConnection(connection);            }                    };        return (Connection)requestContext.execute();    }    /** Monitor url connections using the worker object pattern */    Connection around(final String url) : directConnectionCall(url)       && !nestedConnectionCall() {        RequestContext requestContext =           new ConnectionRequestContext() {            public Object doExecute() {                accessingConnection(url);                Connection connection = proceed(url);                                return addConnection(connection);            }        };        return (Connection)requestContext.execute();    }    /** Get stored name associated with this data source. */     public String getDatabaseName(Connection connection) {        synchronized (connections) {            return (String)connections.get(connection);        }    }    /** Use common accessors to return meaningful name     *  for the resource accessed by this data source.     */    public String getNameForDataSource(DataSource ds) {        // methods used to get names are listed in descending        // preference order         String possibleNames[] =           { "getDatabaseName",               "getDatabasename",               "getUrl", "getURL",               "getDataSourceName",               "getDescription" };        String name = null;        for (int i=0; name == null &&           i<possibleNames.length; i++) {            try {                            Method method =                   ds.getClass().getMethod(possibleNames[i], null);                name = (String)method.invoke(ds, null);            } catch (Exception e) {                // keep trying            }        }        return (name != null) ? name : "unknown";    }        /** Holds JDBC connection-specific context information:     *  a database name and statistics     */    protected abstract class ConnectionRequestContext      extends RequestContext {        private ResourceStats dbStats;                /** set up context statistics for accessing         *  this data source         */         protected void           accessingConnection(final DataSource dataSource) {            addConnection(getNameForDataSource(dataSource),               connection);        }                /** set up context statistics for accessing this database */         protected void accessingConnection(String databaseName) {            this.databaseName = databaseName;            // might be null if there is database access            // caused from a request I'm not tracking...            if (getParent() != null) {                OperationStats opStats =                   (OperationStats)getParent().getStats();                dbStats = opStats.getDatabaseStats(databaseName);                            }        }        /** record the database name for this database connection */         protected Connection           addConnection(final Connection connection) {            synchronized(connections) {                connections.put(connection, databaseName);            }            return connection;        }        protected PerfStats lookupStats() {            return dbStats;        }    };        /** Associates connections with their database names */        private Map/*<Connection,String>*/ connections =       new WeakIdentityHashMap();}

Listing 6 shows my aspect for tracking database connections using AspectJ and the JDBC API. It has a map to associate database names with each JDBC connection.

Inside jdbcConnectionMonitor

Inside the JdbcConnectionMonitor shown in Listing 6, I've defined pointcuts to capture two different ways of connecting to a database: through a data source or directly through a JDBC URL. The connection monitor contains monitoring advice for each case, both of which set up a worker object. The doExecute() methods start by proceeding with the original connection and then pass the returned connection to one of two helper methods named addConnection. In both cases, the pointcut being advised excludes connection calls that result from another connection (for example, if connecting to a data source results in establishing a JDBC connection).

The addConnection() for data sources delegates to a helper method, getNameForDataSource(), to try to determine the name of a database from the data source. The DataSource interface doesn't provide any such mechanism, but almost every implementation provides a getDatabaseName() method. getNameForDataSource() uses reflection to try this and a few other common (and less common) methods that provide a useful identifier for a data source. This addConnection() method then delegates to the addConnection() method that takes a string for a name.

The delegated addConnection() method retrieves the operational statistics from its parent-request context and looks up database statistics based on the database name (or other description string) associated with the given connection. It then stores that information in the dbStats field on the request-context object to update performance information about acquiring the connection. This lets me track the time required to connect to the database (often this is really the time required to get a connection from a pool). The addConnection() method also updates the connections map of connections to database names. This map is used subsequently when JDBC statements are executed to update statistics for the appropriate request. The JdbcConnectionMonitor also provides a helper method, getDatabaseName(), which looks up the string name for a connection from the connections map.

Weak identity maps and aspects

The JDBC monitoring aspects use weak identity hash maps. These maps hold weak references to allow tracked objects like connections to be garbage collected when they are referenced only by the aspect. This is important because the singleton aspects are typically not garbage collected. If the references weren't weak, the application would have a memory leak. The aspects use identity maps to avoid calling the hashCode or equals methods on connections or statements. This is important because I want to track the connections and statements regardless of their state: I don't want to encounter exceptions from the hashCode method, nor should I rely on the hash code of the object remaining the same when its internal state changes (for example, when closed). I experienced this issue when working with dynamic proxy-based JDBC objects (like those from iBatis), which threw exceptions when any method on them was called after a connection had closed. This led to errors in trying to record statistics after finishing an operation.

One lesson to learn from this is to minimize the assumptions you make about third-party code. Using identity maps is a good way to avoid making assumptions about the implementation logic in advised code. In this case, I'm using an open source implementation of a WeakIdentityHashMap from the Emory DCL Java Utilities (see Resources). Tracking metadata information about connections or statements lets me group statistics across each request for equivalent connections or statements. This means I can track based on just object instances; I don't need to use object equality to track these JDBC objects. Another lesson to keep in mind is that JDBC objects are frequently wrapped by decorators (increasingly with dynamic proxies) by various frameworks. It's a bad idea to assume that you're working with a simple, raw implementation of such an interface!

Inside jdbcStatementMonitor

Listing 7 shows the JdbcStatementMonitor aspect. This aspect has two primary responsibilities: tracking information about creating and preparing statements and then monitoring performance statistics for executing JDBC statements.


Listing 7. The JdbcStatementMonitor aspect
/** * Monitor performance for executing JDBC statements,  * and track the connections used to create them,  * and the SQL used to prepare them (if appropriate). */public aspect JdbcStatementMonitor extends AbstractRequestMonitor {        /** Matches any execution of a JDBC statement */    public pointcut statementExec(Statement statement) :         call(* java.sql..*.execute*(..)) &&           target(statement);        /**     * Store the sanitized SQL for dynamic statements.      */    before(Statement statement, String sql,       RequestContext parentContext):       statementExec(statement) && args(sql, ..)         && inRequest(parentContext) {        sql = stripAfterWhere(sql);        setUpStatement(statement, sql, parentContext);    }        /** Monitor performance for executing a JDBC statement. */    Object around(final Statement statement) :      statementExec(statement) {        RequestContext requestContext =           new StatementRequestContext() {            public Object doExecute() {                return proceed(statement);            }        };        return requestContext.execute();    }           /**     * Call to create a Statement.     * @param connection the connection called to     * create the statement, which is bound to      * track the statement's origin      */    public pointcut callCreateStatement(Connection connection):        call(Statement+ Connection.*(..))           && target(connection);    /**     * Track origin of statements, to properly      * associate statistics even in     * the presence of wrapped connections      */    after(Connection connection) returning (Statement statement):      callCreateStatement(connection) {        synchronized (JdbcStatementMonitor.this) {            statementCreators.put(statement, connection);        }    }    /**      * A call to prepare a statement.     * @param sql The SQL string prepared by the statement.      */    public pointcut callCreatePreparedStatement(String sql):        call(PreparedStatement+ Connection.*(String, ..))           && args(sql, ..);    /** Track SQL used to prepare a prepared statement */    after(String sql) returning (PreparedStatement statement):       callCreatePreparedStatement(sql) {        setUpStatement(statement, sql);    }                protected abstract class StatementRequestContext       extends RequestContext {        /**          * Find statistics for this statement, looking for its          * SQL string in the parent request's statistics context          */        protected PerfStats lookupStats() {            if (getParent() != null) {                Connection connection = null;                String sql = null;                synchronized (JdbcStatementMonitor.this) {                    connection =                       (Connection) statementCreators.get(statement);                    sql = (String) statementSql.get(statement);                }                if (connection != null) {                    String databaseName =                       JdbcConnectionMonitor.aspectOf().                        getDatabaseName(connection);                    if (databaseName != null && sql != null) {                        OperationStats opStats =                           (OperationStats) getParent().getStats();                        if (opStats != null) {                            ResourceStats dbStats =                               opStats.getDatabaseStats(databaseName);                            return dbStats.getRequestStats(sql);                        }                    }                }            }            return null;        }    }    /**      * To group sensibly and to avoid recording sensitive data,     * I don't record the where clause (only used for dynamic     * SQL since parameters aren't included     * in prepared statements)     * @return subset of passed SQL up to the where clause     */    public static String stripAfterWhere(String sql) {        for (int i=0; i<sql.length()-4; i++) {            if (sql.charAt(i)=='w' || sql.charAt(i)==              'W') {                if (sql.substring(i+1, i+5).equalsIgnoreCase(                  "here"))                  {                    sql = sql.substring(0, i);                }            }        }        return sql;    }        private synchronized void       setUpStatement(Statement statement, String sql) {        statementSql.put(statement, sql);    }    /** associate statements with the connections     *  called to create them     */    private Map/*<Statement,Connection>*/ statementCreators =       new WeakIdentityHashMap();    /** associate statements with the     *  underlying string they execute     */    private Map/*<Statement,String>*/ statementSql =       new WeakIdentityHashMap();}

The JdbcStatementMonitor maintains two weak identity maps: statementCreators and statementSql. The first tracks the connections used to create a statement. As I noted earlier, I don't want to rely on the getConnection method of the statement because it could refer to a wrapped connection for which I don't have metadata. Note the callCreateStatement pointcut, which I advise to monitoring JDBC statement execution. This matches any call to a method defined on a JDBC connection that returns a Statement or any subclass thereof. This matches the 12 different ways that a statement can be

신고
Posted by Tornado tornado
6. GC 관련 Parameter


GC관련 설정값을 보기전에 앞서서 ?X와 ?XX 옵션에 대해서 먼저 언급하자. 이 옵션들은 표준 옵션이 아니라, 벤더별 JVM에서 따로 제공하는 옵션이기 때문에, 예고 없이 변경되거나 없어질 수 있기 때문에, 사용전에 미리 JVM 벤더 홈페이지를 통해서 검증한다음에 사용해야한다.

1) 전체 Heap Size 조정 옵션

전체 Heap size는 ?ms와 ?mx로 Heap 사이즈의 영역을 조정할 수 있다. 예를 들어 ?ms512m ?mx 1024m로 설정하면 JVM은 전체 Heap size를 application의 상황에 따라서 512m~1024m byte 사이에서 사용하게 된다. 그림2의 Total heap size

메모리가 모자를때는 heap을 늘리고, 남을때는 heap을 줄이는 heap growing과 shirinking 작업을 수행하는데, 메모리 변화량이 큰 애플리케이션이 아니라면 이 min heap size와 max heap size는 동일하게 설정하는 것이 좋다. 일반적으로 1GB까지의 Heap을 설정하는데에는 문제가 없으나, 1GB가 넘는 대용량 메모리를 설정하고자 할 경우에는 별도의 JVM 옵션이 필요한 경우가 있기때문에 미리 자료를 참고할 필요가 있다.

※ IBM AIX JVM의 경우
%export LDR_CNTRL=MAXDATA=0x10000000
%java -Xms1500m -Xmx1500m MyApplication

2) Perm size 조정 옵션

Perm Size는 앞에서도 설명했듯이, Java Application 자체(Java class etc..)가 로딩되는 영역이다. J2EE application의 경우에는 application 자체의 크기가 큰 편에 속하기 때문에, Default로 설정된 Perm Size로는 application class가 loading되기에 모자른 경우가 대부분이기 때문에, WAS start초기나, 가동 초기에 Out Of Memory 에러를 유발하는 경우가 많다.

PermSize는 -XX:MaxPermSize=128m 식으로 지정할 수 있다.
일반적으로 WAS에서 PermSize는 64~256m 사이가 적절하다.

3) New 영역과 Old 영역의 조정New 영역은 ?XX:NewRatio=2 에 의해서 조정이 된다.
NewRatio Old/New Size의 값이다. 전체 Heap Size가 768일때, NewRatio=2이면 New영역이 256m, Old 영역이 512m 로 설정이 된다.
JVM 1.4.X에서는 ?XX:NewSize=128m 옵션을 이용해서 직접 New 영역의 크기를 지정하는 것이 가능하다.

4) Survivor 영역 조정 옵션
-XX:SurvivorRatio=64 (eden/survivor 의 비율) :64이면 eden 이 128m일때, survivor영역은 2m가 된다.

5) -server와 ?client 옵션
JVM에는 일반적으로 server와 client 두가지 옵션을 제공한다.
결론만 말하면 server 옵션은 WAS와 같은 Server환경에 최적화된 옵션이고, client옵션은 워드프로세서와 같은 client application에 최적화된 옵션이다. 그냥 언뜻 보기에는 단순한 옵션 하나로보일 수 있지만, 내부에서 돌아가는 hotspot compiler에 대한 최적화 방법과 메모리 구조자체가 아예 틀리다.

○ -server 옵션

server용 application에 최적화된 옵션이다. Server application은 boot up 시간 보다는 user에 대한 response time이 중요하고, 많은 사용자가 동시에 사용하기 때문에 session등의 user data를 다루는게 일반적이다. 그래서 server 옵션으로 제공되는 hotspot compiler는 java application을 최적화 해서 빠른 response time을 내는데 집중되어 있다.

또한 메모리 모델 역시, 서버의 경우에는 특정 사용자가 서버 운영시간동안 계속 서버를 사용하는게 아니기 때문에 (Login하고, 사용한 후에는 Logout되기 때문에..) 사용자에 관련된 객체들이 오래 지속되는 경우가 드물다. 그래서 상대적으로 Old영역이 작고 New 영역이 크게 배정된다. <그림 7. 참조 >

○ -client 옵션

client application은 워드프로세서 처럼 혼자 사용하는 application이다. 그래서 client application은 response time보다는 빨리 기동되는데에 최적화가 되어 있다. 또한대부분의 client application을 구성하는 object는GUI Component와 같이 application이 종료될때까지 남아있는 object의 비중이 높기 때문에 상대적으로 Old 영역의 비율이 높다.


<그림 7. ?server와 ?client 옵션에 따른 JVM Old와 New영역>


이 두옵션은 가장 간단한 옵션이지만, JVM의 최적화에 아주 큰부분을 차지하고 있는 옵션이기 때문에, 반드시 Application의 성격에 맞춰서 적용하기 바란다.
(※ 참고로, SUN JVM은 default가 client, HPJVM는 default가 server로 세팅되어 있다.)

○ GC 방식에 대한 옵션

GC 방식에 대한 옵션은 앞에서도 설명했지만, 일반적인 GC방식이외에, Concurrent GC,Parallel GC,Inceremental GC와 같이 추가적인 GC Algorithm이 존재한다. 옵션과 내용은 앞장에서 설명한 “다양한 GC알고리즘” 을 참고하기 바란다.


7.JVM GC 튜닝


그러면 이제부터 지금까지 설명한 내용을 기반으로 실제로 JVM 튜닝을 어떻게 하는지 알아보도록 하자.

STEP 1. Application의 종류와 튜닝목표값을 결정한다.

JVM 튜닝을 하기위해서 가장 중요한것은 JVM 튜닝의 목표를 설정하는것이다. 메모리를 적게 쓰는것이 목표인지, GC 횟수를 줄이는것이 목표인지, GC에 소요되는시간이 목표인지, Application의 성능(Throughput or response time) 향상인지를 먼저 정의한후에. 그 목표치에 근접하도록 JVM Parameter를 조정하는것이 필요하다.

STEP 2. Heap size와 Perm size를 설정한다.

-ms와 ?mx 옵션을 이용해서 Heap Size를 정한다. 일반적으로 server application인 경우에는 ms와 mx 사이즈를 같게 하는것이 Memory의 growing과 shrinking에 의한 불필요한 로드를 막을 수 있어서 권장할만하다.

ms와mx사이즈를 다르게 하는 경우는 Application의 시간대별 memory 사용량이 급격하게 변화가 있는 Application에 효과적이다.
PermSize는 JVM vendor에 따라 다소 차이가 있으나 일반적으로 16m정도이다. Client application의 경우에는 문제가 없을 수 있지만, J2EE Server Application의 경우 64~128m 사이로 사용이 된다.

Heap Size와 Perm Size는 아래 과정을 통해서 적정 수치를 얻어가야한다.

STEP 3. 테스트 & 로그 분석.

JVM Option에 GC 로그를 수집하기 위한 ?verbosegc 옵션을 적용한다. (HP의 경우 ?Xverbosegc 옵션을 적용한다.)

LoadRunner나 MS Strest(무료로 MS社의 홈페이지에서 다운로드 받을 수 있다.)와 같은 Strest Test툴을 통해서 Application에 Strest를 줘서. 그 log를 수집한다. 튜닝에서 있어서 가장 중요한것은 목표산정이지만, 그만큼이나 중요한것은 실제 Tuning한 Parameter가 Application에 어떤 영향을 주는지를 테스트하는 방법이 매우 중요하다. 그런 의미에서 적절한 Strest Tool의 선정과, Strest Test 시나리오는 정확한 Tuning을 위해서 매우 중요한 요인이다.

○ Perm size 조정
아래 그림8.은 HP JVM에서 ?Xverbosegc 옵션으로 수집한 GC log를 HP Jtune을 통해서 graph로 나타낸 그래프이다. 그림을 보면 Application이 startup되었을때 Perm 영역이 40m에서. 시간이 지난후에도 50m 이하로 유지되는것을 볼 수 있다. 특별하게 동적 classloading등이 수십m byte가 일어나지 않는등의 큰 변화요인이 없을때, 이 application의 적정 Perm 영역은 64m로 판단할 수 있다.


<그림 8. GC 결과중 Perm 영역 그래프>


○ GC Time 수행 시간 분석

다음은 GC에 걸린 시간을 분석해보자. 앞에 강좌 내용에서도 설명햇듯이. GC Tuning에서 중요한 부분중 하나가 GC에 소요되는 시간 특히 Full GC 시간이다.

지금부터 볼 Log는 모社의 물류 시스템의 WAS 시스템 GC Log이다. HP JVM을 사용하며, -server ?ms512m ?mx512m 옵션으로 기동되는 시스템이다.

그림 9를 보면 Peak 시간 (첫번째 동그라미) 14시간동안에 Full GC(동그란점)가 7번일어난것을 볼 수 있다. 각각에 걸린 시간은2.5~6sec 사이이다.
여기서 STEP 1.에서 설정한 AP Tuning의 목표치를 참고해야하는데.

Full GC가 길게 일어나서 Full GC에 수행되는 시간을 줄이고자 한다면 Old 영역을 줄이면 Full GC가 일어나는 횟수는 늘어나고, 반대로 Full GC가 일어나는 시간을 줄어들것이다.

반대로 Full GC가 일어나는 횟수가 많다면, Old 영역을 늘려주면 Full GC가 일어나는 횟수는 상대적으로 줄어들것이고 반대로 Full GC 수행시간이 늘어날 것이다.

특히 Server Application의 경우Full GC가 일어날때는 JVM자체가 멈춰버리기 때문에, 그림 9의 instance는 14시간동안 총 7번 시스템이 멈추고, 그때마다 2.5~6sec가량 시스템이 response를 못하는 상태가 된것이다. 그래서 멈춘 시간이 고객이 납득할만한 시간인지를 판단해야 하고, 거기에 적절한 Tuning을 해야한다.

Server Application에서 Full GC를 적게일어나게하고, Full GC 시간을 양쪽다 줄이기 위해서는 Old영역을 적게한후에, 여러개의 Instance를 동시에 뛰어서 Load Balancing을 해주면, Load가 분산되기 때문에 Full GC가 일어나는 횟수가 줄어들테고, Old 영역을 줄였기 때문에, Full GC에 드는 시간도 줄어들것이다. 또한 각각의 FullGC가 일어나는동안 하나의 서버 instance가 멈춰져 있어도, Load Balancing이 되는 다른 서버가 response를 하고 있기때문에, Full GC로 인한 Application이 멈추는것에 의한 영향을 최소화할 수 있다.


<그림 9. GC 소요시간>


데이타에 따라서 GC Tuning을 진행한후에는 다시 Strest Test를 진행해서 응답시간과 TPS(Throughput Per Second)를 체크해서 어떤 변화를 주었는지를 반드시 체크해봐야한다.


<그림 10. GC후의 Old 영역>


그림 10은 GC후에 Old 영역의 메모리 변화량을 나타낸다.

금요일 업무시간에 메모리 사용량이 올라가다가. 주말에가서 완만한 곡선을 그리는것을 볼 수 있다. 월요일 근무시간에 메모리 사용량이 매우 많고, 화요일에도 어느정도 메모리 사용량이 있는것을 볼 수 있다. 월요일에 메모리 사용량이 많은것을 볼때, 이 시스템의 사용자들이 월요일에 시스템 사용량이 많을 수 있다고 생각할 수 있고, 또는 다른 주의 로그를 분석해봤을때 이 주만 월요일 사용량이 많았다면, 특별한 요인이나 Application 변경등이 있었는지를 고려해봐야할것이다.

이 그래프만을 봤을때 Full GC가 일어난후에도 월요일 근무시간을 보면 Old 영역이 180M를 유지하고 있는것을 볼 수 있다. 이 시스템의 Full GC후의 Old영역은 80M~180M를 유지하는것을 볼 수 있다. 그래서 이 시스템은 최소 180M이상의 Old 영역을 필요로하는것으로 판단할 수 있다.

STEP 4. Parameter 변경.
STEP 3에서 구한 각 영역의 허용 범위를 기준으로 Old영역과 New 영역을 적절하게 조절한다.
PermSize와 New영역의 배분 (Eden,Survivor)영역등을 조정한다.
PermSize는 대부분 Log에서 명확하게 나타나기 때문에, 크게 조정이 필요가 없고 New영역내의 Eden과 Survivor는 거의 조정하지 않는다. 가장 중요한것은 Old영역과 New 영역의 비율을 어떻게 조정하는가가 관건이다.

이 비율을 결정하면서, STEP1에서 세운 튜닝 목표에 따라서 JVM의 GC Algorithm을 적용한다. GC Algorithm을 결정하는 기본적인 판단 내용은 아래와 같다.



이렇게 Parameter를 변경하면서 테스트를 진행하고, 다시 변경하고 테스트를 진행하는 과정을 거쳐서 최적의 Parameter와 GC Algorithm을 찾아내는것이 JVM의 메모리 튜닝의 이상적인 절차이다.


지금까지 JVM의 메모리 구조와 GC 모델 그리고 GC 튜닝에 대해서 알아보았다.

정리하자면 GC 튜닝은 Application의 구조나 성격 그리고, 사용자의 이용 Pattern에 따라서 크게 좌우 되기때문에, 얼마만큼의 Parameter를 많이 아느냐 보다는 얼마만큼의 테스트와 로그를 통해서 목표 값에 접근하느냐가 가장 중요하다
신고
Posted by Tornado tornado

 http://phpschool.com/bbs2/inc_view.html?id=12307&code=tnt2&start=0&mode=&field=&search_name=&operator=&period=&category_id=&s_que=

 

 

UNICODE :

http://www.unicode.org/standard/translations/korean.html

유니코드에 대해 ?

어떤 플랫폼,
어떤 프로그램,
어떤 언어에도 상관없이
유니코드는 모든 문자에 대해 고유 번호를 제공합니다.



UCS-2 :

대부분의 흔히 쓰는 문자들을 정의한 규격입니다.
2bytes 범위라서 UCS-2 입니다.
이걸 bit 로 표현하여 UTF-16 입니다.
UTF-16LE, UTF-16BE 가 동일한 규격으로 Little Endian, Big Endian 은 단지 byte order (바이트 순서)가 다를뿐 입니다.
iconv --list 를 해보면 쓸데없이 많이 나오는데,
UTF-16LE, UCS-2LE 가 같은거고 BE 끼리 같은거라고 보면 됩니다.
그냥 UTF-16은 UTF-16LE 와 동일하나 앞에 BOM 헤더가 붙습니다.
UCS-2 는 헤더가 붙지 않습니다.


UCS-4 :

UCS-2 의 확장입니다.
뒤의 2bytes 는 UCS-2 와 완전히 호환됩니다.
즉, UCS-2 의 0xFFFF 는 UCS-4 의 0x0000FFFF 와 같은 코드입니다.
UTF-32 로 말만 바꾸어 위의 내용과 동일합니다.
인터넷 브라우저 내부에서 이것이 사용되며,
js 등에서 indexOf() 로 가져오면 UCS-4 코드가 10진수로 반환됩니다.
10진수 이므로 65535 까지는 UCS-2 와 완전 호환 됩니다.


UTF-8 :

UCS-2, UCS-4 는 영어권에서는 분명한 낭비가 있습니다.
ascii 만으로도 충분히 표현 가능한데, 한글자에 쓰지도 않는 바이트가 낭비되지요.
그런점도 있고, 문자열로 뭘 하기에 UTF-7 보다는 편리해서 가장 보편적으로 사용됩니다.
가변길이를 가지는 특징이 있습니다.
단지 계산만으로 UCS-2, UCS-4 규격으로의 상호변환이 가능합니다.
한국어는 UCS-2 규격 내에 있기 때문에 3bytes 내에서 표현 가능합니다.


UTF-7 :

이메일 등 ascii 만으로 표현해야 할 필요성에 의해 만들어졌습니다.
한 글자당 8bit 씩 할당하지만 사용은 7bit 만 합니다.
UTF-8 과 같은 특징을 가지고 있으나,
모든 ascii 값이 실제 ascii 와 같은 의미가 아니기 때문에 이것으로 무엇을 하기는 좀 힘듭니다.



BOM (Byte Order Mark) :

유니코드 종류가 많기 때문에 앞에 이런 헤더를 붙여서 구분하기도 합니다.
EmEditor, UltraEdit, Vim 등의 에디터에서 인식합니다.



코드표

http://www.unicode.org/charts/

각 나라별 코드범위와 정의된 문자를 볼 수 있습니다.
앞에 0 을 적지 않았기 때문에 (Zerofill 이 아니기 때문에) 4자리까지는 UCS-2 려니 하시고,
5 자리 부터는 UCS-4 려니 하십시오.


resy우리말로 된 유니코드 튜토리얼이 있었으면 좋겠다 했는데..
이런 자료라도 참 절실하군요. 아직도 유니코드 = UTF-16(or 다른 인코딩) 으로 착각하는 사람이 많은 현실에...

누군가 총대를 매긴 해야하는데... 거참... ^^:
07/13 2:23:12 코멘트 지우기
resy보충 내용으로...
UCS 는 코드값의 테이블이라고 생각하면 됩니다. UTF 는 인코딩의 방법(즉, 바이트의 연속된 순서를 어떻게 표현할 것이냐 하는 정의)이고, UCS 는 미리 정의되어 있는 각 글자 코드를 테이블 화 해놓은 것입니다. 가령 글자 '가' 는 유니코드에서 U+AC00 에 해당하는데, UCS2 에서는 0xAC00 테이블 좌표에 위치하고 있습니다. 이것을 UTF-8 인코딩하면, 0xEAB080 이 됩니다.

누구도 이런 식으로 설명해주는 사람이 없어서 예전엔 UCS2 = UTF16?? 이라고 헷갈리고 헤맸는데, 이게 정확한 설명인지 모르겠습니다. 잘못된 점이 있으면 누가 지적해주시길... ^^;

문자 집합(Character Set)이랑 인코딩(Encoding)에 대한 차이도 뭐 속시원히 가르쳐주는 데가 없더군요. 결국 시간이 지나다보니 스스로 알게 되었습니다만.. 확실히 외국 자료 빼면 국내는 -_-;

그러고보니 예전에 누군가가 국가 코드표와 인코딩이 가지는 의미에 대해 글 올렸던 거 같은데, locale 에 대한 내용이 그 후로 안올라오는 거 같기도...
07/13 5:19

 

 

신고
Posted by Tornado tornado

http://kr.sun.com/developers/j2se/techtip.html

 

한동안 안봤더니... 많이 올라왔넹..

신고
Posted by Tornado tornado
JMX (Java Management Extensions) Introduce
 
JMx는 프로그래머들에게 자바 어플리케이션의 모니터링과 관리 기능을 제공한다. 실제로 이 API는 웹서버에서 네트워크 디바이스, 웹폰에 이르기까지 자바로 이용가능한 것은 어느 것이든 로컬 혹은 원격으로 처리 할 수 있게 한다.
 
DownLoad
http://java.sun.com/products/JavaManagement/download.html 에서 다운로드 받을 수 있다. 현재 최종 버전은 다음과 같다.
 
l         JMX Remote API 1.0.1_03 Reference Implementation(JSR 3)
l        JMX 1.2.1 Reference Implementation(JSR 160)
 
JMS 환경
l        Instrumentation Tier(JSR 3에 포함) : 어플리케이션, 디바이스, 서비스와 같은 리소스들은 MBean(Managed Bean)이라고 불리는 자바 오브젝트를 이용하여 설치된다. MBean은 원격으로 관리하고 모니터링하는 JMS 에이전트를 통해 속성과 연산으로 구성된 관리 인터페이스를 보여준다.
l        Agent Tier(JSR 3에 포함) : JMS 에이전트의 주요 컴포넌트는 MBean서버이며, 이는  MBean이 등록되는 코어관리 에이전트 오브젝트 서버이다. JMX에이전트는 또한 MBean을 핸들링하기 위한 서비스들을 포함하고 있다. JMX에이전트는 직접적으로 리소스를 제어하고 관리가 가능하도록 한다.
l        Remote Management Tier(JSR 160에 부분 포함) : 이 티어는 에이전트 JVM외부의 원격 관리 어플리케이션으로부터 접근 가능한 JMX에이전트를 만드는 프로토롤 어덥터와 커넥터를 정의 한다. 커넥터는 원격 클라이언트 JMX-aware이고 로컬 클라이언트가 보게될 것과 같은 JMX API를 볼 때 사용된다. 어덥터는 원격 클라이언트가 SNMP(Simple Network Management Protocol) CIM/WBEM(Common Infromation Model and Web Base Enterprise Management)같은 일반 관리 프로토콜을 이용할 때 사용된다.
 
Developer Type
l        리소스 관리 MBean 개발 : JMX 기술은 관리를 위해 보여지는 인터페이스를 정의 한다.
l        에이전트를 생성하고 배치하는 개발
n         MBean 서버를 생성하거나 플랫폼에 의해 제공되는 것을 사용한다.
n         MBean 네이밍 컨베션을 이용하여 리소스를 나타내는 MBean을 등록한다.
n         플랫폼(RMI/SNMP)에 의해 공급된 커넥터와 프로토콜 어뎁터를 형성하거나, 리소스가 원격으로 접근되었을 때는 커스텀 커넥터나 어덥터를 추가한다.
l        원격 매니저를 작성하는 개발 : JMX 에이전트와 상호작용하는 커넥터나 프로토콜을 선택하고, 나타난 MBean을 통해 원격으로 관리되는 리소스의 뷰를 구축한다.
 
MBean 4가지 타입
l        Standard MBean : 표준MBean MBean 가장 일반적인 타입이다. 대부분의 개발자는 외의 MBean타입을 생성할 필요가 없다.
l        Dynamic MBean : introspection 통하는 대신 javax.management.DynamicMBean 인터페이스를 이용하여 관리 인터페이스를 프로그램적으로 구현한다. 이를 위해서는 관리를 위해 나타나는 속성과 작용을 표현하는 정보 클래스에 의존한다. Dynamic MBean MBean 관리 인터페이스가 컴파일시 알려지지 않았을 -예를 들어, XML파일을 파싱하는데 결정되었을 -종종 사용된다.
l        Model MBean : 일반적이고 형성가능한 MBean으로써 어플리케이션이 어떤 리소스든지 다이나믹하게 설치하는 사용한다(API에서 Modle MBean Class Dynamic MBean Class 상속 받는다.). 본질적으로, 관리 인터페이스와 실제 리소스가 프로그램적으로 설정되도록 구현되어온 것이 다이나믹 MBean이다. 이로 인해 자바 다이나믹 관리 에이전트에 연결된 어떤 매니저라도 MBean모델을 다이나믹하게 실증하고 형성할 있다.
l        Open MBean : 오픈 MBean 데이터 타입에 특정 제한을 가진 다이나믹 MBean이며, 이는 관리 어플리케이션과 이들의 관리자들이 런터임시 발견되는 새로운 관리 오브젝트들을 이해하고 사용할 있게 한다. 오픈 MBean JMX스펙에 따르는 넓은 범위의 어플리케이션에 오픈될 필요가 있는 리소스를 설치하는데 있어 유동적인 수단을 제공한다.
 
Sample Program
샘플 프로그램은 JDK1.5.0_03 버전에서 테스트 하였습니다. 1.5 이하 버전에서는 구동되지 않습니다.
 
샘플은 RSS 서버스를 Reader하는 어플리케이션(rssReaderAgent) 관리하는 MBean(rssJMX) 구성하였다. MBean RSS Reader 읽기 횟수, 읽어온 등록들의 총수, 현재 읽어온 글의 수을 모니터링하며 reset()메서드는 위의 수를 초기화 한다. MBean 구성인 rssJMX, rssJMXMBean, rssJMXDevice 같은 이름으로 시작해야 한다.(rssJMX) 샘플을 구동하기 위해 RSS UTIL API 필요하다. 해당 API 아래에서 다운 받을 있다.
신고
Posted by Tornado tornado

예전에 어디다 적어놨던거 같은데... 찾으니 없어서 다시 적음 -.-;

간단한 것이지만 까먹고 나면 문서 찾아봐야 하고 해서 ...


그럼 본문 시작.......


java.util.Collections 클래스에는 sort(List list) 라는 정적 메소드와

sort(List list, Comparator c) 라는 메서드가 있다.


이넘들을 이용해서 객체를 정렬 할 수 있다..

문제는 사용자 정의 객체(대표적으로 Java Bean) 들은 정렬대상에서 제외 된다.

예를 들면..


class TestBean{
 int pk;
 String name;

 public void setPk(int pk){ this.pk = pk; }
 public int getPk(){ return pk; }
 public void setName(String name){ this.name = name; }
 public String getName(){ return name; }

 public String toString(){
  return "[pk = " + pk + " , name = " + name + "]";
 }

}


이런 클래스가 하나 있다.

이 클래스에는 pk 라는 변수가 있는데.. 이 번호를 기준으로 객체를 정렬하고 싶다.

main 메소드를 만들어 정렬을 수행해 보자..

import java.util.*;

public class CompareToTest{
 public static void main(String[] args){
 
  System.out.println("정렬정렬 김정렬..");
  List list = new ArrayList();

  int[] arr = new int[]{8,3,4,6,1,2,7,5,9};
  String[] name = new String[]{"a", "z","y", "k","l","o","t","q", "f"};

  TestBean bean = null;
  for(int i = 0; i < arr.length; i++){
   bean = new TestBean();
   bean.setPk(arr[i]);
   bean.setName(name[i]);

   list.add(bean);
  }

  System.out.println("정렬 전");

  for(int i = 0; i < list.size(); i++){
   bean = (TestBean)list.get(i);
   System.out.println(bean);
  }

  Collections.sort(list);  // <-- 여기서 정렬...

  System.out.println("정렬 후");
  for(int i = 0; i < list.size(); i++){
   bean = (TestBean)list.get(i);
   System.out.println(bean);
  }
 }
}


이 클래스를 실행하면...


정렬정렬 김정렬..
정렬 전
[pk = 8 , name = a]
[pk = 3 , name = z]
[pk = 4 , name = y]
[pk = 6 , name = k]
[pk = 1 , name = l]
[pk = 2 , name = o]
[pk = 7 , name = t]
[pk = 5 , name = q]
[pk = 9 , name = f]
java.lang.ClassCastException
 at java.util.Arrays.mergeSort(Arrays.java:1152)
 at java.util.Arrays.mergeSort(Arrays.java:1163)
 at java.util.Arrays.sort(Arrays.java:1079)
 at java.util.Collections.sort(Collections.java:113)
 at CompareToTest.main(CompareToTest.java:28)
Exception in thread "main"


이런 에러가 발생하게 된다..

에러가 발생하는 건 당연한 이야기 이다.

Collections.sort() 메서드에서 무슨 근거로.. List 안에 들어있는 객체들을 정렬하겠냐 말이쥐..

그럼 이걸 정렬되게 하려면 어떻게 하면 되나...


java.lang 패키지에 있는 Comparable 인터페이스를 구현하면 된다.


java.lang.Comparable 인터페이스에는 compareTo(Object o) 라는 메서드가 있다.

이 녀석을 재정의 하면 된다..

재정의 규칙은 다음과 같다.


o.pk 가 현재 PK 보다 크다면  1 을 반납

o.pk 가 현재 pk 보다 작다면 -1 을 반납

o.pk 가 현재 pk 와 같다면 0 을 반납.


이러면 모든게 끝난다.


그럼 수정한 예를 보자.

class TestBean implements Comparable{
 int pk;
 String name;

 public void setPk(int pk){ this.pk = pk; }
 public int getPk(){ return pk; }
 public void setName(String name){ this.name = name; }
 public String getName(){ return name; }

 public String toString(){
  return "[pk = " + pk + " , name = " + name + "]";
 }


 // 구현 메소드
 public int compareTo(Object o){
  TestBean bean = (TestBean)o;
 
  if(bean.pk < this.pk) return 1;
  else if(bean.pk > this.pk) return -1;
  else return 0;
 }
}


이 예제를 실행하면 아래와 같이 나올것이다..


정렬정렬 김정렬..
정렬 전
[pk = 8 , name = a]
[pk = 3 , name = z]
[pk = 4 , name = y]
[pk = 6 , name = k]
[pk = 1 , name = l]
[pk = 2 , name = o]
[pk = 7 , name = t]
[pk = 5 , name = q]
[pk = 9 , name = f]
정렬 후
[pk = 1 , name = l]
[pk = 2 , name = o]
[pk = 3 , name = z]
[pk = 4 , name = y]
[pk = 5 , name = q]
[pk = 6 , name = k]
[pk = 7 , name = t]
[pk = 8 , name = a]
[pk = 9 , name = f]


Collections.sort(List list) 외에 Arrays.sort(Object[] a) 도 같은 결과가 나온다.


TestBean 의 내부변수인 pk 로 정렬을 해보았다..

이 상태에서... name 으로 비교를 해 볼수도 있다.

방법은 아주 단순하고 간단하다.

바로 java.util.Comparator 인터페이스를 상속하여.. 새로운 클래스를 구현하고 그 클래스를 정렬하는 곳에 알려주면 된다.


java.util.Comparator 인터페이스에는 구현해야할 메소드가 두 개 있다.

public int compare(Object o1, Object o2)

public boolean equals(Object obj)


이렇게 두개만 구현해 주면 된다..


그럼 이름으로 정렬하기 위해 새로운 클래스를 하나 만들어보자.

class MyComparator implements Comparator{
 public int compare(Object o1, Object o2){
  return ((TestBean)o1).getName().compareTo(((TestBean)o2).getName());
 }
 
 public boolean equals(Object o){
  return true;
 }
}

그리고 테스트를 위해 main 메서드의 내부를 아래와 같이 수정했다.

import java.util.*;

public class CompareToTest{
 public static void main(String[] args){
 
  System.out.println("정렬정렬 김정렬..");
  List list = new ArrayList();

  int[] arr = new int[]{8,3,4,6,1,2,7,5,9};
  String[] name = new String[]{"a", "z","y", "k","l","o","t","q", "f"};

  TestBean bean = null;
  for(int i = 0; i < arr.length; i++){
   bean = new TestBean();
   bean.setPk(arr[i]);
   bean.setName(name[i]);

   list.add(bean);
  }

  //-------------------------
  // 추가된 부분
  //-------------------------
  TestBean temp = new TestBean();
  temp.setPk(1);
  temp.setName("m");
  list.add(temp);


  System.out.println("정렬 전");

  for(int i = 0; i < list.size(); i++){
   bean = (TestBean)list.get(i);
   System.out.println(bean);
  }

  Collections.sort(list);

  System.out.println("정렬 후");
  for(int i = 0; i < list.size(); i++){
   bean = (TestBean)list.get(i);
   System.out.println(bean);
  }

  //-------------------------
  // 추가된 부분
  //-------------------------

  Collections.sort(list, new MyComparator()); 

  System.out.println("Comparator 구현 후");
  for(int i = 0; i < list.size(); i++){
   bean = (TestBean)list.get(i);
   System.out.println(bean);
  }
 }
}



이 클래스를 실행하면 아래와 같은 결과가 나온다...


정렬정렬 김정렬..
정렬 전
[pk = 8 , name = a]
[pk = 3 , name = z]
[pk = 4 , name = y]
[pk = 6 , name = k]
[pk = 1 , name = l]
[pk = 2 , name = o]
[pk = 7 , name = t]
[pk = 5 , name = q]
[pk = 9 , name = f]
[pk = 1 , name = m]
정렬 후
[pk = 1 , name = l]
[pk = 1 , name = m]
[pk = 2 , name = o]
[pk = 3 , name = z]
[pk = 4 , name = y]
[pk = 5 , name = q]
[pk = 6 , name = k]
[pk = 7 , name = t]
[pk = 8 , name = a]
[pk = 9 , name = f]
Comparator 구현 후
[pk = 8 , name = a]
[pk = 9 , name = f]
[pk = 6 , name = k]
[pk = 1 , name = l]
[pk = 1 , name = m]

[pk = 2 , name = o]
[pk = 5 , name = q]
[pk = 7 , name = t]
[pk = 4 , name = y]
[pk = 3 , name = z]



훔.. 말주변이 없어서 더이상 설명이 안되네.....

머 두개의 클래스(TestBean 과 MyComparator ) 를 합쳐서 지지고 볶구 해도 되겠징.. ^^;

또는 보기 좋게~ static 메서드로 만들어도 되겠고..

또는...

또는...

또는...

또는....

또는......


머리 깨지네 ㅋ

신고
Posted by Tornado tornado
JVM GC와 메모리 튜닝



모든 Java Application은 JVM(Java Virtual Machine)위에서 동작한다.
이 JVM이 동작하는데 있어서, 메모리의 구조와 특히 GC는 Application의 응답시간과 성능에 밀접한 관계를 미친다. 이번 강좌에서는 JVM 의 메모리 구조와 GC 알고리즘 (JDK 1.4.X에 포함된 새로운 알고리즘 포함) 그리고, JVM의 메모리 튜닝을 통한 Application의 성능향상방법에 대해서 알아보도록 하자.


1.GC란 무엇인가?


GC는 Garbage Collection의 약자로 Java 언어의 중요한 특징중의 하나이다.
GC는 Java Application에서 사용하지 않는 메모리를 자동으로 수거하는 기능을 말한다.
예전의 전통적인 언어 C등의 경우 malloc, free등을 이용해서 메모리를 할당하고, 일일이 그 메모리를 수거해줘야했다. 그러나 Java 언어에서는 GC 기술을 사용함에 따라서 개발자로 하여금 메모리 관리에서 부터 좀더 자유롭게 해주었다.


2.GC의 동작 방법은 어떻게 되는가?


1) JVM 메모리 영역

GC의 동작 방법을 이해하기 위해서는 Java의 메모리 구조를 먼저 이해할 필요가 있다.
일반적으로 Application에서 사용되는 객체는 오래 유지 되는 객체보다, 생성되고 얼마안있어서 사용되지 않는 경우가 많다. <그림 1 참조>


<그림 1. 메모리 foot print>


그래서 Java에서는 크게 두가지 영역으로 메모리를 나누는데 Young 영역과 Old 영역이 그것이다.
Young 영역은 생긴지 얼마 안된 객체들을 저장하는 장소이고, Old영역은 생성된지 오래된 객체를 저장하는 장소이다. 각 영역의 성격이 다른 만큼 GC의 방법도 다르다.
먼저 Java의 메모리 구조를 살펴보자.


<그림 2. Java 메모리 구조>


Java의 메모리 영역은 앞에서 이야기한 두 영역 (Young 영역,Old 영역)과 Perm 영역 이렇게 3가지로 영역으로 구성된다.


<표 1. Java 메모리 영역>



2) GC 알고리즘

그러면 이 메모리 영역을 JVM이 어떻게 관리하는지에 대해서 알아보자.
JVM은 New/Young 영역과, Old영역 이 두영역에 대해서만 GC를 수행한다. Perm영역은 앞에서 설명했듯이 Code가 올라가는 부분이기 때문에, GC가 일어날 필요가 없다. Perm영역은 Code가 모두 Load되고 나면 거의 일정한 수치를 유지한다.


○ Minor GC
먼저 New/Young영역의 GC방법을 살펴보자 New/Young 영역의 GC를 Minor GC라고 부르는데, New/Young영역은 Eden과 Survivor라는 두가지 영역으로 또 나뉘어 진다. Eden영역은 Java 객체가 생성되자 마자 저장이 되는곳이다. 이렇게 생성된 객체는 Minor GC가 발생할때 Survivor 영역으로 이동된다.

Survivor 영역은 Survivor 1과 Suvivor2 영역 두 영역으로 나뉘어 지는데, Minor GC가 발생하면 Eden과 Survivor1에 Alive되어 있는 객체를 Suvivor2로 복사한다. 그리고 Alive되어 있지 않는 객체는 자연히 Suvivor1에 남아있게 되고, Survivor1과 Eden영역을 Clear한다. (결과적으로 Alive된 객체만 Survivor2로 이동한것이다.)
다음번 Minor GC가 발생하면 같은 원리로 Eden과 Survivor2영역에서 Alive되어 있는 객체를 Survivor1에 복사한다. 계속 이런 방법을 반복적으로 수행하면서 Minor GC를 수행한다.

이렇게 Minor GC를 수행하다가, Survivor영역에서 오래된 객체는 Old영역으로 옮기게 된다.

이런 방식의 GC 알고리즘을 Copy & Scavenge라고 한다. 이 방법은 매우 속도가 빠르며 작은 크기의 메모리를 Collecting하는데 매우 효과적이다. Minor GC의 경우에는 자주 일어나기 때문에, GC에 소요되는 시간이 짧은 알고리즘이 적합하다.

이 내용을 그림을 보면서 살펴보도록 하자.


<그림 3-1. 1st Minor GC>


Eden에서 Alive된 객체를 Suvivor1으로 이동한다. Eden 영역을 Clear한다.


<그림 3-2. 2nd Minor GC>


Eden영역에 Alive된 객체와 Suvivor1영역에 Alive된 객체를 Survivor 2에 copy한다.
Eden영역과 Suvivor2영역을 clear한다.


<그림 3-3. 3rd Minor GC>


객체가 생성된 시간이 오래지나면 Eden과 Suvivor영역에 있는 오래된 객체들을 Old 영역으로 이동한다.


○ Full GC

Old 영역의 Garbage Collection을 Full GC라고 부르며, Full GC에 사용되는 알고리즘은 Mark & Compact라는 알고리즘을 이용한다. Mark & Compact 알고리즘은 전체 객체들의 reference를 쭉 따라가다면서 reference가 연결되지 않는 객체를 Mark한다. 이 작업이 끝나면 사용되지 않는 객체를 모두 Mark가 되고, 이 mark된 객체를 삭제한다.<그림 4 참고> (실제로는 compact라고 해서, mark된 객체로 생기는 부분을 unmark된 즉 사용하는 객체로 메꾸어 버리는 방법이다.)

Full GC는 매우 속도가 느리며, Full GC가 일어나는 도중에는 순간적으로 Java Application이 멈춰 버리기 때문에, Full GC가 일어나는 정도와 Full GC에 소요되는 시간은 Application의 성능과 안정성에 아주 큰 영향을 준다.


<그림 4. Full GC>




3. GC가 왜 중요한가?


Garbage Collection중에서 Minor GC의 경우 보통 0.5초 이내에 끝나기 때문에 큰문제가 되지 않는다. 그러나 Full GC의 경우 보통 수초가 소요가 되고, Full GC동안에는 Java Application이 멈춰버리기 때문에 문제가 될 수 있다.
예를 들어 게임 서버와 같은 Real Time Server를 구현을 했을때, Full GC가 일어나서 5초동안 시스템이 멈춘다고 생각해보자.
또 일반 WAS에서도 5~10초동안 멈추면, 멈추는동안의 사용자의 Request가 Queue에 저장되었다가 Full GC가 끝난후에 그 요청이 한꺼번에 들어오게 되면 과부하에 의한 여러 장애를 만들 수 있다..
그래서 원할한 서비스를 위해서는 GC를 어떻게 일어나게 하느냐가 시스템의 안정성과 성능에 큰 변수로 작용할 수 있다.


4. 다양한 GC 알고리즘


앞에서 설명한 기본적인 GC방법 (Scavenge 와 Mark and compact)이외에 JVM에서는 좀더 다양한 GC 방법을 제공하고 그 동작방법이나 사용방법도 틀리다. 이번에는 다양한 GC 알고리즘에 대해서 알아보자. 현재 (JDK 1.4)까지 나와 있는 JVM의 GC방법은 크게 아래 4가지를 지원하고 있다.

- Default Collector
- Parallel GC for young generation (from JDK 1.4 )
- Concurrent GC for old generation (from JDK 1.4)
- Incremental GC (Train GC)

1) Default Collector
이 GC 방법은 앞에서 설명한 전통적인 GC방법으로 Minor GC에 Scavenge를, Full GC에 Mark & compact 알고리즘을 사용하는 방법이다. 이 알고리즘에는 이미 앞에서 설명했기 때문에 별도의 설명을 하지는 않는다.

JDK 1.4에서부터 새로 적용되는 GC방법은 Parallel GC와 Concurrent GC 두가지 방법이 있다. Parallel GC는 Minor GC를 좀더 빨리하게 하는 방법이고 (Throughput 위주) Concurrent GC는 Full GC시에 시스템의 멈춤(Pause)현상을 최소화하는 GC방법이다.

2) Parallel GC
JDK1.3까지 GC는 하나의 Thread에서 이루어진다. Java가 Multi Thread환경을 지원함에도 불구하고, 1 CPU에서는 동시에 하나의 Thread만을 수행할 수 밖에 없기때문에, 예전에는 하나의 CPU에서만 GC를 수행했지만, 근래에 들어서 하나의 CPU에서 동시에 여러개의 Thread를 실행할 수 있는 Hyper Threading기술이나, 여러개의 CPU를 동시에 장착한 HW의 보급으로 하나의 HW Box에서 동시에 여러개의 Thread를 수행할 수 있게 되었다.

JDK 1.4부터 지원되는 Parallel GC는 Minor GC를 동시에 여러개의 Thread를 이용해서 GC를 수행하는 방법으로 하나의 Thread를 이용하는것보다 훨씬 빨리 GC를 수행할 수 있다.


<그림 7. Parallel GC 개념도>


<그림 7> 을 보자 왼쪽의 Default GC방법은 GC가 일어날때 Thread들이 작업을 멈추고, GC를 수행하는 thread만 gc를 수행한다. (그림에서 파란영역), Parallel GC에서는 여러 thread들이 gc를 수행이 가능하기 때문에, gc에 소요되는 시간이 낮아진다.

Parallel GC가 언제나 유익한것은 아니다. 앞에서도 말했듯이 1CPU에서는 동시에 여러개의 thread를 실행할 수 없기 때문에 오히혀 Parallel GC가 Default GC에 비해서 느리다. 2 CPU에서도 Multi thread에 대한 지원이나 계산등을 위해서 CPU Power가 사용되기 때문에, 최소한 4CPU의 256M 정도의 메모리를 가지고 있는 HW에서 Parallel GC가 유용하게 사용된다.

Parallel GC는 크게 두가지 종류의 옵션을 가지고 있는데,Low-pause 방식과 Throughput 방식의 GC방식이 있다.

Solaris 기준에서 Low-pause Parallel GC는 ?XX:+UseParNewGC 옵션을 사용한다. 이 모델은 Old 영역을 GC할때 다음에 설명할 Concurrent GC방법과 함께 사용할 수 있다. 이 방법은 GC가 일어날때 빨리 GC하는것이 아니라 GC가 발생할때 Application이 멈춰지는 현상(pause)를 최소화하는데 역점을 뒀다.

Throughput 방식의 Parallel GC는 ?XX:+UseParallelGC (Solaris 기준) 옵션을 이용하며 Old 영역을 GC할때는 Default GC (Mark and compact)방법만을 사용하도록 되어 있다.Minor GC가 발생했을때, 되도록이면 빨리 수행하도록 throughput에 역점을 두었다.

그외에도 ParallelGC를 수행할때 동시에 몇개의 Thread를 이용하여 Minor영역을 Parallel GC할지를 결정할 수 있는데, -XX:ParallelGCThreads= 옵션을 이용하여 Parallel GC에 사용되는 Thread의 수를 지정할 수 있다.

3) Concurrent GC

앞에서도 설명했듯이, Full GC즉 Old 영역을 GC하는 경우에는 그 시간이 길고 Application이 순간적으로 멈춰버리기 때문에, 시스템 운용에 문제가 된다.

그래서 JDK1.4부터 제공하는 Concurrent GC는 기존의 이런 Full GC의 단점을 보완하기 위해서 Full GC에 의해서 Application이 멈추어 지는 현상을 최소화 하기 위한 GC방법이다.
Full GC에 소요되는 작업을 Application을 멈추고 진행하는것이 아니라, 일부는 Application이 돌아가는 단계에서 수행하고, 최소한의 작업만을 Application이 멈췄을때 수행하는 방법으로 Application이 멈추는 시간을 최소화한다.


<그림 8. Concurrent GC 개념도>


그림 8에서와 같이 Application이 수행중일때(붉은 라인) Full GC를 위한 작업을 수행한다. (Sweep,mark) Application을 멈추고 수행하는 작업은 일부분 (initial-mark, remark 작업)만을 수행하기 때문에, 기존 Default GC의 Mark & Sweep Collector에 비해서 Application이 멈추는 시간이 현저하게 줄어든다.

Solaris JVM에서는 -XX:+UseConcMarkSweepGC Parameter를 이용해 세팅한다.

4) Incremental GC (Train GC)

Incremental GC또는 Train GC라고도 불리는 GC방법은 JDK 1.3에서부터 지원된 GC방법이다. 앞에서 설명한 Concurrent GC와 비슷하게, 의도 자체는 Full GC에 의해서 Application이 멈추는 시간을 줄이고자 하는데 있다.

Incremental GC의 작동방법은 간단하다. Minor GC가 일어날때 마다 Old영역을 조금씩 GC를 해서 Full GC가 발생하는 횟수나 시간을 줄이는 방법이다.


<그림 9. Incremental GC 개념도>


그림 9에서 보듯이. 왼쪽의 Default GC는 FullGC가 일어난후에나 Old 영역이 Clear된다. 그러나, 오른쪽의 Incremental GC를 보면 Minor GC가 일어난후에, Old 영역이 일부 Collect된것을 볼 수 있다.

Incremental GC를 사용하는 방법은 JVM 옵션에 ?Xinc 옵션을 사용하면 된다.
Incremental GC는 많은 자원을 소모하고, Minor GC를 자주일으키고, 그리고 Incremental GC를 사용한다고 Full GC가 없어지거나 그 횟수가 획기적으로 줄어드는 것은 아니다. 오히려 느려지는 경우가 많다. 필히 테스트 후에 사용하도록 하자.

※ Default GC이외의 알고리즘은 Application의 형태나 HW Spec(CPU수, Hyper threading 지원 여부), 그리고 JVM 버전(JDK 1.4.1이냐 1.4.2냐)에 따라서 차이가 매우 크다. 이론상으로는 실제로 성능이 좋아보일 수 있으나, 운영환경에서는 여러 요인으로 인해서 기대했던것만큼의 성능이 안나올 수 있기 때문에, 실환경에서 미리 충분한 테스트를 거쳐서 검증한후에 사용해야 한다.


5. GC 로그는 어떻게 수집과 분석


JVM에서는 GC 상황에 대한 로그를 남기기 위해서 옵션을 제공하고 있다.
Java 옵션에 ?verbosegc 라는 옵션을 주면되고 HP Unix의 경우 ?verbosegc ?Xverbosegc 옵션을 주면 좀더 자세한 GC정보를 얻을 수 있다. GC 정보는 stdout으로 출력이 되기 때문에 “>” redirection등을 이용해서 file에 저장해놓고 분석할 수 있다.

Example ) java ?verbosegc MyApplication

그럼 실제로 나온 GC로그를 어떻게 보는지를 알아보자.


<그림 5. 일반적인 GC 로그, Windows, Solaris>


<그림 5>는 GC로그 결과를 모아논 내용이다. (실제로는 Application의 stdout으로 출력되는 내용과 섞여서 출력된다.)
Minor GC는 ”[GC “로 표기되고, Full GC는 “[Full GC”로 표기된다.
그 다음값은 Heap size before GC인데,GC 전에 Heap 사용량 ( New/Young 영역 + Old 영역 + Perm 영역)의 크기를 나타낸다.

Heap size after GC는 GC가 발생한후에 Heap의 사용량이다. Minor GC가 발생했을때는 Eden과 Survivor 영역으 GC가 됨으로 Heap size after GC는 Old영역의 용량과 유사하다.(Minor GC에서 GC되지 않은 하나의 Survivor영역내의 Object들의 크기도 포함해야한다.)

Total Heap Size는 현재 JVM이 사용하는 Heap Memory양이다. 이 크기는 Java에서 ?ms와 ?mx 옵션으로 조정이 가능한데. 예를 들어 ?ms512m ?mx1024m로 해놓으면 Java Heap은 메모리 사용량에 따라서 512~1024m사이의 크기에서 적절하게 늘었다 줄었다한다. (이 늘어나는 기준과 줄어드는 기준은 (-XX:MaxHeapFreeRatio와 ?XX:MinHeapFreeRation를 이용해서 조정할 수 있으나 JVM vendor에 따라서 차이가 나기때문에 각 vendor별 JVM 메뉴얼을 참고하기 바란다.) Parameter에 대한 이야기는 추후에 좀더 자세히하도록 하자.

그 다음값은 GC에 소요된 시간이다.

<그림 5>의 GC로그를 보면 Minor GC가 일어날때마다 약 20,000K 정도의 Collection이 일어난다. Minor GC는 Eden과 Suvivor영역 하나를 GC하는 것이기 때문에 New/Young 영역을 20,000Kbyte 정도로 생각할 수 있다.

Full GC때를 보면 약44,000Kbyte에서 1,749Kbyte로 GC가 되었음을 볼 수 있다. Old영역에 큰 데이타가 많지 않은 경우이다. Data를 많이 사용하는 Application의 경우 전체 Heap이 512이라고 가정할때, Full GC후에도 480M정도로 유지되는 경우가 있다. 이런 경우에는 실제로 Application에서 Memory를 많이 사용하고 있다고 판단할 수 있기 때문에 전체 Heap Size를 늘려줄 필요가 있다.

이렇게 수집된 GC로그는 다소 보기가 어렵기 때문에, 좀더 쉽게 분석할 수 있게 하기 위해서 GC로그를 awk 스크립트를 이용해서 정제하면 분석이 용이하다.


<표 2. gc.awk 스크립트>


이 스크립트를 작성한후에 Unix의 awk 명령을 이용해서

% awk ?f gc.awk GC로그파일명

을 쳐주면 아래<표 3>와 같이 정리된 형태로 GC 로그만 추출하여 보여준다.


<표 3. gc.awk 스크립트에 의해서 정재된 로그>


Minor와 Major는 각각 Minor GC와 Full GC가 일어날때 소요된 시간을 나타내며, Alive는 GC후에 남아있는 메모리양, 그리고 Freed는 GC에 의해서 collect된 메모리 양이다.

이 로그파일은 excel등을 이용하여 그래프등으로 변환해서 보면 좀더 다각적인 분석이 가능해진다.

※ JDK 1.4에서부터는 ?XX:+PrintGCDetails 옵션이 추가되어서 좀더 자세한 GC정보를 수집할 수 있다.


※ HP JVM의 GC Log 수집

HP JVM은 전체 heap 뿐 아니라 ?Xverbosegc 옵션을 통해서 Perm,Eden,Old등의 모든 영역에 대한 GC정보를 좀더 정확하게 수집할 수 있다.

Example ) java ?verbosegc ?Xverbosegc MyApplication ß (HP JVM Only)

HP JVM의 GC정보는 18개의 필드를 제공하는데 그 내용을 정리해보면 <표 4.>와 같다.

<GC : %1 %2 %3 %4 %5 %6 %7 %8 %9 %10 %11 %12 %13 %14 %15 %16 %17 %18>


<표 4. HP JVM GC 로그 필드별 의미>


이 로그를 직접 보면서 분석하기는 쉽지가 않다. 그래서, HP에서는 좀더 Visual한 환경에서 분석이 가능하도록 HPJtune이라는 툴을 제공한다. 다음 URL에서 다운로드 받을 수 있다.

http://www.hp.com/products1/unix/java/java2/hpjtune/index.html


<그림 6. HP Jtune을 이용해서 GC후 Old영역의 변화 추이를 모니터링하는 화면>




6. GC 관련 Parameter


GC관련 설정값을 보기전에 앞서서 ?X와 ?XX 옵션에 대해서 먼저 언급하자. 이 옵션들은 표준 옵션이 아니라, 벤더별 JVM에서 따로 제공하는 옵션이기 때문에, 예고 없이 변경되거나 없어질 수 있기 때문에, 사용전에 미리 JVM 벤더 홈페이지를 통해서 검증한다음에 사용해야한다.

1) 전체 Heap Size 조정 옵션

전체 Heap size는 ?ms와 ?mx로 Heap 사이즈의 영역을 조정할 수 있다. 예를 들어 ?ms512m ?mx 1024m로 설정하면 JVM은 전체 Heap size를 application의 상황에 따라서 512m~1024m byte 사이에서 사용하게 된다. 그림2의 Total heap size

메모리가 모자를때는 heap을 늘리고, 남을때는 heap을 줄이는 heap growing과 shirinking 작업을 수행하는데, 메모리 변화량이 큰 애플리케이션이 아니라면 이 min heap size와 max heap size는 동일하게 설정하는 것이 좋다. 일반적으로 1GB까지의 Heap을 설정하는데에는 문제가 없으나, 1GB가 넘는 대용량 메모리를 설정하고자 할 경우에는 별도의 JVM 옵션이 필요한 경우가 있기때문에 미리 자료를 참고할 필요가 있다.

※ IBM AIX JVM의 경우
%export LDR_CNTRL=MAXDATA=0x10000000
%java -Xms1500m -Xmx1500m MyApplication

2) Perm size 조정 옵션

Perm Size는 앞에서도 설명했듯이, Java Application 자체(Java class etc..)가 로딩되는 영역이다. J2EE application의 경우에는 application 자체의 크기가 큰 편에 속하기 때문에, Default로 설정된 Perm Size로는 application class가 loading되기에 모자른 경우가 대부분이기 때문에, WAS start초기나, 가동 초기에 Out Of Memory 에러를 유발하는 경우가 많다.

PermSize는 -XX:MaxPermSize=128m 식으로 지정할 수 있다.
일반적으로 WAS에서 PermSize는 64~256m 사이가 적절하다.

3) New 영역과 Old 영역의 조정New 영역은 ?XX:NewRatio=2 에 의해서 조정이 된다.
NewRatio Old/New Size의 값이다. 전체 Heap Size가 768일때, NewRatio=2이면 New영역이 256m, Old 영역이 512m 로 설정이 된다.
JVM 1.4.X에서는 ?XX:NewSize=128m 옵션을 이용해서 직접 New 영역의 크기를 지정하는 것이 가능하다.

4) Survivor 영역 조정 옵션
-XX:SurvivorRatio=64 (eden/survivor 의 비율) :64이면 eden 이 128m일때, survivor영역은 2m가 된다.

5) -server와 ?client 옵션
JVM에는 일반적으로 server와 client 두가지 옵션을 제공한다.
결론만 말하면 server 옵션은 WAS와 같은 Server환경에 최적화된 옵션이고, client옵션은 워드프로세서와 같은 client application에 최적화된 옵션이다. 그냥 언뜻 보기에는 단순한 옵션 하나로보일 수 있지만, 내부에서 돌아가는 hotspot compiler에 대한 최적화 방법과 메모리 구조자체가 아예 틀리다.

○ -server 옵션

server용 application에 최적화된 옵션이다. Server application은 boot up 시간 보다는 user에 대한 response time이 중요하고, 많은 사용자가 동시에 사용하기 때문에 session등의 user data를 다루는게 일반적이다. 그래서 server 옵션으로 제공되는 hotspot compiler는 java application을 최적화 해서 빠른 response time을 내는데 집중되어 있다.

또한 메모리 모델 역시, 서버의 경우에는 특정 사용자가 서버 운영시간동안 계속 서버를 사용하는게 아니기 때문에 (Login하고, 사용한 후에는 Logout되기 때문에..) 사용자에 관련된 객체들이 오래 지속되는 경우가 드물다. 그래서 상대적으로 Old영역이 작고 New 영역이 크게 배정된다. <그림 7. 참조 >

○ -client 옵션

client application은 워드프로세서 처럼 혼자 사용하는 application이다. 그래서 client application은 response time보다는 빨리 기동되는데에 최적화가 되어 있다. 또한대부분의 client application을 구성하는 object는GUI Component와 같이 application이 종료될때까지 남아있는 object의 비중이 높기 때문에 상대적으로 Old 영역의 비율이 높다.


<그림 7. ?server와 ?client 옵션에 따른 JVM Old와 New영역>


이 두옵션은 가장 간단한 옵션이지만, JVM의 최적화에 아주 큰부분을 차지하고 있는 옵션이기 때문에, 반드시 Application의 성격에 맞춰서 적용하기 바란다.
(※ 참고로, SUN JVM은 default가 client, HPJVM는 default가 server로 세팅되어 있다.)

○ GC 방식에 대한 옵션

GC 방식에 대한 옵션은 앞에서도 설명했지만, 일반적인 GC방식이외에, Concurrent GC,Parallel GC,Inceremental GC와 같이 추가적인 GC Algorithm이 존재한다. 옵션과 내용은 앞장에서 설명한 “다양한 GC알고리즘” 을 참고하기 바란다.


7.JVM GC 튜닝


그러면 이제부터 지금까지 설명한 내용을 기반으로 실제로 JVM 튜닝을 어떻게 하는지 알아보도록 하자.

STEP 1. Application의 종류와 튜닝목표값을 결정한다.

JVM 튜닝을 하기위해서 가장 중요한것은 JVM 튜닝의 목표를 설정하는것이다. 메모리를 적게 쓰는것이 목표인지, GC 횟수를 줄이는것이 목표인지, GC에 소요되는시간이 목표인지, Application의 성능(Throughput or response time) 향상인지를 먼저 정의한후에. 그 목표치에 근접하도록 JVM Parameter를 조정하는것이 필요하다.

STEP 2. Heap size와 Perm size를 설정한다.

-ms와 ?mx 옵션을 이용해서 Heap Size를 정한다. 일반적으로 server application인 경우에는 ms와 mx 사이즈를 같게 하는것이 Memory의 growing과 shrinking에 의한 불필요한 로드를 막을 수 있어서 권장할만하다.

ms와mx사이즈를 다르게 하는 경우는 Application의 시간대별 memory 사용량이 급격하게 변화가 있는 Application에 효과적이다.
PermSize는 JVM vendor에 따라 다소 차이가 있으나 일반적으로 16m정도이다. Client application의 경우에는 문제가 없을 수 있지만, J2EE Server Application의 경우 64~128m 사이로 사용이 된다.

Heap Size와 Perm Size는 아래 과정을 통해서 적정 수치를 얻어가야한다.

STEP 3. 테스트 & 로그 분석.

JVM Option에 GC 로그를 수집하기 위한 ?verbosegc 옵션을 적용한다. (HP의 경우 ?Xverbosegc 옵션을 적용한다.)

LoadRunner나 MS Strest(무료로 MS社의 홈페이지에서 다운로드 받을 수 있다.)와 같은 Strest Test툴을 통해서 Application에 Strest를 줘서. 그 log를 수집한다. 튜닝에서 있어서 가장 중요한것은 목표산정이지만, 그만큼이나 중요한것은 실제 Tuning한 Parameter가 Application에 어떤 영향을 주는지를 테스트하는 방법이 매우 중요하다. 그런 의미에서 적절한 Strest Tool의 선정과, Strest Test 시나리오는 정확한 Tuning을 위해서 매우 중요한 요인이다.

○ Perm size 조정
아래 그림8.은 HP JVM에서 ?Xverbosegc 옵션으로 수집한 GC log를 HP Jtune을 통해서 graph로 나타낸 그래프이다. 그림을 보면 Application이 startup되었을때 Perm 영역이 40m에서. 시간이 지난후에도 50m 이하로 유지되는것을 볼 수 있다. 특별하게 동적 classloading등이 수십m byte가 일어나지 않는등의 큰 변화요인이 없을때, 이 application의 적정 Perm 영역은 64m로 판단할 수 있다.


<그림 8. GC 결과중 Perm 영역 그래프>


○ GC Time 수행 시간 분석

다음은 GC에 걸린 시간을 분석해보자. 앞에 강좌 내용에서도 설명햇듯이. GC Tuning에서 중요한 부분중 하나가 GC에 소요되는 시간 특히 Full GC 시간이다.

지금부터 볼 Log는 모社의 물류 시스템의 WAS 시스템 GC Log이다. HP JVM을 사용하며, -server ?ms512m ?mx512m 옵션으로 기동되는 시스템이다.

그림 9를 보면 Peak 시간 (첫번째 동그라미) 14시간동안에 Full GC(동그란점)가 7번일어난것을 볼 수 있다. 각각에 걸린 시간은2.5~6sec 사이이다.
여기서 STEP 1.에서 설정한 AP Tuning의 목표치를 참고해야하는데.

Full GC가 길게 일어나서 Full GC에 수행되는 시간을 줄이고자 한다면 Old 영역을 줄이면 Full GC가 일어나는 횟수는 늘어나고, 반대로 Full GC가 일어나는 시간을 줄어들것이다.

반대로 Full GC가 일어나는 횟수가 많다면, Old 영역을 늘려주면 Full GC가 일어나는 횟수는 상대적으로 줄어들것이고 반대로 Full GC 수행시간이 늘어날 것이다.

특히 Server Application의 경우Full GC가 일어날때는 JVM자체가 멈춰버리기 때문에, 그림 9의 instance는 14시간동안 총 7번 시스템이 멈추고, 그때마다 2.5~6sec가량 시스템이 response를 못하는 상태가 된것이다. 그래서 멈춘 시간이 고객이 납득할만한 시간인지를 판단해야 하고, 거기에 적절한 Tuning을 해야한다.

Server Application에서 Full GC를 적게일어나게하고, Full GC 시간을 양쪽다 줄이기 위해서는 Old영역을 적게한후에, 여러개의 Instance를 동시에 뛰어서 Load Balancing을 해주면, Load가 분산되기 때문에 Full GC가 일어나는 횟수가 줄어들테고, Old 영역을 줄였기 때문에, Full GC에 드는 시간도 줄어들것이다. 또한 각각의 FullGC가 일어나는동안 하나의 서버 instance가 멈춰져 있어도, Load Balancing이 되는 다른 서버가 response를 하고 있기때문에, Full GC로 인한 Application이 멈추는것에 의한 영향을 최소화할 수 있다.


<그림 9. GC 소요시간>


데이타에 따라서 GC Tuning을 진행한후에는 다시 Strest Test를 진행해서 응답시간과 TPS(Throughput Per Second)를 체크해서 어떤 변화를 주었는지를 반드시 체크해봐야한다.


<그림 10. GC후의 Old 영역>


그림 10은 GC후에 Old 영역의 메모리 변화량을 나타낸다.

금요일 업무시간에 메모리 사용량이 올라가다가. 주말에가서 완만한 곡선을 그리는것을 볼 수 있다. 월요일 근무시간에 메모리 사용량이 매우 많고, 화요일에도 어느정도 메모리 사용량이 있는것을 볼 수 있다. 월요일에 메모리 사용량이 많은것을 볼때, 이 시스템의 사용자들이 월요일에 시스템 사용량이 많을 수 있다고 생각할 수 있고, 또는 다른 주의 로그를 분석해봤을때 이 주만 월요일 사용량이 많았다면, 특별한 요인이나 Application 변경등이 있었는지를 고려해봐야할것이다.

이 그래프만을 봤을때 Full GC가 일어난후에도 월요일 근무시간을 보면 Old 영역이 180M를 유지하고 있는것을 볼 수 있다. 이 시스템의 Full GC후의 Old영역은 80M~180M를 유지하는것을 볼 수 있다. 그래서 이 시스템은 최소 180M이상의 Old 영역을 필요로하는것으로 판단할 수 있다.

STEP 4. Parameter 변경.
STEP 3에서 구한 각 영역의 허용 범위를 기준으로 Old영역과 New 영역을 적절하게 조절한다.
PermSize와 New영역의 배분 (Eden,Survivor)영역등을 조정한다.
PermSize는 대부분 Log에서 명확하게 나타나기 때문에, 크게 조정이 필요가 없고 New영역내의 Eden과 Survivor는 거의 조정하지 않는다. 가장 중요한것은 Old영역과 New 영역의 비율을 어떻게 조정하는가가 관건이다.

이 비율을 결정하면서, STEP1에서 세운 튜닝 목표에 따라서 JVM의 GC Algorithm을 적용한다. GC Algorithm을 결정하는 기본적인 판단 내용은 아래와 같다.



이렇게 Parameter를 변경하면서 테스트를 진행하고, 다시 변경하고 테스트를 진행하는 과정을 거쳐서 최적의 Parameter와 GC Algorithm을 찾아내는것이 JVM의 메모리 튜닝의 이상적인 절차이다.


지금까지 JVM의 메모리 구조와 GC 모델 그리고 GC 튜닝에 대해서 알아보았다.

정리하자면 GC 튜닝은 Application의 구조나 성격 그리고, 사용자의 이용 Pattern에 따라서 크게 좌우 되기때문에, 얼마만큼의 Parameter를 많이 아느냐 보다는 얼마만큼의 테스트와 로그를 통해서 목표 값에 접근하느냐가 가장 중요하다
신고
Posted by Tornado tornado

잼있네요.^^ 제소스를 조금 테스트 해보니 후두둑 쏟아지는 버그들 -_-..

근데 여러번 느끼지만, 외국은 맥 정말 많이 쓰네요. 개발자들도.. 냐옹~

PMD로 버그 잡기
목차:
PMD의 설치와 실행
결과 분석
규칙
자신의 규칙 세트 구현하기
결론
참고자료
필자소개
기사에 대한 평가
관련자료:
FindBugs, Part 1: Improve the quality of your code
FindBugs, Part 2: Writing custom detectors
Diagnosing Java code: Unit tests and automated code analysis working together
Subscriptions:
dW newsletters
손쉬운 정적 분석 툴로 버그 잡기

난이도 : 중급

Elliotte Rusty Harold
조교수, Polytechnic University
2005년 1 월 07 일

소스, 정적 분석 툴인 PMD는 버그를 잡기위한 툴로 손색이 없다. PMD의 사용법을 설명한다.

Tom Copeland의 PMD는 오픈 소스(BSD 라이센스) 툴로서 자바 소스 코드를 분석하여 잠재적인 버그를 찾아낸다. 일반적인 부분에서는 FindBugs와 Lint4j(참고자료)같은 툴과 비슷하다. 하지만 이 모든 툴들은 다른 버그들을 찾아내기 때문에 주어진 코드 기반에서 이들 각자를 실행하는 것이 적합하다. 이 글에서 PMD를 사용하는 방법과 활용법을 설명하겠다. PMD의 명령행 인터페이스도 연구할 예정이다. PMD를 Ant와 통합하여 자동 소스-코드 체크를 수행할 수 있고 주요 IDE와 프로그래머의 에디터를 위한 플러그인도 있다.

PMD의 설치와 실행
PMD는 자바로 작성되고 JDK 1.3 또는 이후 버전이 필요하다. 명령행을 사용하는 것이 익숙하다면 PMD의 설치와 실행은 단순하다. zip 파일을 다운로드하고(참고자료), /usr 또는 홈 디렉토리에 저장한다. 이 글에서는 /usr에 저장하는 것으로 간주하겠다.

PMD 를 실행하는 가장 쉬운 방법은 pmd.sh 스크립트(Unix/Linux) 또는 pmd.bat 스크립트(Windows)를 호출하는 것이다. 이 스크립트들은 bin 디렉토리 보다는 pmd-2.1/etc에 있다. 스크립트에는 세 개의 명령행 인자들이 있다:

  • 체크 할 .java 파일로 가는 경로
  • 아웃풋 포맷을 가르키는 html 또는 xml 키워드
  • 실행 할 규칙 세트 이름들

예를 들어, 다음 명령어는 네이밍 규칙 세트를 사용하여 ImageGrabber.java 파일을 검사하여 XML 아웃풋을 만든다:

$ /usr/pmd-2.1/etc/pmd.sh ImageGrabber.java xml rulesets/naming.xml

결과 분석
위 명령어에서 나온 아웃풋은 기본적으로 System.out으로 보내지며 리포트 형식을 취한다(Listing 1):

Listing 1. PMD XML 리포트
 <?xml version="1.0"?><pmd> <file name="/Users/elharo/src/ImageGrabber.java"> <violation line="32" rule="ShortVariable" ruleset="Naming Rules" priority="3"> Avoid variables with short names like j </violation> <violation line="105" rule="VariableNamingConventionsRule" ruleset="Naming Rules" priority="1"> Variables that are not final should not contain underscores (except for underscores in standard prefix/suffix). </violation> </file> <error filename="/Users/elharo/src/ImageGrabber.java" msg="Error while processing /Users/elharo/ImageGrabber.java"/> </pmd>

Listing 1에서 PMD 가 두 개의 문제들을 발견했음을 알 수 있다: ImageGrabber.java의 32번째 줄에서 짧은 변수 이름과 105번째 줄에서 밑줄을 포함하는 이름이 그것이다. 작은 문제인 것 처럼 보이지만 결과는 엄청날 수 있다. 이 경우, 105번째 줄의 밑줄은 10년 묵은 코드의 그저 픽스하기 쉬운 코드의 단편이였다. 하지만 첫 번째 문제를 검사해보면 j 변수를 완전히 제거할 수 있었다는 것을 깨닫게 된다. 왜냐하면 각각 증가되고 있었던 또 다른 변수의 기능들을 중복시키기 때문이다. 프로그램은 작동했지만 앞으로의 변화에 대비해야 하는 것보다는 훨씬 더 위험한 것이었다. 여러분이 제거한 모든 코드 라인들은 버그가 들어올 수 있는 하나의 작은 장소이다.

PMD 아웃풋을 파일로 리다이렉트 하거나 일반적인 방식으로 에디터로 전달(pipe)할 수 있다. 나는 HTML로 아웃풋을 만들어서 이것을 웹 브라우저에 로딩해 보곤 한다. (그림 1)

그림 1. PMD 아웃풋(HTML)
PMD sample output

아웃풋을 파일로 보내는 것은 소스 트리를 검사할 때 특히 유용하다. 디렉토리 이름, zip 파일, JAR 아카이브 파일을 첫 번째 인자로서 전달한다면 PMD 는 그 디렉토리 또는 아카이브에 있는 모든 .java 파일을 반복적으로 검사한다. 소량의 아웃풋이 위협적이다. 특히 PMD가 많은 오류 가능성을 만들어 낼 때 그렇다. 예를 들어 XOM 코드 베이스(참고자료)에서 PMD를 실행할 때, "in과 같은 짧은 이름을 가진 변수를 피하라(Avoid variables with short names like in" 는 보고를 지속적으로 보낸다. "in"은 InputStream을 나타내는 변수 이름으로서 완벽하다고 생각한다. 그럼에도 불구하고 괜찮은 텍스트 에디터에서 아웃풋을 검사한다면 빈번한 오류를 인식하고 지우는 것이 쉽다는 것을 알게 된다. 이들은 매우 비슷하기 때문이다. 그때 다른 재명명 문제를 픽스 할 수 있다.

PMD에서 유일하게 부족한 한 가지 기능은 "lint comment"를 소스 코드에 추가하여 명백히 위험한 연산을 수행한다는 것을 나타내는 기능이다. 이것은 기능이지 버그는 아니다. 이것 외에는 PMD는 대체적으로 괜찮다. 예를 들어, 오랜 시간동안 try-catch 블록은 XOM의 다양한 장소에서 발생했다:

try { this.data = data.getBytes("UTF8"); } catch (UnsupportedEncodingException ex) { // All VMs support UTF-8 }

PMD는 이것을 빈 catch 블록으로 플래그를 단다. VM이 UTF-8 인코딩을 인식하지 못한다는 것을 발견하게 될 때 까지는 문제가 없을 것처럼 보였다. 그래서 이 블록을 다음과 같이 바꾸고 PMD는 괜찮아졌다:

try { this.data = data.getBytes("UTF8"); } catch (UnsupportedEncodingException ex) { throw new RuntimeException("Broken VM: Does not support UTF-8"); }

규칙
PMD는 16 가지 규칙 세트가 있고 자바 코드의 다양한 일반 문제들을 다룬다. 이중 어떤 것은 문제가 있는 것도 있다:

규칙 이름
명령행에서 전달되는 규칙들의 이름은 문서화가 잘 되어있지 않다. 이들을 파악하려면 여러 시도와 오류 과정을 거쳐야 한다. 괄호 안에 있는 이름들을 사용할 수 있다.

  • Basic (rulesets/basic.xml) -- 대부분의 개발자들이 동의하는 규칙: catch 블록들은 비어있어서는 안되고, equals()를 오버라이딩 할 때 마다 hashCode()를 오버라이드한다.

  • Naming (rulesets/naming.xml) -- 표준 자바 네이밍 규약을 위한 테스트: 변수 이름들은 너무 짧아서는 안된다; 메소드 이름은 너무 길어서는 안된다; 클래스 이름은 대문자로 시작해야 하고, 메소드와 필드 이름들은 소문자로 시작해야 한다.

  • Unused code (rulesets/unusedcode.xml) -- 결코 읽히지 않은 프라이빗 필드와 로컬 변수, 접근할 수 없는 문장, 결코 호출되지 않는 프라이빗 메소드 등을 찾기.

  • Design (rulesets/design.xml) -- 다양한 좋은 디자인 원리 체크, 이를 테면: switch 문장은 default 블록을 갖고 있어야 하고, 심하게 중첩된 if 블록은 피해야 하고, 매개변수들은 재할당되어서는 안되며, 더블(double)이 동일함(equality)과 비교되어서도 안된다.

  • Import statements (rulesets/imports.xml) -- 임포트 문장에 대한 작은 문제들 점검. 같은 클래스를 두 번 반입하는 것이나 java.lang에서 클래스를 임포팅하는 것 등.

  • JUnit tests (rulesets/junit.xml) -- 테스트 케이스와 테스트 메소드 관련 특정 문제 검색. 메소드 이름의 정확한 스펠링과 suite() 메소드가 정적이고 퍼블릭인지 여부.

  • Strings (rulesets/string.xml) -- 스트링 관련 작업을 할 때 발생하는 일반적인 문제들 규명. 스트링 리터럴 중복, String 구조체 호출, String 객체에 toString() 호출하기 등.

  • Braces (rulesets/braces.xml) -- for, if, while, else 문장이 괄호를 사용하는지 여부 검사.

  • Code size (rulesets/codesize.xml) -- 과도하게 긴 메소드, 너무 많은 메소드를 가진 클래스, 리팩토링에 대한 유사한 후보들을 위한 테스트.

  • Javabeans (rulesets/javabeans.xml) -- 직렬화 될 수 없는 bean 클래스 같이 JavaBeans 코딩 규약을 위배하는 JavaBeans 컴포넌트 검사.

  • Finalizers -- finalize() 메소드는 자바에서 일반적인 것은 아니기 때문에 사용법에 대한 규칙이 비교적 익숙하지 않다. 이 그룹의 검사는 finalize() 메소드 관련한 다양한 문제들을 찾는다. 이를 테면, 비어있는 finalizer, 다른 메소드를 호출하는 finalize() 메소드 finalize()로의 호출 등이 그것이다.

  • Clone (rulesets/clone.xml) -- clone() 메소드에 대한 규칙: clone()을 오버라이드하는 클래스는 Cloneable을 구현해야 하고, clone() 메소드는 super.clone()을 호출해야 하며, clone() 메소드는 실제로 던지지 않더라도 CloneNotSupportedException을 던지도록 선언되어야 한다.

  • Coupling (rulesets/coupling.xml) -- 클래스들간 과도한 커플링 표시 검색. 지나치게 많은 임포트, supertype 또는 인터페이스가 충분한 곳에서 subclass 유형 사용하기, 너무 적은 필드, 변수, 클래스 내의 리턴 유형 등.

  • Strict exceptions (rulesets/strictexception.xml) -- 예외 테스트: 메소드는 java.lang.Exception을 던지도록 선언되어서는 안되고, 예외는 플로우 제어에 사용되어서는 안되며, Throwable은 잡혀서는 안된다.

  • Controversial (rulesets/controversial.xml) -- 일부 PMD 규칙들은 유능한 자바 프로그래머가 받아들일 수 있는 것들이다. 하지만 어떤 것은 논쟁의 여지가 충분하다. 이 규칙 세트에는 좀더 의심스러운 검사들이 포함되어 있다. 변수에 null 할당하기, 메소드에서 온 다중의 리턴 포인트 sun 패키지에서 임포팅 등이 포함된다.

  • Logging (rulesets/logging-java.xml) -- java.util.logging.Logger를 위험하게 사용하는 경우 검색: 끝나지 않고 정적이지 않은 logger와 한 클래스에 한 개 이상의 logger 등.

명령행에서 이름과 콤마를 분리하여 여러 규칙 세트들을 한번에 검사할 수 있다:

$ /usr/pmd-2.1/etc/pmd.sh ~/Projects/XOM/src html rulesets/design.xml,rulesets/naming.xml,rulesets/basic.xml

자신의 규칙 세트 구현하기
특정 규칙 세트로 종종 검사하고 있다면 이들을 자신만의 규칙 세트 파일로 결합할 수도 있다.(Listing 2) 이 규칙 세트는 기본 규칙, 네이밍 규칙, 디자인 규칙들을 반입한다:

Listing 2. 기본 규칙, 네이밍 규칙, 디자인 규칙들을 반입하는 규칙 세트
 <?xml version="1.0"?> <ruleset name="customruleset"> <description> Sample ruleset for developerWorks article </description> <rule ref="rulesets/design.xml"/> <rule ref="rulesets/naming.xml"/> <rule ref="rulesets/basic.xml"/> </ruleset>

좀더 세분화 된 것을 원한다면 각 세트에서 원하는 개별 규칙들을 선택할 수 있다. 예를 들어 Listing 3은 세 개의 빌트인 세트에서 11 개의 특정 규칙들을 선택한 커스텀 규칙 세트를 보여주고 있다. 큰 코드 기반을 검사하는 것은 많은 시간이 걸리기 때문에 찾고자 하는 특정 문제를 보다 빨리 찾을 수 있다.

Listing 3. 11 가지 특정 규칙을 반입한 규칙 세트
 <?xml version="1.0"?> <ruleset name="specific rules"> <description> Sample ruleset for developerWorks article </description> <rule ref="rulesets/design.xml/AvoidReassigningParametersRule"/> <rule ref= "rulesets/design.xml/ConstructorCallsOverridableMethod"/> <rule ref="rulesets/design.xml/FinalFieldCouldBeStatic"/> <rule ref="rulesets/design.xml/DefaultLabelNotLastInSwitchStmt"/> <rule ref="rulesets/naming.xml/LongVariable"/> <rule ref="rulesets/naming.xml/ShortMethodName"/> <rule ref="rulesets/naming.xml/VariableNamingConventions"/> <rule ref="rulesets/naming.xml/MethodNamingConventions"/> <rule ref="rulesets/naming.xml/ClassNamingConventions"/> <rule ref="rulesets/basic.xml/EmptyCatchBlock"/> <rule ref="rulesets/basic.xml/EmptyFinallyBlock"/> </ruleset>

세트 안에 대부분의 규칙들을 포함시킬 수 있지만 동의하지 않는 것이나 또는 오류 가능성들이 있는 것들은 배제할 수 있다. 예를 들어, XOM은 테이블 검색을 수행할 때 종종 switch 문장을 디폴트 블록 없이 사용한다. 나는 대부분의 디자인 규칙들을 지킬 수 있지만 <exclude name="SwitchStmtsShouldHaveDefault"/> 자식 요소를 디자인 규칙을 반입하는 규칙 요소에 추가하여 default블록을 놓치는 지에 대한 검사를 하지 않는다:

Listing 4. switch 문장이 디폴트를 가져야 한다는 디자인 규칙을 배제한 규칙 세트
 <?xml version="1.0"?> <ruleset name="dW rules"> <description> Sample ruleset for developerWorks article </description> <rule ref="rulesets/design.xml"> <exclude name="SwitchStmtsShouldHaveDefault"/> </rule> </ruleset>

(PMD가 옳다면 대신 디폴트 블록을 추가해야 한다.)

빌트인 규칙에는 제한이 없다. 자바 코드를 작성하고 PMD를 재컴파일 하거나 XPath 식을 작성하여 새로운 규칙을 추가할 수 있다.

결론
(매우 비싼) 빌트인 규칙들을 사용해서 PMD는 코드의 실제 문제들을 반드시 찾아낸다. 이들 중 어떤 것은 미미하지만 어떤 것은 그렇지 않다. 모든 버그를 찾는 것은 아니다. 단위 테스트와 수락 테스트를 수행해야 한다. 또한 PMD는 알려진 버그를 잡을 때 훌륭한 디버거용 대체도 아니다. 하지만 알지 못했던 버그를 찾을 때 빛을 발한다. PMD가 문제를 찾을 수 없었던 코드 베이스를 보지 못했다. PMD는 싸고 쉬우며 재미있는 방식으로 프로그램을 향상시킨다. 전에 PMD를 사용하지 않았다면 시도해보라.

 

출처 : http://www-128.ibm.com/developerworks/kr/library/j-pmd/#N101F9

 

신고
Posted by Tornado tornado

디렉토리만 뽑아와야 하는 상황..

java.io 패키지에 보면 FileFilter 와 FilenameFilter 인터페이스가 있다.

 

두개의 차이점은 정의된 메소드인데..

FileFilter 에는 accept(File pathname) 이 있고.

FilenameFilter 에는 accept(File dir, String name) 이 있다.

 

만약 *.exe 파일만 고르고 싶다고 하면 FilenameFilter 를 쓰면 되고...

디렉토리만 골라서 보고 싶을때는 FileFilter 인터페이스를 구현해 주면 된다.

 

   File[] fileNames = f.listFiles(new FileFilter(){
    public boolean accept(File pathname){     
     return pathname.isDirectory();
    }
   });

 

이런식이 되겠지..

 

만약 확장자로 걸러내고 싶다면...

File[] fileNames = f.listFiles(new FilenameFilter(){
  public boolean accept(File dir, String name){

    return name.endsWith(".exe");

  }

});

 

IO 를 특정부분만 쓰다보니.. 이런데서 잠깐씩 헤매고 있네 ㅡㅡ

신고

'JAVA > JSE' 카테고리의 다른 글

[펌] JVM GC와 메모리 튜닝  (0) 2005.05.18
[펌] 손쉬운 정적 분석 툴로 버그 잡기  (0) 2005.05.11
FileFilter... 까먹기 싫어~~~  (0) 2005.02.12
[nio] Channels 클래스  (0) 2005.01.17
[펌] Sorting Algorithm  (0) 2005.01.15
[link] 자바 검색 봇~  (0) 2005.01.11
Posted by Tornado tornado

간단한 회사 웹 메일 만들던 중 메일 발송 부분에서 자료 첨부를 Struts 의 FormBean 을 이용해서

파일을 보내는데 nio 를 이용해 보려고 했다.

 

폼 파일에서는 FormFile.getInputStream() 이라는 것만 제공해 주는데..

무식하게 스리 getInputStream() 으로 반납되는 InputStream 을 FileInputStream 으로 바꾸려

뻘짓거리를 ㅡㅡ;;

 

그래서 API 를 뒤져보니.. java.nio.channels 클래스를 보니 newChannel() 메소드 발견~

 

아래와 같이 처리 함.

 

File file = new File("경로","파일명");

InputStream is = formFile.getInputStream();

ReadableByteChannel byteChannel = Channels.newChannel( is )

FileChannel outputChannel = new FileOutputStream(file).getChannel();

outputChannel.transferFrom(byteChannel, 0, is.available());

 

일단 되는것 확인...

 

Channels 클래스에 InputStream, OutputStream, Reader, Writer 로 변환해 주는게 다 모여있넹.

 

대충 알았으니 이제 놀자 ㅋㅋ

 

 

 

 

신고

'JAVA > JSE' 카테고리의 다른 글

[펌] 손쉬운 정적 분석 툴로 버그 잡기  (0) 2005.05.11
FileFilter... 까먹기 싫어~~~  (0) 2005.02.12
[nio] Channels 클래스  (0) 2005.01.17
[펌] Sorting Algorithm  (0) 2005.01.15
[link] 자바 검색 봇~  (0) 2005.01.11
[펌] [Collection Framework ] List, Set and Map  (0) 2004.12.29
Posted by Tornado tornado