태터데스크 관리자

도움말
닫기
적용하기   첫페이지 만들기

태터데스크 메시지

저장하였습니다.

'java'에 해당되는 글 25건

  1. 2007.10.19 [Sun Java Forum] PretenureSizeThreshold의 의미... (1)
  2. 2007.10.03 Sun Java Forum - HotSpot 최적화에 의한 착각...
  3. 2007.09.13 [Enterprise Java는 거대한 동기화 머신이다] - 재미있는 GC 성능 사례 - Part3
  4. 2007.09.06 Reference 객체는 어떻게 Garbage Collector와 대화하는가? (5)
  5. 2007.09.04 [ Enterprise Java는 거대한 동기화 머신이다 - GC ] Enterprise Java & Oracle 성능 분석의 기초 - Part7 (2)
  6. 2007.09.03 Java 힙 사이즈 이슈 - Initial Size와 Max Size를 어떻게 설정할 것인가?
  7. 2007.08.31 전혀 새로운 책을 꿈꾸며... (1)
  8. 2007.08.30 [ Enterprise Java는 거대한 동기화 머신이다 - GC ] Enterprise Java & Oracle 성능 분석의 기초 - Part6 (1)
  9. 2007.08.29 [ Enterprise Java는 거대한 동기화 머신이다 - GC ] Enterprise Java & Oracle 성능 분석의 기초 - Part5 (3)
  10. 2007.08.28 [ Enterprise Java는 거대한 동기화 머신이다 - Thread ] Enterprise Java & Oracle 성능 분석의 기초 - Part4 (1)

[Sun Java Forum] PretenureSizeThreshold의 의미...

Enterprise Java 2007.10.19 23:39
PretenureSizeThreshold 옵션을 사용해본 사람이 있는지 모르겠다.
이 옵션은 Object를 Young Generation을 거치지 않고 Old Generation에 직접 저장하는 기능을 활성화한다.
가령 이 값이 1000이면 1000바이트 이상되는 Object들은 Old Generation으로 직접 들어가게 된다.
왜 이런 옵션을 사용할까?
  • 특정 크기 이상의 Object가 Long-Lived, 즉 장수하고 있다는 사실을 미리 알고 있다면...
  • 이런 Object들을 Young Generation에 살게 했다가 나이가 들면 다시 Old Generation으로 옮기는 일련의 과정이 오버헤드가 된다.
  • 따라서 PretenureSizeThreshold 옵션을 사용하면 이런 오버헤드를 줄일 수 있다.
상당히 좋은 기능이다.
하지만, Application을 작성해본 사람은 다 알겠지만, 이렇게 심플한 상황이 어디 흔한가? 크기가 큰 객체도 단명(Short Lived)하는 경우가 비일비재하다. 그런 경우에 이 옵션을 사용했다가는 마치 암 환자를 요양소에 밀어 놓는 꼴이고, 불필요한 Full GC를 유발하게 된다.

아래 글이 이런 내용에 대해 논의를 하고 있다.
이런 옵션을 사용하고 또 이런 내용이 공개적으로 토론이 된다는 사실이 부러울 뿐이다.
(아래 Dion_Cho 가 본인...)

혹시 이 글을 읽는 사람들 중에 CMS Collector를 사용하고 있는 사람은 얼마나 되는지 모르겠다.
언젠가 한번 설문조사를 해보아야 할 듯 하다...


DiegoCarzaniga
Posts:109
Registered: 12.01.06
Using -XX:PretenureSizeThreshold
Oct 17, 2007 2:56 AM

Hi guys,

I hava a question about Gargabe Collector tuning.

My real time application generates every second (with a certainquantity of traffic) some objects with long time life (MEMORY_OLD), andsome with short long life (MEMORY_YOUNG).

After a code review I reduced the quantity of objects created, in orderto optimize GC pauses and frequency. The ratio MEMORY_YOUNG/MEMORY_OLDis augmented to about 3.3.

With this ratio i choose these JVM options:
-Xmx1024m -Xms1024m -XX:MaxNewSize=16m -XX:NewSize=16m

The problem is that with a so high MEMORY_YOUNG/MEMORY_OLD ratio, everyGC young copies many objects to Tenured space... this augments GC YOUNGpauses to about 70 ms. To make smaller these pauses I could sizesmaller -XX:MaxNewSize, but in this way GC young frequency will augmenttoo much causing problems during CMS phases.

So I though about -XX:PretenureSizeThreshold option that copies objects bigger than defined size, directly into Tenured Space.
Does something know if this option can be useful in this scenario?
Tuning it with the right size could be avoid unnecesary promotion toTenure Space... but on the other side it will copy to tenured spacealso objects that otherwise will die in Eden space at the first GCYoung.

Can anyone give me some advice?

Thank you very much all in advance
Diego
Dion_Cho
Posts:60
Registered: 9/6/07
Re: Using -XX:PretenureSizeThreshold
Oct 18, 2007 6:11 AM (reply 1 of 2)

1. Young generation = 16M? Is this just mistake or really 16M? Don'tyou think young generation size is way too small? Especially when yourjava heap size is 1G?
I'm not quite sure about the structure of your app... But, if younggeneration size is this too small, minor gc occurs too frequently andmore objects would be promoted to old generation without any chance tobe collected(as you told yourself in original post)

You can try -XX:+UseParNewGC with bigger young generation size if you're on multiple CPU.
(You may be already turning this option on in recent version...)

2. If you're worried about stop-the-world full gc under CMS collector,you can tweak following options: CMSInitiatingOccupancyFraction,CMSIncrementalMode, UseCMSInitiatingOccupancyOnly, ...
Consult JVM garbage collector tuning guide for more info.

3. PretenureSizeThreshold option exactly matches your request. Justwant to check one thing. Is your big object is a real "one" bigobject(like big array) or just a set of small objects? In latter case,it would not go directly to tenured generation.

Anyway, what JVM version and optionally what OS are you on? Coz every version has its own GC features...

DiegoCarzaniga
Posts:109
Registered: 12.01.06
Re: Using -XX:PretenureSizeThreshold
Oct 18, 2007 7:13 AM (reply 2 of 2)

Thank you Dion_Cho!
My EDEN SPACE is about 16M.... it's not a mistake.
My application is a real time one which needs short stop-the-worldpauses in order to avoid protocol retransmission (which could casecongestion of the application).
So I prefer smaller GC YOUNG pauses.... although in this way GC YOUNG frequency is very high.

I'm using 1.4 JVM on a Sun machine.

I'm already using -XX:+UseConcMarkSweepGC and -XX:+UseParNewGC.

Regard to PretenureSizeThreshold... I tried to set it to 1000 B. Inthis way some big objects can be directly promoted to the TENUREDSPACE. Unfortunately performance didn't improve, probably because inthis way not only old objects, but also some young objects sarepromoted directly.

Thank you again for your reply
Diego


신고
Trackback 0 : Comment 1
  1. FryRachael32 2011.12.01 12:22 Modify/Delete Reply

    관리자의 승인을 기다리고 있는 댓글입니다

Write a comment


Sun Java Forum - HotSpot 최적화에 의한 착각...

Enterprise Java 2007.10.03 23:38
HotSpot JVM의 가장 큰 장점은 "컴파일 최적화"에 있다. 전통적인 JIT 컴파일러의 장점은 그대로 살리면서 단점(늦은 구동시간과 비효율적인 최적화)을 해소하는 것이 HotSpot Compiler의 주 설계 목적이다.

하지만 이 와중에 우리의 시각을 혼란스럽게 만드는 요소가 생긴다.
HotSpot JVM의 지능적이고 자동화된 최적화 기법 때문에 Warming-Up 시간이 필요해진다. 즉 JVM이 바이트코드로부터 최적의 바이트코드나 바이러리 코드를 찾아내는데 일정한 시간이 필요하다. 이 때문에 최초 얼마간의 성능 측정만으로 성능을 좋다 나쁘다를 결정하는 것은 위험할 수 있다.

동일한 구현 방법에 대해서 JVM이 C/C++과 거의 비슷하거나 나은 정도의 성능을 내는데 10분 정도의 시간이 소요되었다는 연구 결과를 본 적이 있다.

아래 논의도 비슷한 내용을 담고 있다. 분명이 성능이 더 좋아야 할 코드가 실제로는 나쁜 성능을 보이는 이유에 대해서 논의한 결과이다. 잠정적인 결론은 HotSpot JVM이 최적의 코드를 생산해내는데 걸리는 Warming-Up 시간으로 인해 초기에는 그런 모순된 결과가 나올 수 있다는 것이다.

섣부른 성능 테스트라는 것이 얼마나 많은 오류를 내포하고 있는지 알 수 있는 기회이기도 하다.

UMTS
Posts:3
Registered: 9/21/07
naive for loops runs faster than "optimized"?
Sep 21, 2007 7:14 AM

well,
I remember certification question (310-025):
what piece of code runs faster:
1.
for(int i=0; i
r = r + s.charAt(i);
}
2.
int l = s.length();
for(int i=0; i
r = r + s.charAt(i);
}

well anybody would say that 2 should be at least not worse than 1. We eliminate repeatable call to length() method of string.
I was really surprised that in 1.6.0 the 1 run is faster (not much but about 10%-15%).
I could understand that method inline-*** could make 1. to be of thesame performance that achieved in 2. but being FASTER than 2 is reallyextraordinary behaviour. Could explain why?

I looked at piece of code (java.util.regex.Mathcer):
/**
* Returns a literal replacement String for the specified
* String.
*
* This method produces a String that will work
* as a literal replacement s in the
* appendReplacement method of the {@link Matcher} class.
* The String produced will match the sequence of characters
* in s treated as a literal sequence. Slashes ('') and
* dollar signs ('$') will be given no special meaning.
*
* @param s The string to be literalized
* @return A literal string replacement
* @since 1.5
*/
public static String quoteReplacement(String s) {
if ((s.indexOf('
') == -1) && (s.indexOf('$') == -1))
return s;
StringBuffer sb = new StringBuffer();
for (int i=0; i
char c = s.charAt(i);
if (c == '
') {
sb.append('
'); sb.append('
');
} else if (c == '$') {
sb.append('
'); sb.append('$');
} else {
sb.append(c);
}
}
return sb.toString();
}
and think that having s.length() as a variable would make code faster,but making small experiment showed me that I might be wrong :(
Dion_Cho
Posts:40
Registered: 9/6/07
Re: naive for loops runs faster than "optimized"?
Sep 21, 2007 9:01 AM (reply 1 of 4)


Hm... weird. My code tells the opposite.

See following:

c:Program FilesJavajdk1.6.0_02bin>java -version
java version "1.6.0_02"
Java(TM) SE Runtime Environment (build 1.6.0_02-b06)
Java HotSpot(TM) Client VM (build 1.6.0_02-b06, mixed mode)

c:Program FilesJavajdk1.6.0_02bin>java -cp . test
1. Elapsed time = 1468 <-- for(int i=0; i
2. Elapsed time = 1156 <-- for(int i=0; i
3. Elapsed time = 1156 <-- for(int i=0; i
4. Elapsed time = 1156 <-- for(int i=0; i<30; i++) s.length() == 30

My test shows that naive loop is slowest and optimized code runs faster. This is natural result.

To make it clear, i recommend you review your test code. If youcan't find any flaw, better post your source code. In that case, mytest code could have flaw.

Anyway, i'm not sure that String.length method is inlined by hotspot jvm.
Hprof shows that: (-Xrunhprof:cpu=times)

rank self accum count trace method
1 50.34% 50.34% 1 303982 test.main
2 31.10% 81.45% 400000 300848 java.lang.String.charAt
3 5.65% 87.09% 102001 300847 java.lang.String.length
4 0.58% 87.67% 12 302704 sun.misc.ASCIICaseInsensitiveComparator.compare
5 0.58% 88.24% 129 302697
...

Here you see String.length method being invoked, which means it's not inlined.

(Note) For String.length inlining, refer to http://java.sun.com/developer/onlineTraining/Programming/JDCBook/perf2.html

====================================================
public class test {

public static void main(String[] args) {

int r1 = 0;
int r2 = 0;
int r3 = 0;

String s = "123456789012345678901234567890";

long startTime = System.currentTimeMillis();

for(int idx=0; idx < 10000000; idx++) {
for(int i=0; i
r1 = r1 + s.charAt(i);
}
}

System.out.printf("1. Elapsed time = %dn", System.currentTimeMillis() - startTime);

startTime = System.currentTimeMillis();

int l = s.length();
for(int idx=0; idx < 10000000; idx++) {
for(int i=0; i
r2 = r2 + s.charAt(i);
}
}

System.out.printf("2. Elapsed time = %dn", System.currentTimeMillis() - startTime);

startTime = System.currentTimeMillis();

for(int idx=0; idx < 10000000; idx++) {
l = s.length();
for(int i=0; i
r2 = r2 + s.charAt(i);
}
}

System.out.printf("3. Elapsed time = %dn", System.currentTimeMillis() - startTime);

startTime = System.currentTimeMillis();
for(int idx=0; idx < 10000000; idx++) {
for(int i=0; i<30; i++) {
r3 = r3 + s.charAt(i);
}
}

System.out.printf("4. Elapsed time = %dn", System.currentTimeMillis() - startTime);
}
}

Edited by: Dion_Cho on Sep 21, 2007 6:24 PM

UMTS
Posts:3
Registered: 9/21/07
Re: naive for loops runs faster than "optimized"?
Sep 22, 2007 12:04 AM (reply 2 of 4)

What you say is just what I would expect, but:
C:alexprototypestringbin>java -version
java version "1.6.0_02"
Java(TM) SE Runtime Environment (build 1.6.0_02-b05)
Java HotSpot(TM) Client VM (build 1.6.0_02-b05, mixed mode, sharing)

C:alexprototypestringbin>java TestA
A: value = 20467200000, elaplsed (seconds): 4.040892348

C:alexprototypestringbin>java TestB
B: value = 20467200000, elaplsed (seconds): 5.67969728

C:alexprototypestringbin>
and code:
public class TestA {

/**
* @param args
*/
public static void main(String[] args) {
String s = "";
for(int j = 0; j<10000; j++) {
s = s + j;
}
long t1 = System.nanoTime();
long r = 0;
for(int j = 0; j<10000; j++) {
for(int i = 0; i < s.length(); i++) {
r = r + s.charAt(i);
}
}
long t2 = System.nanoTime();
System.out.println("A: value = " + r + ", elaplsed (seconds): " + (t2-t1)/1000000000.0);
}

}

-- and B which supposed to be faster --
public class TestB {

/**
* @param args
*/
public static void main(String[] args) {
String s = "";
for(int j = 0; j<10000; j++) {
s = s + j;
}
int l = s.length();
long t1 = System.nanoTime();
long r = 0;
for(int j = 0; j<10000; j++) {
for(int i = 0; i < l; i++) {
r = r + s.charAt(i);
}
}
long t2 = System.nanoTime();
System.out.println("B: value = " + r + ", elaplsed (seconds): " + (t2-t1)/1000000000.0);
}

}

How would you explain that?

But look also at that, I have changed the order of tests in your code:
public class TestTest {

public static void main(String[] args) {

int r1 = 0;
int r2 = 0;
int r3 = 0;

String s = "123456789012345678901234567890";

long startTime = System.currentTimeMillis();

int l = s.length();
for (int idx = 0; idx < 10000000; idx++) {
for (int i = 0; i < l; i++) {
r2 = r2 + s.charAt(i);
}
}

System.out.printf("2. Elapsed time = %dn", System.currentTimeMillis()
- startTime);

startTime = System.currentTimeMillis();

for (int idx = 0; idx < 10000000; idx++) {
l = s.length();
for (int i = 0; i < l; i++) {
r2 = r2 + s.charAt(i);
}
}

System.out.printf("3. Elapsed time = %dn", System.currentTimeMillis()
- startTime);

startTime = System.currentTimeMillis();
for (int idx = 0; idx < 10000000; idx++) {
for (int i = 0; i < s.length(); i++) {
r1 = r1 + s.charAt(i);
}
}

System.out.printf("1. Elapsed time = %dn", System.currentTimeMillis()
- startTime);

startTime = System.currentTimeMillis();
for (int idx = 0; idx < 10000000; idx++) {
for (int i = 0; i < 30; i++) {
r3 = r3 + s.charAt(i);
}
}

System.out.printf("4. Elapsed time = %dn", System.currentTimeMillis()
- startTime);
}
}

and got:
2. Elapsed time = 4140
3. Elapsed time = 922
1. Elapsed time = 1109
4. Elapsed time = 1282

Edited by: UMTS on Sep 22, 2007 12:11 AM

Dion_Cho
Posts:40
Registered: 9/6/07
Re: naive for loops runs faster than "optimized"?
Sep 22, 2007 1:17 AM (reply 3 of 4)

Here goes my result(of your codes):
(my laptop outperforms yours :) )

c:Program FilesJavajdk1.6.0_02bin>java -cp . TestA
A: value = 20467200000, elaplsed (seconds): 2.025779812

c:Program FilesJavajdk1.6.0_02bin>java -cp . TestB
B: value = 20467200000, elaplsed (seconds): 1.90442668

To your suprising, the result is exact opposite of yours.

I think you'd better narrow down your phenomenon.
1. Remove all the codes unrelated to String.length(). Measure the effect of "String.length()" not others
2. Optionally, run profiler using "-Xrunhprof:cpu=times" and check which step is the most time consuming job
3. Optionally, collect gc log using "-XX:+PrintGCDetails -XX:+PrintGCTimeStamps"

PS) Anyway, i think my test code was flawed. Maybe because of theeffect of hotspot jvm optimization. I had seen some researches thathotspot jvm needs almost 10 minutes running time to reach its maximumperformance. It's called "warming up" time, and the exact warming uptime depends on situation.
UMTS
Posts:3
Registered: 9/21/07
Re: naive for loops runs faster than "optimized"?
Sep 24, 2007 3:48 AM (reply 4 of 4)

Seems I got a kind of clue...
I modified the code to run the same piece more times...
public class TestA {
public static void main(String[] args) {
r();
r();
r();
r();
r();
r();
}
public static void r() {
String s = "";
for(int j = 0; j<10000; j++) {
s = s + j;
}
long t1 = System.nanoTime();
long r = 0;
for(int j = 0; j<10000; j++) {
for(int i = 0; i < s.length(); i++) {
r = r + s.charAt(i);
}
}
long t2 = System.nanoTime();
System.out.println("A: value = " + r + ", elaplsed (seconds): " + (t2-t1)/1000000000.0);
}
}

public class TestB {
public static void main(String[] args) {
r();
r();
r();
r();
r();
r();
}
public static void r() {
String s = "";
for(int j = 0; j<10000; j++) {
s = s + j;
}
int l = s.length();
long t1 = System.nanoTime();
long r = 0;
for(int j = 0; j<10000; j++) {
for(int i = 0; i < l; i++) {
r = r + s.charAt(i);
}
}
long t2 = System.nanoTime();
System.out.println("B: value = " + r + ", elaplsed (seconds): " + (t2-t1)/1000000000.0);
}
}
and I got
C:alexprototypestringbin>java TestA
A: value = 20467200000, elaplsed (seconds): 4.029689249
A: value = 20467200000, elaplsed (seconds): 4.058129455
A: value = 20467200000, elaplsed (seconds): 1.603812775
A: value = 20467200000, elaplsed (seconds): 1.603092293
A: value = 20467200000, elaplsed (seconds): 1.605882033
A: value = 20467200000, elaplsed (seconds): {color:#ff0000}1.609466566

C:alexprototypestringbin>java TestB
B: value = 20467200000, elaplsed (seconds): 5.702802171
B: value = 20467200000, elaplsed (seconds): 5.5870017
B: value = 20467200000, elaplsed (seconds): 1.608010236
B: value = 20467200000, elaplsed (seconds): 1.594163784
B: value = 20467200000, elaplsed (seconds): 1.595716215
B: value = 20467200000, elaplsed (seconds): {color:#ff0000}1.595248558

C:alexprototypestringbin>

Now we see that eventually B is faster as everyone would expect...
Butby some reasons optimization of that code tooks a bit longer time thanA... which is strange but understandable (may be optimizer "tried" somemore variants of optimization)...

What strange is that your laptop faster than mine and showed expected behaviour from the very beginning.
I own Toshiba P105-6114, 1GB processor T5500 (dual core), what isyours? May be having 2 processors slows optimization down for a while?

Concerning naive versus optimezed I dare say that effectof optimization is close to 0, and seems I know why... String is afinal class more over it is a class with well-known behaviour:optimizer could asume that length() always produce the same value andthere is no reason to "invoke" it as many times as specified -- it isjust possilbe to call once and "cache" the result...
What do you think?



신고
tags : HotSpot, java
Trackback 0 : Comment 0

Write a comment


[Enterprise Java는 거대한 동기화 머신이다] - 재미있는 GC 성능 사례 - Part3

Enterprise Java 2007.09.13 17:51
3. Heap 여유 공간이 충분한데도 OOME(OutOfMemoryException)이 발생한다?

간혹 Heap의 여유 공간이 충분한데도 OutOfMemory Error가 나는 경우가 있다. 이러한 상황을 이해하려면 Java Application이 사용하는 메모리가 여러 영역으로 나뉜다는 사실을 이해해야 한다.

Java Application이 사용하는 메모리 영역은 보통 다음과 같이 분류된다.

- Permanent Space: Class 정보를 저장
- Java Heap: Object 정보를 저장
- Native Heap: JNI, Thread Stack, 기타 Native 정보를 저장

우리가 흔히 접하는 Memory 문제는 대부분 Java Heap에서 발생한다. Java Application이 할당하는 오브젝트들이 Java Heap에 거주하기 때문에 가장 많은 메모리를 필요로 하기 때문이다.

하지만 위의 제목처럼 GC Log 등을 통해 모니터링을 해 본 결과 Java Heap의 여유 공간이 충분한데도 OOEM이 난다면? 다음과 같은 세가지 문제를 의심해보아야 한다.

- Permanent Space가 부족하지 않은가?
- Native Heap이 부족하지 않은가?
- 버그가 아닌가? :)

Permanet Space와 Native Heap에서 발생할 수 있는 메모리 부족 현상에 대해서 알아보자.

- Permanent Space의 부족
GC Log를 통해 Permanent Space가 부족한 지의 여부를 간접적으로 판단할 수 있다.
(GC Log에 대한 자세한 내용은 Google 검색....)

대부분의 Application이 기본 크기만으로 필요한 클래스들을 관리할 수 있다. 하지만 복잡한 Application 들, 특히 많은 수의 Servlet/JSP/EJB/Library 들을 로딩하는 Web Application의 경우 좀 더 큰 크기의 Permanent Space를 요구하기도한다.

-verbose:class 옵션을 사용하면 어떤 클래스들이 로딩되는지 확인할 수 있다.

Permanent Space의 부족 현상으로 판명나는 경우에는 -XX:PermSize-XX:MaxPermSize 옵션을 이용해 Permanent Space의 크기를 키워주어야 한다.

- Thread Stack Size가 큰 경우
우리가 Thread를 사용할 때마다 Native Heap에는 Thread가 사용하는 메모리가 할당된다. 시스템마다 기본적으로 할당되는 크기는 다르다.

Thread Stack의 크기를 256K라고 가정해보자. 4개의 Thread는 1M를 사용하고, 400개의 Thread는 무려 100M를 사용한다. 수많은 Thread를 사용하는 대형 시스템에서는 결코 간과할 수 없는 크기가 된다.

Application 모니터링을 통해 현재 생성한(사용 중인 아닌) 전체 Thread의 개수를 파악해야 한다. 만일 지나치게 많은(수백개 ~ 수천개) Thread가 생성되었다면 지나치게 큰 크기의 Native Heap을 사용하고 필연적으로 OOEM을 유발한다.

Thread 개수를 줄이는 가장 기본적인 방법은 Thread Pool을 사용하는 것이다. Weblogic/Jeus/Websphere와 같은 WAS들이 수백~수천의 동시 사용자를 감당할 수 있는 것은 Thread Pool을 잘 사용하기 때문이다. 대량의 Client와 통신을 수행하는 Server Application을 작성하는 경우에는 반드시 Thread Pool 기법을 사용해서 실제 생성되는 Thread의 개수를 줄여야 한다.

또 다른 방법은 Thread Stack Size를 줄이는 것이다. Thread 개수를 줄일 수 없다면 -Xss128K 정도의 작은 값을 부여해서 메모리 사용량을 줄일 수 있다. 단, Stack 크기가 줄어든 만큼 Stack Overflow 에러가 날 확률이 높아진다.

(PS) Application이 사용하는 Thread 개수를 모니터링하는 가장 좋은 방법은? Thread Dump(kill -3)가 가장 손쉽고 확실한 방법이다. JDK 1.5에서 추가된 Platform MBean(MXBean)과 JConsole 또한 좋은 방법이다. JConsole에 대해서는 아래 URL을 참조한다.

http://java.sun.com/developer/technicalArticles/J2SE/jconsole.html

- File나 Socket을 지나치게 많이 오픈하는 경우
File/Socket을 Resource Limit를 초과하여 오픈하는 경우에도 OOEM이 발생한다. 우선 File이나 Socket을 열고 닫지 않은, 이른바 Resource Leak을 의심해봐야 하며 필요한 경우 다음과 같이 File/Socket의 Resource Limit을 키워야 한다.

ulimit -n 10000

- JNI를 사용하는 Library들...
JDK가 제공하는 Library나 3rd Party가 제공하는 Library들 중에서 JNI를 사용하는 경우 Native Heap을 사용한다. Native Heap은 Java Heap과는 전혀 독립적으로 사용된다. 따라서 Java Heap은 여유가 있음에도 OOEM이 발생하게 된다.

Max Heap(-Xmx) 크기를 500M 정도로 지정했는데 OS 모니터링을 통해 본 Java Process의 메모리 크기가 2G라면? 십중 팔구 Native Heap의 크기가 지나치게 커진 것이다. Thread Stack 크기와 더불어 JNI Heap을 의심해보아야 한다.

Oracle OCI JDBC Driver가 JNI를 사용하는 대표적인 3rd Party Library이다. OCI JDBC Driver는 Thin Driver에 비해 성능 면에서 다소 유리하지만 Native Heap 공간을 많이 사용하는 경향이 있다. 따라서 OCI Driver를 사용하는 경우에는 Process의 메모리 크기를 잘 모니터링해야 한다.

한가지 역설적인 것은 Native Heap을 좀더 크게 사용하기 위해서 Java Heap의 크기를 줄여야 하는 경우가 있다는 것이다. 즉, OutOfMemory 부족 현상이 Native Heap에서 발생하는 경우에는 Java Heap의 크기를 줄이는 것이 대안이 될 수 있다. 32bit 환경에서는 하나의 Java 프로세스가 사용할 수 있는 최대 메모리 공간은 2G로 제한된다. 이 공간을 Java Heap이 다 써버리면 Native Heap이 사용될 수 있는 공간이 그만큼 줄어든다. 그만큼 OOEM이 날 확률이 높아진다.

- 노파심에...
시스템의 물리적 메모리가 2G이다. Java Application의 성능을 극대화하기 위해 Max Heap Size를 1.8G 정도 부여할려고 한다. 바람직한가? 그럴 수도 있지만 생각치 못한 역효과가 있을 수 있다. 앞에서 소개한 몇가지 사례를 기억하자.

Java Heap에 지니치게 많은 메모리를 할당함으로써 Permanet Space나 Native Heap의
부족을 초래하게 되고 이로 인해 OOEM이나 Paging In/Out에 의한 성능 저하 현상이 유발될 수 있기 때문이다.

메모리는 항상 필요한 만큼만...

--------------------------------------------------------------------------------------------------------
다음 글에 계속

신고
tags : GC, Heap, java, OutOfMemory
Trackback 0 : Comment 0

Write a comment


Reference 객체는 어떻게 Garbage Collector와 대화하는가?

Enterprise Java 2007.09.06 16:36
Reference와 Garbage Collector

앞서 블로그(http://blog.naver.com/ukja/120042116328)에서 Java에서 대용량 데이터를 다룰려면 Reference를 사용할 수 있어야 한다고 언급한 바 있다.

간단한 테스트를 통해 Reference를 사용했을 때 Garbage Collection에서 어떤 차이를 유발
하는지 살펴보자.

Reference와 Reachability
JDK 1.2부터 Reference 객체가 제공되며 java.lang.ref 패키지에 필요한 클래스들이 정의되어 있다. Reference 객체는 Application과 Garbage Collector가 서로 대화를 주고받을 수 있는 유일무이한 객체라는데 그 의미가 있다.

일반적으로 Java에서 객체를 참조하는 방식은 다음과 같다.

SomeObject obj = new SomeObject();
...

위에서 obj 변수는 SomeObject 객체를 참조하고 있다. Garbage Collector는 이 객체가 Reachable한지 Unreachable 한 지에 따라 Collection 여부를 결정한다. 아래 두 경우를 보자.

a) Unreachable 객체
for(...) {
SomeObject obj = new SomeObject();
...
}

b) Reachable 객체
SomeObject obj;
for(...) {
obj = new SomeObject();
...
}

a)의 경우 obj는 for() {} 구문이 끝나고 나면 Unreachable 상태가 된다. 따라서 다음 번 GC에서 Collection 대상이 된다. 반면 b)의 경우 obj는 for() {} 구문이 끝나더라도 여전히 Reachable 상태이다. 따라서 Collection 대상에서 제외된다.

JDK 1.2에서 Reference 객체가 소개되기 전까지는 객체의 상태는 Reachable 또는 Unreachable 둘 중의 하나여야만 했다. 하지만 Reference 객체의 등장으로 보다 다양한 상태를 정의할 수 있게 되었다. Java 객체의 상태는 이제 다음과 같이 다섯개로 나누어진다.

Strongly Reachable: Reference 객체를 사용하지 않으면서 Reachable한 상태. 즉 우리가 사용하는 대부분의 Reachable 객체는 Strongly Reachable 상태이다.
Softly Reachable: SoftReference 객체를 통해서 접근되는 상태.
Weakly Reachable: WeakReference 객체를 통해서 접근되는 상태
Phantomly Reachable: PhantomReference 객체를 통해서 접근되는 상태(본 블로그에서는 다루지 않음)

Strongly Reachable 상태는 우리가 평소에 객체를 액세스하는 방식을 말한다. Softly Reachable 상태와 Weakly Reachable 상태는 Reachablility를 더 세분화한다. 한마디로 부드럽게 참조하는 방법과 약하게 참조하는 방법이 추가된 것이다.

Strongly/Softly/Weakly Reachable의 상태 차이는 Garbage Collector가 객체를 어떻게 청소하느냐에 따라 구분된다. Strongly Reachable 상태의 객체는 절대 Collection이 되지 않는다. Garbage가 아니기 때문이다. 반면 Softly Reachable 상태의 객체는 Memory Pressure(주로 Full GC)가 발생할 때 청소가 가능하다. Weekly Reachable 상태의 객체는 Softly Reachable 상태보다 더 약해서 GC가 발생할 때마다 적극적으로 Collection이 된다.

Softly/Weakly Reachable 상태의 객체는 분명히 Garbage가 아니면서도 메모리 요구량이 높아지는 시점에는 마치 Garbage 처럼 처리된다는 속성을 지니고 있다. 이런 의미에서 Reference 객체를 Application과 Garbage Collector가 서로 통신하는 유일한 방식으로 설명하기도 한다.

항상 참조(Reference)는 하고 있지만, 참조 대상이 반드시 메모리에 보관하고 있을 필요가 없는 객체들을 정의하고 싶다면? 바로 SoftReference나 WeakReference를 사용하면 된다. 예를 들어 사용자가 어떤 이미지를 요구할 때마다 메모리에 올려서 내용을 보여주고, 메모리에 올라온 이미지는 Cache에 담아서 재활용한다고 가정해보자. 사용자가 늘어남에 따라 메모리 요구량이 점점 늘어나서 더 이상은 이미지를 Cache에 담을 수 없게 될 것이다. 이런 경우를 해결하려면 LRU 알고리즘과 같은 기법을 써서 복잡한 자료 구조를 구현해야 한다. 하지만 SoftReference를 쓰면 매우 간단하게 해결된다. 즉 메모리에 올라온 이미지 객체를 직접 참조하지 않고 SoftReference를 통해서 참조하는 것이다. 이렇게 되면 Memory Pressure가 발생할 때 자연스럽게 Collection이 이루어지게 된다.

SoftReference를 사용하는 간단한 예제
SoftReference/WeakReference를 사용할 경우에는 Reference가 참조하고 있는 객체가 언제든지 Collection될 수 있다는 전제하에서 코딩을 해야 한다. 아래에 간단한 예제가 있다.(Java 5의 Template 기능을 사용했음...)

- Strong Reference인 경우: 그냥 쓰면 된다
HashMap map = new HashMap();
ImageObject img = new ImageObject(userId + ".gif");
map.put(
userId, img);
...
ImageObject img = map.get(userId);
if(img == null) { // 아직 put 안되었음 }

- Soft Reference인 경우: 참조 대상 객체가 Collection 되었는지 여부를 확인해야 한다.
HashMap> map
= new HashMapSoftReference>();
ImageObject img = new ImageObject(userId + ".gif");
map.put(userId, new SoftReference(img));
...
SoftReference sr = map.get(userId);
if(sr == null) { // 아직 put 안되었음 }
ImageObject img = sr.get();
if(img == null) { // Oops!! Softly Reachable Object가 Collection 되었음...
img = new ImageObject(userId + ".gif");
sr = new SoftReference(img);
map.put(userId, sr);
}
...

Strong Reference/SoftReference/WeakReference의 행동 방식에 대한 간단한 테스트
아래는 Strong Reference/SoftReference/WeakReference가 GC에 어떤 영향을 미치는지 간단하게 테스트한 결과이다

  • Strong Reference/SoftReference/WeakReference를 이용해 대량의 객체를 생성한다
  • 그 후 대량의 메모리를 할당한다(Case1 = 8M, Case2 = 10M)
  • 참조 대상 객체가 Collection되는지를 확인한다.

(소스는 첨부 파일 참조)

1. Object 생성 후 8M를 요구할 때

- Strong Reference
500000 accomplished
Total Application Pause Time = 0.319163
Full GC: Pause Time = 0.000000, Count = 0, Average=0.000000
Minor GC: Pause Time = 0.319163, Count = 4, Average=0.079791

- SoftReference: 아직 참조 대상이 Collection 되지 않았음(Full GC가 생기지 않았음에 유의)
Null = 0, Not Null = 500000 <-- 전부 Not Null
500000 accomplished
Total Application Pause Time = 0.531873
Full GC: Pause Time = 0.000000, Count = 0, Average=0.000000
Minor GC: Pause Time = 0.531873, Count = 6, Average=0.088645

- WeakReference: 일부 참조 대상이 Collection 되었음(Full GC가 없음에도 불구)
Null = 21224, Not Null = 478776 <-- 일부 Null
500000 accomplished
Total Application Pause Time = 0.514273
Full GC: Pause Time = 0.000000, Count = 0, Average=0.000000
Minor GC: Pause Time = 0.514273, Count = 6, Average=0.085712

2. Object 생성 후 10M를 요구할 때

- Strong Reference
500000 accomplished
Total Application Pause Time = 0.545487
Full GC: Pause Time = 0.226339, Count = 1, Average=0.226339
Minor GC: Pause Time = 0.319148, Count = 4, Average=0.079787

- SoftReference: 참조 대상 전체가 Collection 되었음(Full GC가 생겼음)
Null = 500000, Not Null = 0 <-- 전부 Null
500000 accomplished
Total Application Pause Time = 1.521140
Full GC: Pause Time = 1.027627, Count = 2, Average=0.513814
Minor GC: Pause Time = 0.493513, Count = 5, Average=0.098703

- WeakReference: 참조 대상 전체가 Collection 되었음(Full GC가 생겼음)
Null = 500000, Not Null = 0 <-- 전부 Null
500000 accomplished
Total Application Pause Time = 0.980496
Full GC: Pause Time = 0.506724, Count = 1, Average=0.506724
Minor GC: Pause Time = 0.473772, Count = 5, Average=0.094754

위의 결과를 보면 SoftReference와 WeakReference를 사용할 경우 Memory Pressurce가 있을 때 참조 대상 객체가 Collection되는 것을 확인할 수 있다. 따라서 메모리 사용이 좀 더 효율적이 된다.

단 Reference 객체 자체가 메모리를 차지하기 때문에 오버헤드가 있을 수 있다. 하지만 이런 오버헤드는 메모리의 효율적인 사용에 의해 충분히 상쇄된다.

---(참조)----------------------------------------------------------------
java.util.WeakHashMap은 WeakReference를 이용하는 HashMap이다. 만일 중요하지 않은 객체를 메모리에 올리면서 메모리를 효율적으로 사용하기 원한다면 WeakHashMap을 사용해 볼 것을 권장한다.
-------------------------------------------------------------------------

정말 진지한 Application이라면...
비록 SoftReference/WeakReference가 메모리를 효율적으로 사용하는 매우 편리한 방법을 제공하지만 역시 진지한 Application에서 채용할 만큼 효율적인지는 의문이다.

정말 지능적인 Memory Cache를 구현하려면 LRU에 기반한 복잡한 자료구조를 구현할 수밖에 없을 것이다. Garbage Collector의 행동 양식은 Application 입장에서는 예측이 불가능하기 때문에(즉 자주 사용되지 않는 객체를 언제 메모리에서 내릴 것인가...) 신뢰할 만한 기능을 구현하기 어렵다.

반면 좀 덜 진지한 Application이라면 SoftReference나 WeakReference만으로도 쉽게 원하는 효과를 얻을 수 있을 것이다.


신고
tags : GC, java, Reference
Trackback 0 : Comments 5
  1. 맨땅헤딩 2007.09.07 02:22 신고 Modify/Delete Reply

    글 잘보고 있습니다. 근데 뭐 한가지 질문드려도 될가요. 오라클과는 상관없는데...
    저야 하는일이 그래서인지 코딩이나 어플리케이션쪽은 젬병이라서... 제가 돌리는 자바 프로그램하나가 while loop로 비디오의 모든 프레임을 읽어서 image processing을 처리하는게 있는데, 처음 한 5~10분은 제 속도가 나다가 어느순간 무지하게 느려집니다. 처음에는 gc로 어느정도 효과를 보지만 그나마도 시간이 지나면 다시 같은 현상이 나타납니다. 루프안에는 image processing 특성상 array를 좀 많이 사용합니다. 혹시 뭐 집히시는게 있는지...감사합니다.

  2. 맨땅헤딩 2007.09.07 02:26 신고 Modify/Delete Reply

    아.. 그리고 처리된 데이타를 keep track하는 memory-based video database를 가지고 있습니다. video database라고 해서 뭐 거창한거는 아니고, 그냥 extract된 feature values을 Java object으로 관리하는 겁니다. 컬러...기탕 등등. 그렇다고 해당 database크기가 커 봤쟈 1~200메가인데, 제가 사용하는 시스템이 메모리가 1G이거든요...

  3. 욱짜 2007.09.07 09:27 신고 Modify/Delete Reply

    JDK 종류와 버전, OS 정보, VM Option을 포함한 실행 Command, GC Dump(Option), Thread Dump(Option)를 메일로 보내주시면 시간되는 대로 집히는 바가 있는지 볼께요~ 참 그리고 말씀하신 Java Object를 메모리에 담을 때 어떤 자료구조를 사용하나요? java.util에 있는 Collection인지 아니면 자체 제작한 자료구조인지...

  4. 욱짜 2007.09.08 13:52 신고 Modify/Delete Reply

    메일 안주셔서 답글로 남깁니다.(주말이라서 그런가보네요 ^^) 다음 몇가지를 체크해보세요.
    1. Max Heap Size는 충분한가? (단, OS 차원에서 Paging in/out이 생기지 않을 정도로 크게)
    2. New Generation이 지나치게 크지 않은가? Object를 상주시키기 위한 Tenured Space를 크게 하기 위해 -XX:NewRatio=12 정도로 크게 주세요.
    3. 필요하면 Concurrent GC 사용. -XX:+UseConMarkSweepGC 옵션 사용해보세요.

  5. 맨땅헤딩 2007.09.11 23:13 신고 Modify/Delete Reply

    아..제가 독일 컨퍼런스 갔다온다음에 정신이 없어서 이제야 보내요. JDK는 1.4이고 windows xp에서 돕니다. gc dume, thread dump는 제가 잘 모르는거라... 하는일은 each image에 대해서 모든 픽셀 정보를 읽어서 [w][h][3] w:가로픽셀, h:세로픽셀, 3: Red, Green, Blue의 값을 넣는 것인데, 매 루프(이미지)할때 마다 initialize하고 다시쓰거든요. 필요에 따라 데이터를 DB에 저장하구요. 자료구조는 Graph structure를 사용하고 있고, 제가 직접 define한 object을 사용합니다. 그안에 잡다한 feature가 들어가죠. trajectory, color, position등등...주로는 vector를 씁니다.
    한번 말씀하신 옵션을 사용해 보도록 하겠습니다. 감사합니다.

Write a comment


[ Enterprise Java는 거대한 동기화 머신이다 - GC ] Enterprise Java & Oracle 성능 분석의 기초 - Part7

Enterprise Java 2007.09.04 15:32
[Enterprise Java는 거대한 동기화 머신이다]

IBM JVM

- Heap Management & Garbage Collection
IBM JVM의 Heap 메커니즘은 전통적으로 Sun HotSpot JVM과 매우 다른 방식을 사용해왔다. 간단하게 말하면, IBM JVM은 Generation 기법을 사용하지 않았다. 따라서 Young(New)과 Old(Tenured) Generation의 구분이 존재하지 않았다.

"않았다"라는 과거 시제를 사용한 것에 유의하자. IBM Java 5(JDK 1.5)부터는 Generation 알고리즘에 기반한 GC 기법을 제공한다. 이는 Sun HotSpot JVM 계열에서 보편적으로 검증된 방법론을 흡수한 것으로 이해할 수 있다.

Sun HotSpot JVM의 GC 기법을 다룬 블로그(http://blog.naver.com/ukja/120041944714)에서 성능을 보는 두가지 대표적인 관점인 [응답시간(Response Time)]과 [처리량(Throughput)]에 대해 언급한 바 있다. IBM JVM의 GC도 동일한 관점에서 분류된다. JDK 1.4 까지는 다음 세 개의 Garbage Collector가 제공되었다.

  • Throughput 최적화 Collector: -Xgcpolicy:optthruput 옵션으로 지정 (기본값).
  • Response Time 최적화 Collector: -Xgcpolicy:optavgpause 옵션으로 지정
  • SubPool Collector: -Xgcpolicy:subpool 옵션으로 지정. Heap을 여러 개의 Sub pool로 나누어 관리함으로써 성능 향상을 꾀한다. 16개 이상의 멀티 CPU 환경에서만 사용할 것을 권장한다.

JDK 1.5에서는 다음 Garbage Collector가 추가되었다.

  • Generational Concurrent Collector: -Xgcpolicy:gencon 옵션으로 지정. Sun JVM의 Generation 기법과 매우 유사하다.

- Mark and Sweep + Compaction
IBM JVM이 제공하는 기본 Collector는 Young/Old Generation의 구분을 사용하지 않기 때문에 Minor GC(copy), Major GC(mark and sweep)와 같은 구분 또한 존재하지 않는다. 대신 메모리를 정리하는 일련의 과정을 Mark and Sweep + Compaction으로 구분한다. Mark and Sweep 단계는 Sun JVM에서도 이미 언급된 바 있다. 이른바 Stop the World 작업으로 Application Thread를 멈춘 상태에서 Alive Object를 Mark하고(Dead Object를 찾고), 지우는 작업을 한다. IBM JVM은 Mark and Sweep 단계를 매우 "가벼운" 작업으로 간주한다. 비록 이 단계가 Sun JVM의 Major GC와 비슷한 속성을 지니고 있지만 Major GC에 비해서는 단순한 작업으로 구현되어 있다.

Sun JVM의 Major GC와 가장 비슷한 작업은 Compaction 단계에서 발생한다. Mark and Sweep으로 메모리 정리를 한 후에도 오브젝트 할당에 필요한 여유 메모리를 찾지 못하면 Compaction, 즉 압축이 발생한다. Mark and Sweep 작업이 단순히 Dead Object를 정리하는 작업인데 반해 Compaction 작업은 흩어진 프리 메모리를 합치는 작업을 하기 때문에 Mark and Sweep 단계에 비해 많은 시간을 필요로 한다. 다행히 일반적으로 Compaction 작업은 드물게 발생한다.

Mark and Sweep 단계를 거쳤음에도 불구하고 오브젝트 할당에 필요한 메모리를 찾지 못하는 이유는 단편화(Fragmentation)에 있다. IBM JVM의 Mark and Sweep은 Compaction 작업을 하지 않기 때문에 프리 메모리가 여기 저기에 흩어지는 현상이 생기게 된다. 이런 현상을 단편화라고 부른다. 가령 1M의 프리 메모리가 연속되지 않은 10K 청크 100개로 흩어져 있다고 가정해보자. 이 경우 비록 총 1M의 프리 메모리가 존재하지만 20K의 연속된 메모리는 할당될 수없다. 이런 경우에는 Compaction 작업이 추가로 발생하게 된다.

Sun HotSpot JVM에서는 Major GC를 최적화하는 것이 Heap 튜닝의 대표적인 기법이듯이, IBM JVM에서는 Compaction을 줄이는 것이 Heap 튜닝의 대표적인 기법이다.

---(참조)-----------------------------------------------------------------------------------
IBM이 제시하는 Heap 튜닝 기법중 하나가 길이가 긴 Array를 사용하지 말라는 것이다. 이 기법의 근거가 바로 Heap의 단편화에 있다. Application이 일정 시간 동작하고 나면 필연적으로 단편화가 발생한다. 이럴 때 길이가 긴 Array를 사용하면 연속된 메모리 공간을 할당받지 못해 Compaction 작업이 발생하게 된다. 따라서 효과적인 자료 구조를 사용해서 작은 크기의 Array를 여러 개 사용할 것을 권장하고 있다.
---------------------------------------------------------------------------------------------

- Mark and Sweep과 Garbage Collector
Throughput 최적화 Collector는 말 그대로 Mark and Sweep+Compaction 기법을 사용한다. 주기적으로 Mark and Sweep이 발생하고 필요한 경우 Compaction을 통해 메모리 병합을 시도한다.

이러한 일련의 GC 작업은 Application Thread를 완전히 멈춘 상태에서 진행되기 때문에 GC 작업 자체는 가장 최적화된다. 그 만큼 처리량은 높아지지만, Pause Time이 길어지면서 Response Time이 길어지는 단점이 생긴다.

Response Time 최적화 Collector는 Mark and Sweep+Compaction 기법에 약간의 변화를 가한다. Mark and Sweep 단계를 되도록이면 Application Thread를 멈추지 않는 상태에서 Concurrent 하게 진행한다. 즉 Concurrent Mark 단계와 Concurrent Sweep 단계를 추가로 두어서 Mark and Sweep에 의한 Pause Time을 최소화한다. Concurrent Mark와 Concurrent Sweep 단계는 Application Thread와 같이 동작하며, 그 만큼 Applicaiton Thread의 CPU 자원을 소모한다. 따라서 Throughput 최적화 Collector에 비해 Throughput이 다소 떨어질 수 있다.

---(참조)----------------------------------------------------------------------------------
IBM JVM의 Response Time 최적화 Collector의 GC 수행 기법이 Sun JVM의 Low Pause Collector(CMS Collector)와 매우 유사한 것은 우연이 아니다. 두 Vendor가 최신 Garbage Collection 기법을 서로 차용하고 개발하는 과정에서 자연스럽게 발생하는 공명 현상이다
--------------------------------------------------------------------------------------------

- Global GC와 Scavenger GC
IBM JVM에서는 Minor GC와 Major GC라는 분류법은 존재하지 않는다. 대신 Global GC와 Scavenger GC라는 분류법이 존재한다. Throughput 최적화 Collector(optthruput)와 Response Time 최적화 Collector(optavgpause)가 행하는 GC는 무조건 Global GC이다. 즉 Sun JVM의 관점에서 보면 항상 Major GC를 수행하고 있는 셈이다. 하지만 Compaction이 일어날 때만 진정한 의미에서 Major GC와 같다고 할 수 있다.

IBM JDK 1.5에서 추가된 Generational Concurrent Collector(gencon)에서는 Scavenger GC가 Minor GC의 역할을 하고 Global GC가 Major GC의 역할을 한다. Scavenger GC는 Mark and Sweep 방식이 아닌 Copy 방식(Alive Object를 Allocate Space에서 Survivor Space로 복사하는 것을 의미)을 사용하며 Global GC에서 Mark and Sweep+Compaction을 사용한다.

- IBM JVM의 장점
IBM JVM에 제공하는 기본 Collector(optthruput, optavgpause)의 장점은 안정된 GC 패턴이다. Sun JVM이 제공하는 Generation 기법이 훨씬 지능적이고 효과적인 것은 부인할 수 없는 사실이다. 특히 튜닝을 통해 Major GC를 최소화하고 Minor GC를 최적화했다면 GC에 의한 성능 저하 현상을 대부분 피할 수 있다. 하지만 Major GC에 의한 Spike 현상(갑자기 GC Pause Time이 급증하는 현상)은 항상 고질적인 문제로 남아 있다. Minor GC 시에는 안정된 패턴을 보이다가도 Major GC가 발생할 때 수초 ~ 수십초까지 GC Pause가 발생한다면 성능에 미치는 영향은 치명적일 수 있다.

반면 IBM JVM에서는 상대적으로 Compaction의 발생 빈도가 높지 않기 때문에 전반적으로 안정적인 패턴을 보인다. 이 말은 역설적으로 IBM JVM에서 Compaction이 최적화되고, Sun JVM에서 Major GC가 최적화되면 둘 사이에는 큰 성능 차이가 없을 것이라는 것을 암시하기도 한다.

(위의 말이 IBM JVM이 항상 안정적이라는 말은 아니며, Sun JVM에 비해 성능이 뛰어나다는 의미는 더더욱 아니다)

- IBM JVM의 단점
Sun JVM이 New/Old Generation의 크기, Survivor Ratio 등의 조정을 통해 세밀한 튜닝이 가능한 반면, IBM JVM에서 optthruput이나 optavgpause를 사용할 경우에는 튜닝에서의 세밀함이 부족한 편이다.

JDK 1.5에서 제공하는 gencon Collector를 쓸 경우에는 Sun JVM과 거의 비슷한 설정이 가능하다. 하지만 튜닝가능한 옵션 수는 비교적 작은 편이다.

(하지만 역설적으로 튜닝 옵션이 적다는 것은 더 자동화되어 있고 편리하다는 의미이기도 하다)


- IBM JVM의 GC 관련 중요 옵션들

-Xms, -Xmx: Heap의 최소(시작)크기, 최대 크기를 결정한다.

-Xgcpolicy: : optthruput|optavgpause|gencon|subpool. Garbage Collector의 종류(
따라서 Heap 관리의 종류)를 결정한다.

-Xdisableexplicitgc: System.gc()에 의한 GC를 비활성화한다.

-Xgcthreads: Parallel GC 작업을 할 Thread 개수를 지정한다. Default = CPU#-1. 만
일 여러 프로세스가 CPU를 나누어 쓰는 환경이라면 이 값을 낮출 필요가 있다.

-Xloa: LOA(Large Object Area)를 사용할 지의 여부를 결정한다. Default는 활성화상태이다.

-Xloainitial, -Xloamaximum, -Xloaminium: LOA의 초기 크기, 최대크기, 최소 크기를 지정한다. 0 ~ 1 사이의 값을 지정한다.

-Xmaxe, -Xmine: Heap expansion이 증가할 최대/최소 크기를 지정한다.

-Xmaxf, -Xminf: Heap 크기를 조정할 Free Memory의 비율을 결정한다. Default 값은 0.6(60%), 0.3(30%)이다. 즉, Free Memory가 전체 Memory의 60%이상이 되면 Heap Shrinkage가 발생하고, 전체 Memory의 30% 이하이면 Heap Expansion이 발생한다.

-Xmn, -Xmns, -Xmnx: Generational Concrreunt Collector를 사용할 경우 New(Nursery) Generation의 크기(최대/최소를 동일하게), 최소(시작)크기, 최대크기를 지정한다.

-Xmo, -Xmos, -Xmox: Generational Concurrent Collector를 사용할 경우 Old(Tenured) gEneration의 크기(최대/최소를 동일하게), 최소(시작)크기, 최대크기를 지정한다.

-Xpartialcompactgc: Incremental Compaction을 활성화한다. 만일 Compaction에 의한 성능 저하가 발생한다고 판단되면 이 옵션을 사용해본다.

-Xsoftrefthreshold: Soft Reference 객체를 몇 번째 GC Cycle에 해제할지의 여부를 결정한다. Default는 32이다. 즉, 32번째 GC Cycle까지 Soft Reference가 참조하고 있는 객체가 Mark되지 않았다면(즉 Live Object로 판단되지 않았다면) 메모리에서 해제된다. 이 값을 낮춤으로써 프리 메모리가 좀더 빨리 확보되도록 할 수 있다. 하지만 그 만큼 Soft Reference의 효율성은 떨어진다.

---(참조)--------------------------------------------------------------------------------
Reference 객체의 의미는 다음 문서에 잘 설명되어 있다. http://java.sun.com/developer/technicalArticles/ALT/RefObj/

Reference 객체는 Application 개발자가 Garbage Collector의 행동 양식에 영향을 줄 수 있는 유일한 방법이다. 대용량의 메모리를 사용하는 큰 Application 작성자라면 반드시 Reference 객체를 효과적으로 사용할 수 있어야 한다.
------------------------------------------------------------------------------------------
-verbosegc: GC 로그를 남긴다.

-Xverbosegclog: : GC 로그를 특정 파일명으로 남긴다.

간단한 성능 테스트
아래 결과는 세 가지 주요 Collector(optthruput, optavgpause, gencon)의 성능을 비교 테스트한 결과이다.

Case1: Heap Size = 512M, -Xgcpolicy:optthruput
23999893 allocated
Global GC = 22
Scavenger GC = 0
Total GC Time = 7.967951
Avg GC Time = 0.362180

Case2: Heap Size = 512M, -Xgcpolicy:optavgpause
21612553 allocated
Global GC = 23
Scavenger GC = 0
Total GC Time = 0.915822
Avg GC Time = 0.039818

Case3: Heap Size = 512M, -Xgcpolicy:gencon
14321989 allocated
Global GC = 4
Scavenger GC = 122
Total GC Time = 24.730480
Avg GC Time = 0.196274

위의 결과를 해석해보면
  • 처리량은 optthruput 옵션을 사용한 경우에 가장 뛰어나다. 이것은 예상된 결과이다.
  • GC에 의한 Pause Time은 optavgpause인 경우에 가장 낮다. 이것 역시 예상된 결과이다. IBM JVM의 GC가 매우 안정적이고 예상 가능한 방식으로 작동하는 것을 확인할 수 있다.
  • gencon을 사용한 경우 GC Pause Time은 optthruput에 비해 개선되지만 처리량이 지나치게 낮아지는 것을 알 수 있다. 그 이유는 첫째, gencon을 사용할 경우 약간의 옵션 튜닝이 필요할 수 있다는 것과 현재의 Application 패턴이 gencon에는 맞지 않다는 것이다.
J2EE 환경의 복잡한 Application에서는 gencon을 사용한 경우 더 우수한 성능을 나타낸다는 테스트 결과가 있다. 아래 테스트 결과는 복잡한 J2EE Application에서 optthruput과 gencon 옵션을 사용한 경우의 GC 패턴을 보여주고 있다.

파란색 = optthruput, 녹색 = gencon




위의 결과를 보면 처리량면에서도, Pause Time 면에서도 gencon을 사용한 경우가 더 우수한 성능을 보이는 것을 확인할 수 있다.(단 gencon을 사용한 경우 Global GC에 의한 Spike 현상이 나타난 것을 알 수 있다)


아래 결과는 optthruput을 사용하면서 Heap Size를 조정한 경우의 테스트 결과이다.

Case1: Heap Size = 256M, -Xgcpolicy:optthruput
13000597 allocated
Global GC = 32
Scavenger GC = 0
Total GC Time = 15.830381
Avg GC Time = 0.494699

Case2: Heap Size = 1024M, -Xgcpolicy:optthruput
24214091 allocated
Global GC = 23
Scavenger GC = 0
Total GC Time = 9.089497
Avg GC Time = 0.395196

위의 결과를 보면 Heap Size가 큰 경우가 그렇지 않은 경우에 비해 탁월한 성능 개선 효과가 있는 것을 확인할 수 있다. 또한 Sun JVM에서 볼 수 있었던 Heap Size의 크기와 New/Old Generation의 크기에 의한 미묘한 성능 차이 현상이 발생하지 않고 안정적인 패턴을 보이는 것을 알 수 있다. 이러한 안정적인 패턴이 IBM JVM의 가장 큰 장점이다.

---------------------------------------------------------------------------------------------------
다음 글에 계속...

(PS) 애초에 걱정한 것처럼 점점 용두사미로 전락하고 있다... ㅠㅠ







신고
Trackback 0 : Comments 2
  1. moncler 2013.01.04 15:01 Modify/Delete Reply

    관리자의 승인을 기다리고 있는 댓글입니다

  2. moncler españa 2013.01.05 16:32 Modify/Delete Reply

    관리자의 승인을 기다리고 있는 댓글입니다

Write a comment


Java 힙 사이즈 이슈 - Initial Size와 Max Size를 어떻게 설정할 것인가?

Enterprise Java 2007.09.03 15:59
Java Performance Tuning에 있어서 가장 낮은 과일, 즉 가장 손쉽게 설정하고 성능 효과를 거둘 수 있는 것이 Heap 크기와 관련된 설정이다.

그 중에 Initial Size와 Max Size 관련된 설정이 흔히 성능 개선책으로 많이 언급된다.

Sun HotSpt JVM에서의 일반적인 성능 개선 방안은 이렇다.
  • Initial Size와 Max Size를 동일하게 부여해서 동적인 크기 조정에 따른 부하를 없애라.
이 가이드는 너무나 보편적이어서 누구나 의심없이 항상 고려해야할 성능 개선안으로 받아들인다. 하지만 진짜 그럴까?

아마 이 가정에 대해서 의심을 해보거나 실제로 테스트를 해 본 사람은 거의 없을 것으로 생각된다.

하지만, 큰 Initial Size는 오히려 어플리케이션의 초기 성능을 떨어뜨리는 원인이 될 수 있다. Initial size가 크면 초기 Minor GC와 Major GC에 불필요하게 많은 시간이 소모된다.

반면 Initial Size가 적절히 작으면 초기 Minor GC와 Major GC가 아주 짧게 끝나고 필요할 때 메모리가 확장(Expansion)되므로 오히려 부하가 적다.

아래 테스트 결과를 보자
Case1: java -Xms32M -Xmx512M
Full GC: Pause Time = 5.364361, Count = 16, Average=0.335273
Minor GC: Pause Time = 9.769008, Count = 303, Average=0.032241

Case2: java -Xms512M -Xmx512M
Full GC: Pause Time = 4.609964, Count = 13, Average=0.354613
Minor GC: Pause Time = 10.253730, Count = 259, Average=0.039590

일반적인 성능 개선 추전안과 거꾸로 Initial Size를 작게 한 경우에 오히려 GC에 의한 부담이 작은 것을 확인할 수 있다. 그 이유는 아래와 같이 초기 GC 시간이 작기 때문이다.

Case1:
0.000: [GC 0.000: [DefNew: 2944K->320K(3264K), 0.0061382 secs] 2944K->1298K(32448K), 0.0062603 secs]
0.014: [GC 0.014: [DefNew: 3259K->320K(3264K), 0.0050409 secs] 4237K->2280K(32448K), 0.0051333 secs]
0.023: [GC 0.023: [DefNew: 3264K->320K(3264K), 0.0077398 secs] 5224K->3236K(32448K), 0.0078449 secs]
...
19.840: [GC 19.840: [DefNew: 26176K->2879K(26176K), 0.0233516 secs] 125359K->105130K(258248K), 0.0234460 secs]
19.882: [GC 19.882: [DefNew: 26175K->2879K(26176K), 0.0380722 secs] 128426K->112633K(258248K), 0.0381711 secs]
19.939: [GC 19.939: [DefNew: 26175K->2879K(26176K), 0.0392069 secs] 135929K->120141K(258248K), 0.0392966 secs]

Case2:
0.000: [GC 0.000: [DefNew: 23360K->2880K(26240K), 0.0337995 secs] 23360K->7461K(259264K), 0.0338926 secs]
0.052: [GC 0.052: [DefNew: 26240K->2879K(26240K), 0.0412854 secs] 30821K->14526K(259264K), 0.0413726 secs]
0.113: [GC 0.113: [DefNew: 26239K->2880K(26240K), 0.0406367 secs] 37886K->21559K(259264K), 0.0407367 secs]
...
19.741: [GC 19.741: [DefNew: 23360K->2879K(26240K), 0.0307106 secs] 98231K->82374K(259264K), 0.0307986 secs]
19.789: [GC 19.789: [DefNew: 26239K->2879K(26240K), 0.0393455 secs] 105734K->89908K(259264K), 0.0394374 secs]
19.848: [GC 19.848: [DefNew: 26239K->2879K(26240K), 0.0403141 secs] 113268K->97442K(259264K), 0.0404032 secs]

물론 위의 테스트 시나리오가 너무나 심플해서 복잡한 어플리케이션에서 상황을 재현하지 못한다. 또한 수행시간이 길어지면 초기 구동 성능이 차지하는 비중이 작아지므로 역시 의미가 없다(하지만 이 경우 Initial Size를 크게 주는 것 또한 전혀 의미가 없기는 마찬가지다. 어차피 Heap은 Max Size까지 커질 테니까...)

하지만 엄밀한 테스트와 해석이 없는 교조적인 튜닝 가이드는 무의미하다는 것을 잘 보여준다.

재미있게도 IBM에서 제공하는 JVM 튜닝 가이드에서는 Initial Size와 Max Size를 같이 하지 말고 Initial Size를 작게 지정하라고 권고하고 있다. IBM JVM은 전통적으로 Sun HotSpot JVM과는 전혀 다른 Heap 관리 기법을 사용하기 때문에 이런 가이드가 나온 것으로 이해할 수 있다.(이유는 나중에 기회가 되면 상세하게...)





신고
tags : Heap, java, performance
Trackback 0 : Comment 0

Write a comment


전혀 새로운 책을 꿈꾸며...

Enterprise Java 2007.08.31 19:42
최근 나의 고민 중 하나는 이것이다.

  • Enterprise System을 구성하는 2개의 거대한 축이 있다. Java(WAS)와 Oracle!!
  • 이 두 개의 환경을 관통하고 꿰뚫어 보는 성능 분석 방법론과 튜닝 방법론을 체계화할 순 없을까? 아니, 적어도 두 개의 큰 축을 같은 시각에서 비교 분석할 수는 없을까?
불행하게도 Java 전문가는 Oracle을 모르고, Oracle 전문가는 Java를 모른다.

모두들 외산 언어와 툴을 열심히 익히고 배우기에만 바쁘고, 외산 툴의 기술지원 엔지니어가 최고의 전문가로 인정받는 이 왜곡된 현실에서 누가 선뜻 이런 일을 하겠는가...

다년간 Java와 Oracle 성능 문제를 고민해온 사람들 중 하나로 나 스스로가 이 질문에 답을 던지고자 하는 욕구를 느낀다.

아래 내용 정도면 어떨까?

Enterprise System Performance Tuning: Java 5 and Oracle
- 지금까지 그 누구도 시도하지 못한 책
- "동기화"의 관점에서 Java와 Oracle의 성능 분석 기법을 비교 연구한 책
- Java는 거대한 동기화 머신이다 + Oracle은 거대한 동기화 머신이다

내용:
1. Java Thread vs. Oracle Session

2. Java Heap vs. Oracle Buffer Cache/Shared Pool/Library Cache

3. JDBC vs. Oracle SQL

흩어져서 여기 저기 내 머리 속에 있는 지식과 경험을 어떻게 통합적인 시각에서 풀어낼 것인가!!!

큰 중압감과 함께 굉장히 짜릿한 스릴을 느낀다.

신고
Trackback 0 : Comment 1
  1. jt 2012.08.06 10:27 Modify/Delete Reply

    관리자의 승인을 기다리고 있는 댓글입니다

Write a comment


[ Enterprise Java는 거대한 동기화 머신이다 - GC ] Enterprise Java & Oracle 성능 분석의 기초 - Part6

Enterprise Java 2007.08.30 17:42
Enterprise Java는 거대화 동기화 머신이다.

Sun HotSpot JVM

Heap Management & Garbage Collection
(아래 사실들은 이미 널리 알려진 것들이지만 정리하는 의미에서 간략하게 기술한다)
Sun HotSpot JVM(이후 Sun JVM)은 전통적으로 Generation에 기반한 Heap 관리 방법을 사용한다. Generation, 즉 세대에 기반한다는 것은 오브젝트의 수명에 따라 메모리를 관리한다는 의미다.

Garbage Collection 알고리즘의 개발자들은 통계학적으로 매우 중요한 사실 하나를 발견했다. 그것은 대부분의 오브젝트들이 매우 빨리 죽는다(Short-lived)는 것이다. 즉, 수명이 짧고 덩치가 작은 많은 수의 오브젝트와 수명이 길고 덩치가 큰 적은 수의 오브젝트들이 메모리를 사용한다.

여기서 얻은 통찰력을 기반으로 Generation에 기반한 GC 알고리즘이 등장하게 되었다. 이 알고리즘의 핵심은 어차피 빨리 죽을 오브젝트들은 빨리 메모리에서 밀려나도록, 오래 남을 오브젝트들은 되도록이면 밀려나지 않도록 별도의 공간을 마련해준다는 것이다. Generation 기반의 GC에서는 다음과 같은 방법으로 메모리를 관리한다.
  • 메모리 영역을 New Generation(혹은 Young Generation, Nursery)과 Old Generation(Tenured Generation)으로 나눈다. 즉 어린 오브젝트들이 사는 공간과 나이가 많은 오브젝트들이 사는 공간으로 분리한다.
  • New Generation은 다시 Eden(혹은 Allocate)와 Survivor Space로 나뉜다. Eden은 말 그대로 최초로 태어난(Allocate된) 오브젝트들이 거주하는 공간이다. Eden이 꽉 차면, 죽은 오브젝트들을 정리하고 살아남은 오브젝트들은 Survivor Space(생존자들을 위한 공간)로 옮겨진다. 이 과정에서 흔히 Minor GC으로 불리는 작업이 발생한다.(Survivor Space는 메모리 작업의 효율성을 위해 To Space/From Space라는 두 개의 공간으로 관리된다)
  • Survivor Space가 꽉 차면, 다시 살아남은 오브젝트들은 Tenured Space(종신권을 가진 자들을 위한 공간)으로 옮겨진다.
  • 만일 Tenured Space가 꽉 차면 비상사태가 발생한다. 인구폭발로 인해 더 이상 거주 공간이 없는 상태가 되기 때문이다. 이렇게 되면 JVM은 모든 Thread를 블로킹시키고 Full GC(혹은 Major GC)를 수행한다. 모든 오브젝트들에 대해 생존 여부를 조사해서 죽은 오브젝트들을 실제로 메모리에서 밀어낸다.
  • 이 과정에서 필요하면 메모리 공간을 더 키운다. 확장 가능한 최대 메모리 공간은 -Xmx 파라미터에 의해 지정된다.
  • 이 모든 과정을 수행하고도 메모리를, 즉 오브젝트들의 거주 공간을 확보하지 못하면 OutOfMemory Exception이 발생한다.
(참조)
Full GC를 수행할 때 죽은 오브젝트들을 메모리에서 밀어내는 작업을 흔히 Mark and Sweep이라고 부른다. 즉,살아있는 오브젝트들 "표시(Mark)"하고, Mark되지 않은 오브젝트들을 "쓸어내는(Sweep)" 작업이다.

아래 두 그림은 Sun JVM의 메모리 공간을 표현하고 있다. 그림과 위의 설명을 잘 조합하면 쉽게 이해할 수 있으리라 믿는다.





Sun JVM의 메모리 공간이 매우 다양한 요소들로 이루어져 있기 때문에, 이러한 개별 요소들이 다 세밀한 튜닝의 대상이 된다.

물론 다행인 것은 디폴트 값이나 알고리즘이 어느 정도의 성능을 보장하기 위해 많은 경우 메모리 크기 정도만을 지정하는 것으로 만족할 만한 성능을 얻을 수 있다는 것이다.

Sun JVM이 지원하는 Garbage Collector들
앞서 간략하게 본 것처럼(그리고 널리 알려진 바대로), Sun JVM은 Minor GC(New Generation 정리)와 Full GC(Old Generation 정리)라는 두 종류의 GC를 통해 메모리를 관리한다.

Minor GC 작업과 Full GC 작업이 모두 성능에 지대한 영향을 미치기 때문에, 이 두 개의 GC 작업을 얼마나 효율적으로 할 것인가가 지속적인 관심과 개선의 대상이 되었다. 이러한 작업의 결과로 Sun JVM은 공식적으로 다음과 같은 세 가지 종류의 GC를 제공하게 되었다.
  • Default Serial Garbage Collector: 전통적으로 사용되던 Garbage Collector
  • Throughput Garbage Collector: Throughput 개선에 집중하는 Garbage Collector. Throughput 개선을 위해 New Generation 정리시에 병렬 작업을 수행한다.
  • Low Pause Garbage Collector: Response Time 개선에 집중하는 Garbage Collector. Response Time 개선을 위해 Old Generation 정리 작업을 Applicaiton Thread를 (최대한) 블로킹하지 않는 방식으로 진행한다.


Default Serial Garbage Collector
-XX:+UseSerialGC 옵션에 의해 활성화된다. JDK 1.4에서는 이 Collector가 기본 설정이다. JDK 1.5에서는 약간의 편법(?)이 사용되는데, 만일 JVM이 실행되는 환경이 Server 급이라면 Throughput Garbage Collector가 기본 설정이 된다.

(참조)
Server 급 환경이란? Sun이 설정한 Server 급 환경은 2장 이상의 CPU, 2G 이상의 메모리를 갖춘 환경을 말한다. 이러다 보니 내 노트북이 Server로 분류되는 황당한 상황이... --; 더구나 JDK 1.5에서는 이 기준으로 Server 환경을 정의하고 Server 환경이 되면 -server -XX:UseParalleGC -Xms1024M 같은 옵션을 기본으로 적용한다. 이걸 두고 "인공공학적" 설계라고 부르는데...
사실 좀 장난같다는 느낌이다.

비록 Server급 환경이라도 가용할 수 있는 자원이 많지 않다면 Serial Garbage Collector를 사용하는 것이 일반적으로 바람직하다는 사실에 특히 주의해야 한다. 기계적으로 원칙을 적용해서는 안된다.

Throughput Garbage Collector(혹은 Parallel GC)
-XX:+UseParallelGC
옵션에 의해 지정된다. Throughput Garbage Collector는 New Generation에 의한 Minor GC 작업을 여러 개의 Thread(CPU의 개수만큼)를 이용해 Parallel하게 진행한다.

아래 내용은 Sun JVM에서 -XX:+UseParallelGC 옵션을 지정한 상태에서 생성한 Thread Dump 결과의 일부로, CPU 개수와 동일한 2개의 Garbage Collector Thread가 활성화된 것을 확인할 수 있다.

"GC task thread#0 (ParallelGC)" prio=6 tid=0x00aa67e8 nid=0x8c8 runnable
"GC task thread#1 (ParallelGC)" prio=6 tid=0x00aa7230 nid=0x13a4 runnable

Server 급 머신에서는 이 Collector가 기본 설정이라는 것은 의미하는 바가 크다. 특히 CPU 자원이 풍부한 환경에서는 큰 효과를 얻을 수 있다.

이 Collector를 선택하는데 있어 주의할 점이 하나 있다. 비록 여러 장의 CPU를 갖춘 Server 급 머신이라고 하더라도 다른 프로세스들과 공유해야 환경이라면 이 Collector를 사용하는 것을 바람직하지 않다. 기본적으로 CPU를 충분히 활용하지 못하는 환경이라면 잦은 Context Switching으로 인해 오히려 성능이 저하되기 때문이다.

-XX:ParallelGCThreads 옵션을 이용하면 Parallel Collector Threads의 개수를 조절할 수 있다. 만일 모든 CPU를 충분히 사용하지 못하는 환경이라면 GC Thread의 개수를 줄임으로써 효과적으로 Parallel GC 작업을 할 수 있다.

Low Pause Garbage Collector(혹은 CMS GC)
-XX:+UseConcMarkSweepGC 옵션에 의해 활성화된다. Throughput Garbage Collector와는 달리 Response Time를 최적화하게끔 동작한다. 정확하게 말하면 Response Time을 최소화하기 위해 Full GC 과정을 되도록이면 Thread 블로킹없이 진행한다. Thread의 정지 시간(Pause Time)이 최소화되기 때문에 Low Pause Garbage Collector라는 이름이 붙었다.

Low Pause Garbage Collector는 Full GC 과정에서 오는 Thread Blocking을 최소화하기 위해 다단계의 전략을 사용한다. 아래 내용은 Low Pause Garbage Collector를 사용하는 환경에서 GC Dump를 수행한 내용 중 일부를 발췌한 것으로 Collector의 작업 순서를 알 수 있다.

[GC [1 CMS-initial-mark: 128837K(253952K)] 128919K(262080K), 0.0001592 secs]
[CMS-concurrent-mark-start]
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs]
[1 CMS-remark: 138222K(253952K)] 145527K(262080K), 0.0232208 secs]
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.086/0.199 secs]
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.006/0.006 secs]

위의 과정은 다음과 같이 요약 설명할 수 있다.
  • initial mark(Pause 발생)-->
  • concurrent mark(Pause 없음. 하지만 Collector와 Application Thread가 CPU를 경쟁)-->
  • remark(Pause 발생)-->
  • concurrent sweep(Pause 없음. 하지만 Collector와 Application Thread가 CPU를 경쟁)
즉, Pause를 최소화하면서(inital mark/remark 과정에서만 Pause 발생) 최대한 Collector Thread와 Application Thread가 동시에 작업을 진행하도록 하는 것이 이 Collector 알고리즘의 핵심이다. Full GC를 Concurrent하게 진행하기 위해 initial mark 작업은 Old Generation이 꽉 차기 전에 시작된다. 디폴트로 Old Generation이 68% 이상 점유되면 Full GC 작업이 시작된다(이 값은 -XX:CMSInitiatingOccupancyFraction 값을로 변경 가능하다. 또한 스스로 메모리 사용량을 모니터링하면서 이 값은 동적으로 변경되기도 한다). 반면 Serial Collector와 Throughput Collector는 Old Generation이 꽉 차야만 Full GC를 시작한다.

Pause Time을 최소화함으로써 Response Time은 최적화되지만, Collector Thread와 Application Thread가 CPU 경쟁을 벌임으로써 전반적으로 Throughput, 즉 일량은 감소하는 부작용이 있다는 사실을 기억해야 한다.

한 가지 재밌는 것은 Low Pause Garbage Collector가 비록 Full GC, 즉 Old Generation에 대한 정리 작업을 최적화하는 기법이지만 New Generation에 대한 Minor GC 작업시에도 여러 개의 Thread를 사용함으로써 성능 개선을 꾀한다는 것이다. 이것은 JDk 1.4에서 실험적으로 고안되었고 JDK 1.5에서는 CPU가 여러 장이라면 기본적으로 Parallel GC를 사용한다. 이것은 -XX:+UseParNewGC 옵션으로 결정된다.
아래 내용은 Low Pause Garbage Collector를 사용하는 환경에서 생성한 Thread Dump의 일부로 New Generation 정리를 위해 여러 개의 Thread가 활성화되어 있는 것을 확인할 수 있다.

"Gang worker#0 (Parallel GC Threads)" prio=10 tid=0x0003bb68 nid=0x1338 runnable
"Gang worker#1 (Parallel GC Threads)" prio=10 tid=0x0003bda8 nid=0x594 runnable

Serial Collector, Throughput Collector(Parallel Collector)와 Low Pause Collector(CMS Collector)는 서로 호환되지 않는다. 가장 이상적인 것은 런타임에 Collector를 선택할 수 있는 것이겠지만 불가능하다. 따라서 시스템의 성능(Client/Server)과 요구 사항(Throughput/Response)에 따라 최적의 Collector를 선택할 수 있도록 해야 한다.

CMS Collector를 사용할 때 한가지 유의할 점은 Heap의 크기이다. CMS Collector는 다른 Collector와는 달리 Full GC를 소극적으로 수행한다. 또한 메모리 Compaction을 적극적으로 하지 않는다. 이런 이유 때문에 메모리 요구량이 많은 경우에는 문제가 생길 수 있다. 따라서 CMS Collector를 사용할 경우에는 Heap 크기를 좀 더 크게 지정해야 한다. 보통 20% 이상의 크기를 추가로 부여할 것을 권고한다.

---(참조)----------------------------------------------------------------------------------------------------------
CMS Collector를 사용할 경우 Permanent Space에 대한 GC 작업을 수행하지 않는다. 따라서 많은 수의 Class를 다루는 Application에서는 Permanent Space에서 OutOfMemory 에러가 날 수 있다. 이 경우에는 다음 두 개의 옵션을 활성해주어야 한다.

-XX:+CMSPermGenSweepingEnabled
-XX:+CMSClassUnloadingEnabled
----------------------------------------------------------------------------------------------------

CMS Collector를 사용할 때 또 한가지 주의할 점은 Mark and Sweep GC가 일어나지 않도록 하는 것이다. CMS Collector는 Old 영역의 사용률을 보고 적당한 시점에 Concurrent한 GC 작업을 시작한다. 만일 이 작업의 시작 시간이 메모리 요구량에 비해서 느리면 Application Thread가 메모리를 필요할 때 할당받지 못하게 되어서 Mark and Sweep GC를 수행하게 된다. 이렇게 되면 Pause Time을 줄이고자 하는 목적의 달성이 어려워진다. Concurrent GC 작업의 시작은 다음 옵션으로 제어된다.

-XX:CMSInitiatingOccupancyFraction=<1~100의 값>

만일 이 값이 60이면 Old 영역의 60%가 사용되는 시점에 Concurrent GC 작업이 시작된다. 만일 이 값이 너무 크면 제 때 GC가 이루어지지 못해 Mark and Sweep GC가 작동할 확률이 높아진다. 반면 이 값이 너무 작으면 Concurrent GC 작업이 너무 일찍 시작해서 Applicaiton Thread의 CPU 점유를 방해한다. 따라서 처리량(Throughput)이 낮아질 수 있다.


중요한 VM 옵션들
Sun JVM의 Heap 및 GC 관리 정책을 결정하는 옵션들은 매우 많다. 너무 많아서 어떤 것을 사용해야 할 지 결정하기가 어려울 정도이다. 어떤 옵션들은 문서화가 거의 되어 있지 않아서 정확한 의미를 이해하기 어려운 것들도 있다.(마치 오라클의 Hidden Parameter 처럼) 아래의 몇 가지 옵션들이 경험적으로 그리고 문서로 정의되고 검증된 것들이다. Heap과 GC의 최적화를 달성하려면 이들 옵션들의 정확한 의미를 이해할 수 있어야 한다.

-Xms: Heap의 초기 크기를 지정한다.
-Xmx: Heap의 최대 크기를 지정한다. 서버용 프로그램에서는 -Xms와 -Xmx를 동일하게 부여함으로써 동적인 메모리 관리의 부담을 덜어주어야 한다.
-Xss: Thread의 최대 Native Stack 크기를 지정한다. 만일 Thread의 개수가 지나치게 많다면 이 크기를 줄여줄 필요가 있다.
-Xoss: Thread의 최대 Java Stack 크기를 지정한다. HotSpot JVM에서는 Thread에 대해 Native Stack만을 사용하기 때문에 이 옵션은 무의미한다.
-XX:NewRatio: New Generation과 Old Generation의 크기 비율이다. 만일 NewRatio 값이 2 이면 New Generation:Old Generation = 1:2 가 된다. 이 값의 경험적 기준치는 8~2 사이의 값을 사용하는 것이다.
-XX:NewSize: New Generation의 크기를 직접 지정한다.
-XX:MaxNewSize: New Generation의 최대 크기를 지정한다. Sun JVM은 New Generation의 크기를 상황에 따라 동적으로 변경(Adaptive)한다. NewSize와 MaxNewSize를 동일하게 하면 크기를 고정할 수 있다. 또는 -XX:-UseAdaptiveSizePolicy 옵션을 지정해도 된다.
-XX:SurvivorRatio: Survivor Space와 Eden Space의 크기 비율을 지정한다. 가령 이 값이 6 이면 (To + From) : Eden = 1:1:6 이 된다. 즉 From Space의 크기가 Eden Space의 1/8이 된다. 경험적으로 추천되는 값은 -XX:NewRatio=2, -XX:SurvivorRatio=6 정도의 값을 사용하는 것이다.
-XX:+UseAdaptiveSizePolicy: Adaptive하게 New Generation의 크기가 Survivor Space의 크기를 변경할 것인지의 여부를 지정한다.
-XX:PermSize: Permanent Space의 최초 크기를 지정한다.
-XX:MaxPermSize: Permanent Space의 최대 크기를 지정한다. 매우 많은 수의 클래스를 로딩하는 경우에는 이 값을 늘려주어야 한다.
-XX:+AggressiveHeap: Heap 사용률을 최대한까지 확장한다. 이 옵션을 활성화하면 Heap은 3850M까지, Thread의 Allocaiton Buffer를 256K까지 확대가능하다. 이 옵션을 지정하면 GC 작업도 Parallel하게 진행된다. 만일 서버급 머신을 하나의 JVM이 사용하는 환경이라면 이 옵션만으로도 적절한 성능을 보장할 수 있다. 개인적으로 이 옵션보다는 개별 옵션을 일일이 지정하는 것이 더 바람직하다고 본다.

-XX:+UseSerialGC: Serial Collector를 활성화한다.
-XX:+UseParallelGC: Throughtput Collector(Parallel Collector)를 활성화한다.
-XX:+UseConcMarkSweepGC: Low Pause Collector(CMS Collector)를 활성화한다.
-XX:+UseParNewGC: CMS를 사용할 경우 Minor GC를 Parallel하게 할 지의 여부를 지정한다. CPU가 복수개일 경우는 기본적으로 이 모드를 사용한다.
-XX:+DisableExplicitGC: System.gc() 호출을 통한 강제 GC 작업을 Disable한다.

-verbose:gc : GC 관련 정보를 콘솔에 출력한다.
-xloggc:: GC 관련 정보를 특정 파일에 출력한다.
-XX:+PrintGCDetails: 상세한 GC 정보를 출력한다.
-XX:+PrintGCTimeStamps: GC 발생 시간 정보를 기록한다.
-XX:+PrintGCApplicationStoppedTime: GC로 인해 Application이 정지된 시간 정보를 기록한다.

위의 옵션 들 중 가장 성능에 많은 영향을 주는 것은 Initail Heap Size, Max Heap Size, NewRatio, SurvivorRatio 그리고 GC의 종류 등이다. 해당 이 옵션 들의 정확한 의미를 꼭 이해하기 바란다. 몇가지 생각해 볼 점을 정리해보자...

New Generation의 크기가 미치는 영향은 어떨까?
New Generation의 크기가 크면 그만큼 Minor GC가 늦게 발생한다. 하지만 GC 시간은 길어진다. 지나치게 큰 New Generation의 문제는 Full GC를 유발할 수 있다는 것이다. 큰 크기의 Eden에 있던 오브젝트들이 한꺼번에 Tenured Space로 옮겨지는 상황에서 Full GC가 유발될 수 있다. New Generation의 크기가 지나치게 작으면 Minor GC가 지나치게 자주 발생하게 된다. 서버급 환경에서의 경험적 수치는 New:Old Generation의 크기를 1:2 ~ 1:8 정도에서 결정하는 것이다.

Survivor Ratio의 크기는 미치는 영향은 어떨까?
Eden에서 살아남은 오브젝트들은 Minor GC 시에 Survivor Space로 옮겨지고, Survivor Space가 꽉 차면 Tenured Space로 옮겨간다. 따라서 Survivor Ratio가 너무 작으면 Full GC가 자주 발생할 확률이 높아진다. 반면 이 값이 너무 크면 Eden Space가 작아서 Minor GC가 지나치게 자주 발생하게 된다. 서버급 환경에서의 경험적 수치는 Survivor Ratio의 값을 6 정도로 지정하는 것이다. 이렇게 되면 To:From:Eden = 1:1:6이 된다.

Max Heap Size가 주는 영향은 어떨까?
Max Heap Size가 주는 성능에 주는 영향은 절대적이다. 상식적으로 클 수록 좋다. Sun JVM의 Generation 관리 기법은 Heap Size가 크다고 해서 큰 성능의 저하가 없게끔 작동한다. 서버급 환경에서의 경험적 수치는 1G ~2G 사이의 값을 사용하고, Initial Size와 Max Size를 동일하게 줌으로써 동적 관리의 부담을 덜 수 있다.
하지만, 항상 큰 Heap이 좋은 것은 아니다. 불필요하게 큰 Heap Size는 Full GC에 지나치게 많은 시간이 걸리는 문제를 유발할 수 있다. Generation 기반 알고리즘의 기본 가정은 수명이 짧고 덩치가 작은 오브젝트는 많고, 수명이 길고 덩치가 큰 오브젝트 수는 적다는 것에 기반한다. 만일 이 가정이 어긋나면 예상치 못한 성능 문제가 생길 수 있다. 이런 경우에는 Heap Size를 줄이거나 New Generation의 크기 비율을 줄이는 방법을 사용할 수 있다. 혹은 Low Pause Collector를 사용하는 것이 바람직하다.(개인적으로는 Low Pause Collector를 사용해볼 것을 권장한다)

Max Heap Size 선정에서 한가지 주의할 점은 OS 레벨에서 Paging In/Out이 생기지 않아야 한다는 점이다. JVM을 구동할 당시에는 메모리 여유가 충분해서 문제가 없다가 JVM이 운영되는 중에 물리적 메모리가 부족하게 되면 Heap의 일부가 Page Out이 될 수 있다. 이런 상황에서는 JVM의 성능이 극도로 저하될 수 있다.

GC Dump
Garbage Collector의 종류에 따라 GC Dump의 포맷이 조금씩 다르다. GC Dump를 정확하게 이해하려면 이 차이를 이해할 필요가 있다.

(아래 예에서는 "-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCApplicationStoppedTime" 옵션을 이용해 GC Dump를 수행한다.)

일반적으로 GC 발생시 메모리 변동은 다음과 같은 포맷으로 기록이 된다.

"Generation종류: Before Used Size -> After Used Size (Total Size), 시간"

즉, "DefNew: 18175K->1983K(18176K), 0.0266109 secs"는 다음과 같은 사실을 의미한다.
  • New Generation에 대해 Serial GC를 수행했으며
  • GC 전에는 18M(18175K)를 쓰고 있었고, GC 이후 1.9M(1983K)를 쏘고 있다.
  • New Generation의 전체 크기는 18M(18176K) 이다.
  • GC를 수행하는데는 0.026초 정도 걸렸다.

아래에 간단한 포맷의 예제가 있으니 참조하기 바란다.
1. Serial Collector인 경우
0.272 [GC 0.272: [DefNew: 18175K->1983K(18176K), 0.0266109 secs] 52943K->41607K(260160K), 0.0266978 secs]
...

2. Throughput Collector인 경우
3.303: [GC [PSYoungGen: 15168K->3712K(15744K)] 251928K->244204K(257728K), 0.0386683 secs]
3.341: [Full GC [PSYoungGen: 3712K->0K(15744K)] [PSOldGen: 240492K->95306K(241984K)] 244204K->95306K(257728K) [PSPermGen: 1836K->1836K(16384K)], 0.4015217 secs]

Serial Collector에서 DefNew로 표기되었던 것이 PSYoungGen으로 표기된다. Throughtput Collector의 개념을 이해했다면 그 차이를 정확하게 알 수 있을 것이다.

3. Low Pause Collector인 경우
9.936: [GC [1 CMS-initial-mark: 173808K(253952K)] 174918K(262080K), 0.0011353 secs]
Total time for which application threads were stopped: 0.0029149 seconds
9.937: [CMS-concurrent-mark-start]
9.956: [GC 9.956: [ParNew: 8064K->0K(8128K), 0.0216583 secs] 181872K->176390K(262080K), 0.0217720 secs]
...
10.036: [GC 10.036: [ParNew: 8064K->0K(8128K), 0.0200405 secs] 187044K->181580K(262080K), 0.0201724 secs]
10.060: [CMS-concurrent-mark: 0.057/0.123 secs]
10.060: [CMS-concurrent-preclean-start]
10.061: [CMS-concurrent-preclean: 0.000/0.000 secs]
...
10.189: [GC[YG occupancy: 8064 K (8128 K)]10.190: [Rescan (parallel) , 0.0111151 secs]
10.201: [weak refs processing, 0.0000221 secs] [1 CMS-remark: 191976K(253952K)] 200040K(262080K), 0.0113344 secs]
...
10.663: [CMS-concurrent-sweep: 0.256/0.461 secs]
10.663: [CMS-concurrent-reset-start]
10.670: [CMS-concurrent-reset: 0.007/0.007 secs]
...
Serial Collector에서 DefNew로 표기되었던 것이 ParNew 로 표기되는 것에 주의한다. -XX:+UseParNewGC 옵션이 작동한 결과이다. CMS 로 시작하는 부분은 initial mark->concurrent mark -> remakr -> concurrent sweep 으로 이어지는 Full GC의 활동 상황이 기록된 것이다.

Garbage Collector간의 성능 테스트 예제
아래 표는 Serial Collector/Throughput Collector/Low Pause Collector와 NewRatio의 값을 바꾸어 가면서 성능 비교를 수행한 것이다. JVM 구동시 사용한 다른 옵션들은 다음과 같다
  • -server
  • -Xms=256M
  • -Xmx=256M
  • -XX:+PrintGCDetails
  • -XX:+PrintGCTimeStamps
  • -XX:+PrintGCApplicationStoppedTime
수행한 환경은 다음과 같다.
  • Sun JRE 1.5.06
  • Phsyical Memory 2G
  • CPU = 2 (따라서 JVM 기준으로 서버급 머신)
  • 수행한 프로그램은 첨부한 gc_dump.java 참조
아래에 수행 결과가 있다.

CollectorNewRatio=12
NewRatio=6NewRatio=2
Serial
수행 = 22574172
Pause Time = 14.3
Full GC:
Pause Time = 4.57,
Count = 11,
Average=0.416
Minor GC:
Pause Time = 9.68,
Count = 328, Average=0.029

수행 = 25665194
Pause Time = 14.03
Full GC:
Pause Time = 4.29,
Count = 13,
Average = 0.33
Minor GC:
Pause Time = 9.74,
Count = 195, Average=0.05
수행 = 21854332
Pause Time = 16.1
Full GC:
Pause Time = 11.46,
Count = 29,
Average = 0.39
Minor GC:
Pause Time = 4.68,
Count = 48, Average=0.097
Throughput
(Parallel)
수행 = 17699474
Pause Time = 16.3
Full GC:
Pause Time = 3.46,
Count = 8,
Average=0.43

Minor GC:
Pause Time = 12.8,
Count = 385, Average=0.033
수행 = 19805887
Pause Time = 16.1
Full GC:
Pause Time = 3.67,
Count = 9, Average=0.407

Minor GC:
Pause Time = 12.4,
Count = 233, Average=0.053
수행 = 18852852
Pause Time = 16.5
Full GC:
Pause Time = 5.98,
Count = 13,
Average=0.46

Minor GC:
Pause Time = 10.5,
Count = 103, Average=0.102
Low Pausse
(CMS)
수행 = 17791777
Pause Time = 12.8
Full GC:
Pause Time = 0.00,
Count = 11, Average=0.00

Minor GC:
Pause Time = 12.8,
Count = 555, Average=0.023
수행 = 19268025
Pause Time = 12.3
Full GC:
Pause Time = 0.00,
Count = 13, Average=0.00

Minor GC:
Pause Time = 12.3,
Count = 605, Average=0.02
수행 = 22121742
Pause Time = 13.8
Full GC:
Pause Time = 0.00,
Count = 8,
Average=0.00

Minor GC:
Pause Time = 13.8,
Count = 676, Average=0.02

위의 결과를 보면 Serial Collector이면서 NewRatio가 12인 경우가 Througput 면에서는 가장 우수한 성능을 발휘하는 것을 확인할 수 있다.
반면 Low Pause Collector인 경우에는 NewRatio가 2인 경우에 일량 기준으로도 우수하면서 Pause Time도 가장 낮은 것을 확인할 수 있다.

Low Pause Collector(CMS Collector)는 비록 Throughtput는 낮아질 수 있지만 사용자가 체감하는 응답 시간을 개선시킬 수 있다면 측면에서 적극적으로 활용해볼 만 하다.

Throughput Collector(Parallel Collector)는 가장 좋지 않은 성능을 나타내는데 그 이유는 테스트 장비가 실제로는 서버급이 아니기 때문이다. 많은 수의 CPU를 갖춘 서버급 장비에서는 최소한 Serial Collector보다는 좋은 성능을 내야하는 것이 일반적이다.


Heap Size에 따른 성능 테스트 예제
아래 표는 Heap Size를 256M ~ 1024M로 키우면서 성능을 측정한 결과이다.

Heap Size = 256M
(NewRatio = 2)
Heap Size = 512M
(NewRatio = 2)
Heap Size = 1024M
(NewRatio = 2)
Serial Collector
수행 = 21645002
Pause Time = 16.02
Full GC:
Pause Time = 11.68,
Count = 30,
Average = 0.39
Minor GC:
Pause Time = 4.34,
Count = 45,
Average = 0.096
수행 = 32468630
Pause Time = 13.56
Full GC:
Pause Time = 4.83,
Count 10,
Average = 0.48
Minor GC:
Pause Time = 8.56,
Count = 46,
Average = 0.19
수행 = 42292436
Pause Time = 11.38
Full GC:
Pause Time = 2.0,
Count = 3,
Average = 0.66
Minor GC:
Pause Time = 9.39
Count = 33,
Average = 0.28

위의 패턴을 보면 다음과 같은 사실을 확인할 수 있다.
  • Heap Size가 클 수록 일 처리량(Throughput)은 좋아진다. 그 이유는 GC가 덜 발생하기 때문이다.
  • Heap Size가 클 수록 GC는 적게 발생하지만, GC의 수행 시간은 더 길어진다.
위와 같은 속성을 잘 이해해야 Application에 최적화된 Heap 세팅이 가능하다.

---------------------------------------------------------------------------------
Heap과 GC가 워낙 중요한 이슈이기 때문에 글이 상당히 길어지고 말았다.

다음 글에서는 IBM JVM에 대해서...


신고
tags : Collector, GC, Heap, java
Trackback 0 : Comment 1
  1. 씨알 2008.05.18 00:05 신고 Modify/Delete Reply

    잘 읽고 갑니다. 좋은 정보 감사합니다.

Write a comment


[ Enterprise Java는 거대한 동기화 머신이다 - GC ] Enterprise Java & Oracle 성능 분석의 기초 - Part5

Enterprise Java 2007.08.29 11:22
Enterprise Java는 거대한 동기화 머신이다.

Garbage Collection과 Thread 동기화
Enterprise Java 환경에서 성능에 가장 많은 영향을 미치는 동기화(Synchronization) 문제가 바로 Garbage Collection(GC)이다. WAS 환경을 구축하거나 운영해본 경험이 있는 사람이라면 누구나 GC 문제에 민감할 것이다. 어쩌면 치를 떨지도...?

GC가 발생하는 동안 Application Thread는 어떤 식으로든 작동을 멈추거나 지연이 발생하게 된다. 이런 멈춤(Pause)과 지연이 과도하면 Application의 성능에 치명적인 영향을 미치게 된다. Web Server-->WAS-->DB 로 이루어지는 복잡한 시스템에서는 WAS에서의 GC로 인한 작업 지연이 연쇄 작용을 일으켜 시스템 전체의 성능 저하를 유발할 수 있다.

일반적으로 GC에 의한 성능 저하 문제를 "동기화 문제"로 인식하는 경우가 잘 없기 때문에 "동기화"라는 용어에 고개를 갸우뚱할 지 모르겠다. 필자는 "GC"를 메모리라는 자원을 둘러싼 Thread 동기화의 문제로 보는 것이 가장 정확한 해석이라고 생각한다. Thread가 특정 메모리 영역을 사용하려고 하는 시점에 해당 메모리 영역에 대해 GC 작업이 발생하게 되면 Thread가 블로킹(Blocking)되면서 성능 저하 현상이 발생하는 것이 우리 겪는 GC의 성능 문제이다.

Garbage Collection와 JVM
JVM은 Sun이 정하는 표준이다. 하지만 GC 자체는 표준이 아니다. 다만, Java와 같이 메모리를 핸들링하는 API를 제공하지 않는 어플리케이션에서는 자동화된 메모리 해제 작업이 필요하기 때문에 Garbage Collection 기능이 필수가 된 것 뿐이다.

GC가 표준이 아니기 때문에, GC를 구현하는데 있어서 어떤 알고리즘을 쓸지 또한 전혀 약속된 바가 없다. 각 JVM 벤더들이 자신들 고유의 알고리즘을 통해 GC를 구현한다. 앞으로 몇 차례의 글을 통해 Sun HotSpot JVM과 IBM JVM의 GC의 작동방식과 장단점에 대해 깊이있는 논의를 하게 될 것이다.

Java의 메모리(Non-Heap + Heap) 관리 기법과 GC 알고리즘은 실과 바늘같은 관계이다. GC를 어떻게 할지에 따라 메모리 관리 기법이 결정된다는 것이 정확한 표현일 것이다.

Sun HotSpot JVM과 IBM JVM은 전혀 다른 종류의 GC 기법을 구현해왔다. 최근(특히 Java 5부터)에 와서는 서로의 장점을 흡수해서 상당히 비슷한 개념들을 제공하지만, 여전히 뚜렷한 차이점들을 보인다.

각 JVM이 제공하는 GC의 작동 방식과 장단점을 정확하게 이해해야만 합리적인 성능 개선이 가능하다.

GC의 성능을 보는 두가지 관점
[성능을 개선시킨다]라는 말은 매우 추상적이고 기준이 애매모호하다. 따라서 좀 더 정확한 기준을 정할 필요가 있다. 여기에는 무수히 많은 방법론과 기준이 있겠지만, 보편적으로는 다음과 같은 두 가지의 기준을 사용한다.
  • Throughput 개선: Throughput은 흔히 "처리량"으로 해석된다. 즉 주어진 시간내에 얼마나 많은 일을 하느냐가 곧 Troughput이다.
  • Response Time 개선: Response Time은 "응답시간"으로 해석된다. 즉 특정 요청에 대해 얼마나 빨리 응답을 보내느냐가 Repsonse Time의 기준이다.
Throughput과 Response Time은 상호 배반적인 속성을 지니고 있다. Throughput을 개선시키려면, 즉 주어진 시간내에 최대한 일을 많이 하려면 되도록 통신량을 줄이고 일을 모아서 처리할 수 밖에 없다. 이런 이유로 Throughput을 개선시키기 위해서는 Batch 프로세싱을 사용할 수 밖에 없다.

반면 Response Time을 개선시키려면 작업을 처리하면서 결과가 나오는 즉시 결과를 보내주어야 한다. 이런 이유로 Response Time을 개선시키려면 Looping 프로세싱을 사용할 수 밖에 없다.

(참고) 오라클과 같은 DBMS에서도 동일한 원리가 적용된다. 오라클의 OPTIMIZER_GOAL ALL_ROWS라면 Throughput 기준의 최적화를, FIRST_ROWS라면 Response Time 기준의 최적화를 수행하게 된다. 오라클의 PL/SQL 블록은 항상 수행단위가 블록이기 때문에 예외없이 Throughput 기준의 최적화가 수행된다. 오라클에서도 이 원리를 잘 이해해야만 최적의 쿼리 튜닝이 가능하다.

두 가지 종류의 Garbage Collector
위에서 설명한 이유때문에 어떤 JVM의 Garbage Collector라도 항상 두 가지 종류 이상(Throughput 기준 하나+Response Time 기준 하나)를 제공한다.

Sun HotSpot JVM
IBM JVM
Througput 기준
-XX:+UseParallelGC-Xgcpolicy:optthruput
Response Time 기준
-XX:+UseConnMarkSweepGC
-Xgcpolicy:optavgpause
기타-XX:+UseSerialGC-Xgcpolicy:gencon

Throughput 기준의 Garbage Collector는 공격적으로 GC를 수행한다. 사용자가 체감하는 응답 시간을 다소 희생하더라도 GC 작업 자체를 최대한 효율적이고 집중적으로 수행할 수 있도록 한다. 따라서 GC 작업 시간이 다소 길 수 있고 이로 인해 Thread의 장시간 동기화 및 응답 시간의 지연을 초래하게 된다.

반면 Response Time 기준의 Garbage Collector는 다소 소극적인 GC를 수행한다. GC를 수행하는 과정에서 Thread가 동기화되고 이로 인해 사용자가 대기하는 시간을 최소화하기 위해서 GC 작업 자체에 대한 자원 소모를 줄이면서 오랜 시간에 걸쳐 나누어서 하게 된다. 하지만 그 만큼 비효율적이고 장기적으로는 사용자의 작업을 저해하는 결과를 낳을 수 있다.

각 GC가 제공하는 기능을 정확하게 이해해야만 Application의 요구 사항에 따라 적절한 GC를 선택할 수 있다.

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

다음 글에 계속...




신고
Trackback 0 : Comments 3
  1. 멀더엄마 2007.08.30 17:21 신고 Modify/Delete Reply

    GC 알고리즘도 중요하지만, WAS 인스턴스에 할당하는 메모리 사이즈도 중요한듯.
    메모리 사이즈를 크게주면, Full GC하는 간격이 늘어나지만 그만큼 Full GC 시간이 많이 걸리니 그시간동안 Thread 가 대기하게되는 문제가 있고,
    메모리 사이즈를 작게하면, Full GC 발생 주기가 짧아지는대신 GC 하는 시간이 줄어들어 그만큼 짧은시간동안만 Thread가 대기하게됨.

    WAS 인스턴스 갯수나 request 수 등을 고려해서 적당하게 메모리를 잡으면 될듯. 보통 1~2기가로 잡아주는것같음(???)
    다 ... 아는 얘긴가?

  2. 욱짜 2007.08.30 17:44 신고 Modify/Delete Reply

    Heap Size와 함께 Heap를 구성하는 Young(Eden+Survivor), Tenured Space의 크기도 중요... Part6을 참조

  3. 카이스턴 2007.09.13 11:35 신고 Modify/Delete Reply

    공부중이라 좋은 자료 발견해서 비밀글로 퍼갑니다~

Write a comment


[ Enterprise Java는 거대한 동기화 머신이다 - Thread ] Enterprise Java & Oracle 성능 분석의 기초 - Part4

Enterprise Java 2007.08.28 11:15
Enterprise Java는 거대한 동기화 머신이다.

Sun HotSpot JVM이 사실상의 업계 표준 JVM이지만, 항상 최고의 최적화를 제공하지는 않는다. 개인적인 생각으로는 Sun/HP에서는 Sun HotSpot JVM이, IBM AIX에서는 IBM JVM이, Windows/Linux(x86)에서는 BEA JRockit JVM이 최적의 성능을 낼 것으로 기대한다.

IBM JVM과 JRockit JVM은 Sun이 제정한 JVM 표준을 따르되, 자신들만의 최적화 기법을 통해서 또 다른 뛰어난 성능을 제공한다. 따라서 현재 시스템에서 테스트를 통해 최적의 성능을 지닌 JVM을 취사 선택할 필요가 있다.

블로그 Part3에서 Sun HotSpot JVM에서 Thread 동기화에 의한 블로킹이 Thread Dump에서 어떻게 관찰되는지 살펴본 보았는데, 이제 동일한 현상이 IBM JVM과 JRockit JVM에서는 어떻게 관찰되는지 살펴 보자.

IBM JVM
IBM JVM의 Thread Dump는 Sun HotSpot JVM에 비해서 매우 풍부한 정보를 제공한다. 단순히 Thread들의 현재 상태뿐 아니라, JVM의 상태에 대한 여러 가지 정보를 제공한다. 이러한 다양한 정보들은 JVM의 정확한 상태를 이해하는데 큰 도움이 된다.

Case1: Synchronized에 의한 동기화
아래 내용이 "synchronized" 문장에 의한 Thread 블로킹이 발생한 상황의 Thread Dump의 일부분이다.
-------------------------------------------------------------------------------------------
// 모니터 정보
1LKMONPOOLDUMP Monitor Pool Dump (flat & inflated object-monitors):
...
2LKMONINUSE sys_mon_t:0x3003C158 infl_mon_t: 0x00000000:
3LKMONOBJECT java.lang.Object@30127640/30127648: Flat locked by thread ident 0x08, entry count 1 //<-- 특정 오브젝트가 0x08 Thread에 의해 Locking
3LKNOTIFYQ Waiting to be notified: // <-- 세 개의 Thread가 대기 중
3LKWAITNOTIFY "Thread-1" (0x356716A0)
3LKWAITNOTIFY "Thread-2" (0x356F8020)
3LKWAITNOTIFY "Thread-3" (0x3577FA20)
...
// Java Object Monitor 정보
1LKOBJMONDUMP Java Object Monitor Dump (flat & inflated object-monitors):
...
2LKFLATLOCKED java.lang.Object@30127640/30127648
3LKFLATDETAILS locknflags 00080000 Flat locked by thread ident 0x08, entry count 1
...
// Thread 목록
1LKFLATMONDUMP Thread identifiers (as used in flat monitors):
2LKFLATMON ident 0x02 "Thread-4" (0x3000D2A0) ee 0x3000D080
2LKFLATMON ident 0x0B "Thread-3" (0x3577FA20) ee 0x3577F800
2LKFLATMON ident 0x0A "Thread-2" (0x356F8020) ee 0x356F7E00
2LKFLATMON ident 0x09 "Thread-1" (0x356716A0) ee 0x35671480
2LKFLATMON ident 0x08 "Thread-0" (0x355E71A0) ee 0x355E6F80
// <--
java.lang.Object@30127640/30127648을 점유하고 있는 0x03 Thread의 이름이 Thread-0 임을 알 수 있다
...
// Threrad Stack Dump
2XMFULLTHDDUMP Full thread dump Classic VM (J2RE 1.4.2 IBM AIX build ca142-20050929a (SR3), native threads):
3XMTHREADINFO "Thread-4" (TID:0x300CB530, sys_thread_t:0x3000D2A0, state:CW, native ID:0x1) prio=5 // <-- Thread-4는 CW(Conditional Waiting)상태
3XHNATIVESTACK Native Stack
NULL ------------
3XHSTACKLINE at 0xDB84E184 in xeRunJavaVarArgMethod
...

3XMTHREADINFO "Thread-3" (TID:0x300CB588, sys_thread_t:0x3577FA20, state:CW, native ID:0xA0B) prio=5
4XESTACKTRACE at Thread1.run(dump_test.java:21)
...
3XMTHREADINFO "Thread-2" (TID:0x300CB5E8, sys_thread_t:0x356F8020, state:CW, native ID:0x90A) prio=5
4XESTACKTRACE at Thread1.run(dump_test.java:21)
...
3XMTHREADINFO "Thread-1" (TID:0x300CB648, sys_thread_t:0x356716A0, state:CW, native ID:0x809) prio=5
4XESTACKTRACE at Thread1.run(dump_test.java:21)
...
3XMTHREADINFO "Thread-0" (TID:0x300CB6A8, sys_thread_t:0x355E71A0, state:R, native ID:0x708) prio=5 // <-- Thread-0(ident=0x08) Thread는 Runnable(Running) 상태. 락 홀더에 해당
4XESTACKTRACE at Thread2.run(dump_test.java(Compiled Code))
3XHNATIVESTACK Native Stack
NULL ------------
3XHSTACKLINE at 0x344DE720 in
...

위의 결과를 블로그 Part3에서 설명한 Sun HotSpot JVM의 Thread Dump 결과와 비교해보면, 정보의 양이 훨씬 많고 체계적인 것을 알 수 있다.

위의 결과를 해석하면 "Thread-0(ident=0x08)" Thread가 java.lang.Object@30127640/30127648 오브젝트에 대해 Monitor 락을 점유하고 실행(state:R) 중이며, 나머지 세 개의 Thread "Thread 1,2,3"은 동일 오브젝트에 대해 락을 획득하기 위해 대기(Conditional Waiting)상태이다.

Case2: Wait/Nofity에 의한 동기화
Wait/Nofity에 의한 동기화에 의해 Thread 블로킹이 발생하는 경우에는 한가지 사실을 제외하고는 Case1과 완전히 동일하다.

앞서 블로그 Part3에서 언급한 것처럼, Wait/Notify에 의한 동기화의 경우 실제로 락을 점유하고 있는 Thread는 존재하지 않고, Nofity해주기를 대기할 뿐이다. 따라서 락을 점유하고 있는 Thread가 어떤 Thread인지에 대한 정보가 Thread Dump에 나타나지 않는다.(실제로 락을 점유하고 있지 않기 때문에)
따라서 정확한 블로킹 관계를 해석하려면 좀더 면밀한 분석이 필요하다.

아래 내용은 Wait/Notify에 의한 Thread 동기화가 발생하는 상황의 Thread Dump의 일부이다. Case1과의 차이점에 주의하자. Wait/Notify에 의한 Thread 동기화의 경우에는 락을 점유하고 있는 Thread의 정체를 바로 알 수 없다.(블로킹이 아닌 단순 대기이기 때문에)
...
1LKMONPOOLDUMP Monitor Pool Dump (flat & inflated object-monitors):
...
2LKMONINUSE sys_mon_t:0x3003C158 infl_mon_t: 0x3003BAC0:
3LKMONOBJECT java.lang.Object@30138C58/30138C60:
// <-- Object에 대해 Waiting Thread가 존재하지만 Locking 되어 있지는 않다!!!
// <-- Case1과 잘 비교해서 차이를 이해하자!!!
3LKNOTIFYQ Waiting to be notified:
3LKWAITNOTIFY "Thread-3" (0x3577F5A0)
3LKWAITNOTIFY "Thread-1" (0x355E7C20)
3LKWAITNOTIFY "Thread-2" (0x356F7A20)
...

BEA JRockit
비록 Sun HotSpot JVM이나 IBM JVM 만큼 많이 사용되지는 않지만 JVM의 삼두 마차 중 하나라고 할 수 있다. 특히 IBM의 Websphere와 함께 시장을 양분하고 있는 WAS인 Weblogic을 만든 BEA에서 만든 JVM이라는 면에서 주목할만 한다. JRockit JVM은 x85 아키텍처에 최적화된 JVM으로 Windows나 Linux 플랫폼에서는 최적의 성능을 내게끔 구현되었다. 따라서 기회가 된다면 성능 테스트를 통해 사용을 검토해보기 바란다.

JRockit의 Thread Dump는 두 경쟁 JVM과 비교하면 가장 깔끔하고 예쁜 결과를 보고한다. 그 이유는 아래 예를 보면 금방 알 수 있다.

Case1: Synchronized에 의한 동기화
아래 Thread Dump를 보면 Thread간의 블로킹 관계가 매우 깔끔하고 명확하게 기술되어 있음을 알 수 있다.
===== FULL THREAD DUMP ===============
Sun Aug 26 01:10:42 2007
BEA JRockit(R) R26.4.0-63_CR302700-72606-1.5.0_06-20061127-1108-win-ia32

...
// Thread-0이 java/lang/Object@0x1073DF58 락을 점유하고 있음을 알 수 있다.
"Thread-0" id=9 idx=0x16 tid=3488 prio=5 alive
at Thread2.run()V(dump_test.java:36)
^-- Holding lock: java/lang/Object@0x1073DF58[thin lock]
at jrockit/vm/RNI.c2java(IIII)V(Native Method)
-- end of trace

// Thread1,2,3은 java/lang/Object@0x1073DF58 락을 대기하고 있으며, Blocked 상태이다.
"Thread-1" id=10 idx=0x18 tid=3264 prio=5 alive, in native, blocked
-- Blocked trying to get lock: java/lang/Object@0x1073DF58[thin lock]
at jrockit/vm/Threads.sleep(I)V(Native Method)
at jrockit/vm/Locks.waitForThinRelease(Ljava/lang/Object;I)I(Unknown Source)
at jrockit/vm/Locks.monitorEnterSecondStage(Ljava/lang/Object;I)Ljava/lang/Object;(Unknown Source)
at jrockit/vm/Locks.monitorEnter(Ljava/lang/Object;)Ljava/lang/Object;(Unknown Source)
at Thread1.run()V(dump_test.java:21)
at jrockit/vm/RNI.c2java(IIII)V(Native Method)
-- end of trace

"Thread-2" id=11 idx=0x1a tid=3580 prio=5 alive, in native, blocked
-- Blocked trying to get lock: java/lang/Object@0x1073DF58[thin lock]
at jrockit/vm/Threads.sleep(I)V(Native Method)
at jrockit/vm/Locks.waitForThinRelease(Ljava/lang/Object;I)I(Unknown Source)
at jrockit/vm/Locks.monitorEnterSecondStage(Ljava/lang/Object;I)Ljava/lang/Object;(Unknown Source)
at jrockit/vm/Locks.monitorEnter(Ljava/lang/Object;)Ljava/lang/Object;(Unknown Source)
at Thread1.run()V(dump_test.java:21)
at jrockit/vm/RNI.c2java(IIII)V(Native Method)
-- end of trace

"Thread-3" id=12 idx=0x1c tid=3068 prio=5 alive, in native, blocked
-- Blocked trying to get lock: java/lang/Object@0x1073DF58[thin lock]
at jrockit/vm/Threads.sleep(I)V(Native Method)
at jrockit/vm/Locks.waitForThinRelease(Ljava/lang/Object;I)I(Unknown Source)
at jrockit/vm/Locks.monitorEnterSecondStage(Ljava/lang/Object;I)Ljava/lang/Object;(Unknown Source)
at jrockit/vm/Locks.monitorEnter(Ljava/lang/Object;)Ljava/lang/Object;(Unknown Source)
at Thread1.run()V(dump_test.java:21)
at jrockit/vm/RNI.c2java(IIII)V(Native Method)
-- end of trace

// 가장 유용한 것은 아래의 Lock Chain 정보이다.Thread 간의 Lock Chaining 정보를 직관적으로 파악할 수 있도록 해준다.
Blocked lock chains

===================
Chain 2:
"Thread-2" id=11 idx=0x1a tid=3580 waiting for java/lang/Object@0x1073DF58 held by:
"Thread-0" id=9 idx=0x16 tid=3488 in chain 1

Chain 3:
"Thread-3" id=12 idx=0x1c tid=3068 waiting for java/lang/Object@0x1073DF58 held by:
"Thread-0" id=9 idx=0x16 tid=3488 in chain 1

Open lock chains
================
Chain 1:
"Thread-1" id=10 idx=0x18 tid=3264 waiting for java/lang/Object@0x1073DF58 held by:
"Thread-0" id=9 idx=0x16 tid=3488 (active)

===== END OF THREAD DUMP ===============

Case2: Wait/Nofity에 의한 동기화
Wait/Notify에 의한 동기화가 발생하는 경우에는 블로킹이 발생한 것이 아니라 단순 대기 상태이기 때문에 Thread 간의 블로킹 관계가 명확하게 드러나지 않는다.

아래 예를 보면 Thread1,2,3은 같은 락(java/lang/Object@0x1073DF58)에 대한 Notify를 대기하고 있다는 것을 쉽게 알 수 있지만 락 점유자(는 없기 때문에)의 정보는 알 수 없다. 이것은 어떤 JVM을 막론하고 동일한 현상이다.
...
"Thread-0" id=9 idx=0x16 tid=2200 prio=5 alive
at Thread2.run()V(dump_test2.java:36)
at jrockit/vm/RNI.c2java(IIII)V(Native Method)
-- end of trace

"Thread-1" id=10 idx=0x18 tid=1784 prio=5 alive, in native, waiting
-- Waiting for notification on: java/lang/Object@0x1073DF58[fat lock]
at jrockit/vm/Threads.waitForSignal(J)Z(Native Method)
at jrockit/vm/Locks.wait(Ljava/lang/Object;J)V(Unknown Source)
at java/lang/Object.wait()V(Native Method)
at Thread1.run()V(dump_test2.java:23)
^-- Lock released while waiting: java/lang/Object@0x1073DF58[fat lock]
at jrockit/vm/RNI.c2java(IIII)V(Native Method)
-- end of trace

"Thread-2" id=11 idx=0x1a tid=2904 prio=5 alive, in native, waiting
-- Waiting for notification on: java/lang/Object@0x1073DF58[fat lock]
at jrockit/vm/Threads.waitForSignal(J)Z(Native Method)
at jrockit/vm/Locks.wait(Ljava/lang/Object;J)V(Unknown Source)
at java/lang/Object.wait()V(Native Method)
at Thread1.run()V(dump_test2.java:23)
^-- Lock released while waiting: java/lang/Object@0x1073DF58[fat lock]
at jrockit/vm/RNI.c2java(IIII)V(Native Method)
-- end of trace

"Thread-3" id=12 idx=0x1c tid=3708 prio=5 alive, in native, waiting
-- Waiting for notification on: java/lang/Object@0x1073DF58[fat lock]
at jrockit/vm/Threads.waitForSignal(J)Z(Native Method)
at jrockit/vm/Locks.wait(Ljava/lang/Object;J)V(Unknown Source)
at java/lang/Object.wait()V(Native Method)
at Thread1.run()V(dump_test2.java:23)
^-- Lock released while waiting: java/lang/Object@0x1073DF58[fat lock]
at jrockit/vm/RNI.c2java(IIII)V(Native Method)
-- end of trace


Thread Dump를 통한 Thread 동기화 문제 해결의 실사례
아래 Thread Dump은 실제 운영 환경에서 성능 문제가 발생한 경우에 추출한 것이다. Thread Dump를 분석한 결과 많은 수의 Worker Thread들이 다음과 같이 블로킹되어 있었다.
...

"http8080-Processor2" daemon prio=5 tid=0x042977b0 nid=0x9a6c in Object.wait() [503f000..503fdb8]
at java.lang.Object.wait(Native Method)
- waiting on <0x17c3ca68> (a org.apache.commons.pool.impl.GenericObjectPool)
at java.lang.Object.wait(Object.java:429)
at org.apache.commons.pool.impl.GenericObjectPool.borrowObject(Unknown Source)
- locked <0x17c3ca68> (a org.apache.commons.pool.impl.GenericObjectPool)
at org.apache.commons.dbcp.PoolingDriver.connect(PoolingDriver.java:146)
at java.sql.DriverManager.getConnection(DriverManager.java:512)
- locked <0x507dbb58> (a java.lang.Class)
at java.sql.DriverManager.getConnection(DriverManager.java:193)
- locked <0x507dbb58> (a java.lang.Class)
at org.jsn.jdf.db.commons.pool.DBManager.getConnection(DBManager.java:40)
at org.apache.jsp.managerInfo_jsp._jspService(managerInfo_jsp.java:71)
...
at org.apache.tomcat.util.threads.ThreadPool$ControlRunnable.run(ThreadPool.java:683)
at java.lang.Thread.run(Thread.java:534)

"http8080-Processor1" daemon prio=5 tid=0x043a4120 nid=0x76f8 waiting for monitor entry [4fff000..4fffdb8]
at java.sql.DriverManager.getConnection(DriverManager.java:187)
- waiting to lock <0x507dbb58> (a java.lang.Class)
at org.jsn.jdf.db.commons.pool.DBManager.getConnection(DBManager.java:40)
at org.apache.jsp.loginOK_jsp._jspService(loginOK_jsp.java:130)
at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:137)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:853)
at org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:210)
at org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:295)
at org.apache.jasper.servlet.JspServlet.service(JspServlet.java:241)
...
at org.apache.tomcat.util.threads.ThreadPool$ControlRunnable.run(ThreadPool.java:683)
at java.lang.Thread.run(Thread.java:534)
...

위의 Thread Dump를 분석해보면 다음과 같은 사실을 알 수 있다.
  • java.sql.DriverManager.getConnection() 내부에서 Connection을 얻는 과정에서 Synchronized에 의한 Thread 블로킹이 발생
  • org.apache.commons.pool.impl.GenericObjectPool.borrowObject() 내부에서 Connection을 얻는 과정에서 Wait/Notify에 의한 Thread 블로킹이 발생

즉, Connection Pool에서 Connection을 얻는 과정에서 Thread 경합이 발생하고 있다. 이는 현재 Connection Pool의 완전히 소진되었고 이로 인해 새로운 DB Request에 대해 새로운 Connection을 맺는 과정에서 성능 저하 현상이 생겼다는 것을 의미한다. 만일 Connection Pool의 최대 Connection 수가 낮게 설정되어 있다면 대기 현상은 더욱 심해질 것이다. 다른 Thread가 DB Request를 끝내고 Connection을 놓을 때까지 기다려야 하기 때문이다.

해결책은? Connection Pool의 초기 Connection 수와 최대 Connection수를 키운다. 만일 실제 발생하는 DB Request수는 작은데 Connection Pool이 금방 소진된다면 Connection을 닫지 않는 문제일 가능성이 크다. 이 경우에는 소스 검증이나 모니터링 툴을 통해 Connection을 열고 닫는 로직이 정상적으로 작동하는지 검증해야 한다.

(참고) 다행히 iBatisHibernate같은 프레임웍들이 보편적으로 사용되면서 JDBC Connection을 잘못 다루는 문제는 거의 없어지고 있다. 개발자의 실수를 미연에 방지해준다는 의미에서 매우 바람직한 현상이라고 할 수 있다.

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

다음 글에 계속...

신고
tags : java, ThreadDump
Trackback 0 : Comment 1
  1. Stargazer 2012.12.22 13:37 Modify/Delete Reply

    관리자의 승인을 기다리고 있는 댓글입니다

Write a comment

티스토리 툴바