两个 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 中没有内置任何内容。您可以使用Socket
s 在彼此之间甚至通过第三方进行通信
谁 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.Socket
和java.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;
所以现在你将 MyStructure
和 p
从 ((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 之间的共享内存的主要内容,如果未能解决你的问题,请参考以下文章