Zero-Copy

The “zero” of zero-copy means that the number of times data is copied between user and kernel states is zero.

Traditional data copying (file-to-file, client-to-server, etc.) involves four user-state to kernel-state switches and four copies. two of the four copies between user-state and kernel-state require CPU participation, while two copies between kernel-state and IO devices do not require CPU participation in DMA mode. Zero-copy avoids copying between user and kernel states (2 times) and reduces the switching between user and kernel states twice, so the data transfer efficiency is high.

Zero copy can improve data transfer efficiency, but it is not suitable for scenarios that require data processing during user transfer (e.g., encryption).

A detailed description of zero-copy can be found in Wikipedia.

FileChannel

The FileChannel in NIO has two methods, transferTo and transferFrom, to copy data from a FileChannel directly to another Channel, or to copy data from another Channel directly to a FileChannel. this interface is often used for efficient network/file This interface is often used for efficient network/file data transfers and large file copies. This method does not require copying the source data from the kernel state to the user state and then from the user state to the kernel state of the target channel, if supported by the operating system, and also reduces two context switches between the user and kernel states, i.e., it uses “zero copy”, so its performance is generally higher than that provided by Java IO.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
Transfers bytes from this channel's file to the given writable bytechannel. 

An attempt is made to read up to count bytes starting atthe given position in this channel's file and write them to thetarget channel. An invocation of this method may or may not transferall of the requested bytes; whether or not it does so depends upon thenatures and states of the channels. Fewer than the requested number ofbytes are transferred if this channel's file contains fewer than count bytes starting at the given position, or if thetarget channel is non-blocking and it has fewer than countbytes free in its output buffer. 

This method does not modify this channel's position. If the givenposition is greater than the file's current size then no bytes aretransferred. If the target channel has a position then bytes arewritten starting at that position and then the position is incrementedby the number of bytes written. 

This method is potentially much more efficient than a simple loopthat reads from this channel and writes to the target channel. Manyoperating systems can transfer bytes directly from the filesystem cacheto the target channel without actually copying them. 

*/
public abstract long transferTo(long position, long count,
                                WritableByteChannel target)
    throws IOException;

/*
Transfers bytes into this channel's file from the given readable bytechannel. 

An attempt is made to read up to count bytes from thesource channel and write them to this channel's file starting at thegiven position. An invocation of this method may or may nottransfer all of the requested bytes; whether or not it does so dependsupon the natures and states of the channels. Fewer than the requestednumber of bytes will be transferred if the source channel has fewer than count bytes remaining, or if the source channel is non-blockingand has fewer than count bytes immediately available in itsinput buffer. No bytes are transferred, and zero is returned, if thesource has reached end-of-stream. 

This method does not modify this channel's position. If the givenposition is greater than the file's current size then no bytes aretransferred. If the source channel has a position then bytes are readstarting at that position and then the position is incremented by thenumber of bytes read. 

This method is potentially much more efficient than a simple loopthat reads from the source channel and writes to this channel. Manyoperating systems can transfer bytes directly from the source channelinto the filesystem cache without actually copying them. 
*/
public abstract long transferFrom(ReadableByteChannel src,
                                    long position, long count)
    throws IOException;

Practice

In spring boot applications, you can consider using zero-copy technology when implementing large file downloads.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/download")
public class DownloadController {

    @GetMapping
    public void download(HttpServletRequest request, HttpServletResponse response) throws IOException {

        // A movie file, 3.67 GB 
        Path file = Paths.get("E:\\movie\\NothingtoLose\\NothingtoLose.mkv");

        String contentType = Files.probeContentType(file);
        if (contentType == null) {
            contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
        }

        try (FileChannel fileChannel = FileChannel.open(file); 
                WritableByteChannel outChannel = Channels.newChannel(response.getOutputStream())) {

            long size = fileChannel.size();

            // Content-Type
            response.setContentType(contentType);
            // Content-Disposition
            response.setHeader(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment()
                    .filename(file.getFileName().toString(), StandardCharsets.UTF_8).build().toString());
            // Content-Length
            response.setContentLengthLong(size);

            ;
            
            /**
                * transferTo can only process up to 2147483647 bytes of data at a time. 
                * So it needs to be called multiple times in the loop.
                */
            
            long position = 0;

            while (size > position) {
                long count = fileChannel.transferTo(position, size - position, outChannel);
                if (count > 0) {
                    position += count;
                }
            }
        }
    }
}

Note that if you use the https protocol, then you will not enjoy the performance gains of zero-copy. This is because ssl requires encryption of the data, and encryption requires access to user space for CPU computation, so zero-copy is not possible.