태터데스크 관리자

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

태터데스크 메시지

저장하였습니다.

The Power of Byte Code Instrumentation in Java - Part 6

Enterprise Java 2007.08.13 11:13

Part 5에 이어...

The Power of Byte Code Instrumentation in Java- Part 6

JDK는 전통적으로 JVM이 구동될 때 이 JVM 안에서 특정 작업을 수행할 에이전트(Agent), 즉 요원을 지정할 수 있는 방식을 제공해왔다. Java 5, 즉 JDK 1.5 이전 버전에서는 JVMPI(JVM Profiler Interface)라고 불렀다. Java 5부터는 JVMTI(JVM Tool Interface)라는 새로운 인터페이스가 제공된다. JVMPI/JVMTI의 특징은 C/C++로 구현 가능한 에이전트의 인터페이스를 제공한다는 것이다.

Java 5가 제공하는 희소식 중 하나는 Java로 구현 가능한 에이전트의 인터페이스를 제공한다는 것이다. 만세~~ 다음 명령어로 확인 가능하다.

Prompt> java

....

-agentlib:[=]
load native agent library , e.g. -agentlib:hprof
see also, -agentlib:jdwp=help and -agentlib:hprof=help
-agentpath:[=]
load native agent library by full pathname
-javaagent:[=]
load Java programming language agent, see java.lang.instrument

위의 옵션 들 중 agentlib/agentpath 옵션은 JVMPI/JVMTI 에이전트를 활성화하는 것이다.
반면 javaagent 옵션은 Java로 구현된 에이전트를 활성화하는 역할을 한다.

위의 설명이 (불)친절하게 안내하고 있는바대로 java.lang.instrument 패키지가 Java 에이전트의 인터페이스 역할을 한다.

java.lang.instrument 패키지에 대한 자세한 설명은 http://java.sun.com/j2se/1.5.0/docs/api/java/lang/instrument/package-summary.html를 참조한다.

JavaAgent BCI의 구조

아래 그림은 java.lang.instrument 패키지가 제공하는 기능을 그림으로 간략하게 표현한 것이다.

위의 과정을 간략하게 설명하면 다음과 같다.

1. Java Agent는 premain이라는 메소드를 구현한다. JVM은 Java Agent의 premain 메소드를 호출해서 Agent를 구동한 후에 Application의 main 메소드를 호출한다.

2. Java Agent는 ClassFileTransformer(클래스파일 변환기) 인터페이스를 구현하고, Instrumentation.addTransformer를 이용해서 클래스파일 변환기를 JVM에 등록한다.

3~4. JVM은 클래스 파일을 로드할 때 등록된 ClassFileTransformer의 transform 메소드를 호출해서 클래스파일의 바이트 코드를 변환하고, 변환된 바이트 코드를 원래 클래스 대신 사용한다. 바이트 코드를 변환하기 위해서 ASM 라이브러리를 사용한다.

5. Agent는 필요한 시점에 Instrumentation.redefineClasses 메소드를 이용해 특정 클래스의 바이트 코드를 런타임에 변경한다.

JavaAgent의 간단한 샘플

아래에 JavaAgent의 아주 간단한 샘플 소스가 있다.(실제 바이트 코드 변환에는 ASM 라이브러리를 사용한다)

public class SimpleProfiler implements ClassFileTransformer {

public SimpleProfiler() {
super();
}

public static void premain(String args, Instrumentation inst) {
try {
// Redefine preloadedclasses. Especially rt.jar boot class

ArrayList defs = new ArrayList();
for( Class c : inst.getAllLoadedClasses()) {
try {
if(c.getName().equals("java.io.File")) { // 여기서는 샘플로 java.io.File 만 변환한다.

System.out.println("Redefining class " + c.getName());
ClassReader reader = new ClassReader(c.getName());
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS+ClassWriter.COMPUTE_FRAMES);

// 여기가 구현부. ASM 라이브러리를 이용한다.

// --> 여기서는 소스를제공하지 않으며, 내부적으로 모든 클래스의 메소드의 시작점과 끝점에

// --> Method Start와 Method End를 알리는 코드를 삽입한다.
ClassAdapter adapter = new SimpleClassAdapter(writer, c.getName));

reader.accept(adapter, ClassReader.SKIP_DEBUG);

byte[] result = writer.toByteArray();
if(result != null) defs.add(new ClassDefinition(c, result));
}
} catch(Exception ex) {
ex.printStackTrace();
}
}

ClassDefinition[] cdef = defs.toArray(new ClassDefinition[defs.size()]);
inst.redefineClasses(cdef);
inst.addTransformer(new SimpleProfiler());

(new Thread(new MonitoringThread())).start();
} catch(Exception ex) {
ex.printStackTrace();
}
}
public byte[] transform(ClassLoader l, String className, Class c,
ProtectionDomain pd, byte[] b) throws IllegalClassFormatException {

if(l != ClassLoader.getSystemClassLoader()) {
return b;
}

if(className.startsWith("instrument/profiler")) {
return b;
}

System.out.println("Classloader = " + l);
System.out.println("Class = " + className);

byte[] result = b;
ClassReader reader = new ClassReader(b);
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS+ClassWriter.COMPUTE_FRAMES);
ClassAdapter adapter = new SimpleClassAdapter(writer, className); // 여기가 구현부...
reader.accept(adapter, ClassReader.SKIP_DEBUG);

result = writer.toByteArray();

return result;

}
}

SimpleProfiler 클래스는 JavaAgent로 다음과 같은 일을 수행한다.

1. premain메소드에서 현재까지Load된클래스들(특히rt.jar에 속하는Class들)을 Redefine한다.(같은 방법으로 어떠한 클래스이든지 원하는 대로 바이트 코드를 변환할 수 있다)

2. transform 메소드에서필요한 클래스들이 로딩되는 시점에 변환한다.

Manifest 파일

JavaAgent 파일은 필요한 클래스 파일들을 Jar 파일로 묶어서 JVM에 제공하게 된다. (여기서는 profiler.jar 라고 부르자)

특히 JVM에게 JavaAgent에 대한 정보를 넘겨주기 위해서 Manifest 파일을 작성해서 Jar 파일에 같이 넣어주어야 한다. Mainfest 파일에 기록해야 할 정보는 다음과 같다.

Premain-Class: Premain 메소드를 담고 있는 Agent Class의 이름. 필수 예) instrument.profiler.SimpleProfiler
Boot-Class-Path: Boot classloader에 의해 로딩될 Jar 파일의 경로. 옵션 예) profiler.jar;asm-3.0.jar

Can-Redefine-Classes: Instrmentation.redefineClasses를 통해 Runtime으로 바이트 코드를 변환하는 것을 허용할지의 여부. 옵션. Default는 False 예) True

아래에 샘플로 사용한 Manifest 파일이 있다.

Premain-Class: instrument.profiler.SimpleProfiler
Can-Redefine-Classes: True
Boot-Class-Path: profiler.jar;asm-3.0.jar

위의 정보를 해석하면

첫째, instrument.profiler.SimpleProfiler가 premain 메소드를 구현하고 있는 JavaAgent이며

둘째, 바이트 코드의 런타임 변환(Instrumentation.redefineClasses)을 허용하며

세째, profiler.jar(JavaAgent 자제) 파일과 asm-3.0.jar(ASM 라이브러리) 파일을 Boot class path에 등록한다.

두번째 옵션은 클래스 파일의 런타임 변환에 사용된다. 만일 위의 예제와 같이 rt.jar에 속한 클래스들, 즉 Boot class path에 속한 클래스들의 바이트 코드를 변환하려면 Profiler 자체가 Boot class path에 속해야 한다. 이것은 ClassLoader에 관한 JVM 고유의 속성에 기인한다.

JavaAgent의 실행

위와 같이 JavaAgent Jar 파일을 생성한 후, 다음과 같은 Java 명령어로 실행하면 된다.

java -javaagent:profiler.jar [Your Target Applicatin]

아래에 간단한 실행 결과가 있다. 모든 클래스의 모든 메소드의 실행 결과를 캡쳐할 수 있다.

---------- Call Table -------------------------------
instrument/target/RunThread.do31179[4694]
instrument/target/RunThread.do2791[5730]
instrument/target/RunThread.do1396[3189]
java.io.File.14[0]
java.io.File.getPath8[0]
java.io.File.exists6[0]
instrument/target/RunThread.5[0]
java.io.File.lastModified4[0]
java.io.File.length2[0]
java.io.File.getPrefixLength2[0]
java.io.File.getCanonicalPath2[0]
instrument/target/ProfilerTarget.main1[0]

JavaAgent와 JVMTI와의 관계. 그리고 JMX

JavaAgent와 JVMTI가 모두 Java 5 (JDK1.5)에서 지원되는 것은 결코 우연이 아니다. JavaAgent와 JVMTI는 몇가지 특징을 공유하고 있다. 실시간 클래스 코드 변환이 대표적인 케이스이고, Java 오브젝트의 크기를 계산한는 함수를 제공하는 것도 같은 맥락이다. (Instrumentation.getObjectSize와 JVMTI의 GetObjectSize)

특히 실시간 클래스 코드 변환은 다른 BCI 라이브러리가 제공하지 못하는 뛰어난 기능으로 이제 BCI는 크게 세가지로 분류가 가능해졌다.

  • Static BCI : 특정 클래스의 바이트 코드를 정적으로 변환하는 방식
  • Load-Time BCI : 특정 클래스가 로딩되는 시점에 변환하는 방식
  • Runt-Time BCI : JVM이 실행되고 있는 시점에 특정 클래스의 바이트 코드를 변환하는 방식

Sun이 Java 5에서 JVMTI와 JavaAgent를 제공하면서 의도한 것은 이 두가지 방법을 상호 보완적으로 사용하라는 것이다. 즉, JVM 자체를 프로파일링하는 것은 여전히 JVMTI와 같은 C 언어 레벨의 API를 사용하되, BCI를 통해 특정 클래스아 액션을 프로파일링하는 것은 JavaAgent와 같은 Java 언어 레벨의 API를 사용하기를 권고한다.

여기에 JMX의 Platform MBean(JVM 정보를 제공하는 MBean) 기능까지 추가되었으니, 이제 Java는 성능 관리에 있어 그 어느때보다 폭넓은 기능을 제공하는 셈이다.

앞으로 시간날 때마다 JVMTI나 JMX같은 추가적인 기능들에 대해서도 논의할 기회를 가질려고 한다.

Epilogue

BCI는 성능 관리를 위해 사용가능한 현존하는 방법들 중 가장 보편적이고 편리한 기법이다.많은 공개 라이브러리가 제공되고 있고, 특히Java 5에서는 JVM 레벨에서 BCI를 지원한다.

만일 WAS와 같은 Java Application시스템 모니터링을 계획하고 있다면, BCI에 대한 검토가 반드시 필요할 것이다.

또는실제로 이 기법을 사용할 기회가 없다고 하더라도, 3rd party의 모니터링을 위해 툴을 도입할 때 이런 기법들이 어떻게 사용되고 있는지 이해해야만 툴의 특징을 파악할 수 있을 것이다.

PS)

아래 예제는 BCI를 이용해 Exception, Call Tree, File I/O, Net I/O, JDBC Request 등을 프로파일링한 결과이다. 상용 모니터링 툴들이 제공하는 것과 거의 동일한 수준의 정보를 BCI를 이용해 손쉽게 수집할 수 있다.

--------------------[Exception Tracking]--------------------
Exception count = 9
[Exception] Exception = java.io.IOException, Message = Something Bad2~~, Thread id = 11, Caller = [Sigature]flowlite.server.ExceptionGenerator.doSomething2, Called from = [Sigature]flowlite.server.ExceptionGenerator.doSomething2, When = Mon Aug 13 11:06:10 KST 2007
flowlite.server.ExceptionGenerator.doSomething2(FlowLiteMBeanServer.java:128)
flowlite.server.ExceptionGenerator.run(FlowLiteMBeanServer.java:113)
java.lang.Thread.run(Unknown Source)

[Exception] Exception = java.io.IOException, Message = Something Bad2~~, Thread id = 11, Caller = [Sigature]flowlite.server.ExceptionGenerator.doSomething2, Called from = [Sigature]flowlite.server.ExceptionGenerator.run, When = Mon Aug 13 11:06:10 KST 2007
flowlite.server.ExceptionGenerator.doSomething2(FlowLiteMBeanServer.java:128)
flowlite.server.ExceptionGenerator.run(FlowLiteMBeanServer.java:113)
java.lang.Thread.run(Unknown Source)

...

--------------------[Active Thread & Call Tree Tracking]--------------------
Thread count = 26
[ActiveThread] Thread id = 62, Thread name = Thread-49, Group name = main
[Call Tree]
[Call][Sigature]flowlite.server.CallTreeGenerator$InnerGenerator.depth1_1, duration = 31[ms]
[Call][Sigature]java.util.Random.nextLong, duration = 0[ms]
[Call][Sigature]java.lang.Math.abs, duration = 0[ms]
[Call][Sigature]java.lang.Thread.sleep, duration = 0[ms]
[Call][Sigature]flowlite.server.CallTreeGenerator$InnerGenerator.depth2_1, duration = 15[ms]
[Call][Sigature]java.util.Random.nextLong, duration = 0[ms]
[Call][Sigature]java.lang.Math.abs, duration = 0[ms]
[Call][Sigature]java.lang.Thread.sleep, duration = 15[ms]

...

--------------------[File I/O Tracking]--------------------
File I/O count = 22
[File Info] Thread id = 22, File name = c:test.txt, Status = 2, File mode = [r+w], read time = Mon Aug 13 11:06:15 KST 2007
[File IO], Thread id = 22, File name =c:test.txt, Bytes read = 1024, Access time = Mon Aug 13 11:06:15 KST 2007
[File IO], Thread id = 22, File name =c:test.txt, Bytes read = 1024, Access time = Mon Aug 13 11:06:15 KST 2007
...

Socket Info] Thread id = 20, Host name =localhost:17904, Status = 1, Access time = Mon Aug 13 11:06:13 KST 2007
[Net IO], Thread id = 20, Bytes read = 1024, Access time = Mon Aug 13 11:06:13 KST 2007
[Net IO], Thread id = 20, Bytes read = 1024, Access time = Mon Aug 13 11:06:13 KST 2007
[Net IO], Thread id = 20, Bytes read = 1024, Access time = Mon Aug 13 11:06:13 KST 2007

...

--------------------[JDBC I/O Tracking]--------------------
Connection count = 1
Statement count = 1
FetchCount count = 100
[Connection] Statement count = 10, DB = Oracle, Oracle Database 10g Enterprise Edition Release 10.2.0.3.0 - 64bit Production
With the Partitioning, OLAP and Data Mining options, Created = 2007-08-13
[Statement] Query = SELECT name FROM t_pstmt_test WHERE id = ?, Execution count = 10, Fetch size = 10, Statement Type = Prepared
[Execution] Column Count = 1, Fetch count = 1, {Parameters} = (1, 4),
[Fetch] Fetched value count = 1
(1, name4),

...

신고
Trackback 0 : Comments 2
  1. KGEE 2010.08.26 10:22 신고 Modify/Delete Reply

    안녕하세요 욱짜님!
    좋은글 보고갑니다. 제가 요즘 졸업준비하느라 JVM CPU/MEMORY Profiling 에 골머리가 아프거든요..ㅎㅎ
    리서치 이후 이렇게 좋은 BCI 포스팅은 첨 보네요.. 극찬!!!
    근데요 오래된 글들이라 그런지 사진들이 다 엑박이네요.. 저만 그런지.. Windpw7 인데..
    암튼 사진이 너무 궁금해서요~~~~! ㅋㅋ
    종종 글보러 오겠습니다.

    • 욱짜 2010.08.27 00:33 신고 Modify/Delete

      네이버 블로그에 있던 글을 옮겼는데 이미지는 안 옮겨가는 바람에 그렇게 되었습니다.

      JVM CPU/Memory Profiling이라니 쉽지 않은 길을 택하셨습니다. 무운을 빕니다.

Write a comment

티스토리 툴바