Java压缩20M文件从30秒到1秒的优化过程
Java压缩20M⽂件从30秒到1秒的优化过程
有⼀个需求需要将前端传过来的10张照⽚,然后后端进⾏处理以后压缩成⼀个压缩包通过⽹络流传输出去。之前没有接触过⽤Java压缩⽂件的,所以就直接上⽹了⼀个例⼦改了⼀下⽤了,改完以后也能使⽤,但是随着前端所传图⽚的⼤⼩越来越⼤的时候,耗费的时间也在急剧增加,最后测了⼀下压缩20M的⽂件竟然需要30秒的时间。压缩⽂件的代码如下。
1. public static void zipFileNoBuffer() {
2. File zipFile = new File(ZIP_FILE);
3. try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFile))) {
4. //开始时间
5. long beginTime = System.currentTimeMillis();
6.
7. for (int i = 0; i < 10; i++) {
8. try (InputStream input = new FileInputStream(JPG_FILE)) {
9. zipOut.putNextEntry(new ZipEntry(FILE_NAME + i));
0. int temp = 0;
1. while ((temp = ad()) != -1) {
2. zipOut.write(temp);
3. }
4. }
5. }
6. printInfo(beginTime);
7. } catch (Exception e) {
8. e.printStackTrace();
9. }
0. }
这⾥了⼀张2M⼤⼩的图⽚,并且循环⼗次进⾏测试。打印的结果如下,时间⼤概是30秒。
1. fileSize:20M
2. consum time:29599
过户费
第⼀次优化过程-从30秒到2秒
进⾏优化⾸先想到的是利⽤缓冲区 BufferInputStream。在 FileInputStream中 read()⽅法每次只读取⼀个字节。源码中也有说明。
1. /**
2. * Reads a byte of data from this input stream. This method blocks
3. * if no input is yet available.
4. *
5. * @return the next byte of data, or <code>-1</code> if the end of the
6. * file is reached.
7. * @exception IOException if an I/O error occurs.
8. */
9. public native int read() throws IOException;
这是⼀个调⽤本地⽅法与原⽣操作系统进⾏交互,从磁盘中读取数据。每读取⼀个字节的数据就调⽤⼀次本地⽅法与操作系统交互,是⾮常耗时的。例如我们现在有30000个字节的数据,如果使⽤ FileInputStream那么就需要调⽤30000次的本地⽅法来获取这些数据,⽽如果使⽤缓冲区的话(这⾥假设初始的缓冲区⼤⼩⾜够放下30000字节的数据)那么只需要调⽤⼀次就⾏。因为缓冲区在第⼀次调⽤ read()⽅法的时候会直接从磁盘中将数据直接读取到内存中。随后再⼀个字节⼀个字节的慢慢返回。
BufferedInputStream内部封装了⼀个byte数组⽤于存放数据,默认⼤⼩是8192
优化过后的代码如下
赛尔号索伦森1. public static void zipFileBuffer() {
2. File zipFile = new File(ZIP_FILE);
3. try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFile));
4. BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(zipOut)) {
5. //开始时间
6. long beginTime = System.currentTimeMillis();
7. for (int i = 0; i < 10; i++) {
8. try (BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(JPG_FILE))) {
9. zipOut.putNextEntry(new ZipEntry(FILE_NAME + i));
10. int temp = 0;
11. while ((temp = ad()) != -1) {
12. bufferedOutputStream.write(temp);
13. }
14. }
15. }
16. printInfo(beginTime);
17. } catch (Exception e) {
8. e.printStackTrace();
笔记本电脑键盘9. }
0. }
输出
1. ------Buffer
2. fileSize:20M
3. consum time:1808
可以看到相⽐较于第⼀次使⽤ FileInputStream效率已经提升了许多了
第⼆次优化过程-从2秒到1秒
使⽤缓冲区 buffer的话已经是满⾜了我的需求了,但是秉着学以致⽤的想法,就想着⽤NIO中知识进⾏优化⼀下。
使⽤Channel
为什么要⽤ Channel呢?因为在NIO中新出了 Channel和 ByteBuffer。正是因为它们的结构更加符合操作系统执⾏I/O的⽅式,所以其速度相⽐较于传统IO⽽⾔速度有了显著的提⾼。 Channel就像⼀个包含着煤矿的矿藏,⽽ ByteBuffer则是派送到矿藏的卡车。也就是说我们与数据的交互都是与 ByteBuffer的交互。
在NIO中能够产⽣ FileChannel的有三个类。分别是 FileInputStream、 FileOutputStream、以及既能读⼜能写的 RandomAccessFile。
源码如下
1. public static void zipFileChannel() {
观沧海曹操2. //开始时间
3. long beginTime = System.currentTimeMillis();
4. File zipFile = new File(ZIP_FILE);
5. try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFile));
6. WritableByteChannel writableByteChannel = wChannel(zipOut)) {
7. for (int i = 0; i < 10; i++) {
8. try (FileChannel fileChannel = new FileInputStream(JPG_FILE).getChannel()) {
9. zipOut.putNextEntry(new ZipEntry(i + SUFFIX_FILE));
0. ansferTo(0, FILE_SIZE, writableByteChannel);
1. }
2. }
卖油翁原文及翻译3. printInfo(beginTime);
4. } catch (Exception e) {
5. e.printStackTrace();
16. }
17. }
我们可以看到这⾥并没有使⽤ ByteBuffer进⾏数据传输,⽽是使⽤了 transferTo的⽅法。这个⽅法是将两个通道进⾏直连。
1. This method is potentially much more efficient than a simple loop
2. * that reads from this channel and writes to the target channel. Many
3. * operating systems can transfer bytes directly from the filesystem cache
4. * to the target channel without actually copying them.
这是源码上的描述⽂字,⼤概意思就是使⽤ transferTo的效率⽐循环⼀个 Channel读取出来然后再循环写⼊另⼀个 Channel好。操作系统能够直接传输字节从⽂件系统缓存到⽬标的 Channel中,⽽不需要实际的 copy阶段。
copy阶段就是从内核空间转到⽤户空间的⼀个过程
可以看到速度相⽐较使⽤缓冲区已经有了⼀些的提⾼。
房贷计算方式
1. ------Channel
2. fileSize:20M
3. consum time:1416
内核空间和⽤户空间
那么为什么从内核空间转向⽤户空间这段过程会慢呢?⾸先我们需了解的是什么是内核空间和⽤户空间。在常⽤的操作系统中为了保护系统中的核⼼资源,于是将系统设计为四个区域,越往⾥权限越⼤,所以Ring0被称之为内核空间,⽤来访问⼀些关键性的资源。Ring3被称之为⽤户空间。
image
⽤户态、内核态:线程处于内核空间称之为内核态,线程处于⽤户空间属于⽤户态
那么我们如果此时应⽤程序(应⽤程序是都属于⽤户态的)需要访问核⼼资源怎么办呢?那就需要调⽤内核中所暴露出的接⼝⽤以调⽤,称之为系统调⽤。例如此时我们应⽤程序需要访问磁盘上的⽂件。此时应⽤程序就会调⽤系统调⽤的接⼝ open⽅法,然后内核去访问磁盘中的⽂件,将⽂件内容返回给应⽤程序。⼤致的流程如下
image
直接缓冲区和⾮直接缓冲区
既然我们要读取⼀个磁盘的⽂件,要废这么⼤的周折。有没有什么简单的⽅法能够使我们的应⽤直接操作磁盘⽂件,不需要内核进⾏中转呢?有,那就是建⽴直接缓冲区了。
⾮直接缓冲区:⾮直接缓冲区就是我们上⾯所讲内核态作为中间⼈,每次都需要内核在中间作为中转。
1. ![](upload-images.jianshu.io/upload_images/13146186-6bbfb5446221cb2d.jpg?imageMogr2/auto-
orient/strip|imageView2/2/w/812/format/webp)
2.
3. image
直接缓冲区:直接缓冲区不需要内核空间作为中转copy数据,⽽是直接在物理内存申请⼀块空间,这块空间映射到内核地址空间和⽤户地址空间,应⽤程序与磁盘之间数据的存取通过这块直接申请的物理内存进⾏交互。
image
既然直接缓冲区那么快,我们为什么不都⽤直接缓冲区呢?其实直接缓冲区有以下的缺点。直接缓冲区的缺点:
1、不安全

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。