两个 JVM 之间的共享内存

Posted

技术标签:

【中文标题】两个 JVM 之间的共享内存【英文标题】:Shared Memory between two JVMs 【发布时间】:2014-10-13 08:03:14 【问题描述】:

在 Java 中有没有办法让两个 JVM(在同一台物理机器上运行)使用/共享相同的内存地址空间?假设 JVM-1 中的生产者将消息放在特定的预定义内存位置,如果 JVM-2 上的消费者知道要查看哪个内存位置,是否可以检索消息?

【问题讨论】:

没有。您不能在 Java 中访问任意内存。但是,您可以在两个 JVM 之间共享内存。使用 JNI 和 ipcs。或通过环回的套接字。 AFAIK,核心 API 中没有内置任何内容。您可以使用Sockets 在彼此之间甚至通过第三方进行通信 谁 JVM 或两个 JVM???请更正问题标题。 不,即使这是一个 JVM,您也无法访问预定义的内存位置。你可以通过使用像waratek这样的多租户JVM来share内存空间 为什么要这样做?如果它对性能至关重要,以至于 Unix 套接字无法工作,那么 Java 可能是错误的选择。 【参考方案1】:

解决方案 1:

我认为最好的解决方案是使用内存映射文件。这允许您在任意数量的进程(包括其他非 Java 程序)之间共享内存区域。您不能将 java 对象放入内存映射文件中,除非您将它们序列化。以下示例显示您可以在两个不同的进程之间进行通信,但您需要使其更加复杂以允许进程之间更好的通信。我建议你看一下Java的NIO package,特别是下面示例中使用的类和方法。

服务器:

public class Server 

    public static void main( String[] args ) throws Throwable 
        File f = new File( FILE_NAME );

        FileChannel channel = FileChannel.open( f.toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE );

        MappedByteBuffer b = channel.map( MapMode.READ_WRITE, 0, 4096 );
        CharBuffer charBuf = b.asCharBuffer();

        char[] string = "Hello client\0".toCharArray();
        charBuf.put( string );

        System.out.println( "Waiting for client." );
        while( charBuf.get( 0 ) != '\0' );
        System.out.println( "Finished waiting." );
    

客户:

public class Client 

    public static void main( String[] args ) throws Throwable 
        File f = new File( FILE_NAME );
        FileChannel channel = FileChannel.open( f.toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE );

        MappedByteBuffer b = channel.map( MapMode.READ_WRITE, 0, 4096 );
        CharBuffer charBuf = b.asCharBuffer();

        // Prints 'Hello server'
        char c;
        while( ( c = charBuf.get() ) != 0 ) 
            System.out.print( c );
        
        System.out.println();

        charBuf.put( 0, '\0' );
    



解决方案 2:

另一种解决方案是使用JavaSockets在进程之间来回通信。这还有一个额外的好处,就是可以非常轻松地通过网络进行通信。可以说这比使用内存映射文件要慢,但我没有任何基准来支持该声明。我不会发布实现此解决方案的代码,因为实现可靠的网络协议可能会变得非常复杂,并且是相当特定于应用程序的。有很多不错的社交网站可以通过快速搜索找到。


现在上面的例子是如果你想在两个不同的进程之间共享内存。如果您只想在当前进程中读取/写入任意内存,那么您应该首先了解一些警告。这违背了 JVM 的整个原则,你真的不应该在生产代码中这样做。如果您不非常小心,您会违反所有安全性并且很容易使 JVM 崩溃。

话虽如此,尝试一下还是很有趣的。要在当前进程中读取/写入任意内存,您可以使用 sun.misc.Unsafe 类。这是在我知道并使用过的所有 JVM 上提供的。可以在here找到有关如何使用该类的示例。

【讨论】:

能否举个例子C客户端读取Server的数据? @Qoros C 需要操作系统特定的 API 来处理内存映射文件,例如 mmap 或 VirtualAlloc。否则代码看起来会非常相似。打开文件,将其映射到进程中,然后读写返回的指针。 这是在创建物理文件吗?我不想通过不断流式读/写到磁盘上来杀死人们的硬盘【参考方案2】:

有一些 IPC 库可以通过 Java 中的内存映射文件促进共享内存的使用。

Chronicle-Queue

Chronicle Queue 类似于非阻塞 Java Queue,除了您可以在一个 JVM 中提供消息并在另一个 JVM 中轮询它。

在两个 JVM 中,您应该在同一个 FS 目录中创建一个 ChronicleQueue 实例(如果您不需要消息持久性,请将此目录放在内存挂载的 FS 中):

ChronicleQueue ipc = ChronicleQueueBuilder.single("/dev/shm/queue-ipc").build();

在一个 JVM 中写入消息:

ExcerptAppender appender = ipc.acquireAppender();
appender.writeDocument(w -> 
    w.getValueOut().object(message);
);

在另一个 JVM 中读取消息:

ExcerptTailer tailer = ipc.createTailer();
// If there is no message, the lambda, passed to the readDocument()
// method is not called.
tailer.readDocument(w -> 
    Message message = w.getValueIn().object(Message.class);
    // process the message here
);

// or avoid using lambdas
try (DocumentContext dc = tailer.readingDocument()) 
    if (dc.isPresent()) 
        Message message = dc.wire().getValueIn().object(Message.class);
        // process the message here
     else 
        // no message
    

Aeron IPC

Aeron 不仅仅是 IPC 队列(它是一个网络通信框架),它还提供 IPC 功能。它类似于 Chronicle Queue,一个重要的区别是它使用 SBE 库进行消息编组/解组,而 Chronicle Queue 使用 Chronicle Wire。

Chronicle Map

Chronicle Map 允许通过某个键进行 IPC 通信。在两个 JVM 中,您应该创建一个具有相同配置的映射并持久化到同一个文件(如果您不需要实际的磁盘持久性,则该文件应该本地化到内存挂载的 FS 中,例如在 /dev/shm/ 中):

Map<Key, Message> ipc = ChronicleMap
    .of(Key.class, Message.class)
    .averageKey(...).averageValue(...).entries(...)
    .createPersistedTo(new File("/dev/shm/jvm-ipc.dat"));

然后你可以在一个 JVM 中编写:

ipc.put(key, message); // publish a message

在接收方 JVM 上:

Message message = ipc.remove(key);
if (message != null) 
    // process the message here

【讨论】:

【参考方案3】:

Distributed_cache 是满足您要求的最佳解决方案。

在计算中,分布式缓存是在单一区域设置中使用的传统缓存概念的扩展。一个分布式缓存可以跨越多个服务器,因此它的规模和跨国容量都可以增长。

几个选项:

Terracotta 允许 JVM 集群中的线程使用相同的内置 JVM 工具跨 JVM 边界相互交互,这些工具扩展为具有集群范围的含义

Oracle_Coherence 是专有的1 基于 Java 的内存数据网格,旨在具有比传统关系数据库管理系统更好的可靠性、可扩展性和性能

Ehcache 是一种广泛使用的开源 Java 分布式缓存,用于通用缓存、Java EE 和轻量级容器。它具有内存和磁盘存储、通过复制和无效复制、侦听器、缓存加载器、缓存扩展、缓存异常处理程序、gzip 缓存 servlet 过滤器、RESTful 和 SOAP API

Redis 是一个数据结构服务器。它是开源的、联网的、内存中的,并且存储具有可选持久性的密钥。

Couchbase_Server 是一个开源、分布式(无共享架构)多模型 NoSQL 面向文档的数据库软件包,针对交互式应用程序进行了优化。这些应用程序可以通过创建、存储、检索、聚合、操作和呈现数据来为许多并发用户提供服务。

有用的帖子:

What is Terracotta?

Is Terracotta a distributed cache?

infoq文章

【讨论】:

【参考方案4】:

老实说,您不想共享相同的内存。您应该只将您需要的数据发送到其他 JVM。话虽如此,如果您确实需要共享内存,还有其他解决方案。

发送数据 两个 JVM 不共享相同的内存访问点,因此不可能使用来自一个 JVM 的引用在另一个 JVM 中使用。将简单地创建一个新的引用,因为它们彼此不了解。

但是,您可以将数据传送到其他 JVM,然后以多种方式返回:

1) 使用RMI,您可以设置远程服务器来解析数据。我发现设置起来有点麻烦,因为它需要更改安全性并且数据是Serializable。您可以在链接中找到更多信息。

2) 使用服务器是将数据发送到不同地方的古老方法。实现这一点的一种方法是使用ServerSocket 并在localhost 上连接Socket。如果要使用ObjectOutputStream,对象仍然需要为Serializable


共享数据 这是非常危险和不稳定的,低级的,而且,不安全(字面意思)。

如果你想使用 Java 代码,你可以看看使用s.m.Unsafe,使用正确的内存地址,你将能够检索操作系统中支持 C/C++ 数组存储的对象。

否则,您可以使用native 方法自己访问 C/C++ 数组,尽管我不知道如何实现。

【讨论】:

【参考方案5】:

Jocket,我几年前做的一个实验项目就是这样做的。

如果您想使用Input/OutputStream,它包括java.net.Socketjava.net.ServerSocket 的直接替换。

每个定向通道使用一对循环缓冲区来发送和获取数据(一个用于“数据包”,一个用于数据包地址)。缓冲区是通过RandomAccessFile 获得的。

它包含一个小的 JNI 层 (linux) 来实现 IPC 同步(即通知其他进程数据的可用性),但如果您想轮询数据,这不是强制性的。

【讨论】:

【参考方案6】:

是的,

使用中间程序,您可以写入和读取任意内存位置。你不能纯粹用 Java 来做。

例如,您可以编写一段可以读取任意内存位置并通过 JNI 调用的 C++ 代码。反向写入内存地址也是如此。

首先为应该处理这个问题的类编写一个类定义,例如:

public class MemTest 
    public native byte[] readMemory(int address);
    public native void writeMemory(int address, byte[] values);

然后你编译它。然后使用 javah.exe(或 linux 等效程序)为其生成标头:

javah MemTest

现在您编写一个包含该标头并定义方法的 .cpp 文件。编译为 DLL。要加载 .dll,您可以使用具有适当值的 -Djava.library.path JVM 参数或 System.loadLibrary()

注意事项:我不建议这样做。几乎可以肯定有更好的方法来做你想做的事。

【讨论】:

“任意内存位置”,是的,只要你留在同一个进程中。没有操作系统会让任何进程从另一个进程读取内存! (某些特定的嵌入式操作系统除外)。分页不一样:0x3f7e 不是所有进程的相同物理地址。 @Matthieu:完全不真实。您可以完全不受限制地读取任意内存位置。 您是否尝试过您的解决方案?有一些方法可以侵入另一个进程内存(参见that other question),但它是特定于操作系统的,需要特殊权限。最后,正如您所指出的,这是非常不推荐的。此外,JNI 端的内存映射与 Java 端不同(数组可以来回复制),这使得计算正确的黑客地址更加困难。 我的意思是两个 JVM 将使用不同的虚拟地址空间,因此地址上的数据例​​如JVM1 中的0x3f7e 与JVM2 中地址0x3f7e 的数据不同。从 JVM2 中,如果你想从 JVM1 的堆中读取数据,你应该获取 JVM1 PID,在本地复制它的堆(如果你获得了许可)并在一个可能是 0x3f7e 的地址读取你想要的数据,但也许不是。这就是我所说的“黑客地址”(从 JVM2 看到的 JVM1 中的 0x3f7e 可能与 0x3f7e 不同)。 如果您想进入兔子洞,请从这里开始 1.youtube.com/watch?v=JMEVi_t38uc 2.youtube.com/watch?v=lWvZDZ-oRt0【参考方案7】:

使用枢轴堆外内存不安全

如何使用 Unsafe 将 Object 字节复制到 off-head 区域,然后一些如何将便宜的指针和类名传递给第二个 JVM,后者将使用指针和类名复制堆外空间并将其转换为第二个 JVM 中的堆内对象。 它不是同一个对象实例,而是一个快速复制,没有序列化。

public static Unsafe getUnsafe() 
    try 
        Field f = Unsafe.class.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        return (Unsafe)f.get(null);
     catch (Exception e)  /* ... */ 


MyStructure structure = new MyStructure(); // create a test object
structure.x = 777;

long size = sizeOf(structure);
long offheapPointer = getUnsafe().allocateMemory(size);
getUnsafe().copyMemory(
            structure,      // source object
            0,              // source offset is zero - copy an entire object
            null,           // destination is specified by absolute address, so destination object is null
            offheapPointer, // destination address
            size
    ); // test object was copied to off-heap

Pointer p = new Pointer(); // Pointer is just a handler that stores address of some object
long pointerOffset = getUnsafe().objectFieldOffset(Pointer.class.getDeclaredField("pointer"));
getUnsafe().putLong(p, pointerOffset, offheapPointer); // set pointer to off-heap copy of the test object

structure.x = 222; // rewrite x value in the original object
System.out.println(  ((MyStructure)p.pointer).x  ); // prints 777

....

class Pointer 
    Object pointer;

所以现在你将 MyStructurep 从 ((MyStructure)p.pointer).x 传递到第二个 JVM,你应该能够:

MyStructure locallyImported = (MyStructure)p.pointer;

我可以想象一个用例:假设您有 2 个可能在或可能不在同一服务器上运行的微服务,以及一个可能在容器 AppServer 中实现的客户端策略,它知道服务部署在哪里,以防它检测到请求的服务在本地,它可能使用基于 Unsafe 的服务客户端来透明地查询其他服务。讨厌但有趣的是,我想看看不使用网络、绕过 WebAPI(直接调用处理控制器)和不序列化对性能的影响。在这种情况下,除了控制器参数之外,还应提供控制器本身。甚至没有考虑安全性。

从https://highlyscalable.wordpress.com/2012/02/02/direct-memory-access-in-java/借来的代码sn-ps

【讨论】:

很遗憾,这行不通。每个进程的地址空间都是虚拟的。也就是说,进程一的地址 0x400000 不会映射到与进程二的地址 0x400000 相同的物理地址。因此,不可能按照您建议的方式直接在两个进程之间映射地址。正如接受的答案所解释的那样,这样做的方法是通过内存映射文件。 因为@Vince 评论中的确切原因投反对票

以上是关于两个 JVM 之间的共享内存的主要内容,如果未能解决你的问题,请参考以下文章

如何在工作在同一共享内存区域的两个进程之间共享锁?

共享内存

jvm内存

在两个进程之间共享内存(C、Windows)

在两个应用程序之间共享内存

共享内存的实现