提交 afe449be 编写于 作者: P Pierre-Yves Ricau

OMG ZOMBIES, RUN RUN RUN

上级 6c0bee29
package com.squareup.leakcanary;
import com.squareup.haha.perflib.ClassInstance;
import com.squareup.haha.perflib.Instance;
import java.util.List;
import static com.squareup.leakcanary.HahaHelper.classInstanceValues;
import static com.squareup.leakcanary.HahaHelper.fieldValue;
public class ActivityZombieMatcher implements OOMAutopsy.ZombieMatcher {
@Override public String rootClassName() {
return "android.app.Activity";
}
@Override public boolean isZombie(Instance instance) {
List<ClassInstance.FieldValue> fields = classInstanceValues(instance);
try {
return fieldValue(fields, "mDestroyed");
} catch (IllegalArgumentException ignored) {
return false;
}
}
}
/*
* Copyright (C) 2015 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.leakcanary;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import static com.squareup.leakcanary.HahaHelper.getStackTraceString;
public final class Autopsy implements Serializable {
public static Autopsy result(List<LeakTrace> leakTraces, long analysisDurationMs) {
return new Autopsy(leakTraces, null, analysisDurationMs);
}
public static Autopsy failure(Throwable failure, long analysisDurationMs) {
return new Autopsy(Collections.<LeakTrace>emptyList(), failure, analysisDurationMs);
}
public final List<LeakTrace> leakTraces;
/** Null unless the analysis failed. */
public final Throwable failure;
/** Total time spent analyzing the heap. */
public final long analysisDurationMs;
private Autopsy(List<LeakTrace> leakTraces, Throwable failure, long analysisDurationMs) {
this.leakTraces = leakTraces;
this.failure = failure;
this.analysisDurationMs = analysisDurationMs;
}
@Override public String toString() {
if (failure != null) {
return getStackTraceString(failure);
}
StringBuilder sb = new StringBuilder();
for (LeakTrace leakTrace : leakTraces) {
sb.append(leakTrace).append("\n");
}
return sb.toString();
}
}
\ No newline at end of file
package com.squareup.leakcanary;
public class FragmentSupportV4ZombieMatcher extends FragmentZombieMatcher {
@Override public String rootClassName() {
return "android.support.v4.app.Fragment";
}
}
package com.squareup.leakcanary;
import com.squareup.haha.perflib.ClassInstance;
import com.squareup.haha.perflib.Instance;
import java.util.List;
import static com.squareup.leakcanary.HahaHelper.classInstanceValues;
import static com.squareup.leakcanary.HahaHelper.fieldValue;
public class FragmentZombieMatcher implements OOMAutopsy.ZombieMatcher {
private static final int CREATED = 1;
@Override public String rootClassName() {
return "android.app.Fragment";
}
@Override public boolean isZombie(Instance instance) {
List<ClassInstance.FieldValue> fields = classInstanceValues(instance);
try {
Object mParentFragment = fieldValue(fields, "mParentFragment");
int mState = fieldValue(fields, "mState");
// Awesome internals, right? No value for DESTROY, just going back through CREATED.
return mParentFragment == null && mState < CREATED;
} catch (IllegalArgumentException ignored) {
return false;
}
}
}
......@@ -21,17 +21,29 @@ import com.squareup.haha.perflib.ClassObj;
import com.squareup.haha.perflib.Field;
import com.squareup.haha.perflib.Heap;
import com.squareup.haha.perflib.Instance;
import com.squareup.haha.perflib.RootObj;
import com.squareup.haha.perflib.Snapshot;
import com.squareup.haha.perflib.Type;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.squareup.leakcanary.LeakTraceElement.Holder.ARRAY;
import static com.squareup.leakcanary.LeakTraceElement.Holder.CLASS;
import static com.squareup.leakcanary.LeakTraceElement.Holder.OBJECT;
import static com.squareup.leakcanary.LeakTraceElement.Holder.THREAD;
import static com.squareup.leakcanary.Preconditions.checkNotNull;
import static java.util.Arrays.asList;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
public final class HahaHelper {
private static final String ANONYMOUS_CLASS_NAME_PATTERN = "^.+\\$\\d+$";
private static final Set<String> WRAPPER_TYPES = new HashSet<>(
asList(Boolean.class.getName(), Character.class.getName(), Float.class.getName(),
Double.class.getName(), Byte.class.getName(), Short.class.getName(),
......@@ -143,6 +155,116 @@ public final class HahaHelper {
throw new IllegalArgumentException("Field " + fieldName + " does not exists");
}
static LeakTrace buildLeakTrace(Snapshot snapshot, LeakNode leakingNode) {
// Compute retained size.
snapshot.computeDominators();
List<LeakTraceElement> elements = new ArrayList<>();
// We iterate from the leak to the GC root
LeakNode node = new LeakNode(null, leakingNode, null, null);
while (node != null) {
LeakTraceElement element = buildLeakElement(node);
if (element != null) {
elements.add(0, element);
}
node = node.parent;
}
long retainedSize = leakingNode.instance.getTotalRetainedSize();
return new LeakTrace(elements, retainedSize);
}
static LeakTraceElement buildLeakElement(LeakNode node) {
if (node.parent == null) {
// Ignore any root node.
return null;
}
Instance holder = node.parent.instance;
if (holder instanceof RootObj) {
return null;
}
LeakTraceElement.Type type = node.referenceType;
String referenceName = node.referenceName;
LeakTraceElement.Holder holderType;
String className;
String extra = null;
List<String> fields = new ArrayList<>();
if (holder instanceof ClassObj) {
ClassObj classObj = (ClassObj) holder;
holderType = CLASS;
className = classObj.getClassName();
for (Map.Entry<Field, Object> entry : classObj.getStaticFieldValues().entrySet()) {
Field field = entry.getKey();
Object value = entry.getValue();
fields.add("static " + field.getName() + " = " + value);
}
} else if (holder instanceof ArrayInstance) {
ArrayInstance arrayInstance = (ArrayInstance) holder;
holderType = ARRAY;
className = arrayInstance.getClassObj().getClassName();
if (arrayInstance.getArrayType() == Type.OBJECT) {
Object[] values = arrayInstance.getValues();
for (int i = 0; i < values.length; i++) {
fields.add("[" + i + "] = " + values[i]);
}
}
} else {
ClassInstance classInstance = (ClassInstance) holder;
ClassObj classObj = holder.getClassObj();
for (Map.Entry<Field, Object> entry : classObj.getStaticFieldValues().entrySet()) {
fields.add("static " + fieldToString(entry));
}
for (ClassInstance.FieldValue field : classInstance.getValues()) {
fields.add(fieldToString(field));
}
className = classObj.getClassName();
if (extendsThread(classObj)) {
holderType = THREAD;
String threadName = threadName(holder);
extra = "(named '" + threadName + "')";
} else if (className.matches(ANONYMOUS_CLASS_NAME_PATTERN)) {
String parentClassName = classObj.getSuperClassObj().getClassName();
if (Object.class.getName().equals(parentClassName)) {
holderType = OBJECT;
// This is an anonymous class implementing an interface. The API does not give access
// to the interfaces implemented by the class. Let's see if it's in the class path and
// use that instead.
try {
Class<?> actualClass = Class.forName(classObj.getClassName());
Class<?> implementedInterface = actualClass.getInterfaces()[0];
extra = "(anonymous class implements " + implementedInterface.getName() + ")";
} catch (ClassNotFoundException ignored) {
}
} else {
holderType = OBJECT;
// Makes it easier to figure out which anonymous class we're looking at.
extra = "(anonymous class extends " + parentClassName + ")";
}
} else {
holderType = OBJECT;
}
}
return new LeakTraceElement(referenceName, type, holderType, className, extra, fields);
}
static String getStackTraceString(Throwable throwable) {
if (throwable == null) {
return "";
}
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
throwable.printStackTrace(pw);
pw.flush();
return sw.toString();
}
static long since(long analysisStartNanoTime) {
return NANOSECONDS.toMillis(System.nanoTime() - analysisStartNanoTime);
}
private HahaHelper() {
throw new AssertionError();
}
......
......@@ -15,44 +15,32 @@
*/
package com.squareup.leakcanary;
import com.squareup.haha.perflib.ArrayInstance;
import com.squareup.haha.perflib.ClassInstance;
import com.squareup.haha.perflib.ClassObj;
import com.squareup.haha.perflib.Field;
import com.squareup.haha.perflib.HprofParser;
import com.squareup.haha.perflib.Instance;
import com.squareup.haha.perflib.RootObj;
import com.squareup.haha.perflib.Snapshot;
import com.squareup.haha.perflib.Type;
import com.squareup.haha.perflib.io.HprofBuffer;
import com.squareup.haha.perflib.io.MemoryMappedFileBuffer;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static com.squareup.leakcanary.AnalysisResult.failure;
import static com.squareup.leakcanary.AnalysisResult.leakDetected;
import static com.squareup.leakcanary.AnalysisResult.noLeak;
import static com.squareup.leakcanary.HahaHelper.asString;
import static com.squareup.leakcanary.HahaHelper.buildLeakTrace;
import static com.squareup.leakcanary.HahaHelper.classInstanceValues;
import static com.squareup.leakcanary.HahaHelper.extendsThread;
import static com.squareup.leakcanary.HahaHelper.fieldToString;
import static com.squareup.leakcanary.HahaHelper.fieldValue;
import static com.squareup.leakcanary.HahaHelper.threadName;
import static com.squareup.leakcanary.LeakTraceElement.Holder.ARRAY;
import static com.squareup.leakcanary.LeakTraceElement.Holder.CLASS;
import static com.squareup.leakcanary.LeakTraceElement.Holder.OBJECT;
import static com.squareup.leakcanary.LeakTraceElement.Holder.THREAD;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static com.squareup.leakcanary.HahaHelper.since;
import static java.util.Collections.singletonList;
/**
* Analyzes heap dumps generated by a {@link RefWatcher} to verify if suspected leaks are real.
*/
public final class HeapAnalyzer {
private static final String ANONYMOUS_CLASS_NAME_PATTERN = "^.+\\$\\d+$";
private final ExcludedRefs excludedRefs;
public HeapAnalyzer(ExcludedRefs excludedRefs) {
......@@ -108,112 +96,18 @@ public final class HeapAnalyzer {
Instance leakingRef) {
ShortestPathFinder pathFinder = new ShortestPathFinder(excludedRefs);
ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);
ShortestPathFinder.Result result = pathFinder.findPath(snapshot, singletonList(leakingRef));
// False alarm, no strong reference path to GC Roots.
if (result.leakingNode == null) {
if (result.leakingNodes.size() == 0) {
return noLeak(since(analysisStartNanoTime));
}
LeakTrace leakTrace = buildLeakTrace(result.leakingNode);
LeakTrace leakTrace = buildLeakTrace(snapshot, result.leakingNodes.get(0));
String className = leakingRef.getClassObj().getClassName();
return leakDetected(result.excludingKnownLeaks, className, leakTrace,
since(analysisStartNanoTime));
}
private LeakTrace buildLeakTrace(LeakNode leakingNode) {
List<LeakTraceElement> elements = new ArrayList<>();
// We iterate from the leak to the GC root
LeakNode node = new LeakNode(null, leakingNode, null, null);
while (node != null) {
LeakTraceElement element = buildLeakElement(node);
if (element != null) {
elements.add(0, element);
}
node = node.parent;
}
return new LeakTrace(elements);
}
private LeakTraceElement buildLeakElement(LeakNode node) {
if (node.parent == null) {
// Ignore any root node.
return null;
}
Instance holder = node.parent.instance;
if (holder instanceof RootObj) {
return null;
}
LeakTraceElement.Type type = node.referenceType;
String referenceName = node.referenceName;
LeakTraceElement.Holder holderType;
String className;
String extra = null;
List<String> fields = new ArrayList<>();
if (holder instanceof ClassObj) {
ClassObj classObj = (ClassObj) holder;
holderType = CLASS;
className = classObj.getClassName();
for (Map.Entry<Field, Object> entry : classObj.getStaticFieldValues().entrySet()) {
Field field = entry.getKey();
Object value = entry.getValue();
fields.add("static " + field.getName() + " = " + value);
}
} else if (holder instanceof ArrayInstance) {
ArrayInstance arrayInstance = (ArrayInstance) holder;
holderType = ARRAY;
className = arrayInstance.getClassObj().getClassName();
if (arrayInstance.getArrayType() == Type.OBJECT) {
Object[] values = arrayInstance.getValues();
for (int i = 0; i < values.length; i++) {
fields.add("[" + i + "] = " + values[i]);
}
}
} else {
ClassInstance classInstance = (ClassInstance) holder;
ClassObj classObj = holder.getClassObj();
for (Map.Entry<Field, Object> entry : classObj.getStaticFieldValues().entrySet()) {
fields.add("static " + fieldToString(entry));
}
for (ClassInstance.FieldValue field : classInstance.getValues()) {
fields.add(fieldToString(field));
}
className = classObj.getClassName();
if (extendsThread(classObj)) {
holderType = THREAD;
String threadName = threadName(holder);
extra = "(named '" + threadName + "')";
} else if (className.matches(ANONYMOUS_CLASS_NAME_PATTERN)) {
String parentClassName = classObj.getSuperClassObj().getClassName();
if (Object.class.getName().equals(parentClassName)) {
holderType = OBJECT;
// This is an anonymous class implementing an interface. The API does not give access
// to the interfaces implemented by the class. Let's see if it's in the class path and
// use that instead.
try {
Class<?> actualClass = Class.forName(classObj.getClassName());
Class<?> implementedInterface = actualClass.getInterfaces()[0];
extra = "(anonymous class implements " + implementedInterface.getName() + ")";
} catch (ClassNotFoundException ignored) {
}
} else {
holderType = OBJECT;
// Makes it easier to figure out which anonymous class we're looking at.
extra = "(anonymous class extends " + parentClassName + ")";
}
} else {
holderType = OBJECT;
}
}
return new LeakTraceElement(referenceName, type, holderType, className, extra, fields);
}
private long since(long analysisStartNanoTime) {
return NANOSECONDS.toMillis(System.nanoTime() - analysisStartNanoTime);
}
}
......@@ -28,13 +28,17 @@ import static java.util.Collections.unmodifiableList;
public final class LeakTrace implements Serializable {
public final List<LeakTraceElement> elements;
public final long retainedSize;
LeakTrace(List<LeakTraceElement> elements) {
LeakTrace(List<LeakTraceElement> elements, long retainedSize) {
this.retainedSize = retainedSize;
this.elements = unmodifiableList(new ArrayList<>(elements));
}
@Override public String toString() {
StringBuilder sb = new StringBuilder();
// TODO Human readable size.
sb.append("Memory retained: ").append(retainedSize).append("\n");
for (int i = 0; i < elements.size(); i++) {
LeakTraceElement element = elements.get(i);
sb.append("* ");
......
package com.squareup.leakcanary;
import com.squareup.haha.perflib.ClassObj;
import com.squareup.haha.perflib.HprofParser;
import com.squareup.haha.perflib.Instance;
import com.squareup.haha.perflib.Snapshot;
import com.squareup.haha.perflib.io.HprofBuffer;
import com.squareup.haha.perflib.io.MemoryMappedFileBuffer;
import java.io.File;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import static com.squareup.leakcanary.HahaHelper.buildLeakTrace;
import static com.squareup.leakcanary.HahaHelper.since;
import static java.util.Collections.emptyList;
public class OOMAutopsy {
public interface ZombieMatcher extends Serializable {
String rootClassName();
boolean isZombie(Instance instance);
}
private final ExcludedRefs excludedRefs;
private final List<ZombieMatcher> zombieMatchers;
public OOMAutopsy(ExcludedRefs excludedRefs, List<ZombieMatcher> zombieMatchers) {
this.excludedRefs = excludedRefs;
this.zombieMatchers = zombieMatchers;
}
public Autopsy performAutopsy(File heapDumpFile) {
long analysisStartNanoTime = System.nanoTime();
if (!heapDumpFile.exists()) {
Exception exception = new IllegalArgumentException("File does not exist: " + heapDumpFile);
return Autopsy.failure(exception, since(analysisStartNanoTime));
}
try {
HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
HprofParser parser = new HprofParser(buffer);
Snapshot snapshot = parser.parse();
List<Instance> leakingRefs = findLeakingReferences(snapshot);
List<LeakTrace> leakTraces;
if (leakingRefs.size() == 0) {
leakTraces = emptyList();
} else {
leakTraces = buildLeakTraces(snapshot, leakingRefs);
}
return Autopsy.result(leakTraces, since(analysisStartNanoTime));
} catch (Throwable e) {
return Autopsy.failure(e, since(analysisStartNanoTime));
}
}
private List<Instance> findLeakingReferences(Snapshot snapshot) {
// Those are objects in a destroyed state that were still around. We need to figure out whether
// the GC could have collected them or not.
List<Instance> zombies = new ArrayList<>();
for (ZombieMatcher matcher : zombieMatchers) {
String className = matcher.rootClassName();
ClassObj rootClass = snapshot.findClass(className);
List<ClassObj> classHierarchy = rootClass.getDescendantClasses();
for (ClassObj clazz : classHierarchy) {
List<Instance> instances = clazz.getInstancesList();
for (Instance maybeZombie : instances) {
if (matcher.isZombie(maybeZombie)) {
zombies.add(maybeZombie);
}
}
}
}
return zombies;
}
private List<LeakTrace> buildLeakTraces(Snapshot snapshot, List<Instance> leakingRefs) {
ShortestPathFinder pathFinder = new ShortestPathFinder(excludedRefs);
ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRefs);
if (result.leakingNodes.size() == 0) {
return Collections.emptyList();
}
Set<LeakNode> dominatedNodes = new LinkedHashSet<>();
for (LeakNode maybeDominator : result.leakingNodes) {
dominated:
for (LeakNode maybeDominated : result.leakingNodes) {
if (maybeDominator == maybeDominated) {
continue;
}
LeakNode ancestor = maybeDominated.parent;
while (ancestor != null) {
if (ancestor.instance == maybeDominator.instance) {
dominatedNodes.add(maybeDominated);
continue dominated;
}
ancestor = ancestor.parent;
}
}
}
result.leakingNodes.removeAll(dominatedNodes);
List<LeakTrace> leakTraces = new ArrayList<>();
for (LeakNode leakNode : result.leakingNodes) {
leakTraces.add(buildLeakTrace(snapshot, leakNode));
}
Collections.sort(leakTraces, new Comparator<LeakTrace>() {
@Override public int compare(LeakTrace lhs, LeakTrace rhs) {
return Long.valueOf(rhs.retainedSize).compareTo(lhs.retainedSize);
}
});
return leakTraces;
}
}