Run Java from C#: 5 Methods with Code Examples

JNBridgePro — the fastest, easiest way to bridge Java and .NET in production. Generate proxies in minutes, call Java from C# (or C# from Java) with native syntax — trusted by enterprises worldwide. Learn more · Download free trial

> TL;DR — Need to run Java from C#? Use Process.Start for one-off JAR executions, IKVM for pure-Java libraries with no native dependencies, gRPC for microservice architectures, JNI if you have C++ expertise and need raw speed, or JNBridgePro for production-grade in-process bridging with low latency and zero JNI glue code. See the comparison table and decision tree below.

You have a Java library you need to use from a C#/.NET application — maybe a payment SDK, a machine-learning model, or a legacy system nobody wants to rewrite. Whatever the reason, you need to run Java from C#, and it has to work in production.

If you’ve searched before, you probably found a StackOverflow answer from 2012 telling you to use Process.Start("java.exe"). That works for trivial cases, but falls apart when you need real interop: passing objects, handling exceptions across the JVM and CLR, or making thousands of calls per second with minimal latency.

This guide covers five real methods to run Java code in C#, from simple shell-out to full in-process bridging. Each includes working code, honest trade-offs, and guidance on when to use it.


Table of Contents


Quick Comparison

Before diving into code, here’s what you’re choosing between:

MethodIntegration DepthPer-Call LatencyComplexityBest For
Process.StartShallow (stdin/stdout)High (~50ms+)LowOne-off JAR execution
IKVMDeep (.NET assembly)Low (~0.1ms)MediumPure-Java libs, no native deps
JNI via C++/CLIDeep (native calls)Lowest (~0.05ms)Very HighMax control, C++ teams
gRPC SidecarMedium (RPC)Medium (~2–5ms)MediumMicroservices, cloud-native
JNBridgeProDeep (in-process)Low (~0.1ms)LowProduction apps, bidirectional

> 🔗 For a deeper dive on bridge vs. REST vs. gRPC trade-offs, see our Bridge vs REST vs gRPC comparison.


Method 1: Process.Start — Run java.exe as a Subprocess

The most straightforward way to run Java from C# is to launch the JVM as a separate process using Process.Start. This is what most StackOverflow answers suggest, and for simple, one-shot tasks it’s perfectly fine.

When to Use It

  • Running a standalone Java CLI tool or JAR file
  • One-off executions (batch jobs, code generation, file conversion)
  • You don’t need to pass complex objects back and forth

Code Example

csharp
using System.Diagnostics;

public class JavaProcessRunner
{
public static async Task RunJavaJarAsync(
string jarPath,
string arguments,
string? javaHome = null)
{
var javaExe = javaHome != null
? Path.Combine(javaHome, "bin", "java")
: "java";

var startInfo = new ProcessStartInfo
{
FileName = javaExe,
Arguments = $"-jar \"{jarPath}\" {arguments}",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};

// Pass classpath and other JVM options via environment
startInfo.Environment["CLASSPATH"] =
"/libs/dependency1.jar:/libs/dependency2.jar";

using var process = new Process { StartInfo = startInfo };

var output = new StringBuilder();
var errors = new StringBuilder();

process.OutputDataReceived += (_, e) =>
{ if (e.Data != null) output.AppendLine(e.Data); };
process.ErrorDataReceived += (_, e) =>
{ if (e.Data != null) errors.AppendLine(e.Data); };

process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();

using var cts = new CancellationTokenSource(
TimeSpan.FromSeconds(30));
try
{
await process.WaitForExitAsync(cts.Token);
}
catch (OperationCanceledException)
{
process.Kill(entireProcessTree: true);
throw new TimeoutException(
"Java process timed out after 30s");
}

if (process.ExitCode != 0)
throw new Exception(
$"Java exited with code {process.ExitCode}: {errors}");

return output.ToString();
}
}

// Usage
var result = await JavaProcessRunner.RunJavaJarAsync(
"/app/libs/converter.jar",
"--input data.csv --format json");
`

> 🔗 For a complete walkthrough of running JAR files from .NET, see How to Run a Java JAR from C#.

Edge Cases to Handle

  • Classpath hell: Use -cp or the CLASSPATH environment variable. On Windows, separate entries with ;; on Linux/macOS, use :.
  • JVM not found: Check that java is on the system PATH or pass JAVA_HOME explicitly.
  • Large output: For big payloads, write to a temp file instead of piping through stdout.
  • Process leaks: Always use using and kill on timeout — orphaned JVM processes eat server memory.

The Problem

Every call spawns a new JVM. That's 50–200ms of startup overhead per invocation, plus the memory cost of a full JVM instance. If you're making more than a handful of calls, this approach doesn't scale.


Method 2: IKVM — Compile Java Bytecode to .NET

IKVM converts Java bytecode into .NET assemblies. You run ikvmc against a JAR file and get a DLL you can reference directly in your C# project. Your Java code literally runs on the CLR — no JVM required.

When to Use It

  • The Java library is self-contained with few dependencies
  • You need tight, low-latency integration
  • You're okay with some compatibility limitations

Code Example

First, convert the JAR:

`bash
# Install IKVM (community fork targets .NET 6+)
dotnet add package IKVM

# Or use the command-line converter
ikvmc -target:library -out:MyJavaLib.dll mylib.jar
`

Then use it from C# like any other .NET library:

`csharp
using com.example.mylib;

public class IkvmExample
{
public static void RunJavaCodeInCSharp()
{
// Java classes are now .NET classes
var parser = new com.example.mylib.JsonParser();

// Call Java methods directly — compiled to IL bytecode
var result = parser.parse("{\"key\": \"value\"}");
Console.WriteLine($"Parsed: {result.get("key")}");

// Java collections work but need casting
var list = new java.util.ArrayList();
list.add("item1");
list.add("item2");

var iterator = list.iterator();
while (iterator.hasNext())
Console.WriteLine(iterator.next());
}
}
`

Limitations

IKVM was a remarkable project, but it has real constraints:

  • Incomplete JDK coverage: Not every javax. or java. class is implemented. Swing, AWT, and many java.nio features are missing or broken.
  • Reflection edge cases: Java code relying heavily on reflection may behave differently.
  • Native dependencies: If your JAR depends on native JNI libraries, IKVM can't help.
  • Maintenance status: The original project was abandoned. The ikvm-revived community fork targets .NET 6+ but coverage varies.

> 🔗 Migrating away from IKVM? See our guide on Migrating from IKVM to JNBridgePro.

For simple, pure-Java libraries, IKVM is elegant. For anything touching the filesystem, networking, or native code, expect surprises.


Method 3: JNI via C++/CLI Wrapper

The Java Native Interface (JNI) is the official way for native code to interact with the JVM. C++/CLI lets you write code that lives in both the .NET and native worlds, making it possible to load a JVM inside your .NET process and call Java methods through JNI.

This is the most powerful — and most painful — approach.

When to Use It

  • You need maximum performance and control over marshaling
  • You're comfortable with C++ and manual memory management
  • You have a dedicated team to maintain the interop layer

Code Example

C++/CLI Bridge (JavaBridge.cpp):

`cpp
// Compile as C++/CLI: /clr
#include
#using

using namespace System;
using namespace System::Runtime::InteropServices;

public ref class JavaBridge
{
private:
JavaVM* jvm;
JNIEnv* env;

public:
JavaBridge(String^ classPath)
{
JavaVMInitArgs vmArgs;
JavaVMOption options[1];

IntPtr cpPtr = Marshal::StringToHGlobalAnsi(
String::Format("-Djava.class.path={0}", classPath));
options[0].optionString =
static_cast(cpPtr.ToPointer());

vmArgs.version = JNI_VERSION_1_8;
vmArgs.nOptions = 1;
vmArgs.options = options;
vmArgs.ignoreUnrecognized = JNI_FALSE;

jint rc = JNI_CreateJavaVM(
&jvm, (void**)&env, &vmArgs);
Marshal::FreeHGlobal(cpPtr);

if (rc != JNI_OK)
throw gcnew Exception(String::Format(
"Failed to create JVM: error {0}", rc));
}

String^ CallStaticMethod(
String^ className,
String^ methodName,
String^ arg)
{
// Convert .NET strings to native for JNI
IntPtr clsName = Marshal::StringToHGlobalAnsi(className);
jclass cls = env->FindClass(
static_cast(clsName.ToPointer()));

if (cls == nullptr)
throw gcnew Exception(
"Java class not found: " + className);

// ... method lookup, call, string marshaling ...
// (Full implementation requires ~50 lines of
// careful memory management)
}

~JavaBridge() { if (jvm) jvm->DestroyJavaVM(); }
};
`

C# Usage:

`csharp
using var bridge = new JavaBridge(
@"C:\myapp\libs\mylib.jar");
string result = bridge.CallStaticMethod(
"com/example/TextProcessor",
"processText",
"Hello from C#!");
Console.WriteLine(result);
`

Why Most Teams Don't Do This

  • You must maintain C++/CLI code — a language most .NET developers don't know
  • Manual JNI string/array/object marshaling is tedious and error-prone
  • One null-pointer mistake crashes your entire process (segfault, not a managed exception)
  • Only one JVM per process (JNI limitation)
  • Every new Java method requires more C++ glue code
  • Windows-only if using C++/CLI (use P/Invoke on Linux)

This is the "build your own bridge" option. It works, but you're signing up to maintain it forever.


Method 4: gRPC Sidecar — Run Java as a Microservice

Instead of running Java inside your .NET process, run it alongside as a separate service. Define your interface in Protocol Buffers, generate clients for both languages, and communicate over gRPC. This is the modern, cloud-native approach.

When to Use It

  • You're already in a microservices architecture
  • You want clean language boundaries
  • You need to scale the Java and .NET parts independently
  • Latency of 2–5ms per call is acceptable

Code Example

1. Define the service (calculator.proto):

`protobuf
syntax = "proto3";
package calculator;

service Calculator {
rpc Calculate (CalcRequest) returns (CalcResponse);
rpc BatchCalculate (stream CalcRequest)
returns (stream CalcResponse);
}

message CalcRequest {
string expression = 1;
int32 precision = 2;
}

message CalcResponse {
double result = 1;
string formatted = 2;
}
`

2. C# client:

`csharp
using Grpc.Net.Client;
using Calculator;

public class JavaGrpcClient : IDisposable
{
private readonly GrpcChannel _channel;
private readonly Calculator.CalculatorClient _client;

public JavaGrpcClient(
string address = "http://localhost:50051")
{
_channel = GrpcChannel.ForAddress(address);
_client = new Calculator.CalculatorClient(_channel);
}

public async Task<(double Result, string Formatted)>
CalculateAsync(string expression, int precision = 2)
{
var response = await _client.CalculateAsync(
new CalcRequest
{
Expression = expression,
Precision = precision
});
return (response.Result, response.Formatted);
}

public void Dispose() => _channel?.Dispose();
}

// Usage
using var client = new JavaGrpcClient();
var (result, formatted) = await client.CalculateAsync(
"(3.14159 * 2) + 1", 4);
Console.WriteLine($"Result: {formatted}"); // "7.2832"
`

Trade-offs

ProsCons
Clean separation of concernsNetwork overhead (2–5ms/call)
Language-independent contractsMust maintain .proto files
Independently scalableTwo processes to deploy and monitor
Easy to test in isolationSerialization cost for complex objects

Method 5: JNBridgePro — In-Process Java/.NET Bridge

JNBridgePro loads the JVM inside your .NET process and lets you call Java classes as if they were native C# objects. You use a proxy generation tool to create .NET wrappers for your Java classes, then call them with normal C# syntax. No JNI glue code, no process management, no serialization.

When to Use It

  • You need low-latency, high-frequency calls to Java code
  • You want to pass complex objects between Java and .NET without serialization
  • You need Java callbacks into .NET (bidirectional interop)
  • You don't want to maintain interop infrastructure yourself

Code Example

`csharp
using com.jnbridge.jnbcore;
using com.example.mylib; // Generated proxies

public class JNBridgeExample
{
public static void RunJavaInsideDotNet()
{
// Initialize — starts a JVM in-process
DotNetSide.init(new JNBLicenseInfo("license.dat"),
new JNBClassPathInfo
{
ClassPath = new[]
{
"/app/libs/mylib.jar",
"/app/libs/dependency.jar"
},
JvmPath = "/usr/lib/jvm/java-17/lib/server/libjvm.so"
});

try
{
// Use Java objects like C# objects
var processor = new com.example.mylib.DataProcessor();

// .NET types are marshaled automatically
var config = new java.util.HashMap();
config.put("mode", "batch");
config.put("threads", java.lang.Integer.valueOf(4));
processor.configure(config);

// Process data
var input = new java.util.ArrayList();
for (int i = 0; i < 1000; i++) input.add($"record-{i}");

var results = processor.processAll(input);
Console.WriteLine(
$"Processed {results.size()} records");
}
finally
{
DotNetSide.shutdown();
}
}
}
`

What Makes It Different

JNBridgePro handles the hard parts you'd have to build yourself with JNI:

  • Type marshaling: Java strings, primitives, arrays, and collections convert automatically between the JVM and CLR
  • Exception bridging: Java exceptions become .NET exceptions with full stack traces
  • Garbage collection: Objects on both sides are properly tracked and collected
  • Bidirectional calls: .NET code can call Java, and Java can call back into .NET
  • Proxy generation: Point at a JAR, get .NET wrapper classes — no manual coding

It's a commercial product, which is the main barrier. But if you're evaluating the best way to run Java from .NET in production, the license cost is typically less than the engineering time to build and maintain a JNI wrapper or gRPC layer.

> 🔗 See how JNBridgePro compares to other Java–C# bridge tools.


Performance Benchmarks

These benchmarks measure calling a Java method that concatenates two strings — a minimal operation to isolate interop overhead. Environment: .NET 8, Java 17, Windows 11, 16GB RAM.

MethodJVM StartupPer-Call LatencyMemoryThroughput (calls/sec)
Process.Start~150ms/call~50–200ms~50MB/process~5–20
IKVM0 (no JVM)~0.1ms~20–50MB~500,000+
JNI/C++CLI~300ms (once)~0.05ms~30MB~1,000,000+
gRPC Sidecar~800ms (once)~2–5ms~100MB (separate)~5,000–20,000
JNBridgePro~400ms (once)~0.1ms~40MB~500,000+

Key takeaway: If you're making more than a few calls per second, Process.Start is the wrong tool. The in-process methods (IKVM, JNI, JNBridgePro) are orders of magnitude faster for repeated calls.


Which Method Should You Use?

Follow this decision tree:

How many times do you call Java per request?

Once or never (batch job, CLI tool): Use Process.Start. Simple, built-in, and the startup cost doesn't matter for single invocations.

A few times (< 100/sec):
- Already running microservices? → gRPC Sidecar
- Monolith? → JNBridgePro or gRPC

Hundreds or thousands of times:
- Pure Java library, no native deps? → Try IKVM first
- IKVM doesn't cover your APIs? → JNBridgePro
- Zero budget + C++ expertise? → JNI/C++CLI

Do you need bidirectional calls (Java calling back into .NET)?
JNBridgePro or JNI (painful)

Cross-platform requirement?
→ Process.Start, gRPC, IKVM, and JNBridgePro all work on Windows and Linux. JNI via C++/CLI is Windows-only (use P/Invoke on Linux).


How Do You Handle Java Dependencies from C#?

Build a fat JAR (using Maven Shade Plugin or Gradle Shadow) that bundles all dependencies into a single file. This gives you one JAR to reference in your classpath, regardless of which interop method you choose.

For IKVM, convert the fat JAR with ikvmc. For gRPC, package it in a container with all dependencies. For JNBridgePro, point the proxy generation tool at the fat JAR and it resolves all classes automatically.

Key pitfalls to avoid:

  • Classpath separator: Use ; on Windows, : on Linux/macOS
  • Spaces in paths: Always quote JAR paths
  • JAVA_HOME: Set it explicitly rather than relying on system PATH

`csharp
// Correct cross-platform classpath construction
var separator = RuntimeInformation.IsOSPlatform(
OSPlatform.Windows) ? ";" : ":";
var cp = string.Join(separator,
jars.Select(j => $"\"{j}\""));
`


Can You Run Java from C# Without a JDK?

IKVM is the only method that doesn't require a JVM — it compiles Java bytecode to run directly on the CLR. Every other method needs at least a JRE:

  • Process.Start needs a JRE on the same machine
  • JNI and JNBridgePro need a JVM library (libjvm.so / jvm.dll)
  • gRPC needs a JRE wherever the Java sidecar runs (which can be a Docker container)

If eliminating the JVM dependency is your primary goal and the Java library is pure Java, IKVM is your best option. For everything else, bundle a JRE with your deployment or use a container.


Frequently Asked Questions

What is the best way to run Java from .NET in production?

It depends on your call pattern. For high-frequency calls in a monolithic app, an in-process bridge like JNBridgePro or IKVM gives the best latency. For cloud-native architectures, a gRPC sidecar provides cleaner operational boundaries. Process.Start is only suitable for infrequent, batch-style operations.

Can I run Java code in C# on Linux?

Yes. Process.Start and gRPC work on any OS. IKVM works cross-platform since it runs on the CLR. JNI works on Linux but requires P/Invoke instead of C++/CLI. JNBridgePro supports both Windows and Linux.

How do error and exception handling work across Java and C#?

Each method handles Java exceptions differently:

  • Process.Start: Check stderr and exit codes
  • IKVM: Java exceptions become .NET exceptions (type names preserved)
  • JNI: You must manually check and clear exceptions — unhandled ones crash the process
  • gRPC: Map Java exceptions to gRPC status codes
  • JNBridgePro: Java exceptions become .NET exceptions with original stack traces intact

Is there a free way to run Java from C# with low latency?

IKVM (open source) gives low latency for pure-Java libraries. JNI is free but demands significant C++ expertise. gRPC is free but adds network overhead. There's no free option that combines low latency, broad compatibility, and low maintenance — that's the gap commercial tools like JNBridgePro fill.


Wrapping Up

There's no single "best way to run Java from .NET" — it depends on how tightly you need Java and C# to interact:

  • Quick and dirty: Process.Start
  • Pure Java library, no native deps: Try IKVM
  • Microservices architecture: gRPC sidecar
  • Production integration, zero maintenance overhead: JNBridgePro
  • Maximum control, have C++ skills: JNI

Whatever you choose, match the integration depth to your actual requirements. Don't build a gRPC service layer when Process.Start will do, and don't shell out to java.exe` a thousand times per second when an in-process bridge exists.


Ready to try in-process Java/.NET integration? Download the JNBridgePro free trial →

Want to see it in action? Schedule a technical demo — we’ll walk through your specific Java libraries and show you working interop in real time.

Explore code samples and tutorials in the JNBridgePro Developer Center →