Fix GC handling for HtmlUnit in development mode.

Patch by: jat
Review by: amitmanjhi



git-svn-id: https://google-web-toolkit.googlecode.com/svn/branches/farewellSwt@6215 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannel.java b/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannel.java
index 99629c7..2c589fd 100644
--- a/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannel.java
+++ b/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannel.java
@@ -29,16 +29,9 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
-import java.lang.ref.Reference;
-import java.lang.ref.ReferenceQueue;
-import java.lang.ref.WeakReference;
 import java.net.Socket;
-import java.util.HashSet;
-import java.util.IdentityHashMap;
-import java.util.Iterator;
-import java.util.Map;
+import java.util.Collections;
 import java.util.Set;
-import java.util.TreeMap;
 
 /**
  * 
@@ -48,7 +41,7 @@
   /**
    * Class representing a reference to a Java object.
    */
-  public static class JavaObjectRef {
+  public static class JavaObjectRef implements RemoteObjectRef {
     private int refId;
 
     public JavaObjectRef(int refId) {
@@ -60,6 +53,15 @@
     }
 
     @Override
+    public int hashCode() {
+      return refId;
+    }
+
+    public boolean isException() {
+      return refId < 0;
+    }
+
+    @Override
     public String toString() {
       return "JavaObjectRef(ref=" + refId + ")";
     }
@@ -68,14 +70,8 @@
   /**
    * Class representing a reference to a JS object.
    */
-  public static class JsObjectRef {
+  public static class JsObjectRef implements RemoteObjectRef  {
     
-    // TODO: refactor and remove this method.
-    public static void checkIdMap(int refId) {
-      assert !JSOBJECT_ID_MAP.get().containsKey(refId)
-      || (JSOBJECT_ID_MAP.get().get(refId).get() == null);
-    }
-
     private int refId;
     
     public JsObjectRef(int refId) {
@@ -110,13 +106,77 @@
   /**
    * Enumeration of message type ids.
    * 
-   * NOTE: order is important as this defines the ordinals used in the wire
-   * protocol.
+   * <p>Ids are used instead of relying on the ordinal to avoid sychronization
+   * problems with the client.
    */
   public enum MessageType {
-    INVOKE, RETURN, OLD_LOAD_MODULE, QUIT, LOAD_JSNI, INVOKE_SPECIAL, FREE_VALUE,
-    FATAL_ERROR, CHECK_VERSIONS, PROTOCOL_VERSION, CHOOSE_TRANSPORT,
-    SWITCH_TRANSPORT, LOAD_MODULE;
+    /**
+     * A message to invoke a method on the other side of the wire.  Note that
+     * the messages are asymmetric -- see {@link InvokeOnClientMessage} and
+     * {@link InvokeOnServerMessage}.
+     */
+    INVOKE(0),
+    
+    /**
+     * Returns the result of an INVOKE, INVOKE_SPECIAL, or LOAD_MODULE message. 
+     */
+    RETURN(1),
+    
+    /**
+     * v1 LOAD_MODULE message.
+     */
+    OLD_LOAD_MODULE(2),
+    
+    /**
+     * Normal closure of the connection.
+     */
+    QUIT(3),
+    
+    /**
+     * A request by the server to load JSNI source into the client's JS engine.
+     */
+    LOAD_JSNI(4),
+    
+    INVOKE_SPECIAL(5),
+    
+    FREE_VALUE(6),
+    
+    /**
+     * Abnormal termination of the connection.
+     */
+    FATAL_ERROR(7),
+    
+    CHECK_VERSIONS(8),
+    
+    PROTOCOL_VERSION(9),
+    
+    CHOOSE_TRANSPORT(10),
+    
+    SWITCH_TRANSPORT(11),
+    
+    LOAD_MODULE(12);
+    
+    private final int id;
+    
+    private MessageType(int id) {
+      this.id = id;
+    }
+    
+    public int getId() {
+      return id;
+    }
+  }
+
+  /**
+   * Represents an object on the other side of the channel, known to this side
+   * by an reference ID.
+   */
+  public interface RemoteObjectRef {
+    
+    /**
+      * Return the reference ID for this object.
+      */
+    int getRefid();
   }
 
   /**
@@ -149,10 +209,23 @@
     /**
      * Enumeration of dispatch IDs on object 0 (the ServerMethods object).
      * 
+     * <p>Ids are set specifically rather than relying on the ordinal to avoid
+     * synchronization problems with the client.
+     * 
      * TODO: hasMethod/hasProperty no longer used, remove them!
      */
     public enum SpecialDispatchId {
-      HasMethod, HasProperty, GetProperty, SetProperty,
+      HasMethod(0), HasProperty(1), GetProperty(2), SetProperty(3);
+      
+      private final int id;
+
+      private SpecialDispatchId(int id) {
+        this.id = id;
+      }
+      
+      public int getId() {
+        return id;
+      }
     }
 
     public abstract void freeValue(BrowserChannel channel, int[] ids);
@@ -197,24 +270,28 @@
       /**
        * Primitive values.
        */
-      NULL, BOOLEAN, BYTE, CHAR, SHORT, INT, LONG, FLOAT, DOUBLE, STRING,
+      NULL(0), BOOLEAN(1), BYTE(2), CHAR(3), SHORT(4), INT(5), LONG(6),
+      FLOAT(7), DOUBLE(8), STRING(9),
 
       /**
        * Representations of Java or JS objects, sent as an index into a table
        * kept on the side holding the actual object.
        */
-      JAVA_OBJECT, JS_OBJECT,
+      JAVA_OBJECT(10), JS_OBJECT(11),
 
       /**
        * A Javascript undef value, also used for void returns.
        */
-      UNDEFINED;
+      UNDEFINED(12);
 
-      ValueType() {
+      private final int id;
+
+      private ValueType(int id) {
+        this.id = id;
       }
 
       byte getTag() {
-        return (byte) this.ordinal();
+        return (byte) id;
       }
     }
 
@@ -556,11 +633,11 @@
           hostedHtmlVersion);
     }
 
-    private final int minVersion;
+    private final String hostedHtmlVersion;
 
     private final int maxVersion;
 
-    private final String hostedHtmlVersion;
+    private final int minVersion;
 
     public CheckVersionsMessage(BrowserChannel channel, int minVersion,
         int maxVersion, String hostedHtmlVersion) {
@@ -585,7 +662,7 @@
     @Override
     public void send() throws IOException {
       DataOutputStream stream = getBrowserChannel().getStreamToOtherSide();
-      stream.writeByte(MessageType.CHECK_VERSIONS.ordinal());
+      stream.writeByte(MessageType.CHECK_VERSIONS.getId());
       stream.writeInt(minVersion);
       stream.writeInt(maxVersion);
       writeUtf8String(stream, hostedHtmlVersion);
@@ -625,7 +702,7 @@
     @Override
     public void send() throws IOException {
       DataOutputStream stream = getBrowserChannel().getStreamToOtherSide();
-      stream.writeByte(MessageType.CHOOSE_TRANSPORT.ordinal());
+      stream.writeByte(MessageType.CHOOSE_TRANSPORT.getId());
       stream.writeInt(transports.length);
       for (String transport : transports) {
         writeUtf8String(stream, transport);
@@ -660,7 +737,7 @@
     @Override
     public void send() throws IOException {
       DataOutputStream stream = getBrowserChannel().getStreamToOtherSide();
-      stream.writeByte(MessageType.FATAL_ERROR.ordinal());
+      stream.writeByte(MessageType.FATAL_ERROR.getId());
       writeUtf8String(stream, error);
     }
   }
@@ -686,7 +763,7 @@
     public static void send(BrowserChannel channel, int[] ids)
         throws IOException {
       DataOutputStream stream = channel.getStreamToOtherSide();
-      stream.writeByte(MessageType.FREE_VALUE.ordinal());
+      stream.writeByte(MessageType.FREE_VALUE.getId());
       stream.writeInt(ids.length);
       for (int id : ids) {
         stream.writeInt(id);
@@ -730,19 +807,18 @@
       DataInputStream stream = channel.getStreamFromOtherSide();
       // NOTE: Tag has already been read.
       String methodName = readUtf8String(stream);
-      Value thisRef = readValue(stream);
+      Value thisRef = channel.readValue(stream);
       int argLen = stream.readInt();
       Value[] args = new Value[argLen];
       for (int i = 0; i < argLen; i++) {
-        args[i] = readValue(stream);
+        args[i] = channel.readValue(stream);
       }
       return new InvokeOnClientMessage(channel, methodName, thisRef, args);
     }
 
-    private final String methodName;
-
-    private final Value thisRef;
     private final Value[] args;
+    private final String methodName;
+    private final Value thisRef;
 
     public InvokeOnClientMessage(BrowserChannel channel, String methodName,
         Value thisRef, Value[] args) {
@@ -768,12 +844,12 @@
     public void send() throws IOException {
       final DataOutputStream stream = getBrowserChannel().getStreamToOtherSide();
 
-      stream.writeByte(MessageType.INVOKE.ordinal());
+      stream.writeByte(MessageType.INVOKE.getId());
       writeUtf8String(stream, methodName);
-      writeValue(stream, thisRef);
+      getBrowserChannel().writeValue(stream, thisRef);
       stream.writeInt(args.length);
       for (int i = 0; i < args.length; i++) {
-        writeValue(stream, args[i]);
+        getBrowserChannel().writeValue(stream, args[i]);
       }
       stream.flush();
     }
@@ -793,19 +869,19 @@
       DataInputStream stream = channel.getStreamFromOtherSide();
       // NOTE: Tag has already been read.
       int methodDispatchId = stream.readInt();
-      Value thisRef = readValue(stream);
+      Value thisRef = channel.readValue(stream);
       int argLen = stream.readInt();
       Value[] args = new Value[argLen];
       for (int i = 0; i < argLen; i++) {
-        args[i] = readValue(stream);
+        args[i] = channel.readValue(stream);
       }
       return new InvokeOnServerMessage(channel, methodDispatchId, thisRef,
           args);
     }
 
+    private final Value[] args;
     private final int methodDispatchId;
     private final Value thisRef;
-    private final Value[] args;
 
     public InvokeOnServerMessage(BrowserChannel channel, int methodDispatchId,
         Value thisRef, Value[] args) {
@@ -831,12 +907,12 @@
     public void send() throws IOException {
       final DataOutputStream stream = getBrowserChannel().getStreamToOtherSide();
 
-      stream.writeByte(MessageType.INVOKE.ordinal());
+      stream.writeByte(MessageType.INVOKE.getId());
       stream.writeInt(methodDispatchId);
-      writeValue(stream, thisRef);
+      getBrowserChannel().writeValue(stream, thisRef);
       stream.writeInt(args.length);
       for (int i = 0; i < args.length; i++) {
-        writeValue(stream, args[i]);
+        getBrowserChannel().writeValue(stream, args[i]);
       }
       stream.flush();
     }
@@ -860,13 +936,13 @@
       final int argLen = stream.readInt();
       final Value[] args = new Value[argLen];
       for (int i = 0; i < argLen; i++) {
-        args[i] = readValue(stream);
+        args[i] = channel.readValue(stream);
       }
       return new InvokeSpecialMessage(channel, dispatchId, args);
     }
 
-    private final SpecialDispatchId dispatchId;
     private final Value[] args;
+    private final SpecialDispatchId dispatchId;
 
     public InvokeSpecialMessage(BrowserChannel channel,
         SpecialDispatchId dispatchId, Value[] args) {
@@ -887,11 +963,11 @@
     public void send() throws IOException {
       final DataOutputStream stream = getBrowserChannel().getStreamToOtherSide();
 
-      stream.writeByte(MessageType.INVOKE_SPECIAL.ordinal());
-      stream.writeByte(dispatchId.ordinal());
+      stream.writeByte(MessageType.INVOKE_SPECIAL.getId());
+      stream.writeByte(dispatchId.getId());
       stream.writeInt(args.length);
       for (int i = 0; i < args.length; i++) {
-        writeValue(stream, args[i]);
+        getBrowserChannel().writeValue(stream, args[i]);
       }
       stream.flush();
     }
@@ -913,7 +989,7 @@
     public static void send(BrowserChannel channel, String js)
         throws IOException {
       DataOutputStream stream = channel.getStreamToOtherSide();
-      stream.write(MessageType.LOAD_JSNI.ordinal());
+      stream.write(MessageType.LOAD_JSNI.getId());
       writeUtf8String(stream, js);
       stream.flush();
     }
@@ -959,13 +1035,13 @@
 
     private final String moduleName;
 
-    private final String userAgent;
-
-    private final String url;
-    
     private final String sessionKey;
 
     private final String tabKey;
+    
+    private final String url;
+
+    private final String userAgent;
 
     /**
      * Creates a LoadModule message to be sent to the server.
@@ -1017,7 +1093,7 @@
     @Override
     public void send() throws IOException {
       DataOutputStream stream = getBrowserChannel().getStreamToOtherSide();
-      stream.writeByte(MessageType.LOAD_MODULE.ordinal());
+      stream.writeByte(MessageType.LOAD_MODULE.getId());
       writeUtf8String(stream, url);
       writeUtf8String(stream, tabKey);
       writeUtf8String(stream, sessionKey);
@@ -1069,6 +1145,20 @@
   }
 
   /**
+   * Provides a way of allocating JS and Java object ids without knowing
+   * which one is the remote type, so code can be shared between client and
+   * server.
+   */
+  protected interface ObjectRefFactory {
+
+    JavaObjectRef getJavaObjectRef(int refId);
+
+    JsObjectRef getJsObjectRef(int refId);
+
+    Set<Integer> getRefIdsForCleanup();
+  }
+
+  /**
    * A request from the client that the server load and initialize a given
    * module (original v1 version).
    */
@@ -1085,9 +1175,9 @@
 
     private final String moduleName;
 
-    private final String userAgent;
-
     private final int protoVersion;
+
+    private final String userAgent;
     
     public OldLoadModuleMessage(BrowserChannel channel, int protoVersion,
         String moduleName, String userAgent) {
@@ -1112,7 +1202,7 @@
     @Override
     public void send() throws IOException {
       DataOutputStream stream = getBrowserChannel().getStreamToOtherSide();
-      stream.writeByte(MessageType.OLD_LOAD_MODULE.ordinal());
+      stream.writeByte(MessageType.OLD_LOAD_MODULE.getId());
       stream.writeInt(protoVersion);
       writeUtf8String(stream, moduleName);
       writeUtf8String(stream, userAgent);
@@ -1146,7 +1236,7 @@
     @Override
     public void send() throws IOException {
       DataOutputStream stream = getBrowserChannel().getStreamToOtherSide();
-      stream.writeByte(MessageType.PROTOCOL_VERSION.ordinal());
+      stream.writeByte(MessageType.PROTOCOL_VERSION.getId());
       stream.writeInt(protocolVersion);
       stream.flush();
     }
@@ -1162,7 +1252,7 @@
 
     public static void send(BrowserChannel channel) throws IOException {
       final DataOutputStream stream = channel.getStreamToOtherSide();
-      stream.writeByte(MessageType.QUIT.ordinal());
+      stream.writeByte(MessageType.QUIT.getId());
       stream.flush();
     }
 
@@ -1184,16 +1274,16 @@
         throws IOException {
       final DataInputStream stream = channel.getStreamFromOtherSide();
       final boolean isException = stream.readBoolean();
-      final Value returnValue = readValue(stream);
+      final Value returnValue = channel.readValue(stream);
       return new ReturnMessage(channel, isException, returnValue);
     }
 
     public static void send(BrowserChannel channel, boolean isException,
         Value returnValue) throws IOException {
       final DataOutputStream stream = channel.getStreamToOtherSide();
-      stream.writeByte(MessageType.RETURN.ordinal());
+      stream.writeByte(MessageType.RETURN.getId());
       stream.writeBoolean(isException);
-      writeValue(stream, returnValue);
+      channel.writeValue(stream, returnValue);
       stream.flush();
     }
 
@@ -1203,8 +1293,8 @@
           returnOrException.getReturnValue());
     }
 
-    private final Value returnValue;
     private final boolean isException;
+    private final Value returnValue;
 
     public ReturnMessage(BrowserChannel channel, boolean isException,
         Value returnValue) {
@@ -1270,7 +1360,7 @@
     @Override
     public void send() throws IOException {
       DataOutputStream stream = getBrowserChannel().getStreamToOtherSide();
-      stream.writeByte(MessageType.SWITCH_TRANSPORT.ordinal());
+      stream.writeByte(MessageType.SWITCH_TRANSPORT.getId());
       writeUtf8String(stream, transport);
       writeUtf8String(stream, transportArgs);
     }
@@ -1282,61 +1372,8 @@
 
   public static final int SPECIAL_SERVERMETHODS_OBJECT = 0;
 
-  /**
-   * This accumulates JsObjectRefs that are no longer referenced in the JVM.
-   */
-  private static final ThreadLocal<ReferenceQueue<JsObjectRef>> JSOBJECT_REF_QUEUE = new ThreadLocal<ReferenceQueue<JsObjectRef>>() {
-    @Override
-    protected ReferenceQueue<JsObjectRef> initialValue() {
-      return new ReferenceQueue<JsObjectRef>();
-    }
-  };
-
-  /**
-   * This map associates a JS reference id with a Reference to the JSObjectRef
-   * that currently represents that id.
-   */
-  private static final ThreadLocal<Map<Integer, Reference<JsObjectRef>>> JSOBJECT_ID_MAP = new ThreadLocal<Map<Integer, Reference<JsObjectRef>>>() {
-    @Override
-    protected Map<Integer, Reference<JsObjectRef>> initialValue() {
-      return new TreeMap<Integer, Reference<JsObjectRef>>();
-    }
-  };
-
-  /**
-   * This maps References to JsObjectRefs back to the original refId. Because we
-   * need the refId of the JsValueRef after it's been garbage-collected, this
-   * state must be stored externally.
-   */
-  private static final ThreadLocal<Map<Reference<JsObjectRef>, Integer>> REFERENCE_ID_MAP = new ThreadLocal<Map<Reference<JsObjectRef>, Integer>>() {
-    @Override
-    protected Map<Reference<JsObjectRef>, Integer> initialValue() {
-      return new IdentityHashMap<Reference<JsObjectRef>, Integer>();
-    }
-  };
-
-  /**
-   * Obtain the JsObjectRef that is currently in use to act as a proxy for the
-   * given JS object id.
-   */
-  protected static JsObjectRef getJsObjectRef(int refId) {
-    // Access is implicitly synchronous due to ThreadLocal
-    Map<Integer, Reference<JsObjectRef>> map = JSOBJECT_ID_MAP.get();
-    if (map.containsKey(refId)) {
-      Reference<JsObjectRef> ref = map.get(refId);
-      JsObjectRef toReturn = ref.get();
-      if (toReturn != null) {
-        return toReturn;
-      }
-    }
-
-    JsObjectRef.checkIdMap(refId);
-    JsObjectRef toReturn = new JsObjectRef(refId);
-    Reference<JsObjectRef> ref = new WeakReference<JsObjectRef>(toReturn,
-        JSOBJECT_REF_QUEUE.get());
-    map.put(refId, ref);
-    REFERENCE_ID_MAP.get().put(ref, refId);
-    return toReturn;
+  protected static JavaObjectRef getJavaObjectRef(int refId) {
+    return new JavaObjectRef(refId);
   }
 
   protected static String readUtf8String(DataInputStream stream)
@@ -1347,60 +1384,6 @@
     return new String(data, "UTF8");
   }
 
-  protected static Value readValue(DataInputStream stream) throws IOException {
-    ValueType tag;
-    try {
-      tag = readValueType(stream);
-    } catch (BrowserChannelException e) {
-      IOException ee = new IOException();
-      ee.initCause(e);
-      throw ee;
-    }
-    Value value = new Value();
-    switch (tag) {
-      case NULL:
-        value.setNull();
-        break;
-      case UNDEFINED:
-        value.setUndefined();
-        break;
-      case BOOLEAN:
-        value.setBoolean(stream.readByte() != 0);
-        break;
-      case BYTE:
-        value.setByte(stream.readByte());
-        break;
-      case CHAR:
-        value.setChar(stream.readChar());
-        break;
-      case FLOAT:
-        value.setFloat(stream.readFloat());
-        break;
-      case INT:
-        value.setInt(stream.readInt());
-        break;
-      case LONG:
-        value.setLong(stream.readLong());
-        break;
-      case DOUBLE:
-        value.setDouble(stream.readDouble());
-        break;
-      case SHORT:
-        value.setShort(stream.readShort());
-        break;
-      case STRING:
-        value.setString(readUtf8String(stream));
-        break;
-      case JS_OBJECT:
-        value.setJsObject(getJsObjectRef(stream.readInt()));
-        break;
-      case JAVA_OBJECT:
-        value.setJavaObject(new JavaObjectRef(stream.readInt()));
-        break;
-    }
-    return value;
-  }
-
   protected static ValueType readValueType(DataInputStream stream)
       throws IOException, BrowserChannelException {
     int type = stream.readByte();
@@ -1487,7 +1470,186 @@
     }
   }
 
-  protected static void writeValue(DataOutputStream stream, Value value)
+  private static void writeUndefined(DataOutputStream stream)
+      throws IOException {
+    stream.writeByte(ValueType.UNDEFINED.getTag());
+  }
+
+  private final ObjectRefFactory objectRefFactory;
+
+  private Socket socket;
+
+  private final DataInputStream streamFromOtherSide;
+
+  private final DataOutputStream streamToOtherSide;
+
+  public BrowserChannel(Socket socket, ObjectRefFactory objectRefFactory)
+      throws IOException {
+    this(new BufferedInputStream(socket.getInputStream()),
+        new BufferedOutputStream(socket.getOutputStream()),
+        objectRefFactory);
+    this.socket = socket;
+  }
+
+  protected BrowserChannel(InputStream inputStream, OutputStream outputStream,
+      ObjectRefFactory objectRefFactory)
+      throws IOException {
+    streamFromOtherSide = new DataInputStream(inputStream);
+    streamToOtherSide = new DataOutputStream(outputStream);
+    socket = null;
+    this.objectRefFactory = objectRefFactory;
+  }
+
+  public void endSession() {
+    Utility.close(streamFromOtherSide);
+    Utility.close(streamToOtherSide);
+    Utility.close(socket);
+  }
+
+  /**
+   * @return a set of remote object reference IDs to be freed.
+   */
+  public Set<Integer> getRefIdsForCleanup() {
+    return objectRefFactory.getRefIdsForCleanup();
+  }
+
+  public String getRemoteEndpoint() {
+    if (socket == null) {
+      return "";
+    }
+    return socket.getInetAddress().getCanonicalHostName() + ":"
+        + socket.getPort();
+  }
+
+  public Value invoke(String methodName, Value vthis, Value[] vargs,
+      SessionHandler handler) throws IOException, BrowserChannelException {
+    new InvokeOnClientMessage(this, methodName, vthis, vargs).send();
+    final ReturnMessage msg = reactToMessagesWhileWaitingForReturn(handler);
+    return msg.returnValue;
+  }
+
+  public void reactToMessages(SessionHandler handler) throws IOException,
+      BrowserChannelException {
+    do {
+      getStreamToOtherSide().flush();
+      MessageType messageType = Message.readMessageType(getStreamFromOtherSide());
+      switch (messageType) {
+        case FREE_VALUE:
+          final FreeMessage freeMsg = FreeMessage.receive(this);
+          handler.freeValue(this, freeMsg.getIds());
+          break;
+        case INVOKE:
+          final InvokeOnServerMessage imsg = InvokeOnServerMessage.receive(this);
+          ExceptionOrReturnValue result = handler.invoke(this, imsg.getThis(),
+              imsg.getMethodDispatchId(), imsg.getArgs());
+          sendFreedValues();
+          ReturnMessage.send(this, result);
+          break;
+        case INVOKE_SPECIAL:
+          handleInvokeSpecial(handler);
+          break;
+        case QUIT:
+          return;
+        default:
+          throw new BrowserChannelException("Invalid message type "
+              + messageType);
+      }
+    } while (true);
+  }
+
+  public ReturnMessage reactToMessagesWhileWaitingForReturn(
+      SessionHandler handler) throws IOException, BrowserChannelException {
+    do {
+      getStreamToOtherSide().flush();
+      MessageType messageType = Message.readMessageType(getStreamFromOtherSide());
+      switch (messageType) {
+        case FREE_VALUE:
+          final FreeMessage freeMsg = FreeMessage.receive(this);
+          handler.freeValue(this, freeMsg.getIds());
+          break;
+        case RETURN:
+          return ReturnMessage.receive(this);
+        case INVOKE:
+          final InvokeOnServerMessage imsg = InvokeOnServerMessage.receive(this);
+          ExceptionOrReturnValue result = handler.invoke(this, imsg.getThis(),
+              imsg.getMethodDispatchId(), imsg.getArgs());
+          sendFreedValues();
+          ReturnMessage.send(this, result);
+          break;
+        case INVOKE_SPECIAL:
+          handleInvokeSpecial(handler);
+          break;
+        default:
+          throw new BrowserChannelException("Invalid message type "
+              + messageType + " received waiting for return.");
+      }
+    } while (true);
+  }
+
+  protected DataInputStream getStreamFromOtherSide() {
+    return streamFromOtherSide;
+  }
+
+  protected DataOutputStream getStreamToOtherSide() {
+    return streamToOtherSide;
+  }
+
+  protected Value readValue(DataInputStream stream) throws IOException {
+    ValueType tag;
+    try {
+      tag = readValueType(stream);
+    } catch (BrowserChannelException e) {
+      IOException ee = new IOException();
+      ee.initCause(e);
+      throw ee;
+    }
+    Value value = new Value();
+    switch (tag) {
+      case NULL:
+        value.setNull();
+        break;
+      case UNDEFINED:
+        value.setUndefined();
+        break;
+      case BOOLEAN:
+        value.setBoolean(stream.readByte() != 0);
+        break;
+      case BYTE:
+        value.setByte(stream.readByte());
+        break;
+      case CHAR:
+        value.setChar(stream.readChar());
+        break;
+      case FLOAT:
+        value.setFloat(stream.readFloat());
+        break;
+      case INT:
+        value.setInt(stream.readInt());
+        break;
+      case LONG:
+        value.setLong(stream.readLong());
+        break;
+      case DOUBLE:
+        value.setDouble(stream.readDouble());
+        break;
+      case SHORT:
+        value.setShort(stream.readShort());
+        break;
+      case STRING:
+        value.setString(readUtf8String(stream));
+        break;
+      case JS_OBJECT:
+        value.setJsObject(objectRefFactory.getJsObjectRef(stream.readInt()));
+        break;
+      case JAVA_OBJECT:
+        value.setJavaObject(objectRefFactory.getJavaObjectRef(
+            stream.readInt()));
+        break;
+    }
+    return value;
+  }
+
+  protected void writeValue(DataOutputStream stream, Value value)
       throws IOException {
     if (value.isNull()) {
       writeNull(stream);
@@ -1518,147 +1680,6 @@
     }
   }
 
-  private static void writeUndefined(DataOutputStream stream)
-      throws IOException {
-    stream.writeByte(ValueType.UNDEFINED.getTag());
-  }
-
-  private final DataInputStream streamFromOtherSide;
-
-  private final DataOutputStream streamToOtherSide;
-
-  private Socket socket;
-
-  public BrowserChannel(Socket socket) throws IOException {
-    this(new BufferedInputStream(socket.getInputStream()),
-        new BufferedOutputStream(socket.getOutputStream()));
-    this.socket = socket;
-  }
-
-  protected BrowserChannel(InputStream inputStream, OutputStream outputStream)
-      throws IOException {
-    streamFromOtherSide = new DataInputStream(inputStream);
-    streamToOtherSide = new DataOutputStream(outputStream);
-    socket = null;
-  }
-
-  public void endSession() {
-    Utility.close(streamFromOtherSide);
-    Utility.close(streamToOtherSide);
-    Utility.close(socket);
-  }
-
-  public Set<Integer> getRefIdsForCleanup() {
-    // Access to these objects is inherently synchronous
-    Map<Integer, Reference<JsObjectRef>> objectMap = JSOBJECT_ID_MAP.get();
-    Map<Reference<JsObjectRef>, Integer> refIdMap = REFERENCE_ID_MAP.get();
-    ReferenceQueue<JsObjectRef> q = JSOBJECT_REF_QUEUE.get();
-    Set<Integer> toReturn = new HashSet<Integer>();
-
-    // Find all refIds associated with previous garbage collection cycles
-    Reference<? extends JsObjectRef> ref;
-    while ((ref = q.poll()) != null) {
-      Integer i = refIdMap.remove(ref);
-      assert i != null;
-      toReturn.add(i);
-    }
-
-    /*
-     * Check for liveness. This is necessary because the last reference to a
-     * JsObjectRef could have been cleared and a new reference to that refId
-     * created before this method has been called.
-     */
-    for (Iterator<Integer> i = toReturn.iterator(); i.hasNext();) {
-      Integer refId = i.next();
-      if (objectMap.containsKey(refId)) {
-        if (objectMap.get(refId).get() != null) {
-          i.remove();
-        } else {
-          objectMap.remove(refId);
-        }
-      }
-    }
-
-    return toReturn;
-  }
-
-  public String getRemoteEndpoint() {
-    if (socket == null) {
-      return "";
-    }
-    return socket.getInetAddress().getCanonicalHostName() + ":"
-        + socket.getPort();
-  }
-
-  public Value invoke(String methodName, Value vthis, Value[] vargs,
-      SessionHandler handler) throws IOException, BrowserChannelException {
-    new InvokeOnClientMessage(this, methodName, vthis, vargs).send();
-    final ReturnMessage msg = reactToMessagesWhileWaitingForReturn(handler);
-    return msg.returnValue;
-  }
-
-  public void reactToMessages(SessionHandler handler) throws IOException,
-      BrowserChannelException {
-    do {
-      getStreamToOtherSide().flush();
-      MessageType messageType = Message.readMessageType(getStreamFromOtherSide());
-      switch (messageType) {
-        case FREE_VALUE:
-          final FreeMessage freeMsg = FreeMessage.receive(this);
-          handler.freeValue(this, freeMsg.getIds());
-          break;
-        case INVOKE:
-          final InvokeOnServerMessage imsg = InvokeOnServerMessage.receive(this);
-          ReturnMessage.send(this, handler.invoke(this, imsg.getThis(),
-              imsg.getMethodDispatchId(), imsg.getArgs()));
-          break;
-        case INVOKE_SPECIAL:
-          handleInvokeSpecial(handler);
-          break;
-        case QUIT:
-          return;
-        default:
-          throw new BrowserChannelException("Invalid message type "
-              + messageType);
-      }
-    } while (true);
-  }
-
-  public ReturnMessage reactToMessagesWhileWaitingForReturn(
-      SessionHandler handler) throws IOException, BrowserChannelException {
-    do {
-      getStreamToOtherSide().flush();
-      MessageType messageType = Message.readMessageType(getStreamFromOtherSide());
-      switch (messageType) {
-        case FREE_VALUE:
-          final FreeMessage freeMsg = FreeMessage.receive(this);
-          handler.freeValue(this, freeMsg.getIds());
-          break;
-        case RETURN:
-          return ReturnMessage.receive(this);
-        case INVOKE:
-          final InvokeOnServerMessage imsg = InvokeOnServerMessage.receive(this);
-          ReturnMessage.send(this, handler.invoke(this, imsg.getThis(),
-              imsg.getMethodDispatchId(), imsg.getArgs()));
-          break;
-        case INVOKE_SPECIAL:
-          handleInvokeSpecial(handler);
-          break;
-        default:
-          throw new BrowserChannelException("Invalid message type "
-              + messageType + " received waiting for return.");
-      }
-    } while (true);
-  }
-
-  protected DataInputStream getStreamFromOtherSide() {
-    return streamFromOtherSide;
-  }
-
-  protected DataOutputStream getStreamToOtherSide() {
-    return streamToOtherSide;
-  }
-
   private void handleInvokeSpecial(SessionHandler handler) throws IOException,
       BrowserChannelException {
     final InvokeSpecialMessage ismsg = InvokeSpecialMessage.receive(this);
@@ -1680,4 +1701,17 @@
     }
     ReturnMessage.send(this, retExc);
   }
+
+  private void sendFreedValues() throws IOException {
+    Set<Integer> freed = objectRefFactory.getRefIdsForCleanup();
+    int n = freed.size();
+    if (n > 0) {
+      int[] ids = new int[n];
+      int i = 0;
+      for (Integer id : freed) {
+        ids[i++] = id;
+      }
+      FreeMessage.send(this, ids);
+    }
+  }
 }
diff --git a/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannelClient.java b/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannelClient.java
index 4713e95..c268194 100644
--- a/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannelClient.java
+++ b/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannelClient.java
@@ -21,6 +21,7 @@
 
 import java.io.IOException;
 import java.net.Socket;
+import java.util.Set;
 
 /**
  * Implementation of the BrowserChannel for the client side.
@@ -28,7 +29,32 @@
  */
 public class BrowserChannelClient extends BrowserChannel {
 
-  private static final int PROTOCOL_VERSION = 2;
+  private static class ClientObjectRefFactory implements ObjectRefFactory {
+
+    private final RemoteObjectTable<JavaObjectRef> remoteObjectTable;
+
+    public ClientObjectRefFactory() {
+      remoteObjectTable = new RemoteObjectTable<JavaObjectRef>();
+    }
+
+    public JavaObjectRef getJavaObjectRef(int refId) {
+      JavaObjectRef objectRef = remoteObjectTable.getRemoteObjectRef(refId);
+      if (objectRef == null) {
+        objectRef = new JavaObjectRef(refId);
+        remoteObjectTable.putRemoteObjectRef(refId, objectRef);
+      }
+      return objectRef;
+    }
+
+    public JsObjectRef getJsObjectRef(int refId) {
+      return new JsObjectRef(refId);
+    }
+
+    public Set<Integer> getRefIdsForCleanup() {
+      return remoteObjectTable.getRefIdsForCleanup();
+    }
+  }
+
   private final HtmlUnitSessionHandler htmlUnitSessionHandler;
   private final PrintWriterTreeLogger logger = new PrintWriterTreeLogger();
   private final String moduleName;
@@ -41,7 +67,8 @@
   public BrowserChannelClient(String addressParts[], String url,
       String sessionKey, String moduleName, String versionString,
       HtmlUnitSessionHandler htmlUnitSessionHandler) throws IOException {
-    super(new Socket(addressParts[0], Integer.parseInt(addressParts[1])));
+    super(new Socket(addressParts[0], Integer.parseInt(addressParts[1])),
+        new ClientObjectRefFactory());
     connected = true;
     this.url = url;
     this.sessionKey = sessionKey;
@@ -108,8 +135,8 @@
   private boolean init() throws IOException, BrowserChannelException {
     logger.log(TreeLogger.DEBUG, "sending " + MessageType.CHECK_VERSIONS
         + " message");
-    new CheckVersionsMessage(this, PROTOCOL_VERSION, PROTOCOL_VERSION,
-        versionString).send();
+    new CheckVersionsMessage(this, BROWSERCHANNEL_PROTOCOL_VERSION,
+        BROWSERCHANNEL_PROTOCOL_VERSION, versionString).send();
     MessageType type = Message.readMessageType(getStreamFromOtherSide());
     switch (type) {
       case PROTOCOL_VERSION:
diff --git a/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannelServer.java b/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannelServer.java
index 9ec6599..57ee158 100644
--- a/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannelServer.java
+++ b/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannelServer.java
@@ -20,6 +20,7 @@
 
 import java.io.IOException;
 import java.net.Socket;
+import java.util.Set;
 
 /**
  * 
@@ -27,11 +28,37 @@
 public final class BrowserChannelServer extends BrowserChannel
     implements Runnable {
 
+  private static class ServerObjectRefFactory implements ObjectRefFactory {
+
+    private final RemoteObjectTable<JsObjectRef> remoteObjectTable;
+    
+    public ServerObjectRefFactory() {
+      remoteObjectTable = new RemoteObjectTable<JsObjectRef>();
+    }
+
+    public JavaObjectRef getJavaObjectRef(int refId) {
+      return new JavaObjectRef(refId);
+    }
+
+    public JsObjectRef getJsObjectRef(int refId) {
+      JsObjectRef objectRef = remoteObjectTable.getRemoteObjectRef(refId);
+      if (objectRef == null) {
+        objectRef = new JsObjectRef(refId);
+        remoteObjectTable.putRemoteObjectRef(refId, objectRef);
+      }
+      return objectRef;
+    }
+
+    public Set<Integer> getRefIdsForCleanup() {
+      return remoteObjectTable.getRefIdsForCleanup();
+    }
+  }
+
   public static final String JSO_CLASS = "com.google.gwt.core.client.JavaScriptObject";
 
   private SessionHandler handler;
 
-  private final ObjectsTable javaObjectsInBrowser = new ObjectsTable();
+  private final ServerObjectsTable javaObjectsInBrowser = new ServerObjectsTable();
 
   private TreeLogger logger;
 
@@ -43,7 +70,7 @@
 
   public BrowserChannelServer(TreeLogger initialLogger, Socket socket,
       SessionHandler handler) throws IOException {
-    super(socket);
+    super(socket, new ServerObjectRefFactory());
     this.handler = handler;
     this.logger = initialLogger;
     Thread thread = new Thread(this);
@@ -62,7 +89,7 @@
     }
   }
 
-  public ObjectsTable getJavaObjectsExposedInBrowser() {
+  public ServerObjectsTable getJavaObjectsExposedInBrowser() {
     return javaObjectsInBrowser;
   }
 
@@ -84,7 +111,7 @@
   public void invokeJavascript(CompilingClassLoader ccl, JsValueOOPHM jsthis,
       String methodName, JsValueOOPHM[] args, JsValueOOPHM returnJsValue)
       throws Throwable {
-    final ObjectsTable remoteObjects = getJavaObjectsExposedInBrowser();
+    final ServerObjectsTable remoteObjects = getJavaObjectsExposedInBrowser();
     Value vthis = convertFromJsValue(remoteObjects, jsthis);
     Value[] vargs = new Value[args.length];
     for (int i = 0; i < args.length; ++i) {
@@ -175,7 +202,7 @@
    * @param jsval value to convert
    * @return jsval as a Value object.
    */
-  Value convertFromJsValue(ObjectsTable localObjects, JsValueOOPHM jsval) {
+  Value convertFromJsValue(ServerObjectsTable localObjects, JsValueOOPHM jsval) {
     Value value = new Value();
     if (jsval.isNull()) {
       value.setNull();
@@ -214,7 +241,7 @@
    * @param val Value to convert
    * @param jsval JsValue object to receive converted value.
    */
-  void convertToJsValue(CompilingClassLoader ccl, ObjectsTable localObjects,
+  void convertToJsValue(CompilingClassLoader ccl, ServerObjectsTable localObjects,
       Value val, JsValueOOPHM jsval) {
     switch (val.getType()) {
       case NULL:
diff --git a/dev/oophm/src/com/google/gwt/dev/shell/JsValueOOPHM.java b/dev/oophm/src/com/google/gwt/dev/shell/JsValueOOPHM.java
index 0e3d7c5..4ab0c77 100644
--- a/dev/oophm/src/com/google/gwt/dev/shell/JsValueOOPHM.java
+++ b/dev/oophm/src/com/google/gwt/dev/shell/JsValueOOPHM.java
@@ -144,7 +144,6 @@
    * @param jsRefId pointer to underlying JsRootedValue as an integer.
    */
   public JsValueOOPHM(int jsRefId) {
-    JsObjectRef.checkIdMap(jsRefId);
     this.value = new JsObjectRef(jsRefId);
   }
 
diff --git a/dev/oophm/src/com/google/gwt/dev/shell/OophmSessionHandler.java b/dev/oophm/src/com/google/gwt/dev/shell/OophmSessionHandler.java
index 1eeae51..345580e 100644
--- a/dev/oophm/src/com/google/gwt/dev/shell/OophmSessionHandler.java
+++ b/dev/oophm/src/com/google/gwt/dev/shell/OophmSessionHandler.java
@@ -48,7 +48,7 @@
   @Override
   public void freeValue(BrowserChannel channel, int[] ids) {
     BrowserChannelServer serverChannel = (BrowserChannelServer) channel;
-    ObjectsTable localObjects = serverChannel.getJavaObjectsExposedInBrowser();
+    ServerObjectsTable localObjects = serverChannel.getJavaObjectsExposedInBrowser();
     for (int id : ids) {
       localObjects.free(id);
     }
@@ -60,7 +60,7 @@
     BrowserChannelServer serverChannel = (BrowserChannelServer) channel;
     ModuleSpace moduleSpace = moduleMap.get(serverChannel);
     assert moduleSpace != null;
-    ObjectsTable localObjects = serverChannel.getJavaObjectsExposedInBrowser();
+    ServerObjectsTable localObjects = serverChannel.getJavaObjectsExposedInBrowser();
     try {
       JsValueOOPHM obj = new JsValueOOPHM();
       DispatchObject dispObj;
@@ -91,7 +91,7 @@
   public ExceptionOrReturnValue invoke(BrowserChannel channel, Value thisVal,
       int methodDispatchId, Value[] args) {
     BrowserChannelServer serverChannel = (BrowserChannelServer) channel;
-    ObjectsTable localObjects = serverChannel.getJavaObjectsExposedInBrowser();
+    ServerObjectsTable localObjects = serverChannel.getJavaObjectsExposedInBrowser();
     ModuleSpace moduleSpace = moduleMap.get(serverChannel);
     assert moduleSpace != null;
     CompilingClassLoader cl = moduleSpace.getIsolatedClassLoader();
@@ -190,7 +190,7 @@
     BrowserChannelServer serverChannel = (BrowserChannelServer) channel;
     ModuleSpace moduleSpace = moduleMap.get(serverChannel);
     assert moduleSpace != null;
-    ObjectsTable localObjects = serverChannel.getJavaObjectsExposedInBrowser();
+    ServerObjectsTable localObjects = serverChannel.getJavaObjectsExposedInBrowser();
     try {
       JsValueOOPHM obj = new JsValueOOPHM();
       DispatchObject dispObj;
diff --git a/dev/oophm/src/com/google/gwt/dev/shell/RemoteObjectTable.java b/dev/oophm/src/com/google/gwt/dev/shell/RemoteObjectTable.java
new file mode 100644
index 0000000..f6004a8
--- /dev/null
+++ b/dev/oophm/src/com/google/gwt/dev/shell/RemoteObjectTable.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2009 Google 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.google.gwt.dev.shell;
+
+import com.google.gwt.dev.shell.BrowserChannel.RemoteObjectRef;
+
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Keeps track of references to remote objects.  When the objects are no longer
+ * needed, their ids are returned in {@link #getRefIdsForCleanup()}.
+ * 
+ * @param <T> subtype of RemoteObjectRef contained in this table
+ */
+public class RemoteObjectTable<T extends RemoteObjectRef> {
+
+  /**
+   * This maps References to RemoteObjectRefs back to the original refId.
+   * Because we need the refId of the RemoteObjectRef after it's been
+   * garbage-collected, this state must be stored externally.
+   */
+  private final Map<Reference<T>, Integer> idFromRemoteObject;
+  
+  /**
+   * This accumulates remote objects that are no longer referenced on this side
+   * of the channel.
+   */
+  private final ReferenceQueue<T> refQueue;
+
+  /**
+   * This map associates a remote object ID with a Reference to the
+   * RemoteObjectRef that currently represents that id.
+   */
+  private final Map<Integer, Reference<T>> remoteObjectFromId;
+
+  /**
+   * Create a new RemoteObjectTable.
+   */
+  public RemoteObjectTable() {
+    refQueue = new ReferenceQueue<T>();
+    remoteObjectFromId = new HashMap<Integer, Reference<T>>();
+    idFromRemoteObject = new IdentityHashMap<Reference<T>, Integer>();
+  }
+
+  /**
+   * @return the set of remote object reference IDs that should be freed.
+   */
+  public synchronized Set<Integer> getRefIdsForCleanup() {
+    // Access to these objects is inherently synchronous
+    Map<Integer, Reference<T>> objectMap = remoteObjectFromId;
+    Map<Reference<T>, Integer> refIdMap = idFromRemoteObject;
+    Set<Integer> toReturn = new HashSet<Integer>();
+
+    // Find all refIds associated with previous garbage collection cycles
+    Reference<? extends RemoteObjectRef> ref;
+    while ((ref = refQueue.poll()) != null) {
+      Integer i = refIdMap.remove(ref);
+      assert i != null;
+      toReturn.add(i);
+    }
+
+    /*
+     * Check for liveness. This is necessary because the last reference to a
+     * RemoteObjectRef could have been cleared and a new reference to that refId
+     * created before this method has been called.
+     */
+    for (Iterator<Integer> i = toReturn.iterator(); i.hasNext();) {
+      Integer refId = i.next();
+      if (objectMap.containsKey(refId)) {
+        if (objectMap.get(refId).get() != null) {
+          i.remove();
+        } else {
+          objectMap.remove(refId);
+        }
+      }
+    }
+
+    return toReturn;
+  }
+
+  /**
+   * Obtain the RemoteObjectRef that is currently in use to act as a proxy for
+   * the given remote object ID.
+   * 
+   * @return the RemoteObjectRef or null if the ID is not currently in use
+   */
+  public synchronized T getRemoteObjectRef(int refId) {
+    if (remoteObjectFromId.containsKey(refId)) {
+      Reference<T> ref = remoteObjectFromId.get(refId);
+      T toReturn = ref.get();
+      if (toReturn != null) {
+        return toReturn;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Check to see if this ID does not already exist.
+   * 
+   * @param refId reference ID to check
+   * @return true if this ID is not currently in use
+   */
+  public synchronized boolean isNewObjectId(int refId) {
+    return !remoteObjectFromId.containsKey(refId)
+    || (remoteObjectFromId.get(refId).get() == null);
+  }
+
+  /**
+   * Store a remote object reference in the table.
+   * 
+   * @param refId
+   * @param remoteObjectRef
+   */
+  public synchronized void putRemoteObjectRef(int refId,
+      T remoteObjectRef) {
+    Reference<T> ref = new WeakReference<T>(remoteObjectRef, refQueue);
+    remoteObjectFromId.put(refId, ref);
+    idFromRemoteObject.put(ref, refId);
+  }
+}
diff --git a/dev/oophm/src/com/google/gwt/dev/shell/ObjectsTable.java b/dev/oophm/src/com/google/gwt/dev/shell/ServerObjectsTable.java
similarity index 89%
rename from dev/oophm/src/com/google/gwt/dev/shell/ObjectsTable.java
rename to dev/oophm/src/com/google/gwt/dev/shell/ServerObjectsTable.java
index 1d33104..2391dad 100644
--- a/dev/oophm/src/com/google/gwt/dev/shell/ObjectsTable.java
+++ b/dev/oophm/src/com/google/gwt/dev/shell/ServerObjectsTable.java
@@ -23,7 +23,7 @@
  * A class that keeps track of Java objects which have been exposed to the other
  * side and assigns unique ids.
  */
-public class ObjectsTable {
+public class ServerObjectsTable {
 
   /**
    * A type that records the next-available free id slot to use, without itself
@@ -64,9 +64,10 @@
   }
 
   public void free(int id) {
-    assert objects.get(id) != null : "Trying to free never-used id " + id;
-    assert !(objects.get(id) instanceof Tombstone) : "Duplicate free " + id;
-    refMap.remove(objects.get(id));
+    Object object = objects.get(id);
+    assert object != null : "Trying to free never-used id " + id;
+    assert !(object instanceof Tombstone) : "Duplicate free " + id;
+    refMap.remove(object);
     objects.put(id, new Tombstone(nextFree));
     nextFree = id;
   }
diff --git a/dev/oophm/test/com/google/gwt/dev/shell/BrowserChannelTest.java b/dev/oophm/test/com/google/gwt/dev/shell/BrowserChannelTest.java
index ba7b1d7..d9b0029 100644
--- a/dev/oophm/test/com/google/gwt/dev/shell/BrowserChannelTest.java
+++ b/dev/oophm/test/com/google/gwt/dev/shell/BrowserChannelTest.java
@@ -44,6 +44,7 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.Arrays;
+import java.util.Set;
 
 /**
  * Test for {@link BrowserChannel}.
@@ -55,7 +56,22 @@
   private class TestBrowserChannel extends BrowserChannel {
     public TestBrowserChannel(InputStream inputStream,
         OutputStream outputStream) throws IOException {
-      super(inputStream, outputStream);
+      super(inputStream, outputStream, new ObjectRefFactory() {
+        public JavaObjectRef getJavaObjectRef(int refId) {
+          fail("getJavaObjectRef mocked");
+          return null;
+        }
+
+        public JsObjectRef getJsObjectRef(int refId) {
+          fail("getJsObjectRef mocked");
+          return null;
+        }
+
+        public Set<Integer> getRefIdsForCleanup() {
+          fail("getRefIdsForCleanup mocked");
+          return null;
+        }
+      });
     }
     
     public MessageType readMessageType() throws IOException,
@@ -80,13 +96,13 @@
   public void testBooleanValue() throws IOException {
     Value val = new Value();
     val.setBoolean(true);
-    BrowserChannel.writeValue(oStr, val);
-    val = BrowserChannel.readValue(iStr);
+    channel.writeValue(oStr, val);
+    val = channel.readValue(iStr);
     assertEquals(ValueType.BOOLEAN, val.getType());
     assertEquals(true, val.getBoolean());
     val.setBoolean(false);
-    BrowserChannel.writeValue(oStr, val);
-    val = BrowserChannel.readValue(iStr);
+    channel.writeValue(oStr, val);
+    val = channel.readValue(iStr);
     assertEquals(ValueType.BOOLEAN, val.getType());
     assertEquals(false, val.getBoolean());
   }
diff --git a/dev/oophm/test/com/google/gwt/dev/shell/RemoteObjectTableTest.java b/dev/oophm/test/com/google/gwt/dev/shell/RemoteObjectTableTest.java
new file mode 100644
index 0000000..0103a01
--- /dev/null
+++ b/dev/oophm/test/com/google/gwt/dev/shell/RemoteObjectTableTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2009 Google 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.google.gwt.dev.shell;
+
+import com.google.gwt.dev.shell.BrowserChannel.RemoteObjectRef;
+
+import junit.framework.TestCase;
+
+import java.util.Set;
+
+/**
+ * Test RemoteObjectTable.
+ */
+public class RemoteObjectTableTest extends TestCase {
+
+  /**
+   * Mock implementation of a RemoteObjectRef.
+   */
+  public class MockRemoteObjectRef implements RemoteObjectRef {
+
+    public final int refId;
+
+    public MockRemoteObjectRef(int refId) {
+      this.refId = refId;
+    }
+
+    public int getRefid() {
+      return refId;
+    }
+  }
+
+  public void testFree() throws InterruptedException {
+    RemoteObjectTable<MockRemoteObjectRef> table = new RemoteObjectTable<
+        MockRemoteObjectRef>();
+    MockRemoteObjectRef ref = new MockRemoteObjectRef(1);
+    table.putRemoteObjectRef(1, ref);
+    Set<Integer> freed = table.getRefIdsForCleanup();
+    ensureGC();
+    assertEquals(0, freed.size());
+    ref = null;
+    ensureGC();
+    freed = table.getRefIdsForCleanup();
+    assertEquals(1, freed.size());
+  }
+
+  /**
+   * This method attempts to ensure that a GC has happened and any weak
+   * references have been cleaned up.
+   * 
+   * @throws InterruptedException
+   */
+  private void ensureGC() throws InterruptedException {
+    // TODO(jat): is this sufficient across all VMs?
+    System.gc();
+    Thread.sleep(10);
+    System.gc();
+  }
+}
diff --git a/user/src/com/google/gwt/junit/RunStyle.java b/user/src/com/google/gwt/junit/RunStyle.java
index 95fa4f2..d054429 100644
--- a/user/src/com/google/gwt/junit/RunStyle.java
+++ b/user/src/com/google/gwt/junit/RunStyle.java
@@ -33,10 +33,7 @@
    * the same signature since this will be how the RunStyle is created via
    * reflection.
    * 
-   * @param logger TreeLogger instance
    * @param shell the containing shell
-   * @param args arguments (after the colon in the argument to -runStyle)
-   *     may be null if no argument is supplied
    */
   public RunStyle(JUnitShell shell) {
     this.shell = shell;