咨询:运行仅存于内存且超Java字节数组容量的Jar文件方案
我开发了一款可向Java进程注入DLL的应用,目标是加载并运行远程主机的Jar文件,且Jar全程不得落地磁盘。由于Jar对应的本地缓冲区大小超过Integer.MAX_VALUE,无法直接用byte[]传递,因此通过JNI从C++代码传入缓冲区的起始地址和长度,自定义了MemoryClassLoader类加载器和UnsafeBufferInputStream输入流来读取内存中的Jar数据。
目前该实现已能加载包含入口点的类,但实际运行Jar时遇到诸多问题,且担心此类自定义实现会引发难以排查的运行时问题,现寻求成熟稳定的内存Jar运行预实现方案,或更优的替代思路。
现有实现代码
MemoryClassLoader 实现
public class MemoryClassLoader extends ClassLoader { private final Map<String, byte[]> classes = new HashMap<>(); private final Map<String, byte[]> resources = new HashMap<>(); public MemoryClassLoader() { } // Called from my native c++ code, providing the start address and length of the buffer public void load(long memAddress, long bufSize) { try { String tempFolderPath = System.getProperty("java.io.tmpdir"); // Get the system's temporary folder String className = "net.sxlver.UnsafeBufferInputStream"; // Adjust with your class name Class<?> unsafeStream = null; try { // Read the class file bytes from the temp folder Path filePath = Paths.get(tempFolderPath, "UnsafeBufferInputStream.class"); byte[] classBytes = Files.readAllBytes(filePath); unsafeStream = defineClass(className, classBytes, 0, classBytes.length); } catch (IOException e) { e.printStackTrace(); } //UnsafeBufferInputStream stream = (UnsafeBufferInputStream) unsafeStream.getDeclaredConstructor(long.class, long.class).newInstance(memAddress, bufSize); UnsafeBufferInputStream stream = new UnsafeBufferInputStream(memAddress, bufSize); loadClassesAndResourcesFromJar(stream); }catch(final Exception exception) { JOptionPane.showMessageDialog(null, ""); } } private static Unsafe getUnsafe() { try { java.lang.reflect.Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); return (Unsafe) field.get(null); } catch (Exception e) { throw new RuntimeException("Unsafe access error: " + e.getMessage()); } } private void loadClassesAndResourcesFromJar(UnsafeBufferInputStream stream) throws IOException { JarInputStream jarStream = new JarInputStream(stream); JarEntry entry; while ((entry = jarStream.getNextJarEntry()) != null) { if (!entry.isDirectory()) { byte[] buffer = new byte[1024]; int bytesRead; ByteArrayOutputStream baos = new ByteArrayOutputStream(); while ((bytesRead = jarStream.read(buffer)) != -1) { baos.write(buffer, 0, bytesRead); } if (entry.getName().endsWith(".class")) { classes.put(entry.getName().replace(".class", "").replace("/", "."), baos.toByteArray()); } else { resources.put(entry.getName(), baos.toByteArray()); } } } } // Override findClass to load classes from memory @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classBytes = classes.get(name); if (classBytes == null) { throw new ClassNotFoundException(name); } return defineClass(name, classBytes, 0, classBytes.length); } @Override public Class<?> loadClass(String name) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> loadedClass = findLoadedClass(name); if (loadedClass == null) { try { loadedClass = findClass(name); } catch (ClassNotFoundException e) { loadedClass = super.loadClass(name); } } return loadedClass; } } @Override public InputStream getResourceAsStream(String name) { byte[] resourceBytes = resources.get(name); if (resourceBytes != null) { return new ByteArrayInputStream(resourceBytes); } return super.getResourceAsStream(name); } }
UnsafeBufferInputStream 实现
public class UnsafeBufferInputStream extends InputStream { private Unsafe unsafe; private long address; private long remainingBytes; private static final int BUFFER_SIZE = 1024; // Adjust buffer size as needed public UnsafeBufferInputStream(long bufferAddress, long size) { this.unsafe = getUnsafe(); this.address = bufferAddress; this.remainingBytes = size; } private Unsafe getUnsafe() { try { java.lang.reflect.Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); return (Unsafe) field.get(null); } catch (Exception e) { throw new RuntimeException("Unsafe access error: " + e.getMessage()); } } @Override public int read() throws IOException { if (remainingBytes <= 0) { return -1; } byte value = unsafe.getByte(address++); remainingBytes--; return value & 0xFF; } @Override public int read(byte[] b, int off, int len) throws IOException { if (remainingBytes <= 0) { return -1; } int bytesRead = (int) Math.min(len, remainingBytes); for (int i = 0; i < bytesRead; i++) { b[off + i] = unsafe.getByte(address++); } remainingBytes -= bytesRead; return bytesRead; } @Override public void close() { } }
成熟稳定的替代方案与优化思路
1. 替换Unsafe为NIO MappedByteBuffer(推荐)
Unsafe操作存在兼容性风险(不同JDK版本实现可能变化),且手动字节循环读取效率低下。可以在JNI层将C++分配的内存包装为FileDescriptor,然后通过FileChannel.map将其映射为MappedByteBuffer,再基于该ByteBuffer实现标准InputStream:
- 避免直接使用Unsafe,依赖Java标准API,稳定性更高
- MappedByteBuffer支持批量内存拷贝,读取效率远高于单字节循环
2. 优化ClassLoader的按需加载逻辑
现有实现会一次性将Jar中所有类和资源加载到HashMap,对于超大Jar会直接导致OOM。改为按需加载:
- 保留内存映射的Jar流引用,当
findClass或getResourceAsStream被调用时,才定位到对应的JarEntry读取内容 - 避免预加载所有资源,大幅降低内存占用
3. 复用成熟的类加载器实现
直接使用经过生产环境验证的开源类加载器:
- Apache Commons Lang的
ByteArrayClassLoader:支持从字节数组或流加载类,可扩展为支持内存映射流 - Spring Framework的
Resource体系:自定义Resource实现读取内存中的Jar,配合DefaultResourceLoader实现类加载,自动处理类依赖和资源查找
4. JNI层拆分大缓冲区
如果无法使用MappedByteBuffer,可以在JNI层将超过Integer.MAX_VALUE的缓冲区拆分为多个int大小的块,分批次传递到Java层,用SequenceInputStream拼接成完整的Jar流,兼容Java标准API的int长度限制。
5. 移除临时文件依赖
现有代码中读取临时目录的UnsafeBufferInputStream.class存在安全风险和依赖问题,改为:
- 将
UnsafeBufferInputStream的字节码直接硬编码到MemoryClassLoader中,通过defineClass加载 - 或者将该类打包到注入DLL的辅助资源中,通过JNI直接传递其字节码
内容的提问来源于stack exchange,提问作者SteveOberst

