質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.50%
Java

Javaは、1995年にサン・マイクロシステムズが開発したプログラミング言語です。表記法はC言語に似ていますが、既存のプログラミング言語の短所を踏まえていちから設計されており、最初からオブジェクト指向性を備えてデザインされています。セキュリティ面が強力であることや、ネットワーク環境での利用に向いていることが特徴です。Javaで作られたソフトウェアは基本的にいかなるプラットフォームでも作動します。

オブジェクト

オブジェクト指向において、データとメソッドの集合をオブジェクト(Object)と呼びます。

Q&A

解決済

2回答

21408閲覧

Javaで同一内容のオブジェクトの使用メモリ量が環境により異なる理由

longcat

総合スコア11

Java

Javaは、1995年にサン・マイクロシステムズが開発したプログラミング言語です。表記法はC言語に似ていますが、既存のプログラミング言語の短所を踏まえていちから設計されており、最初からオブジェクト指向性を備えてデザインされています。セキュリティ面が強力であることや、ネットワーク環境での利用に向いていることが特徴です。Javaで作られたソフトウェアは基本的にいかなるプラットフォームでも作動します。

オブジェクト

オブジェクト指向において、データとメソッドの集合をオブジェクト(Object)と呼びます。

2グッド

4クリップ

投稿2021/02/28 16:54

編集2021/03/02 17:41

疑問点の要約

例えば、"山田太郎"という文字列を表すString型のインスタンスのメモリ使用量は同一バージョンのJVM上では同じになると思っていたが、ライブラリを用いて測定したところ、プログラムを実行する環境により異なっていた、なぜ違いが生じたのか?

質問に至る背景、経緯

・Javaプログラムでメモリ使用量の削減を行いたい。
・実際のメモリ使用量を測る(現状のメモリ使用量、コード修正による効果の測定)ため、java-sizeof等のインスタンスのメモリ使用量を調べるライブラリを使用した。
・測定を行う中で、同一内容のデータオブジェクトであってもjava-sizeof等のライブラリが出力する使用メモリの値はプログラムを実行する環境により異なる、ということに気づいた。
・当初の目的はメモリ使用量の削減だが、そのためには「自分が見ているものが何なのか?」という理解も必要と考え、この結果となる理由を知りたいと思ったが、いまいちどういった方面から調べると答えにたどり着けるのかがわからなかった。

実験に使用したコード

Java

1 String str1 = "abcdefghij"; 2 3 System.out.println("str1 = \"abcdefghij\" size(byte) -> " 4 + RamUsageEstimator.sizeOf(str1)); 5 6 7 String str2 = "山田太郎"; 8 9 System.out.println("str2 = \"山田太郎\" size(byte) -> " 10 + RamUsageEstimator.sizeOf(str2));

結果

<環境A> str1 = "abcdefghij" size(byte) -> 64 str2 = "山田太郎" size(byte) -> 48 <環境B> str1 = "abcdefghij" size(byte) -> 80 str2 = "山田太郎" size(byte) -> 64

環境の違い

環境A:
WindowsOS(64bit)上のEclipseから実行>Javaアプリケーションを使用して実行。

環境B:
WindowsOS(64bit)上でビルド済みjarをコマンドライン上からjavaコマンドを使用して起動。

環境A、Bどちらもマイナーバージョンまで同じOracleのJava8のJDKを使用。

(環境について、どのような情報を記載すると回答が得られそうか、
ハードウェアなのか、ソフトウェアなのか、
それともそのような情報はあまり要らないのか…、
ということが今一つわからないため、この程度しか書けないのですが、
回答にはこのような情報が必要、ということがあれば教えてください。)

Javaコマンドのオプションによる結果の違い(追記)

環境により結果が変わると思っていたのですが、
同じ環境でもjavaコマンドのオプションで-Xms32g -Xmx64gをつけるかどうかで、
結果が変わるということがわかりました。
具体的には環境Bでつけていた場合は

>java -jar -Xms32g -Xmx64g *****.jar str1 = "abcdefghij" size(byte) -> 80 str2 = "山田太郎" size(byte) -> 64

となるのですが、
環境Bでもつけていない場合、

>java -jar *****.jar

>java -jar -Xms4g -Xmx8g *****.jar

では、

str1 = "abcdefghij" size(byte) -> 64 str2 = "山田太郎" size(byte) -> 48

となり、環境Aと同じになりました。
なので、もしかしたら環境の違いではなく、こちらが本質なのかもしれません。
逆に環境Aで-Xms32g -Xmx64gを指定したらどうなるのかは、
そんなにメモリを搭載していないため、確認できていません。

メモリ使用量測定に使用したライブラリ(pom.xmlの記載)

<dependency> <groupId>com.carrotsearch</groupId> <artifactId>java-sizeof</artifactId> <version>0.0.5</version> </dependency> <dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.14</version> <scope>provided</scope> </dependency>

(上記のコード例ではjava-sizeofの方を記載しましたが、jol-coreを使用した場合も結果は同じ値になりました。)

System.out.println(System.getProperty("os.arch"))の出力結果

(情報追加依頼により追記)
System.out.println(System.getProperty("os.arch"));
の出力結果は環境A、Bともに同じで、"amd64"でした。

知りたいこと

1.環境により同じデータのインスタンスであっても異なるメモリ使用量となるのは「普通のこと」なのか?それとも、「本来は同じになるはず」…だが、自分の何らかのミス、勘違いにより異なっているのか?

2.上記1が「普通のこと」であった場合、「なぜ出力される値は異なるのか?」。そうでなく、「本来は同じになるはず」の場合、異なっているのにはどのような原因が考えられるのか。

3.java-sizeof等のライブラリは何の値を出力しているのか?「実際のメモリ使用量」を出力するものなのか?

4.何らかの要素(パラメータ?)により、同一内容のインスタンスでも使用メモリ量が変動するのだとしたら、明示的にそれらを指定することによりメモリ使用量をコントロールできないか?

5.仮に環境ごとに使用メモリ量が異なるのは仕方のないことだったとして、環境Aでインスタンスの使用メモリ量(としてjava-sizeof等のライブラリが出力する値)が減少するようなプログラム上の修正を行えば、それは環境Bでもメモリ量削減に有効なのか。ある環境で「のみ」有効であったり、環境Aでは有効なものが環境Bでは逆効果、というように「逆転」したりすることは無い、と考えてよいものなのか(環境により結果がことなることから生じている不安)。

TN8001, mira_tech👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

quickquip

2021/03/01 11:28

両環境で System.out.println(System.getProperty("os.arch")); の結果が違ったりしませんか?
longcat

2021/03/01 14:21

コメントありがとうございます。後ほど確認して追記いたします。
longcat

2021/03/01 17:13

追記しました。 System.out.println(System.getProperty("os.arch")); の出力結果は環境A、Bともに同じで、"amd64"でした。
guest

回答2

0

ベストアンサー

究極のところ https://repo1.maven.org/maven2/com/carrotsearch/java-sizeof/0.0.5/ の sources.jar から実装を読んで、何がどう計算されているのか全部追えばいいと思いました。

ざっと見た感じの範囲だと、違いが起きそうな大きなファクターは、非staticなフィールドに違いがあるか? とJREが64bitかどうか? のように見えました。
という理由からシステムプロパティのos.archを確認してみたらと思ったのですが、実際のコードでは内部実装に属するクラスのsun.misc.Unsafeから取得して判定していました。
ひとまず、以下のコードあたりを試して違いを比べてみてはいかがでしょうか。

java

1import java.lang.reflect.Field; 2import java.lang.reflect.Modifier; 3 4public class GetNonStaticFieldsOfString { 5 public static void main(String[] args) throws Exception { 6 Field[] fields = String.class.getDeclaredFields(); 7 for (final Field f : fields) { 8 Class<?> type = f.getType(); 9 if (!Modifier.isStatic(f.getModifiers())) { 10 System.out.println(f.getName() + " " + f.getType()); 11 } 12 } 13 14 final Class<?> unsafeClass = Class.forName("sun.misc.Unsafe"); 15 final Field unsafeField = unsafeClass.getDeclaredField("theUnsafe"); 16 unsafeField.setAccessible(true); 17 final Object unsafe = unsafeField.get(null); 18 final int addressSize = ((Number) unsafeClass.getMethod("addressSize").invoke(unsafe)).intValue(); 19 System.out.println("is64bit:" + (addressSize >= 8)); 20 } 21}

ここで違いがないと、各フィールドの要素に対して、実際のコードと同じようにsun.misc.Unsafeを使って情報を取りつつ計算していくコードを書いて、検証していく必要があると思います。


プリミティブデータの大きさは言語仕様で決まっているので、(非staticフィールドが同じなのに)違いがあるなら、メモリのアラインメントの影響が一番"ありそう"です。

32bit JREと64bit JREではメモリのアラインメントが違って、推定サイズが変わるのは間違いないでしょう。
それ以外で(同じバージョンのJREで)メモリのアラインメントに違いがでることがあるか? となると不明です。上記の通り、java-sizeofのコードを追いかけつつ違いを検証していくことになるかと思います。


(追記)
64ビット環境だと「あるオブジェクトのアドレス」を表すのには本来64ビットのメモリスペースが必要なわけですが、"ヒープの最大サイズが設定されたらそのアドレス以降にデータが置かれることがあり得ない"とか、"64ビット単位でアラインされてデータが置かれる"とかいった状況があるため(ヒープの最大サイズが十分に小さければ)実際にはもっと小さいメモリスペースで済ませれます。
64ビットOpenJDK実装には、ヒープの最大が32GB以下である時に限り「あるオブジェクトのアドレス」を表すデータ構造が32ビットで済むような仕組みが入っているようです。
https://www.oracle.com/technetwork/jp/articles/java/compressedoops-427542-ja.html をコメントにて教えていただきました)
おそらくはその仕組みのためと思われますが、ヒープの最大サイズが32GB以下か32GBより大きいかで、返ってくる数値が変わるという現象が確認できます。

投稿2021/03/02 00:30

編集2021/03/08 03:08
quickquip

総合スコア11029

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

longcat

2021/03/02 17:32

ありがとうございます。 やはり、最終的にはライブラリのソースコードを確認するしかないですよね。 ご提示いただいたコードを試してみたのですが、どちらの環境も同じで、 value class [C hash int is64bit:true と出力されました。 その後、気づいたことがありまして、 環境により結果が変わると思っていたのですが、 同じ環境でもjavaコマンドのオプションで-Xms32g -Xmx64gをつけるかどうかで、 結果が変わるということがわかりました。 具体的には環境Bでつけていた場合は >java -jar -Xms32g -Xmx64g *****.jar str1 = "abcdefghij" size(byte) -> 80 str2 = "山田太郎" size(byte) -> 64 となるのですが、 環境Bでもつけていない場合、 >java -jar *****.jar や >java -jar -Xms4g -Xmx8g *****.jar では、 str1 = "abcdefghij" size(byte) -> 64 str2 = "山田太郎" size(byte) -> 48 となり、環境Aと同じになりました。 なので、もしかしたら環境の違いではなく、こちらが本質なのかもしれません。 逆に環境Aで-Xms32g -Xmx64gを指定したらどうなるのかは、 そんなにメモリを搭載していないため、確認できていません。
dameo

2021/03/03 11:54

quickquipさんのおっしゃるようにライブラリのコードを見るのが手っ取り早いと思います。 出力が64,48のとき、String型は NUM_BYTES_OBJECT_HEADER: 12bytes name: {offset: 12, length: 4}: char[] 参照 hash: {offset: 16, length: 4}: int (padding): 4bytes のような配置で、中身のchar[]が NUM_BYTES_ARRAY_HEADER: 16bytes sizeof(char) * len: len * 2 bytes (padding): 0~6bytes のようになっているようです。ざっくり言って、ヒープ最大32GBのときと64GBのときで、各HEADERのサイズが違っているのではないかと思います。 RamUsageEstimator.NUM_BYTES_OBJECT_HEADER RamUsageEstimator.NUM_BYTES_ARRAY_HEADER の2つがそれぞれ該当しますので、64GB環境でそれを見てみるのがいいかと思います。 java.sizeofを使用しないで簡単に書いたコードだと以下のような調査になります。 import java.lang.reflect.Field; import java.lang.reflect.Method; public class Sample { private static class Dummy { public byte field; } public static void main(String[] args) throws Exception { Class<?> unsafeClass = Class.forName("sun.misc.Unsafe"); final Field unsafeField = unsafeClass.getDeclaredField("theUnsafe"); unsafeField.setAccessible(true); Object theUnsafe = unsafeField.get(null); final Method arrayBaseOffsetM = unsafeClass.getMethod("arrayBaseOffset", Class.class); int NUM_BYTES_ARRAY_HEADER = ((Number) arrayBaseOffsetM.invoke(theUnsafe, byte[].class)).intValue(); final Method objectFieldOffsetM = unsafeClass.getMethod("objectFieldOffset", Field.class); final Field field = Dummy.class.getDeclaredField("field"); int NUM_BYTES_OBJECT_HEADER = ((Number) objectFieldOffsetM.invoke(theUnsafe, field)).intValue(); System.out.println(NUM_BYTES_ARRAY_HEADER); System.out.println(NUM_BYTES_OBJECT_HEADER); } } このライブラリ5年くらい前のもので、しかもメンテナンスされてないように見えるので、あんまり頑張っても報われないかもしれませんが… 同名のクラスが別のプロジェクトで生き延びて割と違うコードになっているように思います。 https://github.com/apache/lucene-solr/blob/master/lucene/core/src/java/org/apache/lucene/util/RamUsageEstimator.java
longcat

2021/03/03 15:12

dameoさん、ありがとうございます。 ご提案いただいた方法で確認したところ、確かに、 java -jar *****.jar のときは、 NUM_BYTES_ARRAY_HEADER:16 NUM_BYTES_OBJECT_HEADER:12 java -jar -Xms32g -Xmx64g *****.jar のときは、 NUM_BYTES_ARRAY_HEADER:24 NUM_BYTES_OBJECT_HEADER:16 となりました。 ちなみに教えていただいた、 lucene-coreのRamUsageEstimator.sizeOfメソッドを使用した場合も、 java-sizeofのRamUsageEstimator.sizeOfメソッドと出力される値自体は同じでした。 異なった値が出力される理由はわかってきましたが、 「JVMのメモリ割り当てを大きくすると個々のインスタンスのサイズも大きくなり、 メモリ使用量が大きくなる」ということなんでしょうか。 「なぜそういう仕様なのか?」という新たな疑問が湧いてきます。 もしも、お判りになるようでしたら、(dameoさん以外の方でも) 教えていただけましたら幸いです。 当初の疑問は解決しつつあり、別の疑問になってきているので、 この質問は解決済みにして、聞くにしても別の質問にするべきだろうか…、 とも思いつつ。
quickquip

2021/03/04 12:37 編集

配列(Stringは内部にbyteの配列をデータとして保管します)の理論上の長さの上限は2^31-1です。 が、ヒープが256MBしかない状態だとそんな長さの配列は確保できません。 ヒープがある程度より小さい時は巨大な配列を作れない、つまり現実に作れる配列の大きさが理論上限より小さいのですから、「配列を管理するために必要な情報」が小さくなること自体はそんなに不思議な感じはしません。(今見えている数値が妥当なのかまでは考えてません) https://programming.guide/java/array-maximum-length.html あたりからJavaの実装が追えそうです。もし気になるなら、Javaの実装に踏み込むことになりますね。
longcat

2021/03/03 16:31

quickquipさん、ありがとうございます。 なるほど。メモリ割り当てが大きくなるとサイズが大きくなる(そう思っていました)というよりは、 逆に本来の理論上限は大きいんだけれども、メモリ割り当てが小さい場合に 持つことができないために小さく制限されている、ということですね。
quickquip

2021/03/04 12:40

メモリ割り当てが小さいならば不要なメモリを使わない=省メモリで動くような工夫がある、というのが私の解釈です。
dameo

2021/03/04 21:44

「なぜそういう仕様なのか?」は知りません。 lucene-coreの当該実装を追えばすぐ分かるとおり、UseCompressedOopsが有効になっているかどうかの問題かと思います。UseCompressedOopsの説明については以下のとおりです。 https://www.oracle.com/technetwork/jp/articles/java/compressedoops-427542-ja.html 先に貼り付けたコードをSample.javaで保存、コンパイルし、以下で確認できます。 $ java -XX:-UseCompressedOops Sample 24 16 $ java -XX:+UseCompressedOops Sample 16 12 $
longcat

2021/03/05 00:21

quickquipさん dameoさん ありがとうございます。 当初の疑問も解消し、知らなかった有益な情報も得ることができましたので、ベストアンサー選択して解決済みにしたいと思います。
quickquip

2021/03/05 00:58 編集

dameoさん https://www.oracle.com/technetwork/jp/articles/java/compressedoops-427542-ja.html を教えていただいてありがとうございます。 > これによって、(略)最大約32GBのヒープ・サイズに対応できます。 同時に、データ構造はILP32モードと匹敵するほどにコンパクトです。 -Xmx32GB と -Xmx33GB の間に境界があることは実験的に判明していたので、そこに境界があるのであれば内部的にこういことが行われているんだろうなぁ、とぼんやり想像していたことがだいたい確認できました。
guest

0

RamUsageEstimatorの説明読んだ感じ

This class uses assumptions that were discovered for the Hotspot virtual machine. If you use a non-OpenJDK/Oracle-based JVM, the measurements may be slightly wrong.

Google翻訳:このクラスは、Hotspot仮想マシンで検出された仮定を使用します。 OpenJDK / Oracleベース以外のJVMを使用している場合、測定値が少し間違っている可能性があります。

らしいです。
あくまで参考値としたほうが良いかもしれません。

最小構成で確認するのはもちろん良いのですが、
実際のアプリケーションは複雑な処理の組み合わせで成り立っているため、
よりそれに近い形を試した上で、適切にメモリ開放している場合とそうでない場合の測定値を比べた方が
本来の目的である「メモリ使用量の削減」に繋がるように思います。

投稿2021/02/28 21:29

m.ts10806

総合スコア80765

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

longcat

2021/03/01 14:21

回答ありがとうございます。 ご意見参考にさせていただきます。 >OpenJDK / Oracleベース以外のJVMを使用している場合、測定値が少し間違っている可能性があります。 OracleのJDKを使用しており、同一バージョンのJVMを使用しているので、 それでもなるのかなあ…?という感じではありますが、 >Hotspot仮想マシンで検出された仮定 …というのが何なのかをきちんと理解しないと、 正確に何が出力されているのかを知ることは難しいようですかね。 ライブラリのコードを読んで理解出来たらよいのですが、 ちょっと私にはすぐに理解するのは難しそうに感じました。 >実際のアプリケーションは複雑な処理の組み合わせで成り立っているため、 実際処理を行っている際のメモリについても 別途検討の余地はあるかとは思っているのですが、 今回は、まずデータをメモリ上に読み込んでおく処理があり、 その扱うデータが大量になることも想定されているため、 「処理の実行以前に、データ読み込み時にどうしても発生してしまう ベースとなるインスタンスのメモリ使用量、 単純に大量データを読み込んだ際のメモリ使用量をなるべく圧縮、 削減したい」 と考え、インスタンスのメモリ使用量を削減できないか、 という方向から検討しております。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問