Java NIO直接缓冲区的压缩

7

gzip输入/输出流不能直接操作Java直接缓冲区。

是否有任何压缩算法的实现可以直接在直接缓冲区上操作?

这样就不需要将直接缓冲区复制到Java字节数组进行压缩,从而避免了额外的开销。


1
无损压缩是不可能的。直接缓冲区按定义是“特定原始类型固定数据量的容器”。这样的转换,如压缩或加密,必须在缓冲区之外完成。 - Stephen P
1
我理解。我只想进行压缩,而不需要先将整个直接缓冲区数组复制到Java字节数组中以避免额外的惩罚。 - pdeva
3
GZIPInputStream不会创建副本——它会直接从文件中流出数据(基于查看源代码)。因此,我想这种方法可能比创建自己的直接缓冲区并将文件映射到其中更快。如果你真的想使用直接缓冲区,你可以编写自己的InputStream从缓冲区中进行流式传输。 - Russell Zahniser
2
GZIP压缩比仅仅复制数据慢得多,因此它不太可能产生太大的差异。 - Peter Lawrey
1
russell:我的直接缓冲区不是从文件创建的。我正在使用自己的代码创建它以避免垃圾回收。 - pdeva
显示剩余5条评论
3个回答

2
我不是要贬低你的问题,但这个程序中真的有一个好的优化点吗?你用分析器验证过自己确实有问题了吗?从你提出的问题来看,你似乎没有做过任何研究,只是猜测通过分配一个byte[]数组会导致性能或内存问题。由于本线程中所有答案都可能是某种类型的hack,因此在修复之前,您应该确实验证一下是否存在问题。
回到问题本身,如果您想要在ByteBuffer上“就地”压缩数据,则答案是否定的,Java中没有内置的这种功能。
如果您像以下方式分配了缓冲区:
byte[] bytes = getMyData();
ByteBuffer buf = ByteBuffer.wrap(bytes);

您可以像之前的答案建议的那样,通过ByteBufferInputStream过滤您的byte[]。

我接受这个答案,但仍在等待一个提供解决方案的答案,比如使用JNI操作字节缓冲区的库。 - pdeva
我对这个问题很感兴趣,因为我想找到一种方法,在原地仅按名称将文件夹转换为Zip文件,以便快速删除大型文件夹。 - Erik Reppen
1
避免复制数据几乎总是对性能有显著提升的。然而,已经在直接缓冲区中的数据如果不是由操作系统本身完成压缩,则无法进行压缩而不进行复制。 - gregw

2

哇,这是一个旧问题,但今天我偶然发现了它。

可能一些类库如 zip4j 可以处理这个问题,但自从Java 11版本开始,您可以不依赖任何外部类库来完成此任务:

如果您只想压缩数据,可以直接执行以下操作:

void compress(ByteBuffer src, ByteBuffer dst) {
    var def = new Deflater(Deflater.DEFAULT_COMPRESSION, true);
    try {
        def.setInput(src);
        def.finish();
        def.deflate(dst, Deflater.SYNC_FLUSH);

        if (src.hasRemaining()) {
            throw new RuntimeException("dst too small");
        }
    } finally {
        def.end();
    }
}

src和dst都会改变位置,因此在压缩返回后,您可能需要翻转它们。

为了恢复压缩数据:

void decompress(ByteBuffer src, ByteBuffer dst) throws DataFormatException {
    var inf = new Inflater(true);
    try {
        inf.setInput(src);
        inf.inflate(dst);

        if (src.hasRemaining()) {
            throw new RuntimeException("dst too small");
        }

    } finally {
        inf.end();
    }
}

请注意,这两种方法都希望(解)压缩在单个通道中完成,但是我们可以使用稍微修改过的版本以进行流式处理:
void compress(ByteBuffer src, ByteBuffer dst, Consumer<ByteBuffer> sink) {
    var def = new Deflater(Deflater.DEFAULT_COMPRESSION, true);
    try {
        def.setInput(src);
        def.finish();
        int cmp;
        do {
            cmp = def.deflate(dst, Deflater.SYNC_FLUSH);
            if (cmp > 0) {
                sink.accept(dst.flip());
                dst.clear();
            }
        } while (cmp > 0);
    } finally {
        def.end();
    }
}

void decompress(ByteBuffer src, ByteBuffer dst, Consumer<ByteBuffer> sink) throws DataFormatException {
    var inf = new Inflater(true);
    try {
        inf.setInput(src);
        int dec;
        do {
            dec = inf.inflate(dst);

            if (dec > 0) {
                sink.accept(dst.flip());
                dst.clear();
            }

        } while (dec > 0);
    } finally {
        inf.end();
    }
}

例子:

void compressLargeFile() throws IOException {
    var in = FileChannel.open(Paths.get("large"));
    var temp = ByteBuffer.allocateDirect(1024 * 1024);
    var out = FileChannel.open(Paths.get("large.zip"));

    var start = 0;
    var rem = ch.size();
    while (rem > 0) {
        var mapped=Math.min(16*1024*1024, rem);
        var src = in.map(MapMode.READ_ONLY, start, mapped);

        compress(src, temp, (bb) -> {
            try {
                out.write(bb);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        });
        
        rem-=mapped;
    }
}

如果你想要完全符合zip标准的数据:

void zip(ByteBuffer src, ByteBuffer dst) {
    var u = src.remaining();
    var crc = new CRC32();
    crc.update(src.duplicate());
    writeHeader(dst);

    compress(src, dst);

    writeTrailer(crc, u, dst);
}

地点:

void writeHeader(ByteBuffer dst) {
    var header = new byte[] { (byte) 0x8b1f, (byte) (0x8b1f >> 8), Deflater.DEFLATED, 0, 0, 0, 0, 0, 0, 0 };
    dst.put(header);
}

并且:

void writeTrailer(CRC32 crc, int uncompressed, ByteBuffer dst) {
    if (dst.order() == ByteOrder.LITTLE_ENDIAN) {
        dst.putInt((int) crc.getValue());
        dst.putInt(uncompressed);
    } else {
        dst.putInt(Integer.reverseBytes((int) crc.getValue()));
        dst.putInt(Integer.reverseBytes(uncompressed));
    }

因此,zip会带来10+8字节的开销。

为了将直接缓冲区解压缩到另一个缓冲区中,您可以将src缓冲区包装成InputStream:

class ByteBufferInputStream extends InputStream {

    final ByteBuffer bb;

    public ByteBufferInputStream(ByteBuffer bb) {
        this.bb = bb;
    }

    @Override
    public int available() throws IOException {
        return bb.remaining();
    }

    @Override
    public int read() throws IOException {
        return bb.hasRemaining() ? bb.get() & 0xFF : -1;
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        var rem = bb.remaining();

        if (rem == 0) {
            return -1;
        }

        len = Math.min(rem, len);

        bb.get(b, off, len);

        return len;
    }

    @Override
    public long skip(long n) throws IOException {
        var rem = bb.remaining();

        if (n > rem) {
            bb.position(bb.limit());
            n = rem;
        } else {
            bb.position((int) (bb.position() + n));
        }

        return n;
    }
}

并使用:

void unzip(ByteBuffer src, ByteBuffer dst) throws IOException {
    try (var is = new ByteBufferInputStream(src); var gis = new GZIPInputStream(is)) {
        var tmp = new byte[1024];

        var r = gis.read(tmp);

        if (r > 0) {
            do {
                dst.put(tmp, 0, r);
                r = gis.read(tmp);
            } while (r > 0);
        }

    }
}

当然,这并不是很好,因为我们将数据复制到一个临时数组中,但尽管如此,这也算是一种往返检查的方式,证明基于nio的zip编码可以编写可由标准io消费者读取的有效数据。
因此,如果我们忽略crc一致性检查,我们可以仅删除头/尾:
void unzipNoCheck(ByteBuffer src, ByteBuffer dst) throws DataFormatException {
    src.position(src.position() + 10).limit(src.limit() - 8);

    decompress(src, dst);
}

0
如果您正在使用ByteBuffer,您可以使用一些简单的Input/OutputStream包装器,例如:
public class ByteBufferInputStream extends InputStream {

    private ByteBuffer buffer = null;

    public ByteBufferInputStream( ByteBuffer b) {
        this.buffer = b;
    }

    @Override
    public int read() throws IOException {
        return (buffer.get() & 0xFF);
    }
}

public class ByteBufferOutputStream extends OutputStream {

    private ByteBuffer buffer = null;

    public ByteBufferOutputStream( ByteBuffer b) {
        this.buffer = b;
    }

    @Override
    public void write(int b) throws IOException {
        buffer.put( (byte)(b & 0xFF) );
    }

}

测试:

ByteBuffer buffer = ByteBuffer.allocate( 1000 );
ByteBufferOutputStream bufferOutput = new ByteBufferOutputStream( buffer );
GZIPOutputStream output = new GZIPOutputStream( bufferOutput );
output.write("stackexchange".getBytes());
output.close();

buffer.position( 0 );

byte[] result = new byte[ 1000 ];

ByteBufferInputStream bufferInput = new ByteBufferInputStream( buffer );
GZIPInputStream input = new GZIPInputStream( bufferInput );
input.read( result );

System.out.println( new String(result));

3
即使将bytebuffer包装成一个流也无济于事,因为它在内部被复制了(有时会复制两次),这有点违背bytebuffer的初衷。 - bestsss
抱歉,我不明白,那个复制什么时候发生?我已经仔细检查了InputStream、OutputStream甚至GZIP类的代码,但没有找到任何复制。 - Guillaume Serre
这是它的工作原理,检查InflatedInputStream和本地实现需要复制(或固定,取决于JVM/GC)byte[]以将其传递给zlib。 - bestsss

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接