/* * This file is part of the Jikes RVM project (http://jikesrvm.org). * * This file is licensed to You under the Eclipse Public License (EPL); * You may not use this file except in compliance with the License. You * may obtain a copy of the License at * * http://www.opensource.org/licenses/eclipse-1.0.php * * See the COPYRIGHT.txt file distributed with this work for information * regarding copyright ownership. */ package org.mmtk.harness.sanity; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.mmtk.harness.Collector; import org.mmtk.harness.lang.Trace; import org.mmtk.harness.lang.Trace.Item; import org.mmtk.harness.vm.ObjectModel; import org.mmtk.vm.VM; import org.vmmagic.unboxed.Address; import org.vmmagic.unboxed.ObjectReference; /** * A table of live objects. During GC we track where each object gets copied, * and ensure that only one collector ever copies an object. */ public final class ObjectTable { private static final String NEWLINE = System.getProperty("line.separator"); /** * An object recording the copying of an object */ static final class ObjectCopy { private ObjectCopy(int epoch, ObjectReference source, ObjectReference dest) { this.epoch = epoch; this.source = source; this.dest = dest; } private final int epoch; private final ObjectReference source; private final ObjectReference dest; @Override public String toString() { return String.format("copied from %s to %s in gc %d",source,dest,epoch); } } /** * An entry in the table, representing an object in the heap. */ static final class Entry { /** Reserved for future features */ @SuppressWarnings("unused") Address start; /** Reserved for future features */ @SuppressWarnings("unused") int size; ObjectReference reference; private volatile ObjectReference copiedFrom; private final int birthEpoch = Collector.getCollectionCount(); private int deathEpoch = -1; private final int id; private final int site; private List<ObjectCopy> history = null; private Entry(ObjectReference reference, Address region, int size) { this.reference = reference; this.size = size; this.start = region; this.id = ObjectModel.getId(reference); this.site = ObjectModel.getSite(reference); } private void addHistory(ObjectReference dest) { if (history == null) { history = new ArrayList<ObjectCopy>(); } history.add(new ObjectCopy(Collector.getCollectionCount(), reference, dest)); } synchronized void copy(ObjectReference src, ObjectReference dest) { if (!this.reference.equals(src)) { throw new AssertionError("Attempt to copy "+src+" to "+dest+" twice!"); } addHistory(dest); this.setCopiedFrom(reference); this.reference = dest; this.start = ObjectModel.getStartAddressFromObject(dest); } public void kill() { deathEpoch = Collector.getCollectionCount(); } public boolean isLive() { return deathEpoch < 0; } public void assertLive() { if (!isLive()) { throw new AssertionError("Object, "+toString()+" (now "+Collector.getCollectionCount()+")"); } } public int getBirthEpoch() { return birthEpoch; } public void setCopiedFrom(ObjectReference copiedFrom) { this.copiedFrom = copiedFrom; } public ObjectReference getCopiedFrom() { return copiedFrom; } public int getId() { return id; } @Override public String toString() { return String.format("Object [id=%d, reference=%s, birthEpoch=%d, deathEpoch=%d, site=%d, copiedFrom=%s]", id, reference, birthEpoch,deathEpoch,site,copiedFrom); } public List<ObjectCopy> getHistory() { if (history == null) return Collections.emptyList(); return history; } public String formatHistory() { StringBuilder result = new StringBuilder(toString()); result.append(NEWLINE); for (ObjectCopy h : getHistory()) { result.append(" "); result.append(h.toString()); result.append(NEWLINE); } return result.toString(); } } private static final boolean VERBOSE = false; private final ConcurrentMap<ObjectReference,Entry> objects = new ConcurrentHashMap<ObjectReference,Entry>(); private final Set<ObjectReference> copiedObjects = Collections.synchronizedSet(new HashSet<ObjectReference>()); /** * Register allocation of an object * @param region The start of the allocated bytes * @param size The size of the region */ public void alloc(Address region, int size) { // Can't hold a lock while doing anything that might block // in the deterministic scheduler. Therefore we don't lock, // and rely on the fact that objects is a concurrent data structure ObjectReference object = VM.objectModel.getObjectFromStartAddress(region); objects.put(object,new Entry(object,region,size)); } /** * Check whether an object reference is valid. A reference is valid if it was * allocated at some point, or if it has been copied during the current GC. * * This is imprecise, and will report some dead objects as being live, eg * in a generational collector. * * @param object The object * @return {@code true} if it's valid */ public synchronized boolean isValid(ObjectReference object) { return object.isNull() || (objects.containsKey(object) && objects.get(object).isLive()) || copiedObjects.contains(object); } /** * Assert that an object reference is valid * * @param object The object to check */ public void assertValid(ObjectReference object) { if (object.isNull()) return; if (objects.containsKey(object)) { objects.get(object).assertLive(); return; } if (copiedObjects.contains(object)) return; assert false : object + " is not a valid object reference"; } /** * Adjust the object table for an object copy. * * Thread-safe because: * - objects is a concurrent map * - Mutations of the entry are synchronized on the entry itself. * - Attempts by two threads to copy an object simultaneously are * errors that need to be detected. This is detected by an * assertion in the {@link Entry#copy(ObjectReference, ObjectReference)} * method. * @param src Source reference * @param dest Destination reference */ public void copy(ObjectReference src, ObjectReference dest) { Entry entry = objects.get(src); if (entry == null) { entry = objects.get(dest); if (entry == null) { throw new AssertionError("Attempted to copy a nonexistent object, "+src); } throw new AssertionError("Attempt to copy object "+ObjectModel.getString(src)+" twice"); } if (!entry.isLive()) { throw new AssertionError("Attempted to copy a dead object, "+src+", which died in collection "+entry.deathEpoch+" (now "+Collector.getCollectionCount()+")"); } // entry.copy will detect data races and abort the second and subsequent // threads entry.copy(src, dest); // From here on, only one thread can be attempting to move // a given object, and the copiedObjects.add(src); objects.remove(src); objects.put(dest, entry); } /** * After the first pass of the sanity checker, we know exactly which objects are live. * Use this set to trim the set of valid objects after a full-heap collection. * * @param liveSet The known live objects */ public void trimToLiveSet(Set<ObjectReference> liveSet) { int removed = 0; Iterator<ObjectReference> iterator = objects.keySet().iterator(); while (iterator.hasNext()) { ObjectReference current = iterator.next(); if (!liveSet.contains(current)) { Entry entry = objects.get(current); if (VERBOSE || ObjectModel.isWatched(current)) { Trace.printf(Item.SANITY,"Object death: %s",entry.formatHistory()); } entry.kill(); } removed++; } Trace.trace(Item.SANITY, "Trimmed %d dead objects from object table", removed); } /** * After a GC (major or minor) it's an error to refer to a copied object by its previous * location. */ public synchronized void postGcCleanup() { copiedObjects.clear(); } }