JVMプログラミング(Oolong)学習ノート


最近Engelの『Programming for the Java Virtual Machine』を勉強します.この本は実際にJVMの原理とメカニズムを話しているが,アセンブリ言語の形式で説明しているにすぎない.Oolongは大牛Engelさん自身が発明したJVMベースのアセンブリ言語で、彼自身の紹介によると、Oolongは実際にはJVM bytecodeの理解しやすい教育バージョンにすぎず、それ自体は開発応用の価値を備えていないようだ.
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:
サブクラスのでは、スーパークラスのの呼び出しはinvokespecial方式を採用する必要があります.論理は簡単で、invokevirtual方式でスーパークラスのconstructorを呼び出すと、jvmは既存の配布方式で呼び出され、サブクラスのに戻り、性能は無限に再帰的にループします.
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()が持つのは同じロックである.