Java RMI (Remote Method Invocation)
RMI (Remote Method Invocation) lets a Java program call a method on an object that lives in a different JVM, possibly on another machine, as if it were a local object. You write service.marksOf("Navika"); RMI packs up the method name and arguments, sends them across the network, runs the method on the server, and brings the result back. It has been part of Java since version 1.1, in the package java.rmi. RMI is a legacy technology: new systems use REST over HTTP, gRPC or messaging instead. It is still worth understanding, because it appears in older enterprise code and in interviews, and because the ideas behind it (stubs, registries, serialization) appear in every remote-call system. In this lesson, you will learn how RMI works and run a complete example.
How RMI Works
| Part | Role |
|---|---|
| Remote interface | Declares the methods a client may call. It extends java.rmi.Remote, and every method declares throws RemoteException. |
| Remote object | The implementation, living in the server's JVM. |
| Stub | A stand-in object on the client that implements the same interface. Calling it sends the request over the network. |
| Registry | A simple name service: the server registers the stub under a name; the client looks the name up. |
| Serialization | Arguments and results are serialized to travel between JVMs. |
The Classes
| Member | Description |
|---|---|
Remote | The marker interface every remote interface extends. |
RemoteException | It is thrown when the network or the remote side fails. It is checked. |
UnicastRemoteObject.exportObject(obj, port) | It is used to make an object callable from other JVMs and returns its stub. |
LocateRegistry.createRegistry(port) | It starts a registry in this JVM. |
LocateRegistry.getRegistry(host, port) | It connects to a registry on another machine. |
registry.rebind(name, stub) | It registers a stub under a name. |
registry.lookup(name) | It returns the stub registered under a name. |
Example: A Remote Marks Service
A real RMI system has two programs. To run as one file, this example plays both parts in one JVM; the calls still travel through RMI's network machinery.
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.Map;
public class RmiDemo {
// 1. The remote interface: what a client is allowed to call.
public interface MarksService extends Remote {
int marksOf(String student) throws RemoteException;
String grade(int marks) throws RemoteException;
}
// 2. The implementation, which lives on the server.
static class MarksServiceImpl implements MarksService {
private final Map<String, Integer> marks =
Map.of("Navika", 91, "Srihitha", 84, "Advitha", 78);
@Override
public int marksOf(String student) throws RemoteException {
Integer value = marks.get(student);
if (value == null) {
throw new RemoteException("No student named " + student);
}
return value;
}
@Override
public String grade(int marks) {
return marks >= 90 ? "A" : marks >= 35 ? "B" : "F";
}
}
public static void main(String[] args) throws Exception {
// ---- Server side ----
MarksServiceImpl impl = new MarksServiceImpl();
MarksService stub =
(MarksService) UnicastRemoteObject.exportObject(impl, 0);
Registry registry = LocateRegistry.createRegistry(0);
registry.rebind("marks", stub);
System.out.println("Server: bound "
+ String.join(", ", registry.list()));
// ---- Client side ----
MarksService service = (MarksService) registry.lookup("marks");
System.out.println("Client got a proxy? "
+ Proxy.isProxyClass(service.getClass()));
int marks = service.marksOf("Navika");
System.out.println("Navika: " + marks + ", grade "
+ service.grade(marks));
try {
service.marksOf("Nobody");
} catch (RemoteException e) {
System.out.println("RemoteException from the server: "
+ e.getCause().getMessage());
}
// Shut down so the program can exit.
registry.unbind("marks");
UnicastRemoteObject.unexportObject(impl, true);
UnicastRemoteObject.unexportObject(registry, true);
}
}Server: bound marks
Client got a proxy? true
Navika: 91, grade A
RemoteException from the server: No student named NobodyExplanation:
MarksServiceextendsRemote, and both methods declarethrows RemoteException. A remote call can fail in ways a local call can't: the network may be down or the server gone. The checked exception makes callers face that.exportObject(impl, 0)makes the object reachable on a free port and returns its stub.createRegistryandrebindstart a registry and publish the stub under the namemarks. (The usual registry port is 1099; port 0 picks any free one for this demonstration.)registry.lookup("marks")is the client's step. What it receives is a proxy implementingMarksService, not the real object.service.marksOf("Navika")looks like an ordinary call. The stub serializes the argument, sends it to the server object, and deserializes the result.- An exception thrown on the server comes back to the client wrapped in a
RemoteException. - The last three lines unexport the objects. RMI keeps background threads running, so without this the program wouldn't exit.
In a two-machine setup, the server code stays the same, and the client would call LocateRegistry.getRegistry("server-host", 1099) to find the registry.
Rules for Remote Interfaces
| Rule | Why |
|---|---|
The interface extends Remote | It marks the methods as callable remotely |
Every method declares throws RemoteException | Remote calls can fail at any time |
| Arguments and return values must be serializable (or remote objects) | They are copied across the network |
| Objects are passed by copy | Changing an argument on the server doesn't change the client's object |
Why RMI Is Legacy
| Limitation | What replaced it |
|---|---|
| Java only: both sides must be Java | REST with JSON, gRPC: any language |
| Based on Java serialization, a long-standing source of security holes | JSON, Protocol Buffers |
| Uses its own ports, awkward with firewalls and proxies | HTTP on standard ports |
| Client and server tightly coupled to shared interfaces and classes | Versioned APIs and message formats |
| Hides the network behind method calls, so slow and failing calls look local | Explicit requests, timeouts and retries |
Pieces of RMI have already been removed from the JDK: the rmic stub compiler and the RMI activation system are gone. The core API remains, and is still used internally by technologies such as JMX, which monitoring tools rely on.
Common Mistakes
Forgetting throws RemoteException on an interface method. Exporting the object fails.
Passing objects that aren't serializable. The call fails with NotSerializableException.
Expecting changes to an argument to appear on the client. Arguments are copies.
Treating remote calls like local ones. Each call crosses the network: it is thousands of times slower and can fail. Avoid calling in tight loops.
Not unexporting objects. The JVM keeps running.
Interview Corner
What is RMI in Java?
RMI (Remote Method Invocation) is a Java API that lets an object in one JVM invoke methods on an object in another JVM. The server defines a remote interface extending java.rmi.Remote, implements it, exports the object with UnicastRemoteObject and binds its stub in an RMI registry. The client looks up the stub by name and calls methods on it; the stub marshals the arguments using serialization, sends them to the server, and unmarshals the result. Every remote method must declare RemoteException. RMI is Java-specific and has largely been replaced by REST, gRPC and messaging.
What are a stub and a skeleton in RMI?
The stub is the client-side proxy for the remote object: it implements the remote interface, forwards each call over the network and returns the result. The skeleton was the server-side counterpart that received the call and dispatched it to the real object. Since Java 5, stubs are generated dynamically as proxies and skeletons are no longer needed; the rmic compiler that produced them has been removed.
Practice
- Easy: Add a method
average()toMarksServicethat returns the average of all marks, and call it from the client part. - Harder: Split the example into two programs,
ServerandClient, using port 1099 andLocateRegistry.getRegistry("localhost", 1099), and run them in two terminals.
Key Takeaways
Was this tutorial helpful?