Handle missing objects
diff --git a/shark-graph/src/main/java/shark/HeapGraph.kt b/shark-graph/src/main/java/shark/HeapGraph.kt
index 1b64a97..93fafa9 100644
--- a/shark-graph/src/main/java/shark/HeapGraph.kt
+++ b/shark-graph/src/main/java/shark/HeapGraph.kt
@@ -46,6 +46,12 @@
   fun findObjectById(objectId: Long): HeapObject
 
   /**
+   * Returns the [HeapObject] corresponding to the provided [objectId] or null if it cannot be
+   * found.
+   */
+  fun findObjectByIdOrNull(objectId: Long): HeapObject?
+
+  /**
    * Returns the [HeapClass] corresponding to the provided [className], or null if the
    * class cannot be found.
    */
diff --git a/shark-graph/src/main/java/shark/HeapValue.kt b/shark-graph/src/main/java/shark/HeapValue.kt
index b28d8cd..574ef06 100644
--- a/shark-graph/src/main/java/shark/HeapValue.kt
+++ b/shark-graph/src/main/java/shark/HeapValue.kt
@@ -112,12 +112,16 @@
 
   /**
    * If this [HeapValue] if it represents a non null object reference to an instance of the
-   * [String] class, returns a [String] instance with content that matches the string in the heap
-   * dump. Otherwise returns null.
+   * [String] class that exists in the heap dump, returns a [String] instance with content that
+   * matches the string in the heap dump. Otherwise returns null.
    *
    * This may trigger IO reads.
    */
   fun readAsJavaString(): String? {
-    return asObject?.asInstance?.readAsJavaString()
+    if (holder is ReferenceHolder && !holder.isNull) {
+      val heapObject = graph.findObjectByIdOrNull(holder.value)
+      return heapObject?.asInstance?.readAsJavaString()
+    }
+    return null
   }
 }
diff --git a/shark-graph/src/main/java/shark/HprofHeapGraph.kt b/shark-graph/src/main/java/shark/HprofHeapGraph.kt
index 0a23e73..f238e6a 100644
--- a/shark-graph/src/main/java/shark/HprofHeapGraph.kt
+++ b/shark-graph/src/main/java/shark/HprofHeapGraph.kt
@@ -72,7 +72,14 @@
   private val objectCache = LruCache<Long, ObjectRecord>(3000)
 
   override fun findObjectById(objectId: Long): HeapObject {
-    return wrapIndexedObject(index.indexedObject(objectId), objectId)
+    return findObjectByIdOrNull(objectId) ?: throw IllegalArgumentException(
+        "Object id $objectId not found in heap dump."
+    )
+  }
+
+  override fun findObjectByIdOrNull(objectId: Long): HeapObject? {
+    val indexedObject = index.indexedObjectOrNull(objectId) ?: return null
+    return wrapIndexedObject(indexedObject, objectId)
   }
 
   override fun findClassByName(className: String): HeapClass? {
diff --git a/shark-graph/src/main/java/shark/internal/HprofInMemoryIndex.kt b/shark-graph/src/main/java/shark/internal/HprofInMemoryIndex.kt
index 91eb174..5e0d7d0 100644
--- a/shark-graph/src/main/java/shark/internal/HprofInMemoryIndex.kt
+++ b/shark-graph/src/main/java/shark/internal/HprofInMemoryIndex.kt
@@ -143,14 +143,8 @@
     return gcRoots
   }
 
-  fun indexedObject(objectId: Long): IndexedObject {
-    return indexedObjectOrNull(objectId) ?: throw IllegalArgumentException(
-        "Object id $objectId not found in heap dump."
-    )
-  }
-
   @Suppress("ReturnCount")
-  private fun indexedObjectOrNull(objectId: Long): IndexedObject? {
+  fun indexedObjectOrNull(objectId: Long): IndexedObject? {
     var array: ByteSubArray? = classIndex[objectId]
     if (array != null) {
       return IndexedClass(
diff --git a/shark/src/main/java/shark/internal/PathFinder.kt b/shark/src/main/java/shark/internal/PathFinder.kt
index aeb344b..ff3a5b1 100644
--- a/shark/src/main/java/shark/internal/PathFinder.kt
+++ b/shark/src/main/java/shark/internal/PathFinder.kt
@@ -257,29 +257,34 @@
           enqueue(NormalRootNode(gcRoot.id, gcRoot))
         }
         is JavaFrame -> {
-          val (threadInstance, threadRoot) = threadsBySerialNumber.getValue(
-              gcRoot.threadSerialNumber
-          )
-          val threadName = threadNames[threadInstance] ?: {
-            val name = threadInstance[Thread::class, "name"]?.value?.readAsJavaString() ?: ""
-            threadNames[threadInstance] = name
-            name
-          }()
-          val referenceMatcher = threadNameReferenceMatchers[threadName]
+          val threadPair = threadsBySerialNumber[gcRoot.threadSerialNumber]
+          if (threadPair == null) {
+            // Could not find the thread that this java frame is for.
+            enqueue(NormalRootNode(gcRoot.id, gcRoot))
+          } else {
 
-          if (referenceMatcher !is IgnoredReferenceMatcher) {
-            val rootNode = NormalRootNode(threadRoot.id, gcRoot)
-            // Unfortunately Android heap dumps do not include stack trace data, so
-            // JavaFrame.frameNumber is always -1 and we cannot know which method is causing the
-            // reference to be held.
-            val leakReference = LeakReference(LOCAL, "")
+            val (threadInstance, threadRoot) = threadPair
+            val threadName = threadNames[threadInstance] ?: {
+              val name = threadInstance[Thread::class, "name"]?.value?.readAsJavaString() ?: ""
+              threadNames[threadInstance] = name
+              name
+            }()
+            val referenceMatcher = threadNameReferenceMatchers[threadName]
 
-            val childNode = if (referenceMatcher is LibraryLeakReferenceMatcher) {
-              LibraryLeakChildNode(gcRoot.id, rootNode, leakReference, referenceMatcher)
-            } else {
-              NormalNode(gcRoot.id, rootNode, leakReference)
+            if (referenceMatcher !is IgnoredReferenceMatcher) {
+              val rootNode = NormalRootNode(threadRoot.id, gcRoot)
+              // Unfortunately Android heap dumps do not include stack trace data, so
+              // JavaFrame.frameNumber is always -1 and we cannot know which method is causing the
+              // reference to be held.
+              val leakReference = LeakReference(LOCAL, "")
+
+              val childNode = if (referenceMatcher is LibraryLeakReferenceMatcher) {
+                LibraryLeakChildNode(gcRoot.id, rootNode, leakReference, referenceMatcher)
+              } else {
+                NormalNode(gcRoot.id, rootNode, leakReference)
+              }
+              enqueue(childNode)
             }
-            enqueue(childNode)
           }
         }
         is JniGlobal -> {