JVMプログラミング(Oolong)学習ノート
最近Engelの『Programming for the Java Virtual Machine』を勉強します.この本は実際にJVMの原理とメカニズムを話しているが,アセンブリ言語の形式で説明しているにすぎない.Oolongは大牛Engelさん自身が発明したJVMベースのアセンブリ言語で、彼自身の紹介によると、Oolongは実際にはJVM bytecodeの理解しやすい教育バージョンにすぎず、それ自体は開発応用の価値を備えていないようだ.
jvm自体にはすでにある程度熟知しているので、ここには詳細なメモはありません.本の中のいくつかの迷っているところについて説明します.
配列アクション:
配列要素の書き込み操作は、1.5以上のjvmで間違っている例があります.
実験の結果、配列要素の置換のスタックの順序が間違っており、正確には以下の通りであるべきであることが分かった.
本のコードを実行すると、jvmはスタックトップpopからStringオブジェクトを出ようとしたとき、受け取ったものがintであることに気づき、エラーを報告します.
Exception:
.throws文はオプションです.jvmは、.throwsがこの例外タイプを明示しているかどうかにかかわらず、ローカルに切り取られた例外を放出します.しかし.throwsには積極的な設計意義がある:1つのコードが正しいシステムに対して、1つの方法は.throwsに明記された異常だけを投げ出すべきで、その上層caller(他の方法のように)はこの目標方法が.throwsに注ぐ異常だけを投げ出す仮定の上に構築すべきである.一つの方法で、自分が知らない異常を捕まえようとするのは奇妙で、「最小驚きの原則」に違反している.
invokeinterface:
3つの注意点があります.
1.最後のパラメータは、invokeメソッドで使用するスタックスロットの数です.この方法をmyobj.myMethod(long a,int b)と仮定すると、必要なスタックスロットは1(ref)+2(long)+1(int)=4である
2.このパラメータはあまり意味がありません.この数字は推測できるからです.ただjvm bytecodeがそうなので、Oolongはbytecodeへの直訳性を保つために残っています.
3.invokeinterfaceの性能はinvokevirtualより1桁遅いので、慎重に使用してください.
Constructor:
サブクラスのでは、スーパークラスのの呼び出しはinvokespecial方式を採用する必要があります.論理は簡単で、invokevirtual方式でスーパークラスのconstructorを呼び出すと、jvmは既存の配布方式で呼び出され、サブクラスのに戻り、性能は無限に再帰的にループします.
Constant Propagation:
実際、javaを書くときに不変の性質を持つすべての変数にfinal修飾を加えるのが良い実践です.CPUのレジスタはメモリよりはるかに速いので、
いつも間に合わない
来るのが早い.xの宣言にfinalを加えると、コンパイラは明確な指示を得ることができ、第2の方法で最適化することができます(finalがなくても、コンパイラは最適化を完了することができますが、これはコンパイラとの人柄に依存します).
Optimization:
コンパイラはjavacのようにjavaコードをbytecodeに変換していくつかの最適化を行い、jvmインスタンスがクラスをロードするときにJITによってbytecodeをさらに最適化します.インラインはいったいその段階で発生したのか、Engelは明確に提出していないので、さらなる学習研究が必要だ.
InlineはC++においても重要なコンパイラ最適化オプションであり、透明なメソッドやフィールドの呼び出しを特定の具体的なコードに置き換えることを目的としています.
inlineの最適化により、次のようになります.
もちろん、最適化の結果はbytecode形式にすぎませんが、ここではjavaで説明するのは簡単です.
inlineの後に節約されるオーバーヘッドは、パラメータスタック、被呼メソッドスタックフレームの作成、被呼メソッドスタックフレームの破棄などである.
Javaにはpolymorphの特性があるため、inlineは実行できない場合があります.jvmとコンパイラは、copyのどのコードがスーパークラスの方法なのか、サブクラスの上書き方法なのか分からないからです.スーパークラスのメソッドにfinal修飾子を付けるなど、jvmをある程度助けることができ、jvmはinlineがこのメソッドのコードしか使用しないことを決定することができます.
フィールドのinlineについて、Engelが14.3.1章で与えたTeaPartyの例は、結果の説明が少なくともJDK 5/6で間違っていることに注意してください.具体的な例は、私のこの文章を参照してください.
定数を更新したら、classを再コンパイルしてください。
Thread Lock:
スレッドロックを使用する場合、中のメカニズムが分からないとドキドキします.bytecodeレベルでは、ロックの取得と解放はそれぞれ2つの命令によって実現されるため、不思議ではありません.
この2つの命令はペアで現れなければならない.漏れがあると、例えばmonitorexitが1つ足りないと、ロックは他のスレッドに解放されず、スレッドが死ななければ、ロックは永遠にこのスレッドにholdされ、いわゆる「デッドロック」-deadlockになる.したがって、javaコンパイラはデッドロックを回避するために、このjavaコードに対して:
常に次のようにコンパイルされます.
そのためjavaではロックを使うのは安全で、Oolongやbytecodeでは常に問題の漏洩に注意しなければなりません.
多重ロックはどのように実現されますか?ロックにロックをかけると、少し奇妙になりますが、原理は簡単です.例えば、次のjavaコードです.
bytecodeをコンパイルするとき、synchronizedキーワードに出会うたびに、monitorrenter/monitorexitのペアをセットし、上のjavaコードをOolongに翻訳すればいいです(明確にするために、私はすべての方法のコードをinlineしました):
monitorenterを実行するたびにlock_countは1増加します.monitorexitを実行するたびにlock_countは1減少します.
JVMは、lock_count=0の場合、他のスレッドがロックを取得する機会があります.したがって、subLock 1()とsubLock 2()の間に他のスレッドが介入することは不可能であり、mainLock()とsubLock 1()とsubLock 2()が持つのは同じロックである.
jvm自体にはすでにある程度熟知しているので、ここには詳細なメモはありません.本の中のいくつかの迷っているところについて説明します.
配列アクション:
配列要素の書き込み操作は、1.5以上のjvmで間違っている例があります.
iconst_5 ; size
anewarray java/lang/String ; new String[5]
dup ;
ldc "Hello" ;
iconst_0 ; 0
aastore ;
実験の結果、配列要素の置換のスタックの順序が間違っており、正確には以下の通りであるべきであることが分かった.
...
dup
iconst_0
ldc "Hello"
aastore
本のコードを実行すると、jvmはスタックトップpopからStringオブジェクトを出ようとしたとき、受け取ったものがintであることに気づき、エラーを報告します.
Exception:
.throws文はオプションです.jvmは、.throwsがこの例外タイプを明示しているかどうかにかかわらず、ローカルに切り取られた例外を放出します.しかし.throwsには積極的な設計意義がある:1つのコードが正しいシステムに対して、1つの方法は.throwsに明記された異常だけを投げ出すべきで、その上層caller(他の方法のように)はこの目標方法が.throwsに注ぐ異常だけを投げ出す仮定の上に構築すべきである.一つの方法で、自分が知らない異常を捕まえようとするのは奇妙で、「最小驚きの原則」に違反している.
invokeinterface:
3つの注意点があります.
1.最後のパラメータは、invokeメソッドで使用するスタックスロットの数です.この方法をmyobj.myMethod(long a,int b)と仮定すると、必要なスタックスロットは1(ref)+2(long)+1(int)=4である
2.このパラメータはあまり意味がありません.この数字は推測できるからです.ただjvm bytecodeがそうなので、Oolongはbytecodeへの直訳性を保つために残っています.
3.invokeinterfaceの性能はinvokevirtualより1桁遅いので、慎重に使用してください.
Constructor:
サブクラスの
Constant Propagation:
実際、javaを書くときに不変の性質を持つすべての変数にfinal修飾を加えるのが良い実践です.CPUのレジスタはメモリよりはるかに速いので、
; x 1 , 32
iload_1 ;push x
いつも間に合わない
bipush 32 ;push x
来るのが早い.xの宣言にfinalを加えると、コンパイラは明確な指示を得ることができ、第2の方法で最適化することができます(finalがなくても、コンパイラは最適化を完了することができますが、これはコンパイラとの人柄に依存します).
Optimization:
コンパイラはjavacのようにjavaコードをbytecodeに変換していくつかの最適化を行い、jvmインスタンスがクラスをロードするときにJITによってbytecodeをさらに最適化します.インラインはいったいその段階で発生したのか、Engelは明確に提出していないので、さらなる学習研究が必要だ.
InlineはC++においても重要なコンパイラ最適化オプションであり、透明なメソッドやフィールドの呼び出しを特定の具体的なコードに置き換えることを目的としています.
class Demo {
void method_1() {
method_2();
method_3();
}
void method_2() {
System.out.println("Method_2");
}
void method_3() {
System.out.println("Method_3");
}
}
inlineの最適化により、次のようになります.
class Demo {
void method_1() {
System.out.println("Method_2"); //method_2
System.out.println("Method_3"); //method_3
}
void method_2() {
System.out.println("Method_2");
}
void method_3() {
System.out.println("Method_3");
}
}
もちろん、最適化の結果はbytecode形式にすぎませんが、ここではjavaで説明するのは簡単です.
inlineの後に節約されるオーバーヘッドは、パラメータスタック、被呼メソッドスタックフレームの作成、被呼メソッドスタックフレームの破棄などである.
Javaにはpolymorphの特性があるため、inlineは実行できない場合があります.jvmとコンパイラは、copyのどのコードがスーパークラスの方法なのか、サブクラスの上書き方法なのか分からないからです.スーパークラスのメソッドにfinal修飾子を付けるなど、jvmをある程度助けることができ、jvmはinlineがこのメソッドのコードしか使用しないことを決定することができます.
フィールドのinlineについて、Engelが14.3.1章で与えたTeaPartyの例は、結果の説明が少なくともJDK 5/6で間違っていることに注意してください.具体的な例は、私のこの文章を参照してください.
定数を更新したら、classを再コンパイルしてください。
Thread Lock:
スレッドロックを使用する場合、中のメカニズムが分からないとドキドキします.bytecodeレベルでは、ロックの取得と解放はそれぞれ2つの命令によって実現されるため、不思議ではありません.
monitorenter ;
monitorexit ;
この2つの命令はペアで現れなければならない.漏れがあると、例えばmonitorexitが1つ足りないと、ロックは他のスレッドに解放されず、スレッドが死ななければ、ロックは永遠にこのスレッドにholdされ、いわゆる「デッドロック」-deadlockになる.したがって、javaコンパイラはデッドロックを回避するために、このjavaコードに対して:
synchronized (obj) {
//
}
常に次のようにコンパイルされます.
.catch all from begin to end using handler
begin:
aload_1 ;
monitorenter ; aload_1
;
monitorexit ;
end:
goto next_code ; synchronized
handler:
aload_1
monitorexit ; ,
next_code:
;
そのためjavaではロックを使うのは安全で、Oolongやbytecodeでは常に問題の漏洩に注意しなければなりません.
多重ロックはどのように実現されますか?ロックにロックをかけると、少し奇妙になりますが、原理は簡単です.例えば、次のjavaコードです.
class LockDemo {
synchronized void mainLock() {
subLock1();
subLock2();
}
synchronized void subLock1() {
// 1
}
synchronized void subLock2() {
// 2
}
}
bytecodeをコンパイルするとき、synchronizedキーワードに出会うたびに、monitorrenter/monitorexitのペアをセットし、上のjavaコードをOolongに翻訳すればいいです(明確にするために、私はすべての方法のコードをinlineしました):
; ,lock_count = 0
aload_0
monitorenter ;mainLock() , lock_count = 1
aload_0
monitorenter ;subLock1() , lock_count = 2
; 1
aload_0
monitorexit ;subLock1() , lock_count = 1
aload_0
monitorenter ;subLock2() , lock_count = 2
; 2
aload_0
monitorexit ;subLock2() , lock_count = 1
aload_0
monitorexit ;mainLock() , lock_count = 0
monitorenterを実行するたびにlock_countは1増加します.monitorexitを実行するたびにlock_countは1減少します.
JVMは、lock_count=0の場合、他のスレッドがロックを取得する機会があります.したがって、subLock 1()とsubLock 2()の間に他のスレッドが介入することは不可能であり、mainLock()とsubLock 1()とsubLock 2()が持つのは同じロックである.