14

したがって、一連の「特別な」クラスをメモリ内に保持するクラスローダー (MyClassLoader) があります。これらの特別なクラスは動的にコンパイルされ、MyClassLoader 内のバイト配列に格納されます。MyClassLoader がクラスを要求されるspecialClassesと、システム クラスローダーに委譲する前に、辞書にそのクラスが含まれているかどうかを最初にチェックします。次のようになります。

class MyClassLoader extends ClassLoader {
    Map<String, byte[]> specialClasses;

    public MyClassLoader(Map<String, byte[]> sb) {
        this.specialClasses = sb;
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        if (specialClasses.containsKey(name)) return findClass(name);
        else return super.loadClass(name);
    }

    @Override
    public Class findClass(String name) {
        byte[] b = specialClasses.get(name);
        return defineClass(name, b, 0, b.length);
    }    
}

で変換 (計測など) を実行したい場合は、呼び出す前にspecialClassesを変更するだけで実行できます。byte[]defineClass()

System クラスローダによって提供されるクラスも変換したいと思いますが、System クラスローダは、それが提供する生byte[]のクラスにアクセスする方法を提供していないようで、Classオブジェクトを直接提供します。

JVM にロードされたすべてのクラスを計測することもできますが、-javaagent計測したくないクラスにオーバーヘッドが追加されます。MyClassLoader によってロードされたクラスのみを計測したいだけです。

  • byte[]親クラスローダによって提供される生のクラスを取得する方法はありますか?そのため、独自のコピーを定義する前にインストルメント化できますか?
  • byte[]あるいは、 MyClassLoader がすべてのシステム クラス (オブジェクト、文字列など) の独自のコピーを計測および定義できるように、システム クラスローダーの機能をエミュレートする方法はありますか?

編集:

だから私は別のアプローチを試しました:

  • を使用して、ロードされたすべてのクラスの を-javaagentキャプチャし、byte[]クラスの名前をキーとしてハッシュテーブルに保存します。
  • MyClassLoader は、システム クラスをその親クラスローダーに委譲する代わりに、クラス名を使用してこのハッシュテーブルからバイトコードをロードし、それを定義します。

理論的には、これにより、MyClassLoader がインストルメンテーションを使用して独自のバージョンのシステム クラスを定義できるようになります。ただし、それは失敗します

java.lang.SecurityException: Prohibited package name: java.lang

明らかに、JVM は私が自分でクラスを定義することを好みませんjava.lang。たとえ (理論的には)byte[]ブートストラップでロードされたクラスと同じソースから取得する必要があるとしてもです。解決策の探求は続きます。

EDIT2:

私はこの問題に対する (非常に大ざっぱな) 解決策を見つけましたが、Java のクラスローディング/インスツルメンテーションの複雑さについて私よりもよく知っている人が大ざっぱではないものを思い付くことができれば、それは素晴らしいことです。

4

4 に答える 4

11

だから私はこれに対する解決策を見つけました。これはあまり洗練された解決策ではなく、コード レビュー時に多くの怒りのメールが送られますが、うまくいくようです。基本的なポイントは次のとおりです。

Javaエージェント

ajava.lang.instrumentationと aを使用して、後で使用するオブジェクトを-javaagent保存しますInstrumentation

class JavaAgent {
    private JavaAgent() {}

    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("Agent Premain Start");
        Transformer.instrumentation = inst;
        inst.addTransformer(new Transformer(), inst.isRetransformClassesSupported());
    }    
}

ClassFileTransformer

マークされたクラスでのみ機能するTransformerを に追加します。Instrumentation何かのようなもの

public class Transformer implements ClassFileTransformer {
    public static Set<Class<?>> transformMe = new Set<>()
    public static Instrumentation instrumentation = null; // set during premain()
    @Override
    public byte[] transform(ClassLoader loader,
                            String className,
                            Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain,
                            byte[] origBytes) {


        if (transformMe.contains(classBeingRedefined)) {
            return instrument(origBytes, loader);
        } else {
            return null;
        }
    }
    public byte[] instrument(byte[] origBytes) {
        // magic happens here
    }
}

クラスローダー

クラスローダで、ロードされた各クラス (ローディングが親に委譲されているクラスも含む) を明示的にマークしてから、クラスローダに変換transformMeを依頼します。Instrumentation

public class MyClassLoader extends ClassLoader{
    public Class<?> instrument(Class<?> in){
        try{
            Transformer.transformMe.add(in);
            Transformer.instrumentation.retransformClasses(in);
            Transformer.transformMe.remove(in);
            return in;
        }catch(Exception e){ return null; }
    }
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return instrument(super.loadClass(name));
    }
}

...そしてほら!によってロードされるすべてのクラスは、メソッドMyClassLoaderによって変換されます。これには、 やその仲間などのすべてのシステム クラスが含まれますが、デフォルトの ClassLoader によってロードされるすべてのクラスは変更されません。instrument()java.lang.Object

instrument()コールバック フックを挿入してインストルメント化されたバイトコードのメモリ割り当てを追跡するメモリ プロファイリング メソッドを使用してこれを試してみましMyClassLoadた。「通常の」クラスがいいえ。

勝利!

もちろん、これはひどいコードです。どこでも共有の変更可能な状態、非ローカルの副作用、グローバル、想像できるすべてのもの。おそらくスレッドセーフでもありません。しかし、そのようなことが可能であることを示してます。プログラムの「残り」をそのままにして、カスタム ClassLoader の操作の一部として、クラスのバイトコード、さらにはシステム クラスを選択的に計測することができます。

未解決の問題

他の誰かがこのコードをそれほどひどいものにしない方法を考えているなら、私はそれを聞いてうれしいです. 次の方法がわかりませんでした:

  • 他の方法でロードされたインストゥルメント クラスではなく、オンデマンドで唯一のインストルメント クラスをInstrumentation 作成するretransformClasses()
  • global-mutable-hashtable ルックアップなしで、オブジェクトを変換する必要があるかどうかを が判断できるようにするメタデータを各Class<?>オブジェクトに保存します。Transformer
  • Instrumentation.retransformClass()メソッドを使用せずにシステム クラスを変換します。前述のように、ClassLoader.java のハードコーディングされたチェックにより、クラスに動的defineClassに接続しようとすると失敗します。byte[]java.lang.*

誰かがこれらの問題のいずれかを回避する方法を見つけることができれば、これは大雑把ではなくなります. とにかく、JVMの残りの部分をそのままにして(計測オーバーヘッドなしで)、一部のサブシステム(つまり、関心のあるサブシステム)を計測(プロファイリングなど)できることは、他の人にとって役立つと思います私、ここです。

于 2012-10-25T01:38:10.667 に答える
6

ClassFileTransformer を使用しない場合の最初の説明:

Oracle JRE/JDKのライセンスには、java.*パッケージを変更できないことが含まれており、java.langで何かを変更しようとするテストで示したものから、テストが含まれており、次の場合にセキュリティ例外がスローされます試してみて。

そうは言っても、代替をコンパイルし、JRE -Xbootclasspath/p CLI オプションを使用して参照することで、システム クラスの動作を変更できます。

その方法で達成できることを見た後、OpenJDK のカスタム バージョンをコンパイルするために、さらに多くの作業が必要になると思います。Bootstrap クラスローダーは (私が読んだことから) ネイティブ実装であるため、これを期待しています。

私のお気に入りのクラスローダーの概要については、http://onjava.com/pub/a/onjava/2005/01/26/classloading.htmlを参照してください。

ClassFileTransformer を使用すると、次のようになります。

既に示したように、メソッド プログラム (および事前に読み込まれたクラスのその他の特定の側面) を更新できます。あなたが尋ねた質問に答えて:

オンデマンドの計測: ここで重要なのは、ロードされたすべてのクラスに固有の Class インスタンスが関連付けられていることです。そのため、ロードされた特定のクラスをターゲットにしたい場合は、それがどのインスタンスであるかに注意する必要があります。これは、Object.class などのすべてのクラス名に関連付けられたメンバー「クラス」を含むさまざまな方法で見つけることができます。

スレッドセーフですか: いいえ、2 つのスレッドがセットを同時に変更する可能性があり、この問題はさまざまな方法で解決できます。Set の並行バージョンを使用することをお勧めします。

グローバルなど: グローバルは特に必要だと思います (実装をもう少しうまくできると思います) が、おそらく問題はなく、後で Java のコーディングを改善する方法を学ぶことができます (私は約 12 年間、言語の使用に関するいくつかの微妙なことが信じられないでしょう)。

クラス インスタンス内のメタデータ: Java を使用してきたすべての時間の中で、メタデータを添付することは自然ではありませんでした。これにはおそらく正当な理由があります。特定の目的のためにマップを保持することは問題ありません。これは、インスタンスへのポインターとメタデータの間の単なるマップであるため、実際にはメモリを大量に消費するわけではありません。

于 2012-10-27T17:10:35.620 に答える
2

クラスローダーは、すでにロードされたクラスのバイトコードにアクセスするためのパブリック API を提供しません。バイト コードは VM によってどこかにキャッシュされる可能性が高いですが (Oracle VM では、これはネイティブ コードで行われます)、バイト配列として取得することはできなくなります。

ただし、できることは、クラス ファイルをリソースとして再読み込みすることです。明白なことを忘れない限り、 ClassLoader#getResource() または Class#getResource() は、リソースのロードに使用するのと同じ検索パスを使用してクラス ファイルをロードする必要があります。

public byte[] getClassFile(Class<?> clazz) throws IOException {     
    InputStream is =
        clazz.getResourceAsStream(
            "/" + clazz.getName().replace('.', '/') + ".class");
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    int r = 0;
    byte[] buffer = new byte[8192];
    while((r=is.read(buffer))>=0) {
        baos.write(buffer, 0, r);
    }   
    return baos.toByteArray();
}
于 2012-10-24T13:57:27.433 に答える
0

Object.class を再定義したいのですが、このクラスは、クラスローダーを含むプログラムが実行される前に既にロードされています。独自の Object.class を作成したとしても、既に読み込まれているシステム クラスと競合し、混乱を招く可能性があります。

私が見る唯一の方法は、システム クラスをオフラインで計測することです。つまり、rt.jar を取得し、その中のすべてのクラスを計測して書き戻すことです。

于 2012-10-23T15:14:03.373 に答える