IT_Programming/Java

JAVA Perfomance Tuning 1, 2, 3

JJun ™ 2008. 9. 8. 01:07

 

[JAVA Perfomance Tuning 1] 

 

객체 메모리를 효율적으로 사용하는 몇가지 지침들

 

1. 자주 사용하는 루틴에서는 객체생성을 피한다.

   빈번히 호출되므로, 객체를 자주 생성하게 되고, 결과적으로 전체 객체 순환시간이 줄어든다.

   따라서 객체 생성을 하지 않도록 재사용 가능한 객체를 매개변수로 보내여 객체 순환을

   감소시킨다.

 

2. collection 객체의 크기를 필요한 만큼 크게 잡아두도록 한다.

   ex) Vector의 크기를 증가하는 방법은

        더 큰 크기의 내부 배열 객체를 생성 -> 이전 배열에서 새로운 배열로 새롭게 복제 ->

        이전 배열을 삭제하는 방법으로 크기를 증가

    따라서 Collection을 가능한 큰 크기로 잡아두는 것이 객체 삭제 횟수를 줄인다.

 

3. 클래스의 다수 인스턴스가 동일한 객체를 로컬변수를 통해 접근한다면,

    인스턴스마다 별도로 참조하는 것보다 정적 변수를 만드는 편이 더 좋다.

    객체마다 차지하는 공간이 줄어들고, 각각의 인스턴스에서 별도의 객체를 생성하고 있었다면,

    객체 생성 갯수도 감소할 수 있다.

 

4. 예외 인스턴스 재사용은 특별한 스택 추적을 요구하지 않는 경우에만 한다.

 

 

 

객체 재사용

 

application에서 사용하는 collection이 어떤 특성을 갖고 있는지 정확히 알고 있어야 함.

(매우 큰 크기의 collection의 경우,  재사용을 위해 객체를 유지하는데 필요한 공간을 중요하게 다뤄야 하는 경우) 컨테이너 객체 (ex. Vector, Hashtable 등)를 재활용할 경우, 모드 참조를 해제해서 참조하는 객체를

가비지 컬렉션해야한다. 컨테이너 객체를 재사용할 경우에 참조 객체 삭제에 의한 과부하가 있기 때문에

항상 재사용하는 것이 최선은 아니다.

 

컨테이너 객체를 재사용하는 좋은 방법은...

다른 컨테이너를 랩핑할 수 있는 컨테이너 클래스를 직접 만들어서 사용하는것.

이렇게 하면 각 컬렉션 객체의 제어가 매우 편해지며, 특히 재사용을 고려해서 프로그램을 설계할 수가 있다. 재사용을 위해 설계한 클래스를 쓰지 않더라고 풀 관리자를 사용할 수 있다.

 

 

Vector 풀 관리자를 사용한 예

 

// vector pool 관리자 인스턴스이다.

public static VectorPoolManager vectorPoolManager = new VectorPoolManager(25);

.....

public void someMethod()
{
    // 새로운 vector를 가져온다. 이 메소드에서 vector를 활용하고
    // 더 이상 사용하지 않는다. (즉, 반환하거나 변수에 저장하지 않는다.)
    // 따라서 vector를 재사용하기에 적합하다.
    // new Vector() 대신에 factory 메소드를 호출한다.
    Vector v = vectorPoolManager.getVector();
   
    ... // vector로 작업한다.
   
    // 마지막으로 vector 작업이 끝났다는 사실을 pool 관리자에 알린다.
    vectorPoolManager.returnVector(v);
}

 

pool에 반환된 후에도 vector에 대한 핸들이 유지됨으로 인해 애플리케이션 내에 벡터에 대한 핸들이 계속

존재할 수 있다. 따라서 벡터 참조를 모두 제거하였음을 항상 확인해야 한다. 이들 vector는 항상 애플리케이션에서 내부적으로만 사용해야 하며, 핸들의 제거가 불확실한 외부의 서드파티 클래스에서는 사용하면 안된다.

 

 

Vector Pool을 관리하는 클래스

 

package tuning.reuse;

import java.util.Vector;

public class VectorPoolManager
{
    Vector[] pool;
    boolean[] inUse;
   
    public VectorPoolManager(int initialPoolSize)
    {
        pool = new Vector[initialPoolSize];
        inUse = new boolean[initialPoolSize];
       
        for(int i = pool.length-1; i >= 0; i--)
        {
            pool[i] = new Vector();
            inUse[i] = false;
        }
    }
   
    public synchronized Vector getVector()
    {
        for(int i = inUse.length-1; i >= 0; i--)
        {
            if(!inUse[i])
            {
                inUse[i] = true;
                return pool[i];
            }
        }
       
        // 여기까지는 모든 vector 가 사용 중이다. pool에 vector를
        // 10개 추가한다.(여기서 10개는 예제용으로 임의로 설정한 값)
        boolean[] old_inUse = inUse;
        inUse = new boolean[old_inUse.length + 10];
        System.arraycopy(old_inUse, 0, inUse, 0, old_inUse.length);
       
        Vector[] old_pool = pool;
        pool = new Vector[old_pool.length + 10];
        System.arraycopy(old_pool, 0, pool, 0, old_pool.length);
       
        for(int i = old_pool.length; i < pool.length; i++ )
        {
            pool[i] = new Vector();
            inUse[i] = false;
        }
       
        // 마지막 vector를 할당한다.
        inUse[pool.length-1] = true;
        return pool[pool.length - 1];
    }
   
    public synchronized void returnVector(Vector v)
    {
        for(int i = inUse.length-1; i >= 0; i--)
        {
            if(pool[i] == v)
            {
                inUse[i] = false;
                // java.util.Collection 객체에서는 clear(...)를 이용할 수도 있다.
                // setSize(...)를 이용하면 모든 객체가 null이 된다.
                v.setSize(0);
                return;
            }
        }
       
        throw new RuntimeException("Vector was not obtained from the pool: " + v);
    }
}

 

pool에 vector가 반환될 때, vector의 크기를 0으로 초기화하기 떄문에 vector에서 참조하고 있던 모든 객체의 참조가 제거된다. (Vector.setSize() 는 새로운 크기보다 큰 모든 내부 인덱스의 참조를 제거한다)

그러나 vector 자체에 할당된 메모리는 반환되지 않는데, 이는 vector의 현재 크기가 유지되기 때문.

 

* 쓰레드당 객체 하나만을 사용하면서,   동일한 객체가 쓰레드 내에 일관적으로 사용되어야 하는 상황이라면

  동일한 상태정보를 가지는 singleton으로 풀 관리자를 만들어라.

 

public class VectorPoolManager
{
    public static final VectorPoolManager SINGLETON = new VectorPoolManager(10);
    Vector[] pool;
    boolean[] inUse;
   
    // 생성자를 전용으로 정의해서 객체가 생성될 수 없게 한다.
    private VectorPoolManager(int initialPoolSize)
    {
        .....
    }
    ......
}

 

  이 경우 ThreadLocal 객체를 사용하면 된다.

  ThreadLocal은 현재 쓰레드에 대한 지역 객체를 반환하는 접근자다.

  ( 자세한 사항은 자바 퍼포먼스 튜닝 책 P.138을 참조)

 

 -------------------------------------------------------------------------------------------------

 

[JAVA Perfomance Tuning 2 - String과 StringBuffer]

  

 

컴파일과 런타임할 때의 문자열값 부여 비교

 

 컴파일할 때 완전히 결정할 수 있으면, String 객체의 + 연산자를 사용하는 것이 효율적.

 컴파일할 때 결정할 수 없다면 StringBuffer를 이용하는 것이 효율적이다.

 

ex)

1.   String s = "hi " + "Mr. " + " " + "Buddy!";

     이 경우는 컴파일할 때 문자열이 결정된다. 즉, 컴파일시 String s = "hi Mr.  Buddy!";  로 된다.

 

2.   StringBuffer s = (new StringBuffer()).append("hi ").append("Mr. ").append(" ")

                            .append("Buddy!").toString();

     이 경우 컴파일러는 컴파일할 때 문자열을 결정할 수 없다.

     런타임할 때 임시 StringBuffer와 함께 생성된다.

 

 => 컴파일할 때 상수로 결정이 가능하다면 String과 임시 StringBuffer 객체를 생성하는 과부하가 없으며,

      메소드 호출에 필요한 런타임할 때의 추가적 시스템 자원 사용도 피할수 있다.

      그러나, 컴파일할 때 결정할 수 없으면, 연결 연산은 런타임할 때 실행되어야 한다.

 

ex)

public String sayHi(String title, String name)

{

    return "hi " + title + " " + name;

}

 

이경우 변수가 사용되었으므로 컴파일할 때 결정되지않는다. 따라서 아래와 같이 컴파일 될 수 있다.

return (new StringBuffer()).append("hi ").append(title).append(" ").append(name).toString();

또는 return "hi ".concat(title).concat(" ").concat(name);

이 구현을 사용하면 중개 문자열 객체 2개를 생성했다가 버린다.

또한 메소드를 호출할 때마다 문자열 객체를 생성한다.

 

따라서 문자열을 컴파일할 때 완전히 결정할 수 있다면, 연결연산자가 StringBuffer 사용보다 효율적이다.

컴파일할 때 문자열을 결정할 수 없다면, StringBuffer를 이용하는 것이 더 효율적이다.

 

----------------------------------------------------------------

 

[JAVA Perfomance Tuning 3 - 기타 문자열  

 

 

1. StringTokenizer 보다 String[]을 사용하자. 단어의 수를 센다거나 문자열에서 특정문자의 수를 세는

   경우 등 StringTokenizer를 사용하는 경우가 있다. 이럴 땐 String 배열을 사용하는게 퍼포먼스가 더 좋다.

   (ex. JDK1.4를 사용하는 경우 약 5배정도가 더 효율적이다. 임시객체의 수가 StringTokenizer가 더 많다)

 

2. 파일에서 일치하는 문자열을 행 단위로 필터링하는 문자 배열 구현시 

   BufferedReader.readLine() 보다 문자 배열로 하는 것이 더 효율적이다.

    이 경우 파일의 크기에 따라 좋아지는 성능이 변경되는데 보통 2~5배 정도의 성능효과를 볼 수 있다.

   이 소스는 자바 퍼포먼스 튜닝 책 P.193 이나 프랑스의 루앙 대학교(Rouen University)

   웹 사이트인 http://www-igm.univ-mlv.fr/~lecroq/string/ 에서

   '정확안 문자열 대응 알고리즘 (Exact String Matching Algorithms)' 이라는 글을 보면 된다.

 

3. String의 정규 표현식(ex. matches() 등)을 하나씩 사용하는 데 적합. 

   정규 표현식을 반복적으로 사용하는데는 비효율적이다.

 

4. 문자열을 비교해야 한다면, 직접 작성한 메소드보다는 일반적으로 제공하는 문자열 메소드를

   사용하는 이 성능 향상에 더 좋다.

 

   ex) String.equals()와 String.equalsIgnoreCase()를 비교하자

    => 결론부터 말하면  두 문자열이 동일하다면 equals()가 equalsIgnoreCase() 보다 몇 배 더 빠르다. 

        동일한 문자열은 equals() 에서 가장 빠르고 equalsIgnoreCase() 에서 가장 느리다.

       

       반면, 두 문자열의 길이가 다르다면 equalsIgnoreCase()는 2가지를 비교하고 결과를 반환하는데 비해

       equals() 는 4가지를 비교해야 반환할 수 있다.

       따라서 이 경우에는 equalsIgnoreCase() 가 equals() 보다 20% 정도 더 빠르다.

 

  동작법)

   String.equals(Object) : 객체 동일성 확인 → null 확인 → 문자열 형식과 길이가 동일한지를 확인 →

                                    첫 글자부터 시작해 차례대로 비교

 

   String.equalsIgnoreCase(String) : null 확인 → 길이확인 → 대소문자를 무시하고 비교하는

                                                   regionMatches()를 적용.

※ regionMatches()는 첫번째 글자부터 마지막 글자까지 글자 단위로 실행하며, 비교 전에 각 글자를

    대문자로 변환.

 

 

5. String.substring() 과는 달리 String.toUppercase()는 문자열이 길면 길수록

   수행 시간이 길어지며 추가적인 객체를 생성한다. (새로운 문자 배열)

    즉, 반복적으로 String.toUppercase() or String.toLowercase() 를 사용하면 애플리케이션에

    상당한 과부하를 야기한다.

 

6. 국제화한 문자열을 정렬할 때는 java.text.Collator 객체보다는 java.text.CollationKey를 이용.

   

public runsort()
{
    quicksort(stringArray, 0, stringArray.length-1, Collator.getInstance());
}

 

public static void quicksort(String[] arr, int lo, int hi, Collator c)
{
    // CollationKey 배열로 변환한다.
    CollationKey keys[] = new CollationKey[arr.length];
    for( int i = arr.length-1; i >= 0; i-- )
        keys[i] = c.getCollationKey(arr[i]);
       
    // CollationKey를 정렬한다.
    quicksort_collationKey(keys, 0, arr.length-1);
   
    // 정렬한 순서대로 배열에 저장한다.
    for( int i = arr.length-1; i >= 0; i-- )
        arr[i] = keys[i].getSourceString();
}
public static void quicksort_collationKey(CollationKey[] arr, int lo, int hi)
{
    ....
    int mid = (lo + hi) / 2;
    CollationKey middle = arr[mid];     // CollationKey 데이타 형식이다.
    ....
    // CollationKey.compareTo(CollationKey) 를 이용한다.
    if( arr[lo]..compareTo(middle) > 0 )
    ....
}

 

7. 국제화가 필요없다면 String.compareTo()를 이용해서 문자열을 비교한다.