javaでjavaagentとagentlibを調査してみる

http://docs.oracle.com/javase/6/docs/technotes/tools/solaris/java.html
の-agentlib, -javaagentの項目について調べてみた


あれ、なんか俺agentlibとjavaagentを勘違いしている、、、、?

-agentlib
-agentlib:hprofや-agentlib:jdwpなどプロファイラやデバッガなどを利用するネイティブライブラリを指定する
詳しくはJVMTI(Java Virtual Machine Tools Interface)を参照すること

つまりCやC++を使って、VMにアクセスしてプロファイル情報などを引き抜いたり、デバッグ用に挙動を操作したりすることができるらしい


JVMTIの日本語DOC
http://java.sun.com/javase/ja/6/docs/ja/platform/jvmti/jvmti.html#starting
JVMTIを利用する
http://www.02.246.ne.jp/~torutk/javahow2/jvmti.html

-javaagent
java.lang.instrumentパッケージを使ってバイトコードをVM起動時に動的に書き換えたりできる。

下記さくらばさんの書き込みがそのままです
http://www.atmarkit.co.jp/bbs/phpBB/viewtopic.php?topic=26578&forum=12



以下バイトコードを動的に操作 Instrumentationを参考にjavaagentについて色々動作を試してみる

eclipseのworkspaceにできたクラスファイルをjarにまとめる

$ cd ~/Documents/workspace/lick_up_jvm/target/classes
$ jar cvf sample.jar jp/co/watanabe_yusaku/instrument/InstrmentTest.class 
マニフェストが追加されました
InstrmentTest.classを追加中です(入=982)(出=528)(46%収縮されました)


いったんjarを展開してマニフェストファイルにPremain-Classを追記する

$ jar xvf sample.jar
META-INF/が作成されました
\META-INF/MANIFEST.MFが展開されました
\jp/co/watanabe_yusaku/instrument/InstrmentTest.classが展開されました

$ vim META-INF/MANIFEST.MF
↓追記
Premain-Class: jp.co.watanabe_yusaku.instrument.InstrmentTest

$ jar cvfm sample.jar META-INF/MANIFEST.MF jp/co/watanabe_yusaku/instrument/InstrmentTest.class
マニフェストが追加されました
jp/co/watanabe_yusaku/instrument/InstrmentTest.classを追加中です(入=982)(出=528)(46%収縮されました)

実行してみる

$ java -javaagent:sample.jar jp/co//watanabe_yusaku/main/HelloWorld
class java.util.HashMap$EntryIterator
class java.lang.Boolean
略
Hello, World!

ちゃんとHelloWorldクラスのmainが実行される前にInstrmentTestクラスのpremainが実行されてるようだ。


この仕組みを使えば稼働中のアプリケーションの構造(設定ファイルなどを含む)を一切変更せずに挙動を変更することができる。
以下のようなコードをjarで固めてjavaagent指定すると全てのメソッドの実行時間をミリ秒で出力することができた。

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

public class MethodTimer {
	private static ClassPool classPool;
	
	@SuppressWarnings("rawtypes")
	public static void premain(String agentArgs, Instrumentation inst) {
		classPool = ClassPool.getDefault();
		
		inst.addTransformer(new ClassFileTransformer() {
			public byte[] transform(ClassLoader loader,
									String className,
									Class<?> classBeingRedefined, 
									ProtectionDomain protectionDomain,
									byte[] classfileBuffer)
									throws IllegalClassFormatException {
				
				try {
					ByteArrayInputStream stream = new ByteArrayInputStream(classfileBuffer);
					CtClass ctClass = classPool.makeClass(stream);
					
					CtMethod[] ctMethods = ctClass.getDeclaredMethods();
					for(CtMethod ctMethod : ctMethods) {
						ctMethod.addLocalVariable("start", CtClass.longType);
						ctMethod.insertBefore("start = System.currentTimeMillis();");
						ctMethod.insertAfter("System.out.println(\"" 
						+ ctClass.getName() + "."
						+ ctMethod.getName() + "()"
						+ ":exec time = \" + (System.currentTimeMillis() - start));");
					}
					return ctClass.toBytecode();
				}catch(CannotCompileException | IOException e) {
					IllegalClassFormatException ecfe = new IllegalClassFormatException();
					ecfe.initCause(e);
					throw ecfe;
				}
			}
		});
	}
}

マニフェストファイルは

Premain-Class: MethodTimer
Boot-Class-Path: javassist.jar
Can-Redefine-Classes: true


mainクラスは

public class HelloWorld {
    public static void main(String[] args) throws Exception{
    	HelloWorld h = new HelloWorld();
    	h.execute(args);
    }
    
    public void execute(String[] args) throws Exception{
    	this.say("Hello, World0");
    	this.say("Hello, World1");
    	Thread.sleep(3000);
		this.say("Hello, World2");
		this.say("Hello, World3");
    }
    
    public void say(String str) {
    	System.out.println(str);
    }
}


実行ディレクトリにjavasisst.jarとsample.jarを配置してから実行すると

java -javaagent:sample.jar HelloWorld
sun.launcher.LauncherHelper.makePlatformString():exec time = 0
sun.launcher.LauncherHelper.getMainMethod():exec time = 0
sun.launcher.LauncherHelper.checkAndLoadMain():exec time = 6
Hello, World0
HelloWorld.say():exec time = 1
Hello, World1
HelloWorld.say():exec time = 1
Hello, World2
HelloWorld.say():exec time = 0
Hello, World3
HelloWorld.say():exec time = 0
HelloWorld.execute():exec time = 3004
HelloWorld.main():exec time = 3004

AOPを導入するまでもないけど、ちょっと本番環境で設定ファイルなどを含む環境などを一切変更しないでプロファイリングしたい時に便利かもしれないですね