Skip to content

Commit 872e253

Browse files
authored
Merge pull request #2915 from square/py/skip-missing-static-field-references
Skip static fields that reference an object missing from the heap dump
2 parents 01bb4fa + 8fa8a08 commit 872e253

3 files changed

Lines changed: 64 additions & 0 deletions

File tree

docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Releases before 2.8.1 predate these markers.
2929
* 💥 `HeapGraph.findObjectByIndex(0)` threw an `IllegalArgumentException` instead of returning the first object of the heap dump.
3030
* 💥 `LeakTrace.retainedObjectCount` and `Leak.totalRetainedObjectCount` threw a `NoSuchElementException` instead of returning null when the analysis ran without computing retained heap sizes. Both are declared nullable, and their byte size counterparts `LeakTrace.retainedHeapByteSize` and `Leak.totalRetainedHeapByteSize` do return null, so a caller that null checked the count crashed instead. `LeakCanary.Config.computeRetainedHeapSize` defaults to true, so this is about code that turns it off, and about `shark`, where `HeapAnalyzer.analyze()` defaults it to false.
3131
* 💥 Heap growth detection failed with *"Object id 0 not found in heap dump"* on heap dumps that contain a `java.util.LinkedList` with a null element, because the reference reader for linked lists surfaced null elements as references to object id 0.
32+
* 💥 [#2567](https://github.com/square/leakcanary/issues/2567) The analysis failed with *"Object id <id> not found in heap dump"* on heap dumps that contain a class which failed to load. Excluding a dependency that provides a superclass is one way to get there: the subclass can no longer be loaded, but ART still creates the array class for it, points that array class's `$class$componentType` at the class object that failed and keeps it in its class loader's class table, while the heap dumper skips it and never writes it out. The heap dump then names an object it doesn't contain, and reading the static fields of the array class surfaced a reference to it. Static fields pointing at an object missing from the heap dump are now skipped, which is what reading GC roots and object array elements already did. Reproduced on Android 14 and Android 16, so this is not limited to the Android 13 the issue was reported against.
3233
* 💥 [#2835](https://github.com/square/leakcanary/pull/2835) Analyzing a heap dump with hundreds of millions of objects of the same kind failed with a `NegativeArraySizeException`. Each record type was indexed into a single `ByteArray` sized as `recordCount * bytesPerEntry`, which is `Int` arithmetic and wraps negative past 2 GB: a heap dump holding 918610159 instances asked for an array of -346802823 bytes. An index that doesn't fit in one array is now spread over a list of arrays, so what bounds it is the heap the analysis runs with rather than the 2 GB an `Int` can address. Verified on a 27 GB JVM heap dump of 918623867 objects, which indexes in 60 seconds.
3334
* 💥 [#2777](https://github.com/square/leakcanary/issues/2777) Reading a valid heap dump holding an array whose elements take more than 2 GB — an object array of more than 268435455 element ids with 8 byte identifiers, or a `long` or `double` array of more than 268435455 elements — failed with *"Unknown tag 0x00"*. How many bytes to skip over the elements was computed as `arrayLength * elementByteSize` in `Int` arithmetic, which wraps negative that far out, so the parser skipped *backwards* and read an element id as the next record tag. `HprofPrimitiveArrayStripper` truncated the same count and wrote a corrupt heap dump. Arrays that large need a heap far bigger than a phone's, so this is about heap dumps taken from a JVM and analyzed with Shark or the Shark CLI.
3435
* 🐛 [#2777](https://github.com/square/leakcanary/issues/2777) An object whose record takes more than 2 GB in the heap dump reported a negative size. `HeapObject.recordSize` and the `byteSize` of an array were `Int`, narrowed from the `Long` the index holds, so a 2147483672 byte array record came back as -2147483624 and every size derived from it was wrong, including the shallow and retained sizes an analysis reports. Sizes are now `Long` from the index all the way out to `ObjectSizeCalculator`.

shark/shark/src/main/java/shark/ClassReferenceReader.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class ClassReferenceReader(
3131

3232
override fun read(source: HeapClass): Sequence<Reference> {
3333
val ignoredStaticFields = staticFieldNameByClassName[source.name] ?: emptyMap()
34+
val graph = source.graph
3435

3536
return source.readStaticFields().mapNotNull { staticField ->
3637
// not non null: no null + no primitives.
@@ -52,6 +53,15 @@ class ClassReferenceReader(
5253
// Note: instead of calling staticField.value.asObjectId!! we cast holder to ReferenceHolder
5354
// and access value directly. This allows us to avoid unnecessary boxing of Long.
5455
val valueObjectId = (staticField.value.holder as ReferenceHolder).value
56+
57+
// A class can reference an object that the heap dump doesn't contain: when a class fails to
58+
// load, ART creates the matching array class anyway and points its $class$componentType at
59+
// the class object that failed, yet never dumps that class object. Reading GC roots and
60+
// object array elements skips missing objects for the same reason. See #2567.
61+
if (!graph.objectExists(valueObjectId)) {
62+
return@mapNotNull null
63+
}
64+
5565
val referenceMatcher = ignoredStaticFields[fieldName]
5666

5767
if (referenceMatcher is IgnoredReferenceMatcher) {
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package shark
2+
3+
import org.assertj.core.api.Assertions.assertThat
4+
import org.junit.Test
5+
import shark.ValueHolder.ReferenceHolder
6+
7+
/**
8+
* A heap dump can reference an object it doesn't contain: when a class fails to load, ART keeps
9+
* creating the matching array class and points its `$class$componentType` at the class object that
10+
* failed, yet never dumps that class object. See
11+
* [#2567](https://github.com/square/leakcanary/issues/2567).
12+
*/
13+
class DanglingReferenceTest {
14+
15+
/**
16+
* An object id that no record in the heap dump defines, well above the ids the DSL hands out.
17+
*/
18+
private val danglingId = 123456789L
19+
20+
@Test fun `dangling static field reference is skipped`() {
21+
val heapDump = dump {
22+
"GcRoot" clazz {
23+
staticField["leak"] = "Leaking" watchedInstance {}
24+
}
25+
"ArrayClass" clazz {
26+
staticField["\$class\$componentType"] = ReferenceHolder(danglingId)
27+
}
28+
}
29+
30+
// Computing retained sizes makes the traversal exhaustive, so it reaches the array class.
31+
// Without it the traversal stops as soon as it has found every leaking object.
32+
val analysis = heapDump.checkForLeaks<HeapAnalysis>(computeRetainedHeapSize = true)
33+
34+
assertThat(analysis).isInstanceOf(HeapAnalysisSuccess::class.java)
35+
assertThat((analysis as HeapAnalysisSuccess).applicationLeaks.flatMap { it.leakTraces })
36+
.hasSize(1)
37+
}
38+
39+
@Test fun `dangling object array entry is skipped`() {
40+
val heapDump = dump {
41+
"GcRoot" clazz {
42+
staticField["array"] = objectArray(
43+
ReferenceHolder(danglingId),
44+
"Leaking" watchedInstance {}
45+
)
46+
}
47+
}
48+
49+
val analysis = heapDump.checkForLeaks<HeapAnalysisSuccess>()
50+
51+
assertThat(analysis.applicationLeaks.flatMap { it.leakTraces }).hasSize(1)
52+
}
53+
}

0 commit comments

Comments
 (0)