背景とか目的とか
最近、Clojure
をちょっと真面目に使おうと考えています。が、自分はJavaの経験が圧倒的に不足しています。お仕事で使ったことはありますが、それでもJavaの動作の仕組みなどについては曖昧なままで過ごしていました。そういう状態だと、Clojure
でトラブった時に勘所が分からなくて困るということがあります。(既に何回か遭遇しています)
あと、直近の業務でもJVMのことを知っていないと色々困ることがあるなぁと感じることが少なからずあるので観念してJVMについて勉強を始めようかと思いました。
今の自分は、JavaはJava仮想マシン(JVM)で実行されており、JVMは独自のアセンブラ(バイトコード)を持つスタックマシンのような仮想マシンです。javaのソースコードは、javacによってバイトコードに変換され、それをJVMが実行することで動作しています。というような認識を持っています。この理解の精度を高めたいというのが目的です。
理解が曖昧になっているところを書いておきます
- classpathの取り扱い
- バイトコードからシステムコールなどのネイティブな世界にどうやってアクセスしているのか
- JVM自体がバイトコードをどのように解釈して実行しているのか
- jpsなどでJavaの情報が収集出来る仕組みがあるがどうやっているのか
今回の調査で扱わないこと
- GCの詳細
- コンパイラの詳細
環境など
ホストOSはArch Linuxです。特にバージョンとかはないです。利用しているjavaのバージョンは以下の出力のとおりです。 sdkmanで、18-open
を指定してインストールしたものとなっています。
$ java -version
openjdk version "18" 2022-03-22
OpenJDK Runtime Environment (build 18+36-2087)
OpenJDK 64-Bit Server VM (build 18+36-2087, mixed mode, sharing)
class
ファイルについて
classファイルには1つのクラスしか定義できない仕様のようです。内部クラスなどにも対応してクラスファイルを作る必要があり、単一のソースコードから複数のclassファイルが出来ることもあるようです。
This chapter describes the class file format of the Java Virtual Machine.
https://docs.oracle.com/javase/specs/jvms/se18/html/jvms-4.html#jvms-4.1
Each class file contains the definition of a single class, interface, or module.
import java.io.Serializable;
public class TestInnerOuterClass {
class TestInnerChild{
}
Serializable annoymousTest = new Serializable() {
};
}
上記のようなソースコードをコンパイルすると、javac
は複数のファイルを出力します。
具体的には、以下の3つのファイルが出力されます。
'TestInnerOuterClass$1.class'
'TestInnerOuterClass$TestInnerChild.class'
TestInnerOuterClass.class
つまり、内部クラスに関しても1つのクラスとしてclass
ファイルが生成されます。
C言語などのコンパイラでは、翻訳単位(1つのc言語ソースファイルとかヘッダー)に対して1オブジェクトファイルが出来ますが、javaでは必ずしもそうでなく1つの翻訳単位に対して複数のファイルが生成されるということですね。また、クラスファイルにある $
はクラス名と内部クラスの名前を分離するためのセパレータ的なもののようです。
javap
コマンドを利用すると、classファイルの中身を読むことができます。
javap TestInnerOuterClass.class
Compiled from "TestInnerOuterClass.java"
public class TestInnerOuterClass {
java.io.Serializable annoymousTest;
public TestInnerOuterClass();
}
さらに、-v
オプションで詳細を読むことも出来ます。
$ javap -v TestInnerOuterClass.class
Classfile /home/sakura/sandbox/java-test/TestInnerOuterClass.class
Last modified 2022/07/28; size 473 bytes
MD5 checksum 9127bb6529da480495471b41a42aedf2
Compiled from "TestInnerOuterClass.java"
public class TestInnerOuterClass
minor version: 0
major version: 62
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Class #8 // TestInnerOuterClass$1
#8 = Utf8 TestInnerOuterClass$1
#9 = Methodref #7.#10 // TestInnerOuterClass$1."<init>":(LTestInnerOuterClass;)V
#10 = NameAndType #5:#11 // "<init>":(LTestInnerOuterClass;)V
#11 = Utf8 (LTestInnerOuterClass;)V
#12 = Fieldref #13.#14 // TestInnerOuterClass.annoymousTest:Ljava/io/Serializable;
#13 = Class #15 // TestInnerOuterClass
#14 = NameAndType #16:#17 // annoymousTest:Ljava/io/Serializable;
#15 = Utf8 TestInnerOuterClass
#16 = Utf8 annoymousTest
#17 = Utf8 Ljava/io/Serializable;
#18 = Utf8 Code
#19 = Utf8 LineNumberTable
#20 = Utf8 SourceFile
#21 = Utf8 TestInnerOuterClass.java
#22 = Utf8 NestMembers
#23 = Class #24 // TestInnerOuterClass$TestInnerChild
#24 = Utf8 TestInnerOuterClass$TestInnerChild
#25 = Utf8 InnerClasses
#26 = Utf8 TestInnerChild
{
java.io.Serializable annoymousTest;
descriptor: Ljava/io/Serializable;
flags:
public TestInnerOuterClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=4, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #7 // class TestInnerOuterClass$1
8: dup
9: aload_0
10: invokespecial #9 // Method TestInnerOuterClass$1."<init>":(LTestInnerOuterClass;)V
13: putfield #12 // Field annoymousTest:Ljava/io/Serializable;
16: return
LineNumberTable:
line 3: 0
line 8: 4
}
SourceFile: "TestInnerOuterClass.java"
Error: unknown attribute
NestMembers: length = 0x6
00 02 00 17 00 07
InnerClasses:
#7; //class TestInnerOuterClass$1
#26= #23 of #13; //TestInnerChild=class TestInnerOuterClass$TestInnerChild of class TestInnerOuterClass
class
ファイルに関しては一旦ここまでで、次に気になっているクラスのローダーについて調べていきます。
クラスローダについて
次に、Javaが実行時に class
ファイルを読み込む仕組みであるクラスローダについて次は見ていきます。Javaは実行時に読むclassファイルが3種類あるようです。
Bootstrap classes: ランタイムが使うクラス(rt.jar)や、i18n.jarなど。jre/lib/rt.jar
や jre/lib/
にあるファイル
Installed extensions: <JAVA_HOME>/jre/lib/ext
のファイル
The class path: 環境変数やコマンドライン引数などで与えられた、classpathから読みとるクラス
ただし、これらの区分けは参考にした資料が古く実情と合ってない気がするので追加で調査が必要そうです。
参考にしたリンクなどは以下です。
https://qiita.com/ysk24ok/items/57daed08592f7f8d1bea
https://docs.oracle.com/javase/8/docs/technotes/tools/findingclasses.html
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html
jarファイルの扱いについて
classファイルをまとめたものです。zip形式で unzip
などで展開できます。MANIFEST.MF
、ResourceBundle
などの情報もまとめることが出来る。以下のように仕様が定められています。
https://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/ResourceBundle.html
java -classpath foo.jar
で読むときと java -jar foo.jar
で動作が異なることに注意が必要です。jarファイルを読むと、classpathにjarファイル内部のclassfilesがclasspathに追加されるような動作になるようです。-jar
オプションを使った場合は、MANIFEST.MF などを読みmainの場所などを特定して実行してくれます。また、java -jarの時は、特別な動作をするようです。MANIFEST.MFを読みに行って、そこに書いてあるメタデータを使って諸々の設定を行なうようです。ここに Class-Path
といった値を書くことが出来ます。一方で、-jar
を利用する場合は -cp
が無視されます。.properties
ファイルに関する仕組みもこの辺りの仕様を抑えておく必要があります。
jarファイルの作りかた
https://www.ne.jp/asahi/hishidama/home/tech/java/jar.html
独自のクラスローダー
ClassLoaderは自分で作ることも出来て、実行時に自由にクラスを呼んで動作を変更したりといったことも実現出来るようです。URLClassLoader
を継承して作るのが良いようですね。
https://docs.oracle.com/javase/jp/18/docs/api/java.base/java/net/URLClassLoader.html
Java標準のクラス達の在処
<JAVA_HOME>/lib
の下を確認してみると、jarファイルやclassファイルが無く、soファイルばかりがありました。
いろいろ調べてみると、modules
というファイルがあり、これに格納されているようです。jimage extract modules
することで展開でき、この中に起動時に読み込むclassファイルたちを確認できました。この辺はjdkのバージョンなどによっても違うようです。
パッケージとディレクトリなどについて
Javaだとパッケージの区分けとしてディレクトリを使うことが一般的です。javac
としては特にソースコードがパッケージの名前とディレクトリが一致していなくてもコンパイル出来たりします。(-d
を付けるとディレクトリと一致するようにclass
ファイルが生成されるようです)
package jp.other;
public class OtherClass {
static public void hello() {
System.out.println("Hello, Other world.");
}
}
このようなソースコードがあった場合、javac OtherClass.java
を実行すると
$ javac OtherClass.java
$ ls -l
合計 8K
-rw-r--r-- 1 sakura sakura 418 8月 1 13:08 OtherClass.class
-rw-r--r-- 1 sakura sakura 125 8月 1 12:55 OtherClass.java
このようにカレントディレクトリに、class
ファイルは生成されます。
一方、-d
を付けて実行してみると
$ javac -d target OtherClass.java
$ tree
.
├── OtherClass.java
└── target
└── jp
└── other
└── OtherClass.class
3 directories, 2 files
上記のように、パッケージ名に合わせてディレクトリが作られてコンパイル結果が保存されます。
また、依存関係のある複数ファイルのソースコードをコンパイルする場合であっても、javac
コマンドに両方のファイルを渡してしまえば上手くコンパイルしてくれます。
class HelloWorld {
public static void main(String[] args) {
jp.other.OtherClass.hello(); // 別パッケージのメソッドを呼びだす
System.out.println("Hello, world.");
}
}
package jp.other; // 全然違うパッケージを指定
public class OtherClass {
static public void hello() {
System.out.println("Hello, Other world.");
}
}
この2つのファイルを javac HelloWorld.java OtherClass.java
としてまとめてコンパイルするとコンパイル出来ます。HelloWorld.java
単体だとコンパイルに失敗します。
しかし、実行を行なう際にはデフォルトのクラスローダは指定したclasspathとパッケージ名のprefixからclass
ファイルを見つける動作をします。従って、結局パッケージの名前に合わせたディレクトリを作成し、そこにclasss
ファイルをコピーするといった作業が必要になります。
このあたりの動作は以下のリンクに少し言及があります。ただ、この動作が必ずJavaの仕様かというとそうでもなさそうです(Typicallyですし)
Typically, a class or interface will be represented using a file in a hierarchical file system, and the name of the class
https://docs.oracle.com/javase/specs/jvms/se18/html/jvms-5.html#jvms-5.3.1
or interface will be encoded in the pathname of the file to aid in locating it.
パッケージ名と全く関係無いディレクトリ構成でソースを管理することは不可能ではなさそうですが、深い理由が無い限りはパッケージ名と一致した形のディレクトリ管理を行なう方が良さそうです。
長くなったので続きは次回に……。