6LeakCanary2Analyze

Procedure

graph TB
subgraph binarySearch二分查找
SortedBytesMap
end

subgraph 广度优先遍历
State.findPathsFromGcRoots:BFS
end

HeapAnalyzer.analyze-->Hprof.open:HprofFile
HeapAnalyzer.analyze-->HprofHeapGraph.indexHprof
HeapAnalyzer.analyze-->FindLeakInput.analyzeGraph
HprofHeapGraph.indexHprof-->reader.readHprofRecords-->|callback HprofRecord|indexBuilderListener.onHprofRecord-->UnsortedByteEntries("UnsortedByteEntries,ScatterMap")
HprofHeapGraph.indexHprof-->indexBuilderListener.buildIndex-->|Sort entries by keys|SortedBytesMap-->HprofInMemoryIndex-->HprofHeapGraph
FindLeakInput.analyzeGraph-->FindLeakInput.findLeaks
FindLeakInput.findLeaks-->State.findPathsFromGcRoots:BFS-->|start by enqueueGcRoots|findObjectById("graph.findObjectById")-->readFieldsAndEnqueue

Data Flow

graph TB
HprofFile-->HprofRecord
HprofRecord-->UnsortedByteEntries("UnsortedByteEntries(class,instance,objectArray,primitiveArray)")
UnsortedByteEntries-->|Sort entries by keys|SortedBytesMap(SortedBytesMap: get perform binarySearch)
SortedBytesMap-->HprofInMemoryIndex
SortedBytesMap-->|get return|ByteSubArray
ByteSubArray-->|indexedObjectOrNull|IndexedObject
IndexedObject-->|wrapIndexedObject|HeapObject
HprofRecord-->ScatterMap("ScatterMap(hprofStringCache, classNames)")
ScatterMap-->HprofInMemoryIndex
HprofInMemoryIndex-->HprofHeapGraph
LruCache:objectCache-->|cached when readObjectRecord|HprofHeapGraph
//ScatterMap contains: classNames and hprofStringCache

// LRU cache size of 3000 is a sweet spot to balance hits vs memory usage.
// This is based on running InstrumentationLeakDetectorTest a bunch of time on a
// Pixel 2 XL API 28. Hit count was ~120K, miss count ~290K
private val objectCache = LruCache<Long, ObjectRecord>(3000)

HprofRecord Hierarchy

graph LR
HprofRecord-->StringRecord
HprofRecord-->LoadClassRecord
HprofRecord-->StackFrameRecord
HprofRecord-->StackTraceRecord
HprofRecord-->HeapDumpRecord
HeapDumpRecord-->GcRootRecord
HeapDumpRecord-->ObjectRecord
ObjectRecord-->ClassDumpRecord
ObjectRecord-->InstanceDumpRecord
ObjectRecord-->ObjectArrayDumpRecord
ObjectRecord-->PrimitiveArrayDumpRecord

ReferencePathNode Hierarchy

graph LR
ReferencePathNode-->RootNode
ReferencePathNode-->ChildNode
RootNode-->LibraryLeakRootNode
RootNode-->NormalRootNode
ChildNode-->NormalNode:ContainsParent
ChildNode-->LibraryLeakChildNode:ContainsParent
/**
 * Analyzes heap dumps to look for leaks.
 */

HeapAnalyzer

analyze

/**
 * Searches the heap dump for leaking instances and then computes the shortest strong reference
 * path from those instances to the GC roots.
 */
fun analyze(
  heapDumpFile: File,
  leakingObjectFinder: LeakingObjectFinder,
  referenceMatchers: List<ReferenceMatcher> = emptyList(),
  computeRetainedHeapSize: Boolean = false,
  objectInspectors: List<ObjectInspector> = emptyList(),
  metadataExtractor: MetadataExtractor = MetadataExtractor.NO_OP,
  proguardMapping: ProguardMapping? = null
): HeapAnalysis {
  
        Hprof.open(heapDumpFile)
          .use { hprof ->
            val graph = HprofHeapGraph.indexHprof(hprof, proguardMapping)
            val helpers =
              FindLeakInput(graph, referenceMatchers, computeRetainedHeapSize, objectInspectors)
            helpers.analyzeGraph(
                metadataExtractor, leakingObjectFinder, heapDumpFile, analysisStartNanoTime
            )
          }
 

indexhprof

FindLeakInput

private class FindLeakInput(
  val graph: HeapGraph,
  val referenceMatchers: List<ReferenceMatcher>,
  val computeRetainedHeapSize: Boolean,
  val objectInspectors: List<ObjectInspector>
)

FindLeakInput.analyzeGraph

private fun FindLeakInput.analyzeGraph(
  metadataExtractor: MetadataExtractor,
  leakingObjectFinder: LeakingObjectFinder,
  heapDumpFile: File,
  analysisStartNanoTime: Long
): HeapAnalysisSuccess {
  listener.onAnalysisProgress(EXTRACTING_METADATA)
  val metadata = metadataExtractor.extractMetadata(graph)

  listener.onAnalysisProgress(FINDING_RETAINED_OBJECTS)
  val leakingObjectIds = leakingObjectFinder.findLeakingObjectIds(graph)

  val (applicationLeaks, libraryLeaks) = findLeaks(leakingObjectIds)

  return HeapAnalysisSuccess(
      heapDumpFile = heapDumpFile,
      createdAtTimeMillis = System.currentTimeMillis(),
      analysisDurationMillis = since(analysisStartNanoTime),
      metadata = metadata,
      applicationLeaks = applicationLeaks,
      libraryLeaks = libraryLeaks
  )
}

FindLeakInput.findLeaks

private fun FindLeakInput.findLeaks(leakingObjectIds: Set<Long>): Pair<List<ApplicationLeak>, List<LibraryLeak>> {
  val pathFinder = PathFinder(graph, listener, referenceMatchers)
  val pathFindingResults =
    pathFinder.findPathsFromGcRoots(leakingObjectIds, computeRetainedHeapSize)

  SharkLog.d { "Found ${leakingObjectIds.size} retained objects" }
  return buildLeakTraces(pathFindingResults)
}

findpathsfromgcroots

PathFinder

/**
 * Finds the shortest path from leaking references to a gc root, first ignoring references
 * identified as "to visit last" and then visiting them as needed if no path is
 * found.
 */
internal class PathFinder(
  
private class State(
    val leakingObjectIds: Set<Long>,
    val sizeOfObjectInstances: Int,
    val computeRetainedHeapSize: Boolean
  ) {
      /** Set of objects to visit */
    val toVisitQueue: Deque<ReferencePathNode> = ArrayDeque()

    /**
     * Objects to visit when [toVisitQueue] is empty. Should contain [JavaFrame] gc roots first,
     * then [LibraryLeakNode].
     */
    val toVisitLastQueue: Deque<ReferencePathNode> = ArrayDeque()

    /**
     * Enables fast checking of whether a node is already in the queue.
     */
    val toVisitSet = HashSet<Long>()
    val toVisitLastSet = HashSet<Long>()

    val visitedSet = LongScatterSet()

findPathsFromGcRoots

fun findPathsFromGcRoots(
  leakingObjectIds: Set<Long>,
  computeRetainedHeapSize: Boolean
): PathFindingResults {
  listener.onAnalysisProgress(FINDING_PATHS_TO_RETAINED_OBJECTS)

  val sizeOfObjectInstances = determineSizeOfObjectInstances(graph)

  val state = State(leakingObjectIds, sizeOfObjectInstances, computeRetainedHeapSize)

  return state.findPathsFromGcRoots()
}

State

/**
 * Map of objects to their leaking dominator.
 
key: currentObjectId --> value: directDominatorObjectId

 * If an object has been added to [toVisitSet] or [visitedSet] and is missing from
 * [dominatedObjectIds] then it's considered "undomitable" ie it is dominated by gc roots
 * and cannot be dominated by a leaking object.
 */
val dominatedObjectIds = LongLongScatterMap()

State.findPathsFromGcRoots()

//main algorithm to find the shortest path to GCRoot, BFS is useful because when the first time we visit leakObject, the shortest path is found. Use ChildNode to find the parent ReferencePathNode
private fun State.findPathsFromGcRoots(): PathFindingResults {
  //iterate different type GcRoot and enqueue them into toVisitQueue or toVisitLastQueue
  enqueueGcRoots()
  
      val shortestPathsToLeakingObjects = mutableListOf<ReferencePathNode>()
    visitingQueue@ while (queuesNotEmpty) {
      val node = poll()

      if (checkSeen(node)) {
        throw IllegalStateException(
            "Node $node objectId=${node.objectId} should not be enqueued when already visited or enqueued"
        )
      }

      if (node.objectId in leakingObjectIds) {
        shortestPathsToLeakingObjects.add(node)
        // Found all refs, stop searching (unless computing retained size)
        if (shortestPathsToLeakingObjects.size == leakingObjectIds.size) {
          if (computeRetainedHeapSize) {
            listener.onAnalysisProgress(FINDING_DOMINATORS)
          } else {
            break@visitingQueue
          }
        }
      }

      when (val heapObject = graph.findObjectById(node.objectId)) {
        is HeapClass -> visitClassRecord(heapObject, node)
        is HeapInstance -> visitInstance(heapObject, node)
        is HeapObjectArray -> visitObjectArray(heapObject, node)
      }
    }
    return PathFindingResults(shortestPathsToLeakingObjects, dominatedObjectIds)
}

findobjectbyid

State.visitInstance

private fun State.visitInstance(
  instance: HeapInstance,
  parent: ReferencePathNode
) {
      val fieldNamesAndValues = instance.readFields()
        .filter { it.value.isNonNullReference }//only reference type need to be iterate, primitive is not.
        .toMutableList()

    fieldNamesAndValues.sortBy { it.name }

    fieldNamesAndValues.forEach { field ->
      val objectId = field.value.asObjectId!!
      if (computeRetainedHeapSize) {
        updateDominatorWithSkips(parent.objectId, objectId)
      }

      val node = when (val referenceMatcher = fieldReferenceMatchers[field.name]) {
        null -> NormalNode(
            objectId = objectId,
            parent = parent,
            refFromParentType = INSTANCE_FIELD,
            refFromParentName = field.name
        )
        is LibraryLeakReferenceMatcher ->
          LibraryLeakChildNode(
              objectId = objectId,
              parent = parent,
              refFromParentType = INSTANCE_FIELD,
              refFromParentName = field.name,
              matcher = referenceMatcher
          )
        is IgnoredReferenceMatcher -> null
      }
      if (node != null) {
        enqueue(node)
      }
    }

State.enqueue

@Suppress("ReturnCount")
private fun State.enqueue(
  node: ReferencePathNode
) {
  
     val visitLast =
      node is LibraryLeakNode ||
          // We deprioritize thread objects because on Lollipop the thread local values are stored
          // as a field.
          (node is RootNode && node.gcRoot is ThreadObject) ||
          (node is NormalNode && node.parent is RootNode && node.parent.gcRoot is JavaFrame)

    if (toVisitLastSet.contains(node.objectId)) {
      // Already enqueued => shorter or equal distance amongst library leak ref patterns.
      if (visitLast) {
        return
      } else {
        toVisitQueue.add(node)
        toVisitSet.add(node.objectId)
        val nodeToRemove = toVisitLastQueue.first { it.objectId == node.objectId }
        toVisitLastQueue.remove(nodeToRemove)
        toVisitLastSet.remove(node.objectId)
        return
      }
    }
  
      val isLeakingObject = node.objectId in leakingObjectIds

    if (!isLeakingObject) {
      val skip = when (val graphObject = graph.findObjectById(node.objectId)) {
        is HeapClass -> false
        is HeapInstance ->
          when {
            graphObject.isPrimitiveWrapper -> true
            graphObject.instanceClassName == "java.lang.String" -> true
            graphObject.instanceClass.instanceByteSize <= sizeOfObjectInstances -> true
            else -> false
          }
        is HeapObjectArray -> when {
          graphObject.isPrimitiveWrapperArray -> true
          else -> false
        }
        is HeapPrimitiveArray -> true
      }
      if (skip) {
        return
      }
    }
    if (visitLast) {
      toVisitLastQueue.add(node)
      toVisitLastSet.add(node.objectId)
    } else {
      toVisitQueue.add(node)
      toVisitSet.add(node.objectId)
    }

State.enqueueGcRoots()

private fun State.enqueueGcRoots() {
  val gcRoots = sortedGcRoots()
  ......
}

State.updateDominator

private fun State.updateDominator(//contains very detail doc about the algorithm to generate dominator tree
/**
 * A [HeapGraph] that reads from an indexed [Hprof]. Create a new instance with [indexHprof].
 */

HprofHeapGraph

constructor

class HprofHeapGraph internal constructor(
  private val hprof: Hprof,
  private val index: HprofInMemoryIndex
) : HeapGraph {

indexHprof

companion object {
  fun indexHprof(
    hprof: Hprof,
    proguardMapping: ProguardMapping? = null,
    indexedGcRootTypes: Set<KClass<out GcRoot>> = setOf(
        JniGlobal::class,
        JavaFrame::class,
        JniLocal::class,
        MonitorUsed::class,
        NativeStack::class,
        StickyClass::class,
        ThreadBlock::class,
        // ThreadObject points to threads, which we need to find the thread that a JavaLocalPattern
        // belongs to
        ThreadObject::class,
        JniMonitor::class
        /*
        Not included here:

        VmInternal: Ignoring because we've got 150K of it, but is this the right thing
        to do? What's VmInternal exactly? History does not go further than
        https://android.googlesource.com/platform/dalvik2/+/refs/heads/master/hit/src/com/android/hit/HprofParser.java#77
        We should log to figure out what objects VmInternal points to.

        ReferenceCleanup: We used to keep it, but the name doesn't seem like it should create a leak.

        Unknown: it's unknown, should we care?

        We definitely don't care about those for leak finding: InternedString, Finalizing, Debugger, Unreachable
         */
    )
  ): HeapGraph {
    val index = HprofInMemoryIndex.createReadingHprof(hprof, proguardMapping, indexedGcRootTypes)
    return HprofHeapGraph(hprof, index)
  }
}

createreadinghprof

findClassByName

override fun findClassByName(className: String): HeapClass? {
  val classId = index.classId(className)
  return if (classId == null) {
    null
  } else {
    return findObjectById(classId) as HeapClass
  }
}

findObjectById

override fun findObjectById(objectId: Long): HeapObject {
  return findObjectByIdOrNull(objectId) ?: throw IllegalArgumentException(
      "Object id $objectId not found in heap dump."
  )
}

findObjectByIdOrNull

override fun findObjectByIdOrNull(objectId: Long): HeapObject? {
  if (objectId == javaLangObjectClass?.objectId) return javaLangObjectClass

  val indexedObject = index.indexedObjectOrNull(objectId) ?: return null
  return wrapIndexedObject(indexedObject, objectId)
}

indexedobjectornull

wrapIndexedObject

private fun wrapIndexedObject(
  indexedObject: IndexedObject,
  objectId: Long
): HeapObject {
  return when (indexedObject) {
    is IndexedClass -> HeapClass(this, indexedObject, objectId)
    is IndexedInstance -> {
      val isPrimitiveWrapper = index.primitiveWrapperTypes.contains(indexedObject.classId)
      HeapInstance(this, indexedObject, objectId, isPrimitiveWrapper)
    }
    is IndexedObjectArray -> {
      val isPrimitiveWrapperArray =
        index.primitiveWrapperTypes.contains(indexedObject.arrayClassId)
      HeapObjectArray(this, indexedObject, objectId, isPrimitiveWrapperArray)
    }
    is IndexedPrimitiveArray -> HeapPrimitiveArray(this, indexedObject, objectId)
  }
}

HprofInMemoryIndex

createReadingHprof

fun createReadingHprof(
  hprof: Hprof,
  proguardMapping: ProguardMapping?,
  indexedGcRootTypes: Set<KClass<out GcRoot>>
): HprofInMemoryIndex {
  val recordTypes = setOf(
      StringRecord::class,
      LoadClassRecord::class,
      ClassSkipContentRecord::class,
      InstanceSkipContentRecord::class,
      ObjectArraySkipContentRecord::class,
      PrimitiveArraySkipContentRecord::class,
      GcRootRecord::class
  )
  val reader = hprof.reader

   // First pass to count and correctly size arrays once and for all.
      var classCount = 0
      var instanceCount = 0
      var objectArrayCount = 0
      var primitiveArrayCount = 0
      reader.readHprofRecords(setOf(
          LoadClassRecord::class,
          InstanceSkipContentRecord::class,
          ObjectArraySkipContentRecord::class,
          PrimitiveArraySkipContentRecord::class
      ), OnHprofRecordListener { position, record ->
        when (record) {
          is LoadClassRecord -> classCount++
          is InstanceSkipContentRecord -> instanceCount++
          is ObjectArraySkipContentRecord -> objectArrayCount++
          is PrimitiveArraySkipContentRecord -> primitiveArrayCount++
        }
      })

      hprof.moveReaderTo(reader.startPosition)
      val indexBuilderListener =
        Builder(
            reader.identifierByteSize == 8, hprof.fileLength, classCount, instanceCount,
            objectArrayCount, primitiveArrayCount, indexedGcRootTypes.map { it.java }
            .toSet()
        )

      reader.readHprofRecords(recordTypes, indexBuilderListener)

      return indexBuilderListener.buildIndex(proguardMapping)

readhprofrecords

Builder

/**
 * Map of string id to string
 * This currently keeps all the hprof strings that we could care about: class names,
 * static field names and instance fields names
 */
// TODO Replacing with a radix trie reversed into a sparse array of long to trie leaf could save
// memory. Can be stored as 3 arrays: array of keys, array of values which are indexes into
// a large array of string bytes. Each "entry" consists of a size, the index of the previous
// segment and then the segment content.

private val hprofStringCache = LongObjectScatterMap<String>()

/**
 * class id to string id
 */
private val classNames = LongLongScatterMap(expectedElements = classCount)

private val classIndex = UnsortedByteEntries(
    bytesPerValue = positionSize + identifierSize + 4,
    longIdentifiers = longIdentifiers,
    initialCapacity = classCount
)
private val instanceIndex = UnsortedByteEntries(
    bytesPerValue = positionSize + identifierSize,
    longIdentifiers = longIdentifiers,
    initialCapacity = instanceCount
)
private val objectArrayIndex = UnsortedByteEntries(
    bytesPerValue = positionSize + identifierSize,
    longIdentifiers = longIdentifiers,
    initialCapacity = objectArrayCount
)
private val primitiveArrayIndex = UnsortedByteEntries(
    bytesPerValue = positionSize + 1,
    longIdentifiers = longIdentifiers,
    initialCapacity = primitiveArrayCount
)

onHprofRecord

  override fun onHprofRecord(
      position: Long,
      record: HprofRecord
    ) {
      when (record) {
        is StringRecord -> {
          if (PRIMITIVE_WRAPPER_TYPES.contains(record.string)) {
            primitiveWrapperClassNames.add(record.id)
          }
          // JVM heap dumps use "/" for package separators (vs "." for Android heap dumps)
          hprofStringCache[record.id] = record.string.replace('/', '.')
        }
        is LoadClassRecord -> {
          classNames[record.id] = record.classNameStringId
          if (primitiveWrapperClassNames.contains(record.classNameStringId)) {
            primitiveWrapperTypes.add(record.id)
          }
        }
        is GcRootRecord -> {
          val gcRoot = record.gcRoot
          if (gcRoot.id != ValueHolder.NULL_REFERENCE
              && indexedGcRootsTypes.contains(gcRoot.javaClass)
          ) {
            gcRoots += gcRoot
          }
        }
        is ClassSkipContentRecord -> {
          classIndex.append(record.id)//write id in an entry
              .apply {//write value bytes
                writeTruncatedLong(position, positionSize)
                writeId(record.superclassId)
                writeInt(record.instanceSize)
              }
        }
        is InstanceSkipContentRecord -> {
          instanceIndex.append(record.id)
              .apply {
                writeTruncatedLong(position, positionSize)
                writeId(record.classId)
              }
        }
        is ObjectArraySkipContentRecord -> {
          objectArrayIndex.append(record.id)
              .apply {
                writeTruncatedLong(position, positionSize)
                writeId(record.arrayClassId)
              }
        }
        is PrimitiveArraySkipContentRecord -> {
          primitiveArrayIndex.append(record.id)
              .apply {
                writeTruncatedLong(position, positionSize)
                writeByte(record.type.ordinal.toByte())
              }
        }
      }
    }

buildIndex

fun buildIndex(
  proguardMapping: ProguardMapping?
): HprofInMemoryIndex {
  val sortedInstanceIndex = instanceIndex.moveToSortedMap()
  val sortedObjectArrayIndex = objectArrayIndex.moveToSortedMap()
  val sortedPrimitiveArrayIndex = primitiveArrayIndex.moveToSortedMap()
  val sortedClassIndex = classIndex.moveToSortedMap()
  // Passing references to avoid copying the underlying data structures.
  return HprofInMemoryIndex(
      positionSize,
      hprofStringCache, classNames, sortedClassIndex, sortedInstanceIndex,
      sortedObjectArrayIndex,
      sortedPrimitiveArrayIndex, gcRoots,
      proguardMapping,
      primitiveWrapperTypes
  )
}

movetosortedmap

indexedObjectOrNull

fun indexedObjectOrNull(objectId: Long): IndexedObject? {
  var array: ByteSubArray? = classIndex[objectId]
  if (array != null) {
    return IndexedClass(
        position = array.readTruncatedLong(positionSize),
        superclassId = array.readId(),
        instanceSize = array.readInt()
    )
  }
  array = instanceIndex[objectId]
  if (array != null) {
    return IndexedInstance(
        position = array.readTruncatedLong(positionSize),
        classId = array.readId()
    )
  }
   array = objectArrayIndex[objectId]
    if (array != null) {
      return IndexedObjectArray(
          position = array.readTruncatedLong(positionSize),
          arrayClassId = array.readId()
      )
    }
    array = primitiveArrayIndex[objectId]
    if (array != null) {
      return IndexedPrimitiveArray(
          position = array.readTruncatedLong(positionSize),
          primitiveType = PrimitiveType.values()[array.readByte()
              .toInt()]
      )
    }
    return null
  }

shark-hprof/src/main/java/shark/HprofReader.kt

HprofReader

readHprofRecords

/**
 * Reads all hprof records from [source].
 * Assumes the [reader] was has a source that currently points to the start position of hprof
 * records.
 */
@Suppress("ComplexMethod", "LongMethod")
fun readHprofRecords(
  recordTypes: Set<KClass<out HprofRecord>>,
  listener: OnHprofRecordListener
) {
  while (!exhausted()) {
      // type of the record
      val tag = readUnsignedByte()

      // number of microseconds since the time stamp in the header
      skip(intByteSize)

      // number of bytes that follow and belong to this record
      val length = readUnsignedInt()

      when (tag) {
        STRING_IN_UTF8 -> {
          if (readStringRecord) {
            val recordPosition = position
            val id = readId()
            val stringLength = length - identifierByteSize
            val string = readUtf8(stringLength)
            val record = StringRecord(id, string)
            listener.onHprofRecord(recordPosition, record)
          } else {
            skip(length)
          }
        }
        LOAD_CLASS -> {
          if (readLoadClassRecord) {
            val recordPosition = position
            val classSerialNumber = readInt()
            val id = readId()
            val stackTraceSerialNumber = readInt()
            val classNameStringId = readId()
            reusedLoadClassRecord.apply {
              this.classSerialNumber = classSerialNumber
              this.id = id
              this.stackTraceSerialNumber = stackTraceSerialNumber
              this.classNameStringId = classNameStringId
            }
            listener.onHprofRecord(recordPosition, reusedLoadClassRecord)
          } else {
            skip(length)
          }
        }
        STACK_FRAME -> {}
        STACK_TRACE -> {}
        HEAP_DUMP, HEAP_DUMP_SEGMENT -> {
          val heapDumpStart = position
          var previousTag = 0
          var previousTagPosition = 0L
          while (position - heapDumpStart < length) {
            val heapDumpTagPosition = position
            val heapDumpTag = readUnsignedByte()//position increase

            when (heapDumpTag) {
              ROOT_UNKNOWN -> {
                if (readGcRootRecord) {
                  val recordPosition = position
                  val record = GcRootRecord(gcRoot = Unknown(id = readId()))
                  listener.onHprofRecord(recordPosition, record)
                } else {
                  skip(identifierByteSize)//position increase
                }
              }
              ROOT_JNI_GLOBAL -> {
                if (readGcRootRecord) {
                  val recordPosition = position
                  val gcRootRecord =
                    GcRootRecord(gcRoot = JniGlobal(id = readId(), jniGlobalRefId = readId()))
                  listener.onHprofRecord(recordPosition, gcRootRecord)
                } else {
                  skip(identifierByteSize + identifierByteSize)
                }
              }
        }
        HEAP_DUMP_END -> {
          if (readHeapDumpEndRecord) {
            val recordPosition = position
            val record = HeapDumpEndRecord
            listener.onHprofRecord(recordPosition, record)
          }
        }
        else -> {
          skip(length)
        }

HprofRecord

/**
 * A Hprof record. These data structure map 1:1 with how records are written in hprof files.
 */
sealed class HprofRecord {
    class StringRecord(
    val id: Long,
    val string: String
  ) : HprofRecord()
  
    class LoadClassRecord(
    classSerialNumber: Int,
    id: Long,
    stackTraceSerialNumber: Int,
    classNameStringId: Long
  ) : HprofRecord() {
      ......

ObjectRecord

sealed class ObjectRecord : HeapDumpRecord() {
  
    class ClassDumpRecord(
        val id: Long,
        val stackTraceSerialNumber: Int,
        val superclassId: Long,
        val classLoaderId: Long,
        val signersId: Long,
        val protectionDomainId: Long,
        val instanceSize: Int,
        val staticFields: List<StaticFieldRecord>,
        val fields: List<FieldRecord>
      ) : ObjectRecord() {
        }
  
        class InstanceDumpRecord(
        val id: Long,
        val stackTraceSerialNumber: Int,
        val classId: Long,
        /**
         * Instance field values (this class, followed by super class, etc)
         */
        val fieldValues: ByteArray
      ) : ObjectRecord()
  
  
        class ObjectArrayDumpRecord(
        val id: Long,
        val stackTraceSerialNumber: Int,
        val arrayClassId: Long,
        val elementIds: LongArray
      ) : ObjectRecord()
  
  sealed class PrimitiveArrayDumpRecord : ObjectRecord() {
  }
/**
 * An object in the heap dump.
 */

HeapObject

/**
 * This [HeapObject] as a [HeapClass] if it is one, or null otherwise
 */
val asClass: HeapClass?
  get() = if (this is HeapClass) this else null

/**
 * This [HeapObject] as a [HeapInstance] if it is one, or null otherwise
 */
val asInstance: HeapInstance?
  get() = if (this is HeapInstance) this else null



  /**
   * A class in the heap dump.
   */
  class HeapClass internal constructor(
    private val hprofGraph: HprofHeapGraph,
    private val indexedObject: IndexedClass,
    override val objectId: Long
  ) : HeapObject() {
        /**
     * Returns a [HeapField] object that reflects the specified declared
     * field of the class represented by this [HeapClass] object, or null if this field does not
     * exist. The [name] parameter specifies the simple name of the desired field.
     *
     * Also available as a convenience operator: [get]
     *
     * This may trigger IO reads.
     */
    fun readStaticField(fieldName: String): HeapField? {
      for (fieldRecord in readRecord().staticFields) {
        if (hprofGraph.staticFieldName(objectId, fieldRecord) == fieldName) {
          return HeapField(
              this, hprofGraph.staticFieldName(objectId, fieldRecord),
              HeapValue(hprofGraph, fieldRecord.value)
          )
        }
      }
      return null
    }
  }


  /**
   * An instance in the heap dump.
   */
  class HeapInstance internal constructor(
    private val hprofGraph: HprofHeapGraph,
    internal val indexedObject: IndexedInstance,
    override val objectId: Long,
    /**
     * Whether this is an instance of a primitive wrapper type.
     */
    val isPrimitiveWrapper: Boolean
  ) : HeapObject() {
        /**
     * Returns a [HeapField] object that reflects the specified declared
     * field of the instance represented by this [HeapInstance] object, or null if this field does
     * not exist. The [declaringClassName] specifies the class in which the desired field is
     * declared, and the [fieldName] parameter specifies the simple name of the desired field.
     *
     * Also available as a convenience operator: [get]
     *
     * This may trigger IO reads.
     */
    fun readField(
      declaringClassName: String,
      fieldName: String
    ): HeapField? {
      return readFields().firstOrNull { field -> field.declaringClass.name == declaringClassName && field.name == fieldName }
    }
  }

readFields

/**
 * The fields of this instance, as a sequence of [HeapField].
 *
 * This may trigger IO reads.
 */
fun readFields(): Sequence<HeapField> {
  val fieldReader by lazy {
    hprofGraph.createFieldValuesReader(readRecord())
  }
  return instanceClass.classHierarchy
      .map { heapClass ->
        heapClass.readRecord()
            .fields.asSequence()
            .map { fieldRecord ->
              val fieldName = hprofGraph.fieldName(heapClass.objectId, fieldRecord)
              val fieldValue = fieldReader.readValue(fieldRecord)
              HeapField(heapClass, fieldName, HeapValue(hprofGraph, fieldValue))
            }
      }
      .flatten()//change two dimensional sequence into one dimensional
}

IndexedObject

internal sealed class IndexedObject {
    abstract val position: Long

  class IndexedClass(
    override val position: Long,
    val superclassId: Long,
    val instanceSize: Int
  ) : IndexedObject()

  class IndexedInstance(
    override val position: Long,
    val classId: Long
  ) : IndexedObject()
  
    class IndexedObjectArray(
    override val position: Long,
    val arrayClassId: Long
  ) : IndexedObject()

  class IndexedPrimitiveArray(
    override val position: Long,
    primitiveType: PrimitiveType
  ) : IndexedObject() {
    private val primitiveTypeOrdinal: Byte = primitiveType.ordinal.toByte()
    val primitiveType: PrimitiveType
      get() = PrimitiveType.values()[primitiveTypeOrdinal.toInt()]
  }
}
  

UnsortedByteEntries

/**
 * Wraps a byte array of entries where each entry is an id followed by bytes for the value.
 * `id` is a long if [longIdentifiers] is true and an int otherwise. Each entry has [bytesPerValue]
 * value bytes. Entries are appended into the array via [append]. Once done, the backing array
 * is sorted and turned into a [SortedBytesMap] by calling [moveToSortedMap].
 */
internal class UnsortedByteEntries(
  private val bytesPerValue: Int,
  private val longIdentifiers: Boolean,
  private val initialCapacity: Int = 4,
  private val growthFactor: Double = 2.0
) {
  private val bytesPerEntry = bytesPerValue + if (longIdentifiers) 8 else 4

  private var entries: ByteArray? = null
  private val subArray = MutableByteSubArray()
  private var subArrayIndex = 0

  private var assigned: Int = 0
  private var currentCapacity = 0

append

fun append(
  key: Long
): MutableByteSubArray {
  if (entries == null) {
    currentCapacity = initialCapacity
    entries = ByteArray(currentCapacity * bytesPerEntry)
  } else {
    if (currentCapacity == assigned) {
      val newCapacity = (currentCapacity * growthFactor).toInt()
      growEntries(newCapacity)
      currentCapacity = newCapacity
    }
  }
  assigned++
  subArrayIndex = 0
  subArray.writeId(key)
  return subArray
}

moveToSortedMap

fun moveToSortedMap(): SortedBytesMap {
  if (assigned == 0) {
    return SortedBytesMap(longIdentifiers, bytesPerValue, ByteArray(0))
  }
  val entries = entries!!
  // Sort entries by keys, which are ids of 4 or 8 bytes.
  ByteArrayTimSort.sort(entries, 0, assigned, bytesPerEntry, object : ByteArrayComparator {
    override fun compare(
      entrySize: Int,
      o1Array: ByteArray,
      o1Index: Int,
      o2Array: ByteArray,
      o2Index: Int
    ): Int {
      return if (longIdentifiers) {
        readLong(o1Array, o1Index * entrySize)
            .compareTo(
                readLong(o2Array, o2Index * entrySize)
            )
      } else {
        readInt(o1Array, o1Index * entrySize)
            .compareTo(
                readInt(o2Array, o2Index * entrySize)
            )
      }
    }
  })
  val sortedEntries = if (entries.size > assigned * bytesPerEntry) {
    entries.copyOf(assigned * bytesPerEntry)
  } else entries
  this.entries = null
  assigned = 0
  return SortedBytesMap(
      longIdentifiers, bytesPerValue, sortedEntries
  )
}

SortedBytesMap

/**
 * A read only map of `id` => `byte array` sorted by id, where `id` is a long if [longIdentifiers]
 * is true and an int otherwise. Each entry has a value byte array of size [bytesPerValue].
 *
 * Instances are created by [UnsortedByteEntries]
 *
 * [get] and [contains] perform a binary search to locate a specific entry by key.
 */
internal class SortedBytesMap(
  private val longIdentifiers: Boolean,
  private val bytesPerValue: Int,
  private val sortedEntries: ByteArray
) {
  private val bytesPerKey = if (longIdentifiers) 8 else 4
  private val bytesPerEntry = bytesPerKey + bytesPerValue

  private val size = sortedEntries.size / bytesPerEntry

operator get

operator fun get(key: Long): ByteSubArray? {
  val keyIndex = binarySearch(key)
  if (keyIndex < 0) {
    return null
  }
  val valueIndex = keyIndex * bytesPerEntry + bytesPerKey
  return ByteSubArray(sortedEntries, valueIndex, bytesPerValue, longIdentifiers)
}

binarySearch(二分查找)

private fun binarySearch(
  key: Long
): Int {
  val startIndex = 0
  val endIndex = size
  var lo = startIndex
  var hi = endIndex - 1
  while (lo <= hi) {
    val mid = (lo + hi).ushr(1)
    val midVal = keyAt(mid)
    when {
      midVal < key -> lo = mid + 1
      midVal > key -> hi = mid - 1
      else -> return mid
    }
  }
  return lo.inv()
}

LongLongScatterMap

getSlot

/**
 * Being given a key looks it up in the map and returns the slot where element sits, so it later
 * can be retrieved with [getSlotValue]; return '-1' if element not found.
 * Why so complicated and not just make [get] return null if value not found? The reason is performance:
 * this approach prevents unnecessary boxing of the primitive long that would happen with nullable Long?
 */
fun getSlot(key: Long): Int {
  if (key == 0L) {
    return if (hasEmptyKey) mask + 1 else -1
  } else {
    val keys = this.keys
    val mask = this.mask
    var slot = hashKey(key) and mask

    var existing = keys[slot]
    while (existing != 0L) {
      if (existing == key) {
        return slot
      }
      slot = slot + 1 and mask
      existing = keys[slot]
    }

    return -1
  }
}

getSlotValue

 * Being given a slot of element retrieves it from the collection
 */
fun getSlotValue(slot: Int): Long = values[slot]