Androidオブジェクトの実際のサイズ📏


Header image: Deep Dive by Romain Guy.


私は現在どのようにリークカナリアcomputes the retained heap size of objects . クイックリマインダーとして:
オブジェクトの浅いヒープサイズ:メモリ内のオブジェクトサイズ.
オブジェクトのヒープサイズを保持します.オブジェクトの浅いサイズに加え、そのオブジェクトだけでメモリ内で一時的に保持されるすべてのオブジェクトの浅いサイズに加えます.つまり、そのオブジェクトがガベージコレクションされているときに解放されるメモリ量です.

浅いサイズを信頼できない


その仕事の一部として、私はYourKit、Eclipse Memory Analyzer Tools(MAT)とAndroid Studio Memory Analyzerなどの他のヒープダンプツールで報告されているオブジェクトの浅いサイズを比較しました.それは私が何かが間違って実現したときです:すべてのツールは、別の答えを提供します.
私はそれについてジェシー・ウィルソンに尋ねました、そして、彼はアレクシーShipilWhat Heap Dumps Are Lying To You About . いくつかの覚醒:
  • すべてのJava VMは、そのメモリを少し異なる方法でレイアウトし、様々な最適化を実行します.
  • ヒープダンプ形式(. hprof )はstandard . クラスダンプレコードには、クラスのインスタンスサイズと同様に、フィールドとその型のリストが含まれます.Alksey Shipil - v . vはインスタンスサイズがメモリのインスタンスの実際のサイズであることを尋ねましたthe answer was nope : hprofダンプのサイズはVMとパディング独立であり、消費ツールからの期待を破るのを避けるためです.
  • 道具があるJOL JVMランタイムは、実際のオブジェクトのサイズを報告します.Alekseyは、HPROFベースのツールで報告されたサイズを実際のサイズと比較するために使用して、彼らが異なる方法ですべて間違っていたとわかりました.

  • インExploring Java's Hidden Costs , ジェイクWhartonは、Julを使う方法を示しました.残念なことに、JOLはJVMSだけでなく、DalvikやArt Runtimeで動作します.ジェイクを引用する

    In this case, because the classes are exactly the same and the JVM 64 bit, and Android's now 64 bit, the number should be translatable. If that bothers you, treat them as an approximation and allow for some 20% variance. It's certainly a lot easier than figuring out how to get the object sizes on Android itself. Which is not impossible, it's just a lot easier this way.


    ダム、私たちが持っているすべてのリークキャラはヒープダンプです.我々は簡単な方法を行く必要がありますね!
    私はロマン人にこれについて尋ねました、そして、彼はJVM TI , Android 8 +に実装されているエージェントインターフェイスART TI . JVM Ti露出GetObjectSize API.

    ソースを読む



    Learn to Read the Source, Luke - Coding Horror


    Androidはオープンソースなので、いつも私たちの質問に対する答えを見つけることができます.限り、我々は検索する場所を知っている限り!

    JVMチタン


    ここでの実装ですJVM TI GetObjectSize() :
    jvmtiError ObjectUtil::GetObjectSize(env* env ATTRIBUTE_UNUSED,
                                         jobject jobject,
                                         jlong* size_ptr) {
      art::ObjPtr<art::mirror::Object> object = 
          soa.Decode<art::mirror::Object>(jobject);
    
      *size_ptr = object->SizeOf();
      return ERR(NONE);
    }
    
    興味深いコードはObject::SizeOf() :
    template<VerifyObjectFlags kVFlags>
    inline size_t Object::SizeOf() {
      size_t result;
      constexpr VerifyObjectFlags kNewFlags = RemoveThisFlags(kVFlags);
      if (IsArrayInstance<kVFlags>()) {
        result = AsArray<kNewFlags>()->template SizeOf<kNewFlags>();
      } else if (IsClass<kNewFlags>()) {
        result = AsClass<kNewFlags>()->template SizeOf<kNewFlags>();
      } else if (IsString<kNewFlags>()) {
        result = AsString<kNewFlags>()->template SizeOf<kNewFlags>();
      } else {
        result = GetClass<kNewFlags, kWithoutReadBarrier>()
            ->template GetObjectSize<kNewFlags>();
      }
      return result;
    }
    

    インスタンスサイズ


    最後のインスタンスサイズに注目しましょうelse その条件で.オブジェクトがインスタンスである場合、そのサイズはGetClass()->GetObjectSize() を返すobject_size_ in class.h :
    // Total object size; used when allocating storage on gc heap.
    // (For interfaces and abstract classes this will be zero.)
    // See also class_size_.
    uint32_t object_size_;
    

    インスタンス割り当て


    これを実際に使用しているメモリのインスタンスの大きさをチェックしてみましょう.我々はそれを見つけるobject_size_Class::Alloc() in class-alloc-inl.h
    template<bool kIsInstrumented, Class::AddFinalizer kAddFinalizer, 
        bool kCheckAddFinalizer>
    inline ObjPtr<Object> Class::Alloc(Thread* self, 
        gc::AllocatorType allocator_type) {
      gc::Heap* heap = Runtime::Current()->GetHeap();
      return heap->AllocObjectWithAllocator<kIsInstrumented, false>(
          self, this, this->object_size_, allocator_type,
          VoidFunctor()
      );
    }
    
    したがって、オブジェクトに割り当てられた実際のメモリはobject_size_ in class.h .

    Note: the memory allocated by Heap::AllocObjectWithAllocator() in heap-inl.h might be rounded up to a multiple of 8 when using a Thread-local bump allocator (TLAB, see Trash Talk by Chet Haase and Romain Guy). However the default CMS GC does not use that allocator.


    クラスリンク


    より多くの用法を見たobject_size_ 我々はそれが設定されてClassLinker::LinkFields() in class_linker.cc クラスのリンク
    bool ClassLinker::LinkFields(Thread* self,
                                 Handle<mirror::Class> klass,
                                 bool is_static,
                                 size_t* class_size) {
      MemberOffset field_offset(0);
    
      ObjPtr<mirror::Class> super_class = klass->GetSuperClass();
      if (super_class != nullptr) {
        field_offset = MemberOffset(super_class->GetObjectSize());
      }
    
      // ... code that increases field_offset as fields are added
    
      size_t size = field_offset.Uint32Value();
      klass->SetObjectSize(size);
    
      return true;
    }
    

    ヒープダンプに戻る


    インスタンスの実際のサイズを取得する方法を知ったので、Androidヒープダンプで報告されているインスタンスサイズと比較しましょう.

    ヒープダンプをトリガーするときDebug.dumpHprofData() , VM呼び出しDumpHeap() in hprof.cc . 見ましょうHprof::DumpHeapClass() , より具体的にはinstance size of a class is added :
    // Instance size.
    if (klass->IsClassClass()) {
      // As mentioned above, we will emit instance fields as
      // synthetic static fields. So the base object is "empty."
      __ AddU4(0);
    } else if (klass->IsStringClass()) {
      // Strings are variable length with character data at the end 
      // like arrays. This outputs the size of an empty string.
      __ AddU4(sizeof(mirror::String));
    } else if (klass->IsArrayClass() || klass->IsPrimitive()) {
      __ AddU4(0);
    } else {
      __ AddU4(klass->GetObjectSize());  // instance size
    }
    
    最後のelse その条件では、ほとんどのインスタンスのインスタンスサイズであり、もう一度object_size_ in class.h .
    したがって、オープンJDKヒープダンプとは異なり、Androidのヒープダンプには実際のメモリインスタンスのサイズが含まれます.

    ヒープダンプ記録の探索


    インExploring Java's Hidden Costs , JakeはJulからの出力を示しましたandroid.util.SparseArray :
    android.util.SparseArray object internals:
    SIZE     TYPE DESCRIPTION 
    4        (object header)
    4        (object header)
    4        (object header)
    4        int SparseArray.mSize
    1        boolean SparseArray.mGarbage
    3        (alignment/padding gap)
    4        int[] SparseArray.mKeys
    4        Object[] SparseArray.mValues
    4        (loss due to the next object alignment)
    
    Instance size: 32 bytes
    
    リークキャリーヒープダンプパーサーを使いましょうShark ) Androidヒープダンプレポートを参照するには、次の手順に従います.
    val hprofFile = "heap_dump_android_o.hprof".classpathFile()
    val sparseArraySize = hprofFile.openHeapGraph().use { graph ->
      graph.findClassByName("android.util.SparseArray")!!.instanceByteSize
    }
    println("Instance size: $sparseArraySize bytes")
    
    結果:
    Instance size: 21 bytes
    
    いいね、Julによって報告された32バイトより少ない方法です!
    報告された分野の詳細を見てみましょう.
    val description = hprofFile.openHeapGraph().use { graph ->
      graph.findClassByName("android.util.SparseArray")!!
          .classHierarchy
          .flatMap { clazz ->
            clazz.readRecord().fields.map { field ->
              val fieldSize = if (field.type == REFERENCE_HPROF_TYPE)
                graph.identifierByteSize
              else
                byteSizeByHprofType.getValue(field.type)
              val typeName =
                if (field.type == REFERENCE_HPROF_TYPE)
                  "REF"
                else
                  primitiveTypeByHprofType.getValue(field.type).name
              val className = clazz.name
              val fieldName = clazz.instanceFieldName(field)
              "$fieldSize $typeName $className#$fieldName"
            }.asSequence()
          }.joinToString("\n")
    }
    println(description)
    
    結果:
    1 BOOLEAN android.util.SparseArray#mGarbage
    4 REF android.util.SparseArray#mKeys
    4 INT android.util.SparseArray#mSize
    4 REF android.util.SparseArray#mValues
    4 REF java.lang.Object#shadow$_klass_
    4 INT java.lang.Object#shadow$_monitor_
    
    したがって、すべてのSparseRayインスタンスは21バイトの浅いサイズを持ちます.そして、それはオブジェクト・クラスから8バイトとそれ自身のフィールドのために13バイトを含みます.と0バイト無駄!

    ギャップとアライメント


    ClassLinker::LinkFields() in class_linker.cc メモリ内のすべてのフィールドの位置を決定します.
  • 最初のNバイトは、親クラスに基づいて親クラスのフィールド値を格納するために使用されますClass::GetObjectSize() . Nは何か、奇数でさえありえました.親クラスがギャップ(未使用のバイト)を持つ場合、これらは触れません.
  • それから、フィールドは挿入されて、それらのサイズに整列します:longsは8バイト整列しています、そして、INTSは4バイト整列しています.
  • 挿入順序は最初に参照し、最初に最大のフィールドを持つプリミティブフィールドです.例えばreference then long then int then char then boolean .
  • フィールドは自分のサイズで整列しなければならないので、隙間があるかもしれません.以下は32ビットARM VMの例です.
  • open class Parent {
      val myChar = 'a'
      val myBool1 = true
      val myBool2 = false
    }
    
    class Child : Parent() {
      val ref1 = Any()
      val ref2 = Any()
      val myLong = 0L
    } 
    
    # java.lang.Object is 8 bytes
    4 REF     java.lang.Object#shadow$_klass_
    4 INT     java.lang.Object#shadow$_monitor_
    # com.example.Parent is 8 + 3 = 11 bytes
    2 CHAR    com.example.Parent#myChar
    1 BOOLEAN com.example.Parent#myBool1
    1 BOOLEAN com.example.Parent#myBool2
    # com.example.Child is 11 + 21 = 32 bytes
    1 GAP for 4 byte alignment for refs
    4 REF     com.example.Child#ref1
    4 REF     com.example.Child#ref2
    4 GAP for 8 byte alignment for long
    8 LONG    com.example.Child#myLong
    
    ヒアcom.example.Child は、5バイトを含む32バイトで、フィールドアライメントに無駄です.
  • フィールドが既存のギャップに収まると、そのフィールドは前方に移動します.
  • open class Parent {
      val myChar = 0
      val myBool1 = true
      val myBool2 = false
    }
    
    class Child : Parent() {
      val ref1 = Any()
      val ref2 = Any()
      val myLong = 0L
      // Added myInt and myBool3
      val myInt = 0
      val myBool3 = true
    } 
    
    # java.lang.Object is 8 bytes
    4 REF     java.lang.Object#shadow$_klass_
    4 INT     java.lang.Object#shadow$_monitor_
    # com.example.Parent is 8 + 3 = 11 bytes
    2 CHAR    com.example.Parent#myChar
    1 BOOLEAN com.example.Parent#myBool1
    1 BOOLEAN com.example.Parent#myBool2
    # com.example.Child is 11 + 21 = still 32 bytes!
    1 BOOLEAN com.example.Parent#myBool3 (1 byte gap)
    4 REF     com.example.Child#ref1
    4 REF     com.example.Child#ref2
    4 INT     com.example.Child#myInt (4 byte gap)
    8 LONG    com.example.Child#myLong
    
    この例では、intとbooleanChild インスタンスのサイズは変更されません.

    結論

  • オープンJDKヒープダンプとは異なり、Androidヒープダンプには、実際のインスタンスのメモリサイズが含まれます.
  • 我々は、最新の実装では、インスタンスサイズを見ただけです.クラスサイズと配列サイズについて同様の調査をすることは興味深いでしょう.また、Dalvikで同じ結果を見ることができます.
  • Androidのソースを読む楽しみです!たとえ私のように、あなたはC +がどのように働くか全くわからない.コメント、シンボル名とGit史は、それを理解するのに十分な詳細を通常提供します.
  • ロマン・ガイ、ジェシー・ウィルソンとアーテム・チュバリアンに感謝し、多くのポインターを私に送ってくれた.