DEV Community

Cover image for Run Java from C#: 5 Methods with Code Examples
JNBridge
JNBridge

Posted on • Originally published at jnbridge.com

Run Java from C#: 5 Methods with Code Examples

If you need to call Java from a .NET app, the first answer is usually “just shell out to java.exe.” That works right up until you need objects, exceptions, throughput, or a deployment story that doesn’t become a pile of scripts.

This Dev.to version walks through five practical ways to run Java from C# — from Process.Start to IKVM, JNI, gRPC, and in-process bridging — with code and the tradeoffs that matter in production.


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:

Method Integration Depth Per-Call Latency Complexity Best For
Process.Start Shallow (stdin/stdout) High (~50ms+) Low One-off JAR execution
IKVM Deep (.NET assembly) Low (~0.1ms) Medium Pure-Java libs, no native deps
JNI via C++/CLI Deep (native calls) Lowest (~0.05ms) Very High Max control, C++ teams
gRPC Sidecar Medium (RPC) Medium (~2–5ms) Medium Microservices, cloud-native
JNBridgePro Deep (in-process) Low (~0.1ms) Low Production 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

using System.Diagnostics;
using System.Text;

public class JavaProcessRunner

{

    public static async Task<string> 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");
Enter fullscreen mode Exit fullscreen mode

🔗 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:

# 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
Enter fullscreen mode Exit fullscreen mode

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

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());

    }

}
Enter fullscreen mode Exit fullscreen mode

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):

// Compile as C++/CLI: /clr
#include <jni.h>
#using <mscorlib.dll>

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(); }

};
Enter fullscreen mode Exit fullscreen mode

C# Usage:

using var bridge = new JavaBridge(

    @"C:\myapp\libs\mylib.jar");

string result = bridge.CallStaticMethod(

    "com/example/TextProcessor",

    "processText",

    "Hello from C#!");

Console.WriteLine(result);
Enter fullscreen mode Exit fullscreen mode

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):

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;

}
Enter fullscreen mode Exit fullscreen mode

2. C# client:

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"
Enter fullscreen mode Exit fullscreen mode

Trade-offs

Pros Cons
Clean separation of concerns Network overhead (2–5ms/call)
Language-independent contracts Must maintain .proto files
Independently scalable Two processes to deploy and monitor
Easy to test in isolation Serialization 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

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();

        }

    }

}
Enter fullscreen mode Exit fullscreen mode

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.

Method JVM Startup Per-Call Latency Memory Throughput (calls/sec)
Process.Start ~150ms/call ~50–200ms ~50MB/process ~5–20
IKVM 0 (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

// Correct cross-platform classpath construction

var separator = RuntimeInformation.IsOSPlatform(

    OSPlatform.Windows) ? ";" : ":";

var cp = string.Join(separator,

    jars.Select(j => $"\"{j}\""));
Enter fullscreen mode Exit fullscreen mode

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.Startwill do, and don't shell out tojava.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 →


Originally published on the JNBridge blog.

Top comments (0)